使用一个 api 我们要了解它到底做了什么才可以实现我们想要的功能,不仅要会用还要知道为什么
# 实现一个 new 操作符
函数执行前面加个
new
做了哪些事情,为啥就会构造一个对象出来
- 创建了一个新对象
- 链接到了原型
- 执行函数,绑定了 this 指向新创建的对象上
- 返回一个对象,如果函数中有
return
关键字,看 return 了什么出来,如果是一个对象,那么返回这个对象,如果不是则返回我们新建的这个对象 - 实现过程如下:
function New() {
// 创建了一个新对象
const obj = {};
// 取得构造函数
const F = [].shift.call(arguments);
// 链接到了原型
obj.__proto__ = F.prototype;
// 绑定this,执行构造函数
const result = F.apply(obj, arguments);
// 看看构造函数返回了什么
if (typeof result !== null && (typeof result === 'object' || typeof result === 'function')) {
return result;
}
return obj;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# instanceof 实现
实现了一个
new
是不是也得判断一下,那我们来实现一个instanceof
instanceof
都做了什么事?instanceof
是拿着左边实例的__proto__
与右边构造函数的prototype
进行对比的- 尝试着实现下
function instance(L, R) {
if (L.__proto__ === null) {
return false;
}
if (L.__proto__ === R.prototype) {
return true;
}
return instance(L.__proto__, R);
}
instance(p, Function); // false
instance(Function, Object); // true >> Function.__proto__.__proto__ === Object.prototype
2
3
4
5
6
7
8
9
10
11
还有种写法是直接把L.__proto__
直接赋值为 L.__proto__.__proto__
,但是这样写会影响到原型,导致如果查询不到则会抛出异常,不推荐下面的写法
function instance(L, R) {
while (true) {
if (L.__proto__ === null) {
return false;
}
if (L.__proto__ === R.prototype) {
return true;
}
L.__proto__ = L.__proto__.__proto__;
}
}
2
3
4
5
6
7
8
9
10
11
# call 和 apply
call 和 apply
我们都知道 js
的函数中 this
是动态的,不同执行上下文会导致 this
执行不同的地方,总得来说 this
执行有四种情况
- 函数自执行,
this
执行指向window
- 谁打点调用函数,
this
指向这个谁
,也就是 . 前面的那个对象 call
和apply
的硬绑定this
- 函数加
new
关键字后,this
执行该构造函数的实例
那么第三条第四条规则为什么就会改变 this
执行,其实说白了函数中 this
应该都遵循 1、2
两条规则,3、4
其实都是在底层实现了,让其 this
执行了我们想要指向的地方,比如说 new
关键字,我们在本文的第一个 api
中就讲了他的实现,他是利用 apply
或者 call
来绑定上去的,这里我们来讲下 call
和 apply
的实现
# 实现 call 和 apply
call
和apply
两者很像除了传递的参数不同,一个是传递的是值,一个传递的是一个数组
// call
Function.prototype.call2 = function(context = window) {
// example:fnA.call(obj, 1)
context.fn = this;
const args = [...arguments].slice(1);
// 执行 context.fn(...args) 此时就相当于 obj.fnA(1)
const result = context.fn(...args);
delete context.fn;
return result;
};
// apply
Function.prototype.apply2 = function(context = window) {
context.fn = this;
let result;
// 看是否有第二个参数,也可以不传参数
if (arguments[1]) {
// 因为传递过来的是一个数组,所以要解构一下
result = context.fn(...arguments[1]);
} else {
result = context.fn();
}
delete context.fn;
return result;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
解释 call 实现原理
举个栗子: fnA.call(obj, 1)
call2 函数 第一个参数是要绑定的对象(obj)
我们根据谁打点调用函数 this
执行谁,我们在这个对象中新增一个属性 fn
,给它赋值为此时的 this
那么就相当于 给我们传进来的 obj
新增一个属性 fn
,让他等于这个 this
因为 call2
是定义在函数的原型的对象上的,那么此时这个 this 就是 调用 call2
方法函数的实例,也就是 fnA
也就是说 context.fn
就相当于 给 obj
新增了一个属性 fn(fnA)
然后 obj.fn
执行了,那么谁打点调用 this
执行谁,此时 this
指向了 这个 obj
这就是 call
方法实现的基本思路
注:字体加粗并且有删除的 this 是 在 call 函数中的 this;有背景底色的 this
指的是我们绑定后的 this
# 一道有趣的面试题
曾看到这么一道面试题:
const arrayLike = {};
[].push.call(arrayLike, 1);
console.log(arrayLike); // { 0: 1, length: 1 }
// 接下来我们改成这样
const call = [].push.call;
call(arrayLike, 1);
console.log(arrayLike);
// 此时会打印什么?
// 答案是会报错,call is not a function
// 为什么?给自己一个思考问题的机会吧
2
3
4
5
6
7
8
9
10
# bind
# bind 用法
bind 用法和 call 很类似,但是 bind 不会立即执行函数,而是返回一个绑定了 this 的新函数
const obj = { name: 'cym' };
function fn(age) {
console.log(this.name + '今年' + age + '岁了');
}
// 如上代码,我们要让 this 指向 obj
const bindFn = fn.bind(obj);
bindFn(24); // cym今年24岁了
2
3
4
5
6
7
# 基本功能的实现
根据上面的用法,我们不难 bind
的方法不仅可以绑定 this
还可以绑定参数,我们来简单实现一下
Function.prototype.bind2 = function(ctx = globalThis) {
// 取到我们要绑定的参数
const args = [...arguments].slice(1);
// 缓存 this,因为返回一个函数 this 就会变成新的函数
const that = this;
// 返回一个函数
return function() {
// 返回函数里面的 arguments 是返回函数传入的参数哦,别搞混了
that.apply(ctx, args.concat([...arguments]));
};
};
2
3
4
5
6
7
8
9
10
11
# 返回函数作为构造函数
bind
方法的实现其实蛮有意思的,因为 bind
方法返回一个函数,那么返回的这个函数如果被当做构造函数怎么办
const obj = { name: 'cym' };
function fn() {
console.log(this);
}
// 如上代码,我们要让 this 指向 obj
const bindFn = fn.bind(obj);
const instance = new bindFn(24); // fn {}
2
3
4
5
6
7
根据上面的代码返回结果来看,我们发现当绑定的函数作为构造函数来用的话,this
指向了原来的函数的实例,那么我们来实现一下完整的 bind
方法
Function.prototype.bind2 = function(ctx = globalThis) {
// 取得参数
const args = [...arguments].slice(1);
// 取得函数
const that = this;
// 要返回一个函数,还要判断是否有进行实例化的操作
function Fn() {
const allArgs = args.concat([...arguments]);
// 如果被实例化了
if (this instanceof Fn) {
that.apply(this, allArgs);
} else {
that.apply(ctx, allArgs);
}
}
// 但是我们需要保证原型不能丢失,还得是原来函数的实例
// 这种写法可能不雅观,因为直接让两个原型指向了同一个地址,一般情况下我们会使用一个临时性构造函数来处理一下
// Fn.prototype = this.prototype
Fn.prototype = Object.create(this.prototype);
// 返回这个绑定好 this 的函数
return Fn;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
来看下用法
const obj = { name: 'cym' };
function fn() {
console.log(this);
}
// 如上代码,我们要让 this 指向 obj
const bindFn = fn.bind2(obj);
const instance = new bindFn(); // fn {}
bindFn(); // {name: 'cym'}
2
3
4
5
6
7
8
# 柯利化
柯利化的核心是:只传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数
比如说实现一个 add 函数
const addFn = (a, b, c, d, e) => {
return a + b + c + d + e;
};
const add = curry(addFn);
add(1)(2)(3)(4, 5); // 15
add(1)(2)(3, 4, 5); // 15
add(1, 2, 3)(4, 5); // 15
2
3
4
5
6
7
面试要求就是实现这么一个函数
function curry(fn, ...args) {
// 如果参数大于等于了要改变函数的参数了,那么直接执行就可以了
if (args.length >= fn.length) {
return fn(...args);
}
// 否则就返回一个函数,函数把所有参数都累积到一起
return function(...args2) {
return curry(fn, ...args, ...args2);
};
}
2
3
4
5
6
7
8
9
10
# Number.isNaN
NaN
是一个特殊值,他和自身不相等,是一个非自反值(自反,reflexive,即 x === x 不成立)的值。但是 NaN != NaN
为 true
// 根据此特性我们可以实现一下 Number.isNaN
if (!Number.isNaN) {
Number.isNaN = function(n) {
return n !== n;
};
}
// 也可以使用window.isNaN来实现
if (!Number.isNaN) {
Number.isNaN = function(n) {
// window.isNaN(n) 不判断数据类型
return typeof n === 'number' && window.isNaN(n);
};
}
2
3
4
5
6
7
8
9
10
11
12
13
对了,在 JavaScript
中 1 / 0
返回的不是 NaN
而是 Infinity
,但是 Infinity / Infinity
返回 NaN
# Object.is
ES6 新增了一个工具方法,判断两个值是否绝对相等,可以用来处理 -0
的情况,因为 -0 === 0
// Object.is 的实现
if (!Object.is) {
Object.is = function(v1, v2) {
// 判断是否为 -0,因为-0 === 0
if (v1 === 0 && v2 === 0) {
// 因为 1 / 0 === Infinity,1 / -0 === -Infinity
return 1 / v1 === 1 / v2;
}
// 判断是否是 NaN
if (v1 !== v1) {
return v2 !== v2;
}
// 其他情况
return v1 === v2;
};
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Object.is 主要用来处理一些特殊情况的,所以效率并不是很高,能使用 ==
或 ===
尽量使用。
# 防抖和节流
scroll 事件本身会触发页面的重新渲染,同时 scroll 事件的 handler 又会被高频度的触发, 因此事件的 handler 内部不应该有复杂操作,例如 DOM 操作就不应该放在事件处理中。针对此类高频度触发事件问题(例如页面 scroll ,屏幕 resize,监听用户输入等),有两种常用的解决方法,防抖和节流。
# 防抖
每次触发高频事件都取消上次的延时操作
function debounce(fn, delay) {
let timer = null;
return function() {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
};
}
2
3
4
5
6
7
8
9
# 节流
每次执行函数先判断是否有还在等待执行的函数,若没有则执行
function throttle(fn, delay) {
let canRun = true;
return function() {
if (!canRun) return;
canRun = false;
setTimeout(() => {
fn.apply(this, arguments);
canRun = true;
}, delay);
};
}
// 测试
var a = 0;
setInterval(
throttle(() => {
a++;
console.log(a);
}, 2000),
500
);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# mixins 实现
function mixins() {
const target = [].shift.call(arguments, 1);
const args = arguments;
for (let i = 0, len = args.length; i < len; i++) {
if ([].toString.call(args[i]) !== '[object Object]') throw 'The argument must be an object';
for (let key in args[i]) {
if (!(key in target)) {
target[key] = args[i][key];
}
}
}
return target;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 深浅拷贝
深拷贝问题一直是面试过程中被问到频率特别高的问题
拷贝分两种,浅拷贝和深拷贝,分别来实现一下
工作中遇到深拷贝的问题的话,我们一般会选择 lodash
库中的 deepClone
来处理
# 浅拷贝
浅拷贝很简单只要第一层地址不一样便可以
// 可以直接使用 Es6 的 rest 语法实现
function copy(target) {
return { ...target };
}
// 也可以使用 for in 实现
function copy(target) {
const result = {};
for (let key in target) {
result[key] = target[key];
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
# 深拷贝
深拷贝要求所有引用类型的地址都不是一个地址都是复制的值,那可以考虑使用递归来实现
function deepClone(target) {
if (typeof target !== 'object') return target;
const result = Array.isArray(target) ? [] : {};
for (let key in target) {
result[key] = deepClone(target[key]);
}
return result;
}
2
3
4
5
6
7
8
# 循环引用
但是上面的方法如果对象中出现循环引用了,那么就不能用了,需要单独考虑,考虑以下对象
const obj = {
name: 'cym',
age: 25,
home: { name: '北京' },
hobbies: ['抽烟', '喝酒', '打游戏'],
sayHi: () => 'Hi',
};
// 循环引用
obj.obj = obj;
// 可以使用 Map 对象对一层比较即可处理这个问题
function clone(target, map = new Map()) {
if (typeof target !== 'object') return target;
if (map.get(target)) return map.get(target);
const result = Array.isArray(target) ? [] : {};
map.set(target, result);
for (let key in target) {
result[key] = clone(target[key], map);
}
return result;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Emitter
node
事件流基本是基于发布订阅模式来实现的监听的(观察者和发布订阅介绍,看这里),尝试着实现下- 里面有几个常用的方法,一个订阅事件,一个发布事件,一旦发布事件触发订阅者执行相应的回调
function Emitter() {
// 事件对象,用来存储各种类型的事假
this.handlers = {};
}
Emitter.prototype = {
// 订阅事件,事件类型,事件回调
on: function(type, handler) {
if (!(this.handlers[type] in this.handlers)) {
// 事件队列
this.handlers[type] = [];
}
this.handlers[type].push(handler);
},
// 发布事件,订阅者执行相应的回调
emit: function(type) {
if (!this.handlers[type]) return;
const args = [].slice.call(arguments, 1);
this.handlers[type].forEach(handler => {
handler.apply(this, args);
});
},
off: function(type, handler) {
const handlers = this.handlers[type];
if (!handlers) return undefined;
let result = undefined;
for (let i = 0, len = handlers.length; i <= len; i++) {
if (handlers[i] === handler) {
result = handlers.splice(i, 1);
}
}
return result;
},
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
- 虽然实现的一些基本的 api 都是用底层语言来实现,但是用
Es6
来实现Emitter
很好看,接下来用Es6
来实现一下,而且好理解,还是Es6
好看
class Emitter {
constructor() {
this.handlers = {};
}
on(type, handler) {
if (!(this.handlers[type] in this.handlers)) {
this.handlers[type] = [];
}
this.handlers[type].push(handler);
}
emit(type) {
if (!this.handlers[type]) return;
const args = [...arguments].slice(1);
this.handlers[type].forEach(handler => {
// 执行函数
handler.apply(this, args);
});
}
off(type, handler) {
if (!this.handlers[type]) return void 0;
return this.handlers[type].find((fn, idx) => {
if (fn === handler) {
return this.handlers[type].splice(idx, 1);
} else {
return void 0;
}
});
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
- 持续更新中....