call、apply、bind实现
感谢 @purain 的指正,call 和 apply 更完美的实现要借助 Symbol。具体请看Issue
实现 call
来看下call
的原生表现形式:
1 2 3 4 5 6 7 8 9 10 11 12
| function test(arg1, arg2) { console.log(arg1, arg2); console.log(this.a, this.b); } test.call( { a: "a", b: "b" }, 1, 2 );
|
好了,开始手动实现我们的call2
。在实现的过程有个关键:
如果一个函数作为一个对象的属性,那么通过对象的**_*_.
_****运算符调用此函数,***this
**_就是此对象
1 2 3 4 5 6 7 8 9 10
| let obj = { a: "a", b: "b", test: function(arg1, arg2) { console.log(arg1, arg2); console.log(this.a, this.b); } }; obj.test(1, 2);
|
知道了实现关键,下面就是我们模拟的call
:
1 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
| Function.prototype.call2 = function(context) { if (typeof this !== "function") { throw new TypeError("Error"); } context = context || window; const { fn } = context; context.fn = this; const args = [...arguments].slice(1); const result = context.fn(...args); context.fn = fn; return result; };
function test(arg1, arg2) { console.log(arg1, arg2); console.log(this.a, this.b); } test.call2( { a: "a", b: "b" }, 1, 2 );
|
实现 apply
apply
和call
实现类似,只是传入的参数形式是数组形式,而不是逗号分隔的参数序列。
因此,借助 es6 提供的...
运算符,就可以很方便的实现数组和参数序列的转化。
1 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
| Function.prototype.apply2 = function(context) { if (typeof this !== "function") { throw new TypeError("Error"); } context = context || window; const { fn } = context; context.fn = this; let result; if (Array.isArray(arguments[1])) { result = context.fn(...arguments[1]); } else { result = context.fn(); } context.fn = fn; return result; };
function test(arg1, arg2) { console.log(arg1, arg2); console.log(this.a, this.b); } test.apply2( { a: "a", b: "b" }, [1, 2] );
|
实现 bind
bind
的实现有点意思,它有两个特点:
- 本身返回一个新的函数,所以要考虑
new
的情况
- 可以“保留”参数,内部实现了参数的拼接
1 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
| Function.prototype.bind2 = function(context) { if (typeof this !== "function") { throw new TypeError("Error"); } const that = this; const args = [...arguments].slice(1); return function F() { if (this instanceof F) { return new that(...args, ...arguments); } return that.apply(context, args.concat(...arguments)); }; };
function test(arg1, arg2) { console.log(arg1, arg2); console.log(this.a, this.b); } const test2 = test.bind2( { a: "a", b: "b" }, 1 ); test2(2);
|
深拷贝对象
实现一个对象的深拷贝函数,需要考虑对象的元素类型以及对应的解决方案:
- 基础类型:这种最简单,直接赋值即可
- 对象类型:递归调用拷贝函数
- 数组类型:这种最难,因为数组中的元素可能是基础类型、对象还可能数组,因此要专门做一个函数来处理数组的深拷贝
1 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 34 35 36 37 38 39
|
function cloneArr(src, target) { for (let item of src) { if (Array.isArray(item)) { target.push(cloneArr(item, [])); } else if (typeof item === "object") { target.push(deepClone(item, {})); } else { target.push(item); } } return target; }
function deepClone(src, target) { const keys = Reflect.ownKeys(src); let value = null; for (let key of keys) { value = src[key]; if (Array.isArray(value)) { target[key] = cloneArr(value, []); } else if (typeof value === "object") { target[key] = deepClone(value, {}); } else { target[key] = value; } } return target; }
|
这段代码是不是比网上看到的多了很多?因为考虑很周全,请看下面的测试用例:
1 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 34 35 36
| let a = { age: 1, jobs: { first: "FE" }, schools: [ { name: "shenda" }, { name: "shiyan" } ], arr: [ [ { value: "1" } ], [ { value: "2" } ] ] }; let b = {}; deepClone(a, b); a.jobs.first = "native"; a.schools[0].name = "SZU"; a.arr[0][0].value = "100"; console.log(a.jobs.first, b.jobs.first); console.log(a.schools[0], b.schools[0]); console.log(a.arr[0][0].value, b.arr[0][0].value); console.log(Array.isArray(a.arr[0]));
|
看到测试用例,应该会有人奇怪为什么最后要输出Array.isArray(a.arr[0])
。这主要是因为网上很多实现方法没有针对 array 做处理,直接将其当成 object,这样拷贝后虽然值没问题,但是 array 的元素会被转化为 object。这显然是错误的做法。
而上面所说的深拷贝函数就解决了这个问题。
浅拷贝实现
实现思路:遍历对象的属性,直接拷贝即可。
注意事项:
- 只处理对象
- 对于数组,结合JS特性特殊处理。否则数组返回的是
{}
,就不对了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function shalloCopy(obj) { if (typeof obj !== 'object') { return; } const newObj = Array.isArray(obj) ? [] : {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; }
|
Event事件类实现
实现思路:这里涉及了“订阅/发布模式”的相关知识。参考addEventListener(type, func)
和removeEventListener(type, func)
的具体效果来实现即可。
1 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 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
class Event { constructor() { this._cache = {}; } on(type, callback) { this._cache[type] = this._cache[type] || []; let fns = this._cache[type]; if (fns.indexOf(callback) === -1) { fns.push(callback); } return this; } trigger(type, ...data) { let fns = this._cache[type]; if (Array.isArray(fns)) { fns.forEach(fn => { fn(...data); }); } return this; } off(type, callback) { let fns = this._cache[type]; if (Array.isArray(fns)) { if (callback) { let index = fns.indexOf(callback); if (index !== -1) { fns.splice(index, 1); } } else { fns = []; } } return this; } }
const event = new Event(); event .on("test", a => { console.log(a); }) .trigger("test", "hello");
|