fe-questions's People
fe-questions's Issues
37. 判断 x, y 引用对象内数据是否相等,返回一个新对象,a / y 相等的数据则使用 x 的数据,不相等则使用 y 的数据
来源是当初面微软的第四面给的笔试题,没有答上来,感觉还是比较可惜,编码能力还是不够强,得多练习。
-
React 的 PureComponent 会自定义一个
shouldComponentUpdate
函数,这个函数会比较之前的props
与现在的props
比较,比较的规则就是 JS 是否全等( === ) -
如果原先有个
props
,是个引用的数据,React 的组件更新有个规则,如果父组件更新,那么不管传递给子组件的 props 是否有变化,都会触发子组件的更新,所以我们子组件会使用PureComponent
-
一般我们通过 API 从后端请求数据返回,那么第二次进行请求时,即使数据是一样的,但因为
PureComponent
比较的是引用数据是否全等,所以如果直接将第二次的数据传递给组件,因为引用改变了,所以还会触发子组件的渲染 -
所以问题来了,假设第一次请求返回的引用数据是 x,第二次返回的引用数据是 y,那么我们需要根据 x 和 y 进行比较,生成一个新的数据 z ,如果 x 和 y 中的某条数据相等,那么就将 x 中的这一条数据放到 z 中,如果不相等,那么就将 y 中的这一条数据放到 z 中,因为 y 是返回新的数据吗,所以需要更新。
初步的写法如下,感觉还是可以优化的
const createNew = function (x, y) {
let result = Array.isArray(y) ? [] : {};
const create = function (x, y, obj) {
// 如果所有子的对象都相等的话,就直接把 x 的值放到 obj 中去
// 而不是 obj[key] = y[key];
// 如果引用类型里面比对的都对,就不将 obj 返回了,直接返回 x,这样才能比对成功,所以增加了一个 allEqual 的变量
let allEqual = true;
for (let key in y) {
// 相等的时候,是不看两个是基础类型还是引用类型的,只是在不相等的时候,才进行比较
// 目的比对的,就是引用类型里面所有基础的数据类型是一致的,那么就先赋给 obj,如果后续有数据不对,就将 obj 进行返回
if (x[key] === y[key]) {
obj[key] = y[key];
}
else if (typeof y[key] === 'object' && y[key] !== null) {
// 所有的重点,都是在这个分支的
if (typeof x[key] === 'object' && x[key] !== null) {
// 这里 x[key] / y[key] 都是对象,所以需要看下是否都是 Array
if (Array.isArray(x[key]) && Array.isArray(y[key])) {
let newObj = Array.isArray(y[key]) ? [] : {};
let temp = create(x[key], y[key], newObj);
if (temp === x[key]) {
obj[key] = x[key];
}
else {
obj[key] = temp;
allEqual = false;
}
}
// 两个中如果有一个是数组,那么两个数据就不相等,就直接赋值给 obj 好了
else if (Array.isArray(y[key]) || Array.isArray(x[key])) {
// 既然类型不同,就直接放到给 obj 好了,不用比较了
obj[key] = y[key];
allEqual = false;
}
// 这两个都是 Object,再进行递归的比较一次
else {
let newObj = Array.isArray(y[key]) ? [] : {};
let temp = create(x[key], y[key], newObj);
if (temp === x[key]) {
obj[key] = x[key];
}
else {
obj[key] = temp;
allEqual = false;
}
}
}
else {
obj[key] = y[key];
allEqual = false;
}
}
else {
obj[key] = y[key];
allEqual = false;
}
}
// 如果全部都相等的话,就直接将 x 给 obj,也就是引用对象,也是直接给 obj 的,这样才是直接给引用地址的
if (allEqual) {
return x;
}
return obj;
}
create(x, y, result);
return result;
}
// 测试
const x = {
a: 1,
b: 2,
c: {
name: 'jack',
age: 213,
j: {
address: 'heibei'
}
},
d: {
name: 'json',
age: 32
}
}
const y = {
a: 1,
b: 2,
c: {
name: 'jack',
age: 13,
j: {
address: 'heibei'
}
},
d: {
name: 'json',
age: 31
}
}
let res = createNew(x, y);
console.log(res)
console.log(res.c == x.c);
console.log(res.c.j == x.c.j);
console.log(res.d == y.d);
16. 实现在不定宽中,文字超出宽度后,使用省略号显示
实现在不定宽中,文字超出宽度后,使用省略号显示
max-width: 100%; /* 父元素的100%,定宽的时候,就设置为固定的宽度值 */
overflow: hidden; /* 这三个条件缺一不可 */
word-break: keep-all;
text-overflow: ellipsis;
7. 函数执行的输出(区分是否是严格模式)
执行代码求输出,并说明为什么,严格模式下输出有变化吗,为什么
var a = function () {
this.b = 3;
}
var c = new a();
a.prototype.b = 9;
var b = 7;
a();
console.log(b);
console.log(c.b);
10. 实现instanceOf/new/bind/apply
实现 instanceof
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。
function instance_of(obj, cons) {
// 错误判断 构造函数必须是一个function 其他的均报错
if (typeof cons !== 'function') throw new Error('instance error')
if (!obj || (typeof obj !== 'object' && typeof obj !== 'function')) return false
// 获取到原型对象
let proto = cons.prototype
// 如果obj的原型对象不是null
while (obj.__proto__) {
if (obj.__proto__ === proto) return true
obj = obj.__proto__
}
return false
}
// 测试
console.log(instance_of(() => {}, Function)) // true
var str = 'hello world'
str instanceof String // false
var str1 = new String('hello world')
str1 instanceof String // true
对于原始类型来说,你想直接通过 instanceof
来判断类型是不行的,当然我们还是有办法让 instanceof
判断原始类型的
class PrimitiveString {
static [Symbol.hasInstance](x) {
return typeof x === 'string'
}
}
console.log('hello world' instanceof PrimitiveString) // true
你可能不知道 Symbol.hasInstance
是什么东西,其实就是一个能让我们自定义 instanceof
行为的东西,以上代码等同于 typeof 'hello world' === 'string'
,所以结果自然是 true
了。这其实也侧面反映了一个问题, instanceof
也不是百分之百可信的。
/**
* @param {object} obj
* @param {object} target
* @return {boolean}
*/
function myInstanceOf(obj, target) {
// If the object does not exist or we've reached the base Object constructor, return false
if(!obj || typeof obj !== 'object') return false
// Check if the target is a valid object
if(!target.prototype) throw Error
// If the object's prototype matches our target's prototype, return true
// Otherwise, recurse down the prototypal chain
if(Object.getPrototypeOf(obj) === target.prototype) {
return true
} else {
return myInstanceOf(Object.getPrototypeOf(obj), target)
}
}
实现 memoizeOne()
/**
* @param {Function} func
* @param {(args: any[], newArgs: any[]) => boolean} [isEqual]
* @returns {any}
*/
const defaultIsEqual = (args1, args2) => {
return JSON.stringify(args1) === JSON.stringify(args2)
}
function memoizeOne(func, isEqual = defaultIsEqual) {
let cache = {
}
return function(...args) {
if (this === cache.self && isEqual(args, cache.args)) {
return cache.value
}
cache.args = args
cache.self = this
cache.value = func.apply(this, args)
return cache.value
}
}
24. visibility=hidden, opacity=0,display:none的区别
opacity=0
该元素隐藏起来了,但不会改变页面布局,并且,如果该元素已经绑定一些事件,如click事件,那么点击该区域,也能触发点击事件的
visibility=hidden
该元素隐藏起来了,但不会改变页面布局,但是不会触发该元素已经绑定的事件
display=none
把元素隐藏起来,并且会改变页面布局,可以理解成在页面中把该元素删除掉一样。
18. 防抖和节流
这个还是要放进来,毕竟我真的有次在回答的时候没搞懂
防抖函数
原理
在事件被触发的n秒后再执行回调,如果在这n秒内又被触发,则重新计时
使用场景
- 按钮提交场景:防止多次提交按钮,只执行最后提交的一次
- 服务端验证场景:表单验证需要服务端配合,只执行一段连续的输入事件的最后一次,还有搜索联想词功能类似
实现
const debounce = (fn, delay) => {
let timer = null;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, args);
}, delay)
}
}
节流函数
原理
规定在一个单位事件内,只能触发一次函数,如果这个单位事件内触发多次函数,只有一次生效。防抖是延迟执行,而节流是间隔执行,函数节流即每隔一段时间就执行一次。
适用场景
- 拖拽场景:固定时间内只执行一次,防止超高频次触发位置变动
- 缩放场景:监控浏览器的resize
- 动画场景: 避免短时间内多次触发动画引起性能问题
实现
// 定时器实现
const throttle = (fn, delay = 500) => {
let flag = true;
return (...args) => {
if (!flag) return;
flag = false;
setTimeout(() => {
fn.apply(this, args);
flag = true;
}, delay);
};
}
// 时间戳实现
const throttle = (fn, delay = 500) => {
let preTime = Date.now();
return (...args) => {
const nowTime = Date.now();
if (nowTime - preTime >= delay) {
preTime = Date.now();
fn.apply(this, args);
}
}
}
34. 实现对 Ajax 事件请求的监听
/*
* 1. class 的使用,new 对象
* 2. this 指向
* 3. apply, call 的运用
* 4. Object.defineProperty 的应用
* 5. 代码的设计能力
* 6. hook 的理解
* */
/*
* 总体的逻辑
* 1. 重写原生的 XMLHttpRequest 方法
* 2. 获取到原生的方法和属性进行劫持
* 3. 在原生的方法中增加 beforeHooks 和 afterHooks
* 4. 使用 Object.defineProperty 重写属性
* */
class XhrHook {
constructor(beforeHooks = {}, afterHooks = {}) {
this.XHR = window.XMLHttpRequest;
this.beforeHooks = beforeHooks;
this.afterHooks = afterHooks;
this.init();
}
init() {
let _this = this;
// 这里使用 function 而不是使用箭头函数,是因为每次使用 XMLHttpRequest 都是使用 new
// 使用箭头函数会改变 this 指向
window.XMLHttpRequest = function() {
this._xhr = new _this.XHR();
_this.overwrite(this);
}
}
overwrite(proxyXHR) {
for (let key in proxyXHR._xhr) {
if (typeof proxyXHR._xhr[key] === 'function') {
this.overwriteMethod(key, proxyXHR);
continue;
}
this.overwriteAttributes(key, proxyXHR);
}
}
overwriteMethod(key, proxyXHR) {
let beforeHooks = this.beforeHooks;
let afterHooks = this.afterHooks;
proxyXHR[key] = (...args) => {
// beforeHooks 拦截如果返回 false,那么就不会执行原有的函数了
if (beforeHooks[key]) {
const res = beforeHooks[key].call(proxyXHR, args);
if (res === false) {
return;
}
}
const res = proxyXHR._xhr[key].apply(proxyXHR._xhr, args);
// afterHooks 中需要把原生的函数执行的结果传进来
afterHooks[key] && afterHooks[key].call(proxyXHR._xhr, res);
return res;
}
}
overwriteAttributes(key, proxyXHR) {
Object.defineProperty(proxyXHR, key, this.setPropertyDescriptor(key, proxyXHR));
}
setPropertyDescriptor(key, proxyXHR) {
let obj = Object.create(null);
let _this = this;
obj.set = function(val) {
// 不是 onreadystatechange/onload 这种属性,而是函数
if (!key.startsWith('on')) {
proxyXHR['__' + key] = val;
return;
}
if (_this.beforeHooks[key]) {
this._xhr[key] = function (...args) {
_this.beforeHooks[key].call(proxyXHR);
val.apply(proxyXHR, args);
}
return;
}
this._xhr[key] = val;
}
obj.get = function() {
return proxyXHR['__' + key] || this._xhr[key];
}
return obj;
}
}
35. 实现vue2/vue3 实现监听的方式
vue2 使用 defineProperty
需要注意在调用 defineReactive 进行设置的时候,需要先把 data[key] 取出来,要不然在defineReactive 中的 get 方法中调用data[key] 会造成死循环
let data = {
name: 'king'
}
observe(data);
data.name = 'dmp';
console.log(data.name, '--- get')
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(function(key) {
defineReactive(data, key, data[key]);
});
}
function defineReactive(data, key, val) {
observe(val);
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('获取值');
// 注意这里不是 data[key],因为如果是data[key]就会造成死循环了,会一直触发 get 函数
// 所以在 observe 函数中调用 defineReactive 时会事先取出来
return val;
},
set: function (newVal) {
console.log('监听到值的变化', val, newVal);
// 这里有一点不明白的是,val 是一个值,为什么可以设置 val = newVal,但是这里不是 return newVal
val = newVal;
}
})
}
26. 实现sum(1,2,3)==sum(1)(2)(3)
add(1)(2)(3); // 6
add(1)(2, 3); // 6
add(1, 2)(3); // 6
add(1, 2, 3); // 6
解法
const curry = fn => {
const len = fn.length;
return function curried(...args) {
if (args.length === len) {
return fn.apply(null, args);
}
return (..._args) => {
return curried.apply(null, [...args, ..._args]);
};
};
};
const sum = (x, y, z) => x + y + z;
const add = curry(sum);
// 6
add(1, 2, 3);
// 6
add(1,2)(3);
// 6
add(1)(2,3);
// 6
add(1)(2)(3);
参考
- 第 84 题:请实现一个 add 函数
这题里面有 add(1) // 1 ,也就是无论是几个参数都会进行计算。但是看了下所有网友给的题解,都是在函数中来接收参数,然后使用
toString
方法来计算总和,那怎么才能去拿到函数的toString
计算的值呢?使用add(1)
返回的是[Function add]
这种格式啊
3. 实现一些主流框架的循环渲染
问题
var items = [
{ name: 'item1' },
{ name: 'item2' }
];
var str = '<div ali-for="item in items">{{item.name}}<div>';
// 对应生成的dom
ParseDom(str);
// <div>item1</div>
// <div>item2</div>
实现
let items = [
{
name: 'item1'
},
{
name: 'item2'
}
];
const s = {};
s.items = items;
function ParseDom(str) {
const mid = document.createElement('div');
mid.innerHTML = str;
const {children} = mid;
let res = '';
[...children].forEach(child => {
const attrs = [...child.attributes];
const targetAttr = attrs.find(x => x.name === 'ali-for');
const nodeName = child.nodeName.toLocaleLowerCase();
// 所以非 ali-for="item in items" 的属性累加
const attrsStr = attrs.reduce((r, c) => {
if (child.name !== 'ali-for') {
r += ` ${c.name}="${c.value}"`
}
return r;
}, '');
if (!targetAttr) {
res += `<${nodeName}${attrsStr}>${child.innerHTML}</${nodeName}>`;
}
const vfor = targetAttr.nodeValue; // 拿到 ali-for="item in items" 中的 "item in items"
const o = vfor.split(' in ')[1]; // 拿到 ali-for="item in items" 中的 items
const k = child.innerText.match(/\{\{(.*)\}\}/)[1].split('.')[1]; // 拿到{{item.name}} 中的 item.name
s[o].forEach(x => {
res += `<${nodeName}${attrsStr}>${x[k]}</${nodeName}>`; // 把 {{item.name}} 替换成 item.name 的值
});
})
return res;
}
const str = '<div ali-for="item in items">{{item.name}}</div>';
let result = ParseDom(str);
console.log(result);
平铺JS数组的层级,实现 array flat 方法
给定这样的一个数组,当传入 nums = 2 的时候,将第二层的数组展开,不传的时候 nums = 1
const input = [[[[1],2],3],4],5]
const res = flat(input) // [[[[1],2],3],4,5]
const res = flat(input, 2) // [[[1],2],3,4,5]
也就是依次将数组的层级减少
function flat(array, num = 1) {
let arr = array.concat()
while (num) {
// arr = arr.reduce 是最重要的地方,将之前的结果给下一次的循环
arr = arr.reduce((prev, cur) => {
if (Array.isArray(cur)) {
prev.push(... cur)
} else {
prev.push(cur)
}
return prev;
}, [])
Num--;
}
return arr
}
const res = flat(input, 2)
console.log(JSON.stringify(res));
5. 金额的人民币表示方法(每三位数逗号分割)
解法
let money = 1003450.89;
// $1 匹配到 (\d),也就是上面金额中的 1 和 3
// 也就是匹配到了小数点前面的多个三个数字然后加上英文逗号,
// 正向预查,即?=n(匹配任何其后紧接指定字符串 n 的字符串)
let result = money.toFixed(2).replace(/(\d)(?=(\d{3})+\.)/g, '$1,')
// 第二种方法
let str = "100000000000",
// \B是表示匹配非单词边界的元字符,与其互为补集的元字符是\b,表示匹配单词边界。
//
reg = /(?=(\B\d{3})+$)/g;
let res = str.replace(reg, ",");
关于第二种方法
var str = "10000000000",
reg = /(?=(\d{3})+$)/g;
console.log(str.match(reg)); // 这里会得到 三个"",这里没有明白为什么会是三个"",是因为`?=`不会匹配到结果中
注意
如果 money 中没有小数,使用如下的正则式是不成功的,因为匹配不到结果,需要在(?=(\d{3})+)
改成(?=(\d{3})+$)
也就是增加一个 $
符号,在上面是有一个 \.
来匹配到小数点,也就是从小数点这里开始。
money.replace(/(\d)(?=(\d{3})+)/g, '$1,')
所以合理的方案就是先让money
使用toFixed
来增加小数,然后再使用\.
来匹配到小数点,这样就可以实现效果了。
参考
?=
是向前匹配,详情正则教程
29. 函数/变量提升
日常使用变量以及函数会涉及到变量/函数提升
foo(); // 1
var foo;
function foo() {
console.log( 1 );
}
foo = function() {
console.log( 2 );
};
这里的 function foo
会提升到最顶部,而 foo = function()
会在执行的时候才会给 foo
赋值。
ES6
中的let / const
都是在执行的时候才会赋值,提前使用会出现ReferenceError
这里提一个
foo(); // 这里会出现什么
var a = true;
if (a) {
function foo() { console.log("a"); } }
else {
function foo() { console.log("b"); }
}
会出现 TypeError: foo is not a function
多举几个例子
foo(); // 不是 ReferenceError, 而是 TypeError!
var foo = function bar() { // ...
};
这段程序中的变量标识符 foo() 被提升并分配给所在作用域(在这里是全局作用域),因此 foo() 不会导致 ReferenceError。但是 foo 此时并没有赋值(如果它是一个函数声明而不 是函数表达式,那么就会赋值)。foo() 由于对 undefined 值进行函数调用而导致非法操作, 因此抛出 TypeError 异常。
同时也要记住,即使是具名的函数表达式,名称标识符在赋值之前也无法在所在作用域中使用:
foo(); // TypeError
bar(); // ReferenceError
var foo = function bar() { // ...
};
这个代码片段经过提升后,实际上会被理解为以下形式:
var foo;
foo(); // TypeError
bar(); // ReferenceError
foo = function() {
var bar = ...self... // ...
}
词法作用域 与 动态作用域
词法作用域是一套关于引擎如何寻找变量以及会在何处找到变量的规 则。词法作用域最重要的特征是它的定义过程发生在代码的书写阶段(假设你没有使用 eval() 或 with)
所以以下代码中的 foo
中会输出 2 而不是3
function foo() {
console.log( a ); // 2
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
而动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调 用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
因此,如果 JavaScript 具有动态作用域,理论上,下面代码中的 foo() 在执行时将会输出 3。
function foo() {
console.log( a ); // 3(不是 2 !)
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
为什么会这样?因为当 foo() 无法找到 a 的变量引用时,会顺着调用栈在调用 foo() 的地 方查找 a,而不是在嵌套的词法作用域链中向上查找。由于 foo() 是在 bar() 中调用的, 引擎会检查 bar() 的作用域,并在其中找到值为 3 的变量 a。
需要明确的是,事实上 JavaScript 并不具有动态作用域。它只有词法作用域,简单明了。 但是 this 机制某种程度上很像动态作用域。
主要区别:词法作用域是在写代码或者说定义时确定的,而动态作用域是在运行时确定 的。(this 也是!)词法作用域关注函数在何处声明,而动态作用域关注函数从何处调用。
参考
- 《You-dont-know-js 上卷》第一章 作用域和闭包
压缩字符串
compress('a') // 'a'
compress('aa') // 'a2'
compress('aaa') // 'a3'
compress('aaab') // 'a3b'
compress('aaabb') // 'a3b2'
compress('aaabba') // 'a3b2a'
/**
* @param {string} str
* @return {string}
*/
function compress(str) {
const res = [];
let count = 0;
for (let i = 0; i < str.length; i++) {
count++;
if (str[i] !== str[i+1]) {
res.push(str[i]);
if (count > 1) res.push(count);
count = 0;
}
}
return res.join('');
}
21 valueOf/toString 的区别
let bb = {
i: 10,
toString: function() {
console.log('toString');
return 20;
},
valueOf: function() {
console.log('valueOf');
return 30;
}
}
let cc = {
i: 10
};
console.log(bb);
console.log(+bb);
console.log(''+bb);
console.log(cc);
console.log(+cc);
console.log(''+cc);
如果没有重写Object
的toString/valueOf
方法,那么
console.log(cc); // { i: 10 }
console.log(+cc); // NaN
console.log(''+cc); // [object Object]
那么重写了toString/valueOf
的时候呢
console.log(bb);
// 输出
{ i: 10,
toString: [Function: toString],
valueOf: [Function: valueOf] }
console.log(+bb); // 30 valueOf
console.log(''+bb); // 30 valueOf
也就是在重写了Object
的时候,如果有运算符,那么都是使用的valueOf
那么如果不重写valueOf
而只是重写toString
呢,如
let bb = {
i: 10,
toString: function() {
console.log('toString');
return 20;
}
}
console.log(+bb);
console.log(''+bb);
输出什么?
console.log(+bb); // 20 toString
console.log(''+bb); // 20 toString
总结
- 没有重写
Object
的toString/valueOf
方法时,使用+obj
会转换成toPrimitive
,这时候得到的就是NaN
,使用''+obj
会调用toString
会输出[object Object]
- 强制转换时,也就是使用
Number(obj)
转换成数字的时候,优先调用valueOf
,如果是使用String(obj)
转换成字符串的时候,优先调用toString
方法
(在没有重写
valueOf/toString
的时候是这样的规律,那么在重写了valueOf/toString
,那么String(obj)/Number(obj)
都是调用valueOf
,如果只是重写了toString
,那么String(obj)/Number(obj)
都是调用toString
方法 )
- 在有运算符的情况下,
valueOf
的优先级高于toString
这种问题考察的就是你的基础,所以最重要的是看文档,mdn toString / valueOf 和 ecma 的标准文档
参考
12. 判断 Object.keys 的输出
问题:请分别写出下列的输出结果是什么
/* --- 1 --- */
let a = {};
let b = '123';
let c = 123;
a[b] = 'b';
a[c] = 'c';
console.log(a[b]);
console.log(Object.keys(a))
/* --- 2 --- */
let a = {};
let b = Symbol('123');
let c = Symbol('123');
a[b] = 'b';
a[c] = 'c';
console.log(a[b]);
console.log(Object.keys(a))
/* --- 3 --- */
let a = {};
let b = {key: '123'};
let c = {key: '456'};
a[b] = 'b';
a[c] = 'c';
console.log(a[b]);
解答
-
最基础的知识点,
Object
的键值只能是字符串,非字符串的都会自动调用toString
方法转换成字符串,所以第一题中的数字123
也会转成字符串的123
,最终对象a
只有一个键值,输出Object.keys(a)
得到['123']
-
使用
Symbol
来作为对象的键值,Symbol
每次创建都是唯一的,所以即使两次使用Symbol('123')
,对象a
会有两个key
,这时候输出a[b]
是b
,但是,Symbol
作为Object
的key
是隐藏的,使用Object.key()
是输出不了的在术语-Symbol 中也写了
数据类型 “symbol” 是一种原始数据类型,该类型的性质在于这个类型的值可以用来创建匿名的对象属性。该数据类型通常被用作一个对象属性的键值——当你想让它是私有的时候。例如,symbol 类型的键存在于各种内置的 JavaScript 对象中。同样,自定义类也可以这样创建私有成员。
当一个 symbol 类型的值在属性赋值语句中被用作标识符,该属性(像这个 symbol 一样)是匿名的;并且是不可枚举的。因为这个属性是不可枚举的,它不会在循环结构 “
for( ... in ...)
” 中作为成员出现,也因为这个属性是匿名的,它同样不会出现在 “Object.getOwnPropertyNames()
” 的返回数组里。所以这时候输出
Object.keys()
输出为空数组,使用for...in...
也是无法输出的有个疑问,在使用
Symbol
作为Object
的键值,默认会隐藏,这个在mdn-术语-symbol 中有看到,但是在 mdn-propertyIsEnumerable 中看到这样一句话每个对象都有一个
propertyIsEnumerable
方法。此方法可以确定对象中指定的属性是否可以被for...in
循环枚举虽然
Symbol
可以通过propertyIsEnumerable
输出为true
,但是为什么不能通过for...in
来输出呢?我在 segmengFault 也提了这个问题
另外,使用
Symbol
作为Obeject
的键值,可以通过Object.getOwnPropertySymbols()
来输出2.1
for...in
和for...of
的区别for...in
是可以判断当前的属性是否可以enumerable
的,Object
的内置属性的enumerable
是false
,所以不会在for...in
的时候输出,for...of
是需要当前值是为iteraor
的时候才可以输出mdn-for...in 上第一句话就说明了
for...in
是可以输出他继承的属性的,所以如果不需要输出他继承的属性,可以在输出时,增加hasOwnProperty
来检测属性因为
Object
不是iteraor
的,所以不能,应该就是Object
没有[Symbol.iteraor]
,参考mdn-Symbol.iterator 中第一句就是Symbol.iterator 为每一个对象定义了默认的迭代器。该迭代器可以被
for...of
循环使用。所以就需要知道内置类型有
Symbol.iterator
属性一些内置类型拥有默认的迭代器行为,其他类型(如
Object
)则没有。下表中的内置类型拥有默认的@@iterator
方法:Array.prototype[@@iterator]()
TypedArray.prototype[@@iterator]()
String.prototype[@@iterator]()
(一般问这个问题的时候,会面试官会重点想听下String
有没有iterator
属性Map.prototype[@@iterator]()
Set.prototype[@@iterator]()
当然,你可以为
Object
手动创建Symbol.iterator
属性let myIterable = {}; myIterable[Symbol.iterator] = function *() { yield 1; yield 2; yield 3; } for (let i of myIterable) { console.log(i); // 1 2 3 }
-
第三个问题的答案是 非字符串的都会自动调用
toString
方法转换成字符串,所以我们需要清楚,JavaScript
的数据类型中,使用toString
后,会输出什么,这个可以查看mdn-Object-toString
每个对象都有一个
toString()
方法,当该对象被表示为一个文本值时,或者一个对象以预期的字符串方式引用时自动调用。默认情况下,toString()
方法被每个Object
对象继承。如果此方法在自定义对象中未被覆盖,toString()
返回 "[object type]",
也就是如果以{key: '123'}
作为 Object
的键值,那么其实会被转换成[object Object]
,所以第三题中,其实使用Object.keys(a)
输出后,只有一个长度为1的数组 ['[object Object]']
实现JS原生 map/reduce
Array.prototype.myReduce = function (callback, initialValue) {
const argsLength = arguments.length
if (argsLength === 1 && this.length === 0) {
throw new Error()
}
let index = argsLength === 1 ? 1 : 0
let resultValue = argsLength === 1 ? this[0] : initialValue
for (let i = index; i < this.length; i += 1) {
resultValue = callback(resultValue, this[i], i, this)
}
return resultValue
}
19. 实现Promise.all/race 实现并发控制asyncLimit
考察Promise 一般都会让你说 Promise 的原理,比如如何实现链式调用,
then
是如何实现的,参考下实现 Promise
需要说明的是,如果你说 then 是再new
一个promise
,这个是自己实现的then
,不是原生的promise
实现的then
实现 ALL
function promiseAll(promises) {
return new Promise(function(resolve, reject) {
if (!Array.isArray(promises)) {
throw new TypeError('argument must be a array');
}
let resolvedCounter = 0;
let promiseNum = promises.length;
let resolvedResult = [];
for (let i = 0; i < promiseNum; i++) {
Promise.resolve(promises[i]).then(value => {
resolvedCounter++;
resolvedResult[i] = value;
if (resolvedCounter === promiseNum) {
return resolve(resolvedResult);
}
}, error => {
return reject(error);
})
}
})
}
测试:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 1000)
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(2)
}, 1000)
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(3)
}, 1000)
});
promiseAll([p3, p1, p2]).then(res => {
console.log(res)
})
9. 大数相加,如add('12345678', '321');
const add = function (a, b) {
// 从a/b的 length - 1开始,也就是从最后一位开始
let i = a.length - 1;
let j = b.length - 1;
let carry = 0;
let ret = '';
while (i >= 0 || j >= 0) {
let x = 0;
let y = 0;
let sum;
if (i >= 0) {
x = a[i] - '0'; // 使用 - '0' 来让当前的数转换为整数
i --;
}
if (j >= 0) {
y = b[j] - '0';
j --;
}
sum = x + y + carry;
// 每次相加如果大于10,只取个位数,然后下一次的计算的结果加上1 就可以了
if (sum >= 10) {
carry = 1;
sum -= 10;
} else {
carry = 0;
}
// 0 + ''
ret = sum + ret;
}
// 为什么不在 while 里,因为在while里,carry 每次都是在下一次计算,那么在 while 计算后,还需要看看是否需要进位
if (carry) {
ret = carry + ret;
}
return ret;
}
测试
// add('999', '1');
// add('1', '999');
// add('123', '321');
// console.log(add('999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999', '1'));
20. 实现函数(包括异步/同步)按顺序调用
如实现下面的四个函数,依次按照顺序输出
// 延迟一秒输出1,然后2延迟3秒输出3,然后4
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const log = console.log
createFlow([
() => delay(1000).then(()=>log(1)),
() => log(2),
() => delay(3000).then(()=>log(3)),
() => log(4),
])
解法
使用 reduce 的按照线性执行的特性,结合Promise.resolve().then(() => fn()).then(()=>fn())...
中 then 可以无限加且保持线性的特性
function createFlow(fns) {
fns.reduce(
(previous, fn) => previous.then(() => fn()),
Promise.resolve()
);
}
原生的解法就是 async await
的方法最简单了,但一般不会考这个,你提一下就可以了。
1. 实现sum(1,2,3)==sum(1)(2)(3)
const curry = fn => {
const len = fn.length;
return function curried(...args) {
if (args.length === len) {
return fn.apply(null, args);
}
return (..._args) => {
return curried.apply(null, [...args, ..._args]);
};
};
};
const sum = (x, y, z) => x + y + z;
const add = curry(sum);
// 6
add(1, 2, 3);
// 6
add(1,2)(3);
// 6
add(1)(2,3);
// 6
add(1)(2)(3);
思路确实很好
const len = fn.length;
计算函数的正常需要多少个参数- 如果
args.length === len
也就是按照正常的传参(1, 2, 3)
- 如果不等于,就相当于是
(1,2)(3);
或者(1)(2,3);
或者(1)(2)(3);
这种传参方式 - 这时候
return
一个函数,内部继续调用curried
来计算参数的个数,使用apply
第二个参数是个数组,把所有的参数都汇总成成单个数组, - 到最后
args.length === len
的时候,就是按照正常的传参来计算了
8. 实现 indexOf 方法
看到这种题目会默认的想到使用for
循环,但是还是有更高级一点的办法,如正则
const indexOf = function(str, searchStr, fromIndex = 0) {
let regex = new RegExp(`${searchStr}`, 'ig');
regex.lastIndex = fromIndex;
// 没有匹配到的时候,会返回 null
let result = regex.exec(str);
return result ? result.index : -1;
}
上面的方法是只能适用于字符串,但是因为字符串和数组都有 indexOf
的方法,所以需要有个通用的办法
const indexOf = function(input, search, fromIndex = 0) {
if (!Array.isArray(input)) {
input = input.split('');
}
for (let i = fromIndex, len = input.length; i < len; i++) {
if (input[i] === search) {
return i;
}
}
return -1;
}
实现 Object.create()
前提是明白 create 做了哪些事情?
/**
* @param {any} proto
* @return {object}
*/
function myObjectCreate(proto) {
function Constructor() {}
Constructor.prototype = proto.prototype || proto
return new Constructor();
}
36. 随机生成rgb的值
要考你这道题,是肯定不会让你先写出[0,...9, a, ... , f]
然后每次从这里获取一位然后拼接的,如果这道题满分十分,这样写就是两分。
这里要用到toString
,而且还是Number
的toString
方法
function generateRGB() {
return '#' + Array.from({length: 6}).map(() => {
// 要知道Number 的 toString 的方法,才会转换进制,如果是 String(14).toString(16),输出的还是14,
return Math.ceil(Math.random() * 16).toString(16);
// 再补充一个知识点,parseInt(xx, 16) 是十六进制转换成十进制,不是转换成十六进制
// MDN文档说明:返回值是以第一个参数作为指定基数 radix 的转换后的十进制整数
}).join('');
}
let result = generateRGB();
console.log(result);
28. CSS 画扇形/三角形
.effect {
width: 0;
height: 0;
border: solid 100px red;
border-color: red transparent transparent transparent;
border-radius: 100px; // 没有border-radius 就是三角形了
}
设置width/height
为0,然后设置border
的宽度,也为三角形的宽度,border-color
设置其中一个为具体的颜色,其他的都是空白transparent
。设置三角形的方向就是设置border-color
为哪一个有颜色即可。
11. 批量请求数据,可以通过 max 参数控制请求的并发度,当所有请求结束之后,执行 callback 回调函数
请实现以下的函数,可以批量请求数据,所有的URL地址在urls参数中,同时可以通过 max 参数控制请求的并发度,当所有请求结束之后,需要执行 callback 回调函数。发请求的函数可以直接使用 fetch 即可
function sendRequest(urls: sring[],max:number,callback:()=>void){
//TODO
}
参考
JS 输出 - 考察函数作用域
第一道题
var a = 10;
function a() {}
function foo() {
console.log(a);
a = 5;
console.log(window.a);
}
foo()
输出为 10 / 5
第二道题
var a = 10;
function a() { };
function foo() {
console.log(a)
a = 5
console.log(window.a);
var a = 20;
console.log(a)
}
foo();
输出为 undefined / 10 / 20
问题:为什么在同样的两道题中,输出的 window.a 不一样
解答:
首先看区别,第二道题的区别就在于,后面还有两条语句
var a = 20;
console.log(a);
造成的结果的不同,就在于 var a = 20
,核心就在于JS 的作用域提升的问题。
首先会有一个全局的作用域提升,然后在函数 foo 中有一个函数内作用域提升
第一道题解释:
- 首先函数 foo 的作用域提升,在不执行 foo 的时候,foo 函数里的 a = 5 中的 a 定义会提升到全局作用域。
- 在 foo 函数内,这里的 console.log(a) 首先是在当前函数中看是否有 a 的定义提升,然后再往上查找 a 的定义,因为这时候 foo 函数里面没有 a 的定义,所以这里的 console.log(a) 首先是输出 10,然后执行了 a = 5 ,注意 这里的 a 是从作用域里查找的 a。所以这里定义的是全局的 a,所以 console.log(window.a) 会被重新定义为 5
var a = 10;
function a() {}
function foo() {
console.log(a);
a = 5;
console.log(window.a);
}
foo()
第二道题解释:
-
首先在 foo 函数中的 a = 5 中的 a 也会提升到全局中,但是因为在 foo 函数中也有 var a = 20,这里的 a 会提升到 foo 函数的顶部,所以在这里要注意,在 foo 函数中的第一个 a 是获取的 foo 函数中的 var a = 20 的 a ,不是全局作用域的 a.
在函数中获取变量,首先是从当前函数的作用域中取,然后再依次往上查找。
所以第一个 console.log(a) 是 undefined
-
a = 5 这一条语句尤其要注意,首先在这里要获取 a 的定义,因为 var a = 20 的 a 已经提升到函数作用域的顶部了,所以这里的 a 和第一道题中的 a 不一样,这里的 a 是 foo 函数中的 a
所以第二个 console.log(window.a) 是获取全局作用域的 a ,所以这里是 10
-
var a = 20 这里会对 a 进行重新定义,所以这里的 console.log(a) 是foo 函数中的 a ,所以这里是 20 是理所应当的
var a = 10;
function a() { };
function foo() {
console.log(a)
a = 5
console.log(window.a);
var a = 20;
console.log(a)
}
foo();
23. 实现自适应正方形
<style media="screen">
/* 法一 */
.box{
/* 这里的 width 不可以写具体的像素,因为 padding-top 是相对于 .box 的父元素 */
width:50%;
padding-top:50%;
background-color: red;
}
/* 法二 */
.box2{
/* 这里的 width 是可以写具体的元素的,因为.margin 是想对于.box2的 */
width:50%;
overflow: hidden;
background-color: red;
}
.margin{
margin-top: 100%;
}
/* 法三 */
.box3{
width:50%;
height:50vw;
/* 这里还可以写 height: 50%; width: 50vh; */
background-color: red;
}
/* 法四 */
.box4{
/* 这里的 width 写像素和百分比都行,因为:after是相对于.box4来说的 */
width:100px;
overflow: hidden;
background-color: red;
}
.box4:after{
content: '';
display: block;
margin-top: 100%;
}
</style>
<body>
<div class="box"></div>
<div class="box2">
<div class="margin">
</div>
</div>
<div class="box3"></div>
<div class="box4"></div>
</body>
22. 实现一个累加函数的功能比如sum(1,2,3)(2).valueOf()//8
function sum(...args) {
args = [...args];
let cache = 0;
if (args.length === 0) {
throw new Error('参数不能为空');
}
else {
for (let i = 0; i < args.length; i++) {
cache += args[i];
}
}
let add = function(...args1) {
args1 = [...args1];
if (args1.length === 0) {
throw new Error('参数不能为空');
}
for (let i = 0; i < args1.length; i++) {
cache += args1[i];
}
return add;
}
add.valueOf = function() {
console.log(cache)
}
return add;
}
31. a.x = a = {n:2}
问题
var a = {n:1};
var b = a;
a.x = a = {n:2};
console.log(a.x);
console.log(b.x);
解析
重点的语句其实就是 a.x = a = {n: 2}
,这个语句很让人迷惑。
首先在这条语句中赋值的顺序是
a.x = // 这里的 a 还是 {n:1}
a = // 这里的 a 还是 {n:1}
{n:2} // 到这里后,a 就变成了 {n:2},也就是这时候 旧 的 a 已经被这个新的 a 替换掉了
执行完 a = {n: 2}
的时候,a.x =
这里的 仍然还是旧的 a ,也就是 {n: 1}
, 和新的 a 已经没有什么关系了
当执行完 a.x = {n:2}
的时候,会存在两个引用地址,新的 a 的地址和旧的 a 的地址
因为 b 引用了旧的 a 的引用地址,所以这时候
console.log(b.x) ;// {n: 2}
但是输出 a.x 就是 未定义了。
console.log(a.x); // undefined
为了验证是存在旧的a 和新的 a,
var a = {n: 1};
var b = a;
Object.freeze(a);
try {
a.x = a = {n: 2}
} catch (e) {
}
console.log(a.n); // 2 证明 a = {n: 2} 的赋值是成功的
console.log(a.x); // undefined 证明 a.x = {n: 2} 的赋值是在旧的 a 而不是在新的 a 上
console.log(b.n); // 1 证明 a = {n: 2} 的赋值是在旧的 a 上
console.log(b.x); // 如果没有Object.freeze(a) 这里就是 {n:2},如果有就是 undefined,证明 a.x = {n: 2} 的赋值是在旧的 a 而不是在新的 a 上
参考
实现数组的 map
这里的重点是,你自定义之后的传参,以及如何返回
- 第一个参数是
Array.prototype.myMap = function (callback, thisObj) {
const result = [];
this.forEach((...args) => {
const index = args[1];
result[index] = callback.apply(thisObj, args);
});
return result;
};
const arr = [1, 2, 3];
const res = arr.myMap((curr, id, array) => {
console.log(curr, id, array);
return curr + 1;
});
console.log(res);
通过 es 标准实现
// Based on ECMAScript spec https://262.ecma-international.org/6.0/#sec-array.prototype.map
Array.prototype.myMap = function(callback, ...args) {
let thisObject = Object(this)
if(!thisObject) {
throw new TypeError("this is null")
}
if(typeof callback !== "function"){
throw new TypeError(callback + " is not a function")
}
let length = thisObject.length, mapped = new Array(length)
let thisParameter = args.length ? args[0] : undefined
for(let i = 0; i < length; i++){
// Handle sparse array
if(i in thisObject){
// support this binding
mapped[i] = callback.call(thisParameter, thisObject[i], i, thisObject)
}
}
return mapped
}
汇总参考来源
15. getName 以及所有涉及到 prototype 的题
问题
function Foo() {
getName = function () { console.log (1); };
return this;
}
Foo.getName = function () { console.log (2);};
Foo.prototype.getName = function () { console.log (3);};
var getName = function () { console.log (4);};
function getName() { console.log (5);}
//请写出以下输出结果:
Foo.getName();
getName();
Foo().getName();
getName();
new Foo.getName();
new Foo().getName();
new new Foo().getName();
6. 函数执行的输出
function test(a, b) {
console.log(b)
return {
test: function(c){
return test(c, a);
}
};
}
var retA = test(0);
retA.test(2);
retA.test(4);
retA.test(8);
// 上面几个都好说,下面几个好好想想
var retB = test(0).test(2).test(4).test(8);
var retC = test('good').test('bad');
retC.test('good');
retC.test('bad');
retC.test('good');
38. 四个对角线同时生成气泡,每秒产生一个,然后往相反的对角线移动,到达了对角线自动消失
面滴滴的题,在页面中有四个点,每个点开始定时产生气泡,比如在 topLeft 产生一个气泡,然后开始移动到 bottomRight 的点,到达后自动消失。其他三个点同理。
初步实现方案如下
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<style>
html, body {
margin: 0;
padding: 0;
}
.topLeft {
position: absolute;
top: 0;
left: 0;
}
.topRight {
position: absolute;
top: 0;
right: 0;
}
.bottomLeft {
position: absolute;
bottom: 0;
left: 0;
}
.bottomRight {
position: absolute;
bottom: 0;
right: 0;
}
.bubble {
width: 50px;
height: 50px;
border-radius: 25px;
background: lightblue;
}
</style>
</head>
<body>
<div class="topLeft"></div>
<div class="topRight"></div>
<div class="bottomLeft"></div>
<div class="bottomRight"></div>
</body>
<script>
let winWidth = document.documentElement.clientWidth;
let winHeight = document.documentElement.clientHeight;
function getElem(cls) {
return document.querySelector(`.${cls}`);
}
/*
* step 是要计算的,也就是在规定的时间内,我们要计算多少个 step,这里设置了走 20 步
* */
function generateBubble(pos, count = 20) {
// 因为屏幕的宽高是不相等的,所以气泡在移动时候的 x 与 y 的每一步的距离都需要格外计算
let xStep = Math.ceil(winWidth / count);
let yStep = Math.ceil(winHeight / count);
// 要根据方位来生成 bubble,所以需要生成不同方位的 bubble
let bubble = document.createElement('div');
bubble.classList.add('bubble', pos);
if (pos === 'topLeft') {
bubble.style.transition = 'top .3s ease, left .3s ease';
// bubble 要开始 animation 了
let interval = setInterval(() => {
// 这里要开始计算当前元素是否到了最底下
let currentTop = +bubble.style.top.replace('px', '');
let currentLeft = +bubble.style.left.replace('px', '');
let bubbleWidth = 50;
let bubbleHeight = 50;
// 如果到了对角线的位置,那么就移除自身
// 因为定位都是各自的零点,所以只要比较移动的距离是否对于屏幕的宽高即可
if (
(currentTop + bubbleHeight >= winWidth)
|| (currentLeft + bubbleWidth >= winHeight)
) {
bubble.remove();
clearInterval(interval);
}
bubble.style.top = currentTop + yStep + 'px';
bubble.style.left = currentLeft + xStep + 'px';
}, 500);
}
// 以下三个同理
else if (pos === 'topRight') {
bubble.style.transition = 'top .3s ease, right .3s ease';
// bubble 要开始 animation 了
let interval = setInterval(() => {
// 这里要开始计算当前元素是否到了最底下
let currentTop = +bubble.style.top.replace('px', '');
let currentRight = +bubble.style.right.replace('px', '');
let bubbleWidth = 50;
let bubbleHeight = 50;
if (
(currentTop + bubbleHeight >= winWidth)
|| (currentRight + bubbleWidth >= winHeight)
) {
bubble.remove();
clearInterval(interval);
}
bubble.style.top = currentTop + yStep + 'px';
bubble.style.right = currentRight + xStep + 'px';
}, 500);
}
else if (pos === 'bottomLeft') {
bubble.style.transition = 'bottom .3s ease, left .3s ease';
// bubble 要开始 animation 了
let interval = setInterval(() => {
// 这里要开始计算当前元素是否到了最底下
let currentBottom = +bubble.style.bottom.replace('px', '');
let currentLeft = +bubble.style.left.replace('px', '');
let bubbleWidth = 50;
let bubbleHeight = 50;
if (
(currentBottom + bubbleHeight >= winWidth)
|| (currentLeft + bubbleWidth >= winHeight)
) {
bubble.remove();
clearInterval(interval);
}
bubble.style.bottom = currentBottom + yStep + 'px';
bubble.style.left = currentLeft + xStep + 'px';
}, 500);
}
else if (pos === 'bottomRight') {
bubble.style.transition = 'bottom .3s ease, right .3s ease';
// bubble 要开始 animation 了
let interval = setInterval(() => {
// 这里要开始计算当前元素是否到了最底下
let currentBottom = +bubble.style.bottom.replace('px', '');
let currentRight = +bubble.style.right.replace('px', '');
let bubbleWidth = 50;
let bubbleHeight = 50;
if (
(currentBottom + bubbleHeight >= winWidth)
|| (currentRight + bubbleWidth >= winHeight)
) {
bubble.remove();
clearInterval(interval);
}
bubble.style.bottom = currentBottom + yStep + 'px';
bubble.style.right = currentRight + xStep + 'px';
}, 500);
}
return bubble;
}
function start() {
let poses = {
topLeft: 'topLeft',
topRight: 'topRight',
bottomLeft: 'bottomLeft',
bottomRight: 'bottomRight'
};
for (const key in poses) {
// 获取到每个角的元素,这里自动生成其实也可以
let posElm = getElem(poses[key]);
// 生成气泡,气泡的动画其实也是在这里添加
let bubble = generateBubble(poses[key]);
// 把气泡放到各个角中
posElm.append(bubble);
}
}
setInterval(start, 1000);
</script>
</html>
33 实现一个模板字符串,能够匹配到提供的数据
实现一个 render 函数,第一个参数是模板,第二个参数是数据,将数据映射到模板中
render('你好,我们公司是{{ company }},我们属于{{group.name}}业务线,我们在招聘各种方向的人才,包括{{group.jobs[0]}}、{{group["jobs"][1] }}等。', {group: {name: '高德', jobs: ['前端', '后端']}, company: '阿里巴巴'});
14. 实现一个 LazyMan & 升级实现每个都能返回自己的 then (即asyncQueue)
问题
实现一个LazyMan,可以按照以下方式调用:
LazyMan("Hank")
// 输出:
Hi! This is Hank!
LazyMan("Hank").sleep(10).eat("dinner")
// 输出
Hi! This is Hank!
//等待10秒..
Wake up after 10
Eat dinner
LazyMan("Hank").eat(“dinner”).eat("supper")
//输出
Hi This is Hank!
Eat dinner
Eat supper
LazyMan("Hank").sleepFirst(5).eat("supper")
//输出
//等待5秒
Wake up after 5
Hi This is Hank!
Eat supper
解法
对于这个问题,首先创建一个任务队列,然后利用next()函数来控制任务的顺序执行:
核心是队列和事件执行
这里有两个点是要注意的,第一个是在_LazyMan 的时候需要执行next函数,但不能立即执行,而是需要放到 setTimeout 里面去,这样才不会导致在 sleepFirst 的时候,已经把fns中第一个函数执行完了,先执行同步任务,然后再执行异步任务,这样才会把sleepFirst 放到第一个fns 中第二个是在 eat/sleep 函数中,都是在 setTimeout 中执行 next 函数的
function _LazyMan(name) {
this.fns = [];
let self = this;
const fn = () => {
setTimeout(() => {
console.log('Hi! This is ' + name);
self.next();
});
}
this.fns.push(fn);
// 如果这里直接使用 self.next(),就无法将 sleepFirst 放到第一位来执行了
setTimeout(() => {
self.next();
}, 0)
}
// 使用 next 来执行下一次的函数
_LazyMan.prototype.next = function() {
let fn = this.fns.shift();
fn && fn();
}
_LazyMan.prototype.sleep = function(time) {
let self = this;
const fn = () => {
setTimeout(() => {
console.log('Wake up after ' + time);
self.next();
}, time * 1000);
}
this.fns.push(fn);
return this;
}
_LazyMan.prototype.eat = function(food) {
let self = this;
const fn = () => {
setTimeout(() => {
console.log('Eat ' + food);
self.next();
}, 0);
}
this.fns.push(fn);
return this;
}
// 看到 sleepFirst 就知道要建立队列的概念
_LazyMan.prototype.sleepFirst = function(time) {
let self = this;
const fn = () => {
setTimeout(() => {
console.log('Wake up after ' + time);
self.next();
}, time * 1000);
}
this.fns.unshift(fn);
return this;
}
function LazyMan(name) {
return new _LazyMan(name);
}
LazyMan('jack').sleep(2).eat('orange').sleepFirst(3)
参考
30. bind/call/apply 绑定相关题型
function foo() {
console.log(this.a);
}
var a = 2;
var o = {a: 3, foo: foo};
var p = {a: 4};
o.foo();
(p.foo = o.foo)(); // 2
在浏览器环境中(p.foo = o.foo)();
输出为2,也就是
其实(p.foo = o.foo)()
也就是 (bar = p.foo = o.foo)()
,其实也就是直接执行 foo 函数
实现 memo 函数
const func = (arg1, arg2) => {
return arg1 + arg2
}
const memoed = memo(func)
memoed(1, 2)
// 3, func is called
memoed(1, 2)
// 3 is returned right away without calling func
memoed(1, 3)
// 4, new arguments, so func is called
Default cache key could be just Array.from(arguments).join('_')
17. JS 所有隐式转换题
第一题
let a = [];
let b = "0";
console.log(a == 0);
console.log(a == !a);
console.log(b == 0);
console.log(a == b);
解答:
console.log(a == 0); // true
console.log(a == !a); // true
console.log(b == 0); // true
console.log(a == b); // false
其中a == !a
这个真的很牛逼了,首先在比较的时候,会转换成原始类型来比较,所以[].toString()
为空字符串,而![]
为false,所以空字符串和 false
确实是相等的。
a == b
这个为false
也是同理,首先a转换成空字符串,"" 和 "0" 确实是不相等的。所以就会有 a==0
为true
,b==0
也为true
,但是 a==b
确实false
的情况。
根据 html string 获取所有的 a 标签
extract(`
<div>
<a>link1< / a><a href="https://bfe.dev">link1< / a>
<div<abbr>bfe</abbr>div>
<div>
<abbr>bfe</abbr><a href="https://bfe.dev" class="link2"> <abbr>bfe</abbr> <span class="l">l</span><span class="i">i</span> nk2 </a>
</div>
</div>
`)
// [
// '<a>link1< / a>',
// '<a href="https://bfe.dev">link1< / a>',
// '<a href="https://bfe.dev" class="link2"> <abbr>bfe</abbr> <span class="l">l</span><span //class="i">i</span> nk2 </a>'
//]
解答
/**
* @param {string} str
* @return {string[]}
*/
function extract(str) {
// your code here
let a = new DOMParser();
let c = a.parseFromString(str, "text/html");
let d = document.createTreeWalker(c, NodeFilter.SHOW_ELEMENT)
let curr = d.currentNode;
let res = [];
while(curr){
if(curr.nodeName === "A") res.push(curr.outerHTML);
curr = d.nextNode()
}
return res;
}
这里同样可以获取到所有的节点,使用 d.nextNode() 可以不断获取到整个 DOM 结构所有的节点
当然,还可以使用正则的方式来解决这种问题
function extract(str) {
return str.match(/<a(\s[^>]*)?>.*?<\s*\/\s*a>/g) || [];
}
25. 实现对象扁平化
{
"a": {
"b": {
"c": {
"d": 1
}
}
},
"aa": 2,
"c": [
1,
2
]
}
//输出 { 'a.b.c.d': 1, aa: 2, 'c[0]': 1, 'c[1]': 2 }
解法
let str = '';
let o = {};
function objFlatten (obj) {
// 只要遍历一次 obj 就行
Object.keys(obj).map(item => {
if (Object.prototype.toString.call(obj[item]) === '[object Object]') {
str += item + '.';
// 如果当前值是对象,继续往内层遍历
objFlatten(obj[item]);
} else if (Object.prototype.toString.call(obj[item]) === '[object Array]') {
obj[item].forEach((ele, index) => o[item+`[${index}]`] = ele);
} else {
str += item;
o[str] = obj[item];
str = '';
}
})
}
优化一下
function flattenObj(obj) {
let str = '';
let res = {};
function getType(data) {
return Object.prototype.toString.call(data).replace(/\[object\s+(\w+)\]/, '$1');
}
const flatten = (obj) => {
Object.keys(obj).map(item => {
if (getType(obj[item]) === 'Object') {
str += item + '.';
// 如果当前值是对象,继续往内层遍历
flatten(obj[item]);
} else if (getType(obj[item]) === 'Array') {
// 这里还可以考虑一下多维数组,加深一下难度
obj[item].forEach((ele, index) => res[item+`[${index}]`] = ele);
} else {
str += item;
res[str] = obj[item];
str = '';
}
})
};
flatten(obj)
return res;
}
32. 深复制/浅复制
浅拷贝: 仅仅是指向被复制的内存地址,如果原地址发生改变,那么浅复制出来的对象也会相应的改变。
创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。
深拷贝: 在计算机中开辟一块 新的内存地址 用于存放复制的对象。
将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象
深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用。
浅拷贝实现方法:
- Object.assign
- Object.assign是一个浅拷贝,它只是在根属性(对象的第一层级)创建了一个新的对象,但是对于属性的值是仍是对象的话依然是浅拷贝
- 不会拷贝对象继承的属性
- 不可枚举的属性
- 可以拷贝Symbol类型
- 扩展运算符、slice、concat
- 和assgin一样只拷贝一层
深拷贝实现方法:
- 循环+递归
- 只能实现object、array的深拷贝
- for...in 无法获得 Symbol 类型的键,而 Reflect 可以获取
- JSON.stringify
- 拷贝的对象的值中如果有函数,undefined,symbol则经过JSON.stringify()序列化后的JSON字符串中这个键值对会消失
- 无法拷贝不可枚举的属性, 无法拷贝对象的原型链
- 拷贝Date引用类型会变成字符串
- 拷贝RegExp引用类型会变成空对象
- 对象中含有NaN、 Infinity和 - Infinity, 则序列化的结果会变成null
- 无法拷贝对象的循环应用(即obj[key] = obj)
13. 防抖和节流
防抖
之前一直没有搞清楚防抖的含义,只是知道防抖要用在输入的时候,减少每次输入都进行请求的问题
今天晚上才搞明白
输入的时候会建立一次请求,这个请求还不会发送,这时候等一个时间,如果这个时间内还有请求发送过来了,那就取消掉之前建立的请求,根据现在新有的数据,建立一次新的请求。
首先写一个非常简单的防抖函数
function debounce(fn, wait) {
var timeout = null;
return function() {
if (timeout != null) clearTimeout(timeout);
timeout = setTimeout(fn, wait);
}
}
2. 实现 Promise.retry,成功后 resolve 结果,失败后重试,尝试超过一定次数才真正的 reject
实现一个 retry
函数,第一个参数是需要执行的函数,第二个参数是执行失败之后,隔多久开始重试,但三个参数是重试的次数
如果执行完 times 次后还是执行失败,就返回失败,需要返回一个 Promise 对象
function retry(fn, delay, times) {
return new Promise((resolve, reject) => {
let timeout;
function attempt() {
fn().then(data => {
resolve(data);
}).catch(e => {
if (times > 0) {
times--;
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(() => {
attempt();
}, delay);
} else {
clearTimeout(timeout);
reject(e);
}
});
}
attempt();
});
}
进行测试
function getData() {
return new Promise((resolve, reject) => {
let num = Math.ceil(Math.random() * 10); //生成1-10的随机数
console.log('随机数生成的值:', num)
if(num < 2){
console.log('符合条件,值为' + num)
resolve(num);
}
else{
reject('数字大于2,执行失败');
}
});
};
retry(getData, 1000, 4)
.then(data => {
console.log(data);
})
.catch(e => {
console.log(e);
})
实现 react 的 useState 逻辑
一般面试中,不会要你实现整个流程,因为 useState 和整个 react 联系的比较紧密,都是让你实现以下的功能
const [name, setName] = useState('jack')
const [age, setAge] = useState(23)
console.log(name, age)
setName('jon')
console.log(name, age)
重点
- 使用了
const [name, setName] = useState('jack')
之后,通过 setName 是会触发重新渲染的,也就是第二次渲染的时候,获取到 name 的值是你通过setName
之后的值
实现
// 参考https://juejin.cn/post/6844903975838285838
import React from "https://cdn.skypack.dev/[email protected]";
import ReactDOM from "https://cdn.skypack.dev/[email protected]";
// useState原理,很简单,就是利用必包返回一个setState,主动render
// let state
// function useState(initialState) {
// state = state || initialState;
// function setState(newState) {
// state = newState;
// render();
// }
// return [state, setState];
// }
// 当有多个state怎么处理呢,我们的状态从全局变量state升级为全局数组
// 第一次渲染时候,根据 useState 顺序,逐个声明 state 并且将其放入全局 Array 中。每次声明 state,都要将 cursor 增加 1。
// 更新 state,触发再次渲染的时候,cursor 被重置为 0。按照 useState 的声明顺序,依次拿出最新的 state 的值,视图更新。
// 1. 可以知道为什么不能在循环、判断内部使用 Hook;
// 2. 可知道useState初始值只有第一次生效,即使initialState值发生变化,也不会影响State
const states = [];
let cursor = 0;
function useState(initialState){
const currenCursor = cursor;
states[currenCursor] = states[currenCursor] || initialState;
function setState(newState) {
states[currenCursor] = newState;
render();
}
++cursor;
return [states[currenCursor], setState];
}
let tag = true;
function App() {
const [num, setNum] = useState([0,1]);
console.log(num,'iii')
// 会出现错乱
// if (tag) {
// const [unusedNum] = useState(1);
// tag = false;
// }
const [num2, setNum2] = useState(0);
return (
<div>
<div>num: {num}</div>
<div>
<button onClick={() => {num.push(1);setNum(num)}}>加 1</button>
</div>
<hr />
<div>num2: {num2}</div>
<div>
<button onClick={() => setNum2(num2+2)}>加2</button>
</div>
</div>
);
}
function render() {
ReactDOM.render(<App />, document.getElementById("root"));
cursor = 0; // 重置cursor
}
render(); // 首次渲染
真正的要看 react 实现的 useState,可以看 build-your-own-react/
27. node 中的event loop 题
setInterval(() => {
console.log('setInterval')
}, 100)
process.nextTick(function tick () {
process.nextTick(tick)
})
执行上面的代码,输出什么
解
运行结果:setInterval 永远不会打印出来。
解释:process.nextTick 会无限循环,将 event loop 阻塞在 microtask 阶段,导致 event loop 上其他 macrotask 阶段的回调函数没有机会执行。
解决方法通常是用 setImmediate 替代 process.nextTick,如下:
setInterval(() => {
console.log('setInterval')
}, 100)
setImmediate(function immediate () {
setImmediate(immediate)
})
运行结果:每 100ms 打印一次 setInterval。
解释:process.nextTick 内执行 process.nextTick 仍然将 tick 函数注册到当前 microtask 的尾部,所以导致 microtask 永远执行不完; setImmediate 内执行 setImmediate 会将 immediate 函数注册到下一次 event loop 的 check 阶段,而不是当前正在执行的 check 阶段,所以给了 event loop 上其他 macrotask 执行的机会。
4. 求最终 left、right 的宽度
问题:
<div class="container">
<div class="left"></div>
<div class="right"></div>
</div>
<style>
* {
padding: 0;
margin: 0;
}
.container {
width: 600px;
height: 300px;
display: flex;
}
.left {
flex: 1 2 500px;
background: red;
}
.right {
flex: 2 1 400px;
background: blue;
}
</style>
解法:
子元素的 flex-shrink 的值分别为 2,1
溢出:500+400 - 600 = 300。
总权重为 2 * 500+ 1 * 400 = 1400
两个元素分别收缩:
// 就是溢出的 300 乘以 left 在 shrink 之后(2 * 500)占据总的 (1400) 的比例,就是 left 需要减去的
300 * 2(flex-shrink) * 500(width) / 1400= 214.28
300 * 1(flex-shrink) * 400(width) / 1400= 85.72
三个元素的最终宽度分别为:
500 - 214.28 = 285.72
400 - 85.72 = 314.28
附加
如果container的宽度是1000px, left 和 right 的宽度是怎么计算的?
子元素的 flex-grow的值分别为 1,2
剩余空间:1000 - 500+400= 100。
子元素所得到的多余空间分别为:
100 * 1 / 3= 33.33
100 * 2 / 3 = 66.67
子元素最终宽度分别为:
500 + 33.33 = 533.33
400 + 66.67 =466.67
附加的是通过 flex-grow 来计算,因为 container 超出了 left 和 right 的总宽度了
参考
实现lodash 相关函数 _.chunk() _.get()
chunk([1,2,3,4,5], 1)
// [[1], [2], [3], [4], [5]]
chunk([1,2,3,4,5], 2)
// [[1, 2], [3, 4], [5]]
chunk([1,2,3,4,5], 3)
// [[1, 2, 3], [4, 5]]
chunk([1,2,3,4,5], 4)
// [[1, 2, 3, 4], [5]]
chunk([1,2,3,4,5], 5)
// [[1, 2, 3, 4, 5]]
解法,来源 implement-lodash-chunk
/**
* @param {any[]} items
* @param {number} size
* @returns {any[][]}
*/
function chunk(items, size) {
if (size === 0) return [];
const res = [];
for (let i=0; i<items.length; i=i+size) {
res.push(items.slice(i, i+size));
}
return res;
}
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.