以前写过两篇《面试官问:可否模拟实现JS
的new
操做符》和《面试官问:可否模拟实现JS
的bind
方法》前端
其中模拟bind
方法时是使用的call
和apply
修改this
指向。但面试官可能问:可否不用call
和apply
来实现呢。意思也就是须要模拟实现call
和apply
的了。git
附上以前写文章写过的一段话:已经有不少模拟实现call
和apply
的文章,为何本身还要写一遍呢。学习就比如是座大山,人们沿着不一样的路爬山,分享着本身看到的风景。你不必定能看到别人看到的风景,体会到别人的心情。只有本身去爬山,才能看到不同的风景,体会才更加深入。
MDN
认识下call
和apply
MDN 文档:Function.prototype.call()
语法
github
fun.call(thisArg, arg1, arg2, ...)
thisArg
在fun
函数运行时指定的this
值。须要注意的是,指定的this
值并不必定是该函数执行时真正的this
值,若是这个函数处于非严格模式下,则指定为null
和undefined
的this
值会自动指向全局对象(浏览器中就是window
对象),同时值为原始值(数字,字符串,布尔值)的this
会指向该原始值的自动包装对象。
arg1, arg2, ...
指定的参数列表
返回值
返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined
。
面试
MDN 文档:Function.prototype.apply()
chrome
func.apply(thisArg, [argsArray])
thisArg
可选的。在 func
函数运行时使用的 this
值。请注意,this
可能不是该方法看到的实际值:若是这个函数处于非严格模式下,则指定为 null
或 undefined
时会自动替换为指向全局对象,原始值会被包装。
argsArray
可选的。一个数组或者类数组对象,其中的数组元素将做为单独的参数传给 func
函数。若是该参数的值为 null
或 undefined
,则表示不须要传入任何参数。从ECMAScript 5
开始可使用类数组对象。
返回值
调用有指定this值和参数的函数的结果。
直接先看例子1segmentfault
call
和 apply
的异同相同点:
一、call
和apply
的第一个参数thisArg
,都是func
运行时指定的this
。并且,this
可能不是该方法看到的实际值:若是这个函数处于非严格模式下,则指定为 null
或 undefined
时会自动替换为指向全局对象,原始值会被包装。
二、均可以只传递一个参数。
不一样点:apply
只接收两个参数,第二个参数能够是数组也能够是类数组,其实也能够是对象,后续的参数忽略不计。call
接收第二个及之后一系列的参数。
看两个简单例子1和2**:设计模式
// 例子1:浏览器环境 非严格模式下 var doSth = function(a, b){ console.log(this); console.log([a, b]); } doSth.apply(null, [1, 2]); // this是window // [1, 2] doSth.apply(0, [1, 2]); // this 是 Number(0) // [1, 2] doSth.apply(true); // this 是 Boolean(true) // [undefined, undefined] doSth.call(undefined, 1, 2); // this 是 window // [1, 2] doSth.call('0', 1, {a: 1}); // this 是 String('0') // [1, {a: 1}]
// 例子2:浏览器环境 严格模式下 'use strict'; var doSth2 = function(a, b){ console.log(this); console.log([a, b]); } doSth2.call(0, 1, 2); // this 是 0 // [1, 2] doSth2.apply('1'); // this 是 '1' // [undefined, undefined] doSth2.apply(null, [1, 2]); // this 是 null // [1, 2]
typeof
有7
种类型(undefined number string boolean symbol object function
),笔者都验证了一遍:更加验证了相同点第一点,严格模式下,函数的this
值就是call
和apply
的第一个参数thisArg
,非严格模式下,thisArg
值被指定为 null
或 undefined
时this
值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()
。数组
从新认识了call
和apply
会发现:它们做用都是同样的,改变函数里的this
指向为第一个参数thisArg
,若是明确有多少参数,那能够用call
,不明确则可使用apply
。也就是说彻底能够不使用call
,而使用apply
代替。
也就是说,咱们只须要模拟实现apply
,call
能够根据参数个数都放在一个数组中,给到apply
便可。
浏览器
apply
既然准备模拟实现apply
,那先得看看ES5
规范。ES5规范 英文版
,ES5规范 中文版
。apply
的规范下一个就是call
的规范,能够点击打开新标签页去查看,这里摘抄一部分。缓存
Function.prototype.apply (thisArg, argArray)
当以thisArg
和argArray
为参数在一个func
对象上调用apply
方法,采用以下步骤:1.若是
IsCallable(func)
是false
, 则抛出一个TypeError
异常。
2.若是argArray
是null
或undefined
, 则返回提供thisArg
做为this
值并以空参数列表调用func
的[[Call]]
内部方法的结果。
3.返回提供thisArg
做为this
值并以空参数列表调用func
的[[Call]]
内部方法的结果。
4.若是Type(argArray)
不是Object
, 则抛出一个TypeError
异常。
5~8 略
9.提供thisArg
做为this
值并以argList
做为参数列表,调用func
的[[Call]]
内部方法,返回结果。apply
方法的length
属性是2
。在外面传入的
thisArg
值会修改并成为this
值。thisArg
是undefined
或null
时它会被替换成全局对象,全部其余值会被应用ToObject
并将结果做为this
值,这是第三版引入的更改。
结合上文和规范,如何将函数里的this
指向第一个参数thisArg
呢,这是一个问题。
这时候请出例子3:
// 浏览器环境 非严格模式下 var doSth = function(a, b){ console.log(this); console.log(this.name); console.log([a, b]); } var student = { name: '轩辕Rowboat', doSth: doSth, }; student.doSth(1, 2); // this === student // true // '轩辕Rowboat' // [1, 2] doSth.apply(student, [1, 2]); // this === student // true // '轩辕Rowboat' // [1, 2]
能够得出结论1:在对象student
上加一个函数doSth
,再执行这个函数,这个函数里的this
就指向了这个对象。那也就是能够在thisArg
上新增调用函数,执行后删除这个函数便可。
知道这些后,咱们试着容易实现初版本:
// 浏览器环境 非严格模式 function getGlobalObject(){ return this; } Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 属性是 `2`。 // 1.若是 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。 if(typeof this !== 'function'){ throw new TypeError(this + ' is not a function'); } // 2.若是 argArray 是 null 或 undefined, 则 // 返回提供 thisArg 做为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。 if(typeof argsArray === 'undefined' || argsArray === null){ argsArray = []; } // 3.若是 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 . if(argsArray !== new Object(argsArray)){ throw new TypeError('CreateListFromArrayLike called on non-object'); } if(typeof thisArg === 'undefined' || thisArg === null){ // 在外面传入的 thisArg 值会修改并成为 this 值。 // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window thisArg = getGlobalObject(); } // ES3: 全部其余值会被应用 ToObject 并将结果做为 this 值,这是第三版引入的更改。 thisArg = new Object(thisArg); var __fn = '__fn'; thisArg[__fn] = this; // 9.提供 thisArg 做为 this 值并以 argList 做为参数列表,调用 func 的 [[Call]] 内部方法,返回结果 var result = thisArg[__fn](...argsArray); delete thisArg[__fn]; return result; };
__fn
同名覆盖问题,thisArg
对象上有__fn
,那就被覆盖了而后被删除了。针对问题1
解决方案一:采用ES6
Sybmol()
独一无二的。能够原本就是模拟ES3
的方法。若是面试官不容许用呢。
解决方案二:本身用Math.random()
模拟实现独一无二的key
。面试时能够直接用生成时间戳便可。
// 生成UUID 通用惟一识别码 // 大概生成 这样一串 '18efca2d-6e25-42bf-a636-30b8f9f2de09' function generateUUID(){ var i, random; var uuid = ''; for (i = 0; i < 32; i++) { random = Math.random() * 16 | 0; if (i === 8 || i === 12 || i === 16 || i === 20) { uuid += '-'; } uuid += (i === 12 ? 4 : (i === 16 ? (random & 3 | 8) : random)) .toString(16); } return uuid; } // 简单实现 // '__' + new Date().getTime();
若是这个key
万一这对象中仍是有,为了保险起见,能够作一次缓存操做。好比以下代码:
var student = { name: '轩辕Rowboat', doSth: 'doSth', }; var originalVal = student.doSth; var hasOriginalVal = student.hasOwnProperty('doSth'); student.doSth = function(){}; delete student.doSth; // 若是没有,`originalVal`则为undefined,直接赋值新增了一个undefined,这是不对的,因此需判断一下。 if(hasOriginalVal){ student.doSth = originalVal; } console.log('student:', student); // { name: '轩辕Rowboat', doSth: 'doSth' }
ES6
扩展符...
解决方案一:采用eval
来执行函数。
eval
把字符串解析成代码执行。
MDN 文档:eval
语法
eval(string)
参数
string
表示JavaScript
表达式,语句或一系列语句的字符串。表达式能够包含变量以及已存在对象的属性。
返回值
执行指定代码以后的返回值。若是返回值为空,返回undefined
解决方案二:但万一面试官不容许用eval
呢,毕竟eval
是魔鬼。能够采用new Function()
来生成执行函数。
MDN 文档:Function
语法
new Function ([arg1[, arg2[, ...argN]],] functionBody)
参数
arg1, arg2, ... argN
被函数使用的参数的名称必须是合法命名的。参数名称是一个有效的JavaScript
标识符的字符串,或者一个用逗号分隔的有效字符串的列表;例如“×”
,“theValue”
,或“A,B”
。
functionBody
一个含有包括函数定义的JavaScript
语句的字符串。
接下来看两个例子:
简单例子: var sum = new Function('a', 'b', 'return a + b'); console.log(sum(2, 6));
// 稍微复杂点的例子: var student = { name: '轩辕Rowboat', doSth: function(argsArray){ console.log(argsArray); console.log(this.name); } }; // var result = student.doSth(['Rowboat', 18]); // 用new Function()生成函数并执行返回结果 var result = new Function('return arguments[0][arguments[1]](arguments[2][0], arguments[2][1])')(student, 'doSth', ['Rowboat', 18]); // 个数不定 // 因此能够写一个函数生成函数代码: function generateFunctionCode(argsArrayLength){ var code = 'return arguments[0][arguments[1]]('; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ','; } code += 'arguments[2][' + i + ']'; } code += ')'; // return arguments[0][arguments[1]](arg1, arg2, arg3...) return code; }
ES三、ES5
中 undefined
是能修改的可能大部分人不知道。ES5
中虽然在全局做用域下不能修改,但在局部做用域中也是能修改的,不信能够复制如下测试代码在控制台执行下。虽然通常状况下是不会的去修改它。
function test(){ var undefined = 3; console.log(undefined); // chrome下也是 3 } test();
因此判断一个变量a
是否是undefined
,更严谨的方案是typeof a === 'undefined'
或者a === void 0;
这里面用的是void
,void
的做用是计算表达式,始终返回undefined
,也能够这样写void(0)
。
更多能够查看韩子迟
的这篇文章:为何用「void 0」代替「undefined」
解决了这几个问题,比较容易实现以下代码。
new Function()
模拟实现的apply
// 浏览器环境 非严格模式 function getGlobalObject(){ return this; } function generateFunctionCode(argsArrayLength){ var code = 'return arguments[0][arguments[1]]('; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ','; } code += 'arguments[2][' + i + ']'; } code += ')'; // return arguments[0][arguments[1]](arg1, arg2, arg3...) return code; } Function.prototype.applyFn = function apply(thisArg, argsArray){ // `apply` 方法的 `length` 属性是 `2`。 // 1.若是 `IsCallable(func)` 是 `false`, 则抛出一个 `TypeError` 异常。 if(typeof this !== 'function'){ throw new TypeError(this + ' is not a function'); } // 2.若是 argArray 是 null 或 undefined, 则 // 返回提供 thisArg 做为 this 值并以空参数列表调用 func 的 [[Call]] 内部方法的结果。 if(typeof argsArray === 'undefined' || argsArray === null){ argsArray = []; } // 3.若是 Type(argArray) 不是 Object, 则抛出一个 TypeError 异常 . if(argsArray !== new Object(argsArray)){ throw new TypeError('CreateListFromArrayLike called on non-object'); } if(typeof thisArg === 'undefined' || thisArg === null){ // 在外面传入的 thisArg 值会修改并成为 this 值。 // ES3: thisArg 是 undefined 或 null 时它会被替换成全局对象 浏览器里是window thisArg = getGlobalObject(); } // ES3: 全部其余值会被应用 ToObject 并将结果做为 this 值,这是第三版引入的更改。 thisArg = new Object(thisArg); var __fn = '__' + new Date().getTime(); // 万一仍是有 先存储一份,删除后,再恢复该值 var originalVal = thisArg[__fn]; // 是否有原始值 var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; // 9.提供 `thisArg` 做为 `this` 值并以 `argList` 做为参数列表,调用 `func` 的 `[[Call]]` 内部方法,返回结果。 // ES6版 // var result = thisArg[__fn](...args); var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result; };
apply
模拟实现call
Function.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ // argsArray.push(arguments[i + 1]); argsArray[i] = arguments[i + 1]; } console.log('argsArray:', argsArray); return this.applyFn(thisArg, argsArray); } // 测试例子 var doSth = function (name, age){ var type = Object.prototype.toString.call(this); console.log(typeof doSth); console.log(this === firstArg); console.log('type:', type); console.log('this:', this); console.log('args:', [name, age], arguments); return 'this--'; }; var name = 'window'; var student = { name: '轩辕Rowboat', age: 18, doSth: 'doSth', __fn: 'doSth', }; var firstArg = student; var result = doSth.applyFn(firstArg, [1, {name: 'Rowboat'}]); var result2 = doSth.callFn(firstArg, 1, {name: 'Rowboat'}); console.log('result:', result); console.log('result2:', result2);
细心的你会发现注释了这一句argsArray.push(arguments[i + 1]);
,事实上push
方法,内部也有一层循环。因此理论上不使用push
性能会更好些。面试官也可能根据这点来问时间复杂度和空间复杂度的问题。
// 看看V8引擎中的具体实现: function ArrayPush() { var n = TO_UINT32( this.length ); // 被push的对象的length var m = %_ArgumentsLength(); // push的参数个数 for (var i = 0; i < m; i++) { this[ i + n ] = %_Arguments( i ); // 复制元素 (1) } this.length = n + m; // 修正length属性的值 (2) return this.length; };
行文至此,就基本结束了,你可能还发现就是写的非严格模式下,thisArg
原始值会包装成对象,添加函数并执行,再删除。而严格模式下仍是原始值这个没有实现,并且万一这个对象是冻结对象呢,Object.freeze({})
,是没法在这个对象上添加属性的。因此这个方法只能算是非严格模式下的简版实现。最后来总结一下。
经过MDN
认识call
和apply
,阅读ES5
规范,到模拟实现apply
,再实现call
。
就是使用在对象上添加调用apply
的函数执行,这时的调用函数的this
就指向了这个thisArg
,再返回结果。引出了ES6 Symbol
,ES6
的扩展符...
、eval
、new Function()
,严格模式等。
事实上,现实业务场景不须要去模拟实现call
和apply
,毕竟是ES3
就提供的方法。但面试官能够经过这个面试题考察候选人不少基础知识。如:call
、apply
的使用。ES6 Symbol
,ES6
的扩展符...
,eval
,new Function()
,严格模式,甚至时间复杂度和空间复杂度等。
读者发现有不妥或可改善之处,欢迎指出。另外以为写得不错,能够点个赞,也是对笔者的一种支持。
// 最终版版 删除注释版,详细注释看文章 // 浏览器环境 非严格模式 function getGlobalObject(){ return this; } function generateFunctionCode(argsArrayLength){ var code = 'return arguments[0][arguments[1]]('; for(var i = 0; i < argsArrayLength; i++){ if(i > 0){ code += ','; } code += 'arguments[2][' + i + ']'; } code += ')'; return code; } Function.prototype.applyFn = function apply(thisArg, argsArray){ if(typeof this !== 'function'){ throw new TypeError(this + ' is not a function'); } if(typeof argsArray === 'undefined' || argsArray === null){ argsArray = []; } if(argsArray !== new Object(argsArray)){ throw new TypeError('CreateListFromArrayLike called on non-object'); } if(typeof thisArg === 'undefined' || thisArg === null){ thisArg = getGlobalObject(); } thisArg = new Object(thisArg); var __fn = '__' + new Date().getTime(); var originalVal = thisArg[__fn]; var hasOriginalVal = thisArg.hasOwnProperty(__fn); thisArg[__fn] = this; var code = generateFunctionCode(argsArray.length); var result = (new Function(code))(thisArg, __fn, argsArray); delete thisArg[__fn]; if(hasOriginalVal){ thisArg[__fn] = originalVal; } return result; }; Function.prototype.callFn = function call(thisArg){ var argsArray = []; var argumentsLength = arguments.length; for(var i = 0; i < argumentsLength - 1; i++){ argsArray[i] = arguments[i + 1]; } return this.applyFn(thisArg, argsArray); }
《JavaScript设计模式与开发实践》- 第二章 第 2 章 this、call和apply
JS魔法堂:再次认识Function.prototype.call
不用call和apply方法模拟实现ES5的bind方法
JavaScript深刻之call和apply的模拟实现
做者:常以轩辕Rowboat若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,惟善学。
我的博客segmentfault
前端视野专栏,开通了前端视野专栏,欢迎关注
掘金专栏,欢迎关注
知乎前端视野专栏,开通了前端视野专栏,欢迎关注
github,欢迎follow
~