👇 内容速览 👇
手动实现call
/apply
/bind
实现深拷贝函数
基于ES5
/ES6
实现双向绑定
instanceof
原理与实现
实现支持绑定、解绑和派发的事件类
手动撸个 call/apply/bind 实现 call 来看下call
的原生表现形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 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 11 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 30 31 32 33 34 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 32 33 34 35 36 37 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 33 34 35 36 37 38 39 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 40 41 42 43 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 37 38 39 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 。这显然是错误的做法。
而上面所说的深拷贝函数就解决了这个问题。
基于 ES5/ES6 实现“双向绑定” 要想实现,就要先看看什么是“双向数据绑定”,它和“单向数据绑定”有什么区别?这样才能知道要实现什么效果嘛。
双向绑定 :视图(View)的变化能实时让数据模型(Model)发生变化,而数据的变化也能实时更新到视图层。
单向数据绑定 :只有从数据到视图这一方向的关系。
ES5 的 Object.defineProperty 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 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" /> <meta name ="viewport" content ="width=device-width, initial-scale=1.0" /> <meta http-equiv ="X-UA-Compatible" content ="ie=edge" /> <title > Document</title > <script > const obj = { value: "" }; function onKeyUp (event ) { obj.value = event.target.value; } Object .defineProperty(obj, "value" , { get: function ( ) { return value; }, set: function (newValue ) { value = newValue; document .querySelector("#value" ).innerHTML = newValue; document .querySelector("input" ).value = newValue; } }); </script > </head > <body > <p > 值是:<span id ="value" > </span > </p > <input type ="text" onkeyup ="onKeyUp(event)" /> </body > </html >
ES6 的 Proxy 随着,vue3.0 放弃支持了 IE 浏览器。而且Proxy
兼容性越来越好,能支持 13 种劫持操作。
因此,vue3.0 选择使用Proxy
来实现双向数据绑定,而不再使用Object.defineProperty
。
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 <!DOCTYPE html> <html lang="en" > <head> <meta charset="UTF-8" > <meta name="viewport" content="width=device-width, initial-scale=1.0" > <meta http-equiv="X-UA-Compatible" content="ie=edge" > <title>Document</title> <script> const obj = {} const newObj = new Proxy (obj, { get: function (target, key, receiver ) { return Reflect .get(target, key, receiver) }, set: function (target, key, value, receiver ) { if (key === 'value' ) { document .querySelector('#value' ).innerHTML = value document .querySelector('input' ).value = value } return Reflect .set(target, key, value, receiver) } }) function onKeyUp (event ) { newObj.value = event.target.value } </script> </head> <body> <p> 值是:<span id="value" ></span> </p> <input type="text" onkeyup="onKeyUp(event)" > </body> </html>
instanceof 原理与实现 instanceof
是通过原型链来进行判断的,所以只要不断地通过访问__proto__
,就可以拿到构造函数的原型prototype
。直到null
停止。
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 instanceof2 (left, right ) { let prototype = right.prototype; left = left.__proto__; while (1 ) { if (left === null || left === undefined ) { return false ; } if (left === prototype) { return true ; } left = left.__proto__; } } console .log(instanceof2([], Array )); function Test ( ) {}let test = new Test();console .log(instanceof2(test, Test));
实现支持绑定、解绑和派发的事件类 实现思路 :这里涉及了“订阅/发布模式”的相关知识。参考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 52 53 54 55 56 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" );