计划
?会整理一系列js知识,每周按空闲时间长短至少两篇,按照你不知道的js的内容排版顺序跟着写,目的扎实基础。写一篇文章会查阅参考大量有关内容,笔者写得安心,读者看得放心。此处轮子代码和思路来源于@冴羽大大。 (咦,mac自带输入法打不出冴ya这个字。)
Function.prototype.call()
引用MDN关于此方法的一些描述:
- 语法: fun.call(thisArg, arg1, arg2, ...)。 参数含义:thisArg: 在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于non-strict mode(非严格模式),则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。arg1, arg2,...:指定的参数列表
- 允许为不同的对象分配和调用属于一个对象的函数/方法。 提供新的 this 值给当前调用的函数/方法。你可以使用call来实现继承:写一个方法,然后让另外一个新的对象来继承它(而不是在新对象中再写一次这个方法)。
举个例子:
function Pig(name, taste) { this.name = name; this.taste = taste;}function Cat(name, taste) { Pig.call(this, name, taste); this.shape = 'cute';}console.log(new Cat('honey', 'delicious').name) // honey复制代码
这个例子注意到3点:
- Pig函数执行了
- call改变了Pig函数的this指向,指向到Cat
- 上面的Cat函数体等同于把Pig的函数体给"拿"了过来:
function Cat(name, taste) { this.name = name, this.taste = taste, this.shape = 'cute'}
再来个例子:
function Pig(name) { this.name = name; console.log(this.name); console.log(this.taste);}const foo = { taste: 'delicious'}Pig.call(foo, 'honey') //输出: honey, delicious复制代码
如果再给foo加上个name属性呢?注意看
function Pig(name) { this.name = name; console.log(this.name); console.log(this.taste);}const foo = { taste: 'delicious', name: "foo's name"}Pig.call(foo, 'honey') // 输出: honey, deliciousconsole.log(foo.name) // 输出:honey复制代码
现在的情况可以看出,Pig函数体执行的时候,this会指向foo, Pig本身的函数体里this相关的修改同步于它指向的foo。
造轮子第一步
要模拟之前想想思路:根据call的特性,大致是调用的函数执行,执行时指向call的第一个参数里的this,可以传参,并且函数体里可以同步指向的this相关修改。
一步步来。我们把foo对象改变一下:
const foo = { taste: 'delicious', name: "foo's name", Pig: function() { console.log(this.name); console.log(this.taste); }}foo.Pig(); // 输出:foo's name复制代码
想想,此时是不是Pig函数体里的this指向了foo对象了?单论这点,等同于 Pig.call(foo)。
造轮子的第一步思路就在这里,分步骤来说就是:
1.将函数设置为对象的属性
2.执行该函数
3.删除该函数
1.foo.Pig = Pig2.foo.Pig()3.delete foo.Pig复制代码
根据这个思路写出造轮子第一步的代码:
// 第一步完整代码Function.prototype.call2 = function(obj) { obj.fn = this // foo.Pig = this 此处this可以获取调用call2()的函数的函数体。this的显示绑定机制。 obj.fn() delete obj.fn}// have a testconst foo = { taste: 'delicious', name: "foo's name"}function Pig() { console.log(this.taste); console.log(this.name);}Pig.call2(foo); // 输出:delicious foo's nameconsole.log(foo); // 输出:{ taste: 'delicious', name: 'foo\'s name' }复制代码
看看输出结果,符合预期。调用的函数Pig执行了,执行时也是指向的第一个参数foo对象的this,并且函数体里可以同步指向的this相关修改。但是此时参数只能传第一个。接下来去改善。
造轮子第二步
根据call的特性,大致是调用的函数执行,执行时指向call的第一个参数里的this,可以传参,并且函数体里可以同步指向的this相关修改。我们现在参数那里还没有完善。
举个例子:
function Pig(weight, age) { console.log(this.taste); console.log(this.name); console.log(weight); console.log(age);}const foo = { taste: 'delicious', name: 'honey'}Pig.call(foo, 100, 6); // 输出:delicious honey 100 6复制代码
正常的传参应该是这样的,怎么去完善咱们的代码呢?
我们可以利用arguments类数组对象来取得传入的参数。以上一个例子而言,arguments应该是:
arguments = { 0: foo, 1: 100, 2: 6, length: 3}复制代码
利用ES6语法和call本身的第二步第一版代码
那我们像下面那样修改行吗?
Function.prototype.call2 = function(obj) { obj.fn = this; obj.fn(...arguments.slice(1)) // 注意此处 delete obj.fn}复制代码
不行的,类数组对象没有slice这个方法。我们得用
Array.prototype.slice.call(arguments, 1)
来把arguments这个类数组对象转换成数组,然后从第一个元素往后切,返回一个新数组。
所以代码修改后应该为:
// 第二步第一版完整代码Function.prototype.call2 = function(obj) { obj.fn = this; var arr = Array.prototype.slice.call(arguments, 1); // 这个例子就是[100, 6] obj.fn(...arr); delete obj.fn;}// have a testconst foo = { taste: 'delicious', name: 'honey'}function Pig(weight, age) { console.log(this.taste); console.log(this.name); console.log(weight, age)}Pig.call2(foo, 100, 6)// delicious// honey// 100 6复制代码
测试成功。但这里用到了ES6和call本身,下面写第二版代码,ES3原汁原味。
原汁原味ES3的第二步第二版代码
同样利用arguments。
类数组对象,有length属性,我们可以用循环构建一个参数数组。
const arr = [];for (let i = 1, len = arguments.length; i < len; i++) { arr.push('arguments[' + i + ']');}// arr => ['arguments[1]', 'arguments[2]...']复制代码
好了,不定参参数数组有了。接下来就是想办法把这个数组的每一项以参数的格式放进obj.fn()的形参中。
我们可以先把arr数组变成字符串。有两种办法。
arr.toString / arr.join()
返回值都是 "arguments[1], arguments[2]..."
。
然后可以利用eval()这个魔鬼来直接把这字符串的两个引号去掉,达到形参格式要求。看下面逻辑。
eval('obj.fn(' + arr + ')')
在eval里此处的arr会自动调用arr.toString()方法。
最终相当于执行了obj.fn(arguments[1], arguments[2], ...)
。
所以:
// 原汁原味ES3的第二步第二版完整代码Function.prototype.call2 = function(obj) { obj.fn = this; const arr = []; for (let i = 1, len = arguments.length; i < len; i++) { arr.push('arguments[' + i + ']'); // 写出arr.push(argumentsp[i])也可以 } eval('obj.fn(' + arr + ')'); delete obj.fn;}// have a testconst foo = { taste: 'delicious', name: 'honey'}function Pig(weight, age) { console.log(this.taste); console.log(this.name); console.log(weight, age);}Pig.call2(foo, 100, 6); // delicious// honey// 100 6复制代码
成功。这个就比较兼容了。
造轮子第三步
再完善两点。
- thisArg参数在非严格模式下,指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象)。
- 函数可以有返回值。
这比较容易解决,看代码:
// 第三步完整代码:Function.prototype.call2 = function(obj) { var obj = obj || window; obj.fn = this; const arr = []; for (let i = 1, len = arguments.length; i < len; i++) { arr.push('arguments[' + i + ']'); } const result = eval('obj.fn(' + arr + ')'); delete obj.fn; return result;}// have a testvar name = "Darling";const foo = { name: 'honey'}function Pig(weight, age) { console.log(this.name); return { weight: weight, age: age, name: this.name }}Pig.call2(null);// Darling// Object { // weight: undefined// age: undefined// name: "Darling"// }Pig.call2(foo, 100, 6);// honey// Object { // weight: 100,// age: 6,// name: 'honey'//}复制代码
OK,目前为止,造完了一个简单的call轮子call2?。
Function.prototype.apply()的轮子
与call就参数差异,直接上代码。
Function.prototype.apply2 = function(obj, arr) { var obj = obj || window; obj.fn = this; var result; if (!arr) { result = obj.fn(); } else { var args = []; for (let i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('obj.fn(' + args + ')'); } delete obj.fn; return result;}复制代码
参考:
1.
2.
3.