Function.prototype.call
,手写系列,万文面试系列,必会系列必包含的内容,足见其在前端的份量。
本文基于MDN 和 ECMA 标准,和你们一块儿重新认识call
。javascript
涉及知识点:前端
面试官的问题:
麻烦你手写一下Function.prototype.call
java
ES6的拓展运算符
版本Function.prototype.call = function(context) {
context = context || window;
context["fn"] = this;
let arg = [...arguments].slice(1);
context["fn"](...arg);
delete context["fn"];
}
复制代码
这个版本,应该不是面试官想要的真正答案。不作太多解析。node
eval
的版本Function.prototype.call = function (context) {
context = (context == null || context == undefined) ? window : new Object(context);
context.fn = this;
var arr = [];
for (var i = 1; i < arguments.length; i++) {
arr.push('arguments[' + i + ']');
}
var r = eval('context.fn(' + arr + ')');
delete context.fn;
return r;
}
复制代码
这个版本值得完善的地方面试
this
是否是函数没有进行判断eval
必定会被容许执行吗在咱们真正开始写Function.prototype.call
以前,仍是先来看看MDN和 ECMA是怎么定义她的。算法
function.call(thisArg, arg1, arg2, ...)
复制代码
thisArg
编程
可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:若是这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象,原始值会被包装。
arg1, arg2, ...
指定的参数列表。小程序
这里透露了几个信息,我已经加粗标注:浏览器
window
。固然MDN这里说是window也没太大问题。我想补充的是 nodejs
也实现了 ES标准。因此咱们实现的时候,是否是要考虑到 nodejs
环境呢。Object(val)
,即完成了对原始值val
的包装。在 Function.prototype.call() - JavaScript | MDN的底部罗列了ES规范版本,每一个版本都有call实现的
说明。安全
咱们实现的,是要基于ES的某个版原本实现的。
由于ES的版本不一样,实现的细节可能不同,实现的环境也不同。
规范版本 | 状态 | 说明 |
---|---|---|
ECMAScript 1st Edition (ECMA-262) | Standard | 初始定义。在 JavaScript 1.3 中实现。 |
ECMAScript 5.1 (ECMA-262) Function.prototype.call |
Standard | |
ECMAScript 2015 (6th Edition, ECMA-262) Function.prototype.call |
Standard | |
ECMAScript (ECMA-262) Function.prototype.call |
Living Standard |
在ES3标准中关于call
的规范说明在11.2.3 Function Calls
, 直接搜索就能查到。
咱们今天主要是基于2009年ES5标准下来实现Function.prototype.call
,有人可能会说,你这,为嘛不在 ES3标准下实现,由于ES5下能涉及更多的知识点。
(context == null || context == undefined) ? window : new Object(context)
上面代码的 undefined
不必定是可靠的。
引用一段MDN的话:
在现代浏览器(JavaScript 1.8.5/Firefox 4+),自ECMAscript5标准以来undefined是一个不能被配置(non-configurable),不能被重写(non-writable)的属性。即使事实并不是如此,也要避免去重写它。
在没有交代上下文的状况使用 void 0
比直接使用 undefined
更为安全。
有些同窗可能没见过undefined被改写的状况,没事,来一张图:
void
这个一元运算法除了这个 准备返回 undefined
外, 还有另外两件常见的用途:
a标签的href,就是什么都不作
<a href="javascript:void(0);">
IIFE当即执行
;void function(msg){
console.log(msg)
}("你好啊");
复制代码
固然更直接的方式是:
;(function(msg){
console.log(msg)
})("你好啊");
复制代码
浏览器环境:
typeof self == 'object' && self.self === self
复制代码
nodejs环境:
typeof global == 'object' && global.global === global
复制代码
如今已经有 globalThis, 在高版本浏览器和nodejs里面都支持。
显然,在咱们的这个场景下,还不能用,可是其思想能够借鉴:
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
复制代码
Strict mode 严格模式,是ES5引入的特性。那咱们怎么验证你的环境是否是支持严格模式呢?
var hasStrictMode = (function(){
"use strict";
return this == undefined;
}());
复制代码
正常状况都会返回true
,放到IE8里面执行:
在非严格模式下,函数的调用上下文(this的值)是全局对象。在严格模式下,调用上下文是undefined。
知道是否是支持严格模式,还不够,咱们还要知道咱们是否是处于严格模式下。
以下的代码能够检测,是否是处于严格模式:
var isStrict = (function(){
return this === undefined;
}());
复制代码
这段代码在支持严格模式的浏览器下和nodejs
环境下都是工做的。
var r = eval('context.fn(' + arr + ')');
delete context.fn;
复制代码
如上的代码直接删除了context上的fn
属性,若是原来的context上有fn
属性,那会不会丢失呢?
咱们采用eval
版本的call
, 执行下面的代码
var context = {
fn: "i am fn",
msg: "i am msg"
}
log.call(context); // i am msg
console.log("msg:", context.msg); // i am msg
console.log("fn:", context.fn); // fn: undedined
复制代码
能够看到context的fn
属性已经被干掉了,是破坏了入参,产生了不应产生的反作用。
与反作用对应的是函数式编程中的 纯函数。
对应的咱们要采起行动,基本两种思路:
均可以,不过以为 方案2更简单和容易实现:
基本代码以下:
var ctx = new Object(context);
var propertyName = "__fn__";
var originVal;
var hasOriginVal = ctx.hasOwnProperty(propertyName)
if(hasOriginVal){
originVal = ctx[propertyName]
}
...... // 其余代码
if(hasOriginVal){
ctx[propertyName] = originVal;
}
复制代码
eval
的实现,基本以下基于标准ECMAScript 5.1 (ECMA-262) Function.prototype.call
When the call method is called on an object func with argument thisArg and optional arguments arg1, arg2 etc, the following steps are taken:
1. If IsCallable(func) is false, then throw a TypeError exception.
2. Let argList be an empty List.
3. If this method was called with more than one argument then in left to right
order starting with arg1 append each argument as the last element of argList
4. Return the result of calling the [[Call]] internal method of func, providing
thisArg as the this value and argList as the list of arguments.
The length property of the call method is 1.
NOTE The thisArg value is passed without modification as the this value. This is a
change from Edition 3, where a undefined or null thisArg is replaced with the
global object and ToObject is applied to all other values and that result is passed
as the this value.
复制代码
对咱们比较重要的是 1
和 Note
:
看看咱们的基础实现
var hasStrictMode = (function () {
"use strict";
return this == undefined;
}());
var isStrictMode = function () {
return this === undefined;
};
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
function isFunction(fn){
return typeof fn === "function";
}
function getContext(context) {
var isStrict = isStrictMode();
if (!hasStrictMode || (hasStrictMode && !isStrict)) {
return (context === null || context === void 0) ? getGlobal() : Object(context);
}
// 严格模式下, 妥协方案
return Object(context);
}
Function.prototype.call = function (context) {
// 不能够被调用
if (typeof this !== 'function') {
throw new TypeError(this + ' is not a function');
}
// 获取上下文
var ctx = getContext(context);
// 更为稳妥的是建立惟一ID, 以及检查是否有重名
var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
var originVal;
var hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(propertyName) : false;
if (hasOriginVal) {
originVal = ctx[propertyName]
}
ctx[propertyName] = this;
// 采用string拼接
var argStr = '';
var len = arguments.length;
for (var i = 1; i < len; i++) {
argStr += (i === len - 1) ? 'arguments[' + i + ']' : 'arguments[' + i + '],'
}
var r = eval('ctx["' + propertyName + '"](' + argStr + ')');
// 还原现场
if (hasOriginVal) {
ctx[propertyName] = originVal;
} else {
delete ctx[propertyName]
}
return r;
}
复制代码
当前版依旧存在问题,
Obeject
进行了封装。会致使严格模式下传递非对象的时候,this的指向是不许的, 不得以的妥协。 哪位同窗有更好的方案,敬请指导。
因此完美的解决方法,就是产生一个UID.
eval
的执行,可能会被 Content-Security-Policy 阻止大体的提示信息以下:
[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an
allowed source of script in the following Content Security Policy directive: "script-src ......... 复制代码
前面两条都应该还能接受,至于第三条,咱们不能妥协。
这就得请出下一位嘉宾, new Function
。
new Function ([arg1[, arg2[, ...argN]],] functionBody)
其基本格式如上,最后一个为函数体。
举个简单的例子:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));
// expected output: 8
复制代码
咱们call
的参数个数是不固定,思路就是从arguments
动态获取。
这里咱们的实现借用面试官问:可否模拟实现JS的call和apply方法 实现方法:
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;
}
复制代码
var hasStrictMode = (function () {
"use strict";
return this == undefined;
}());
var isStrictMode = function () {
return this === undefined;
};
var getGlobal = function () {
if (typeof self !== 'undefined') { return self; }
if (typeof window !== 'undefined') { return window; }
if (typeof global !== 'undefined') { return global; }
throw new Error('unable to locate global object');
};
function isFunction(fn){
return typeof fn === "function";
}
function getContext(context) {
var isStrict = isStrictMode();
if (!hasStrictMode || (hasStrictMode && !isStrict)) {
return (context === null || context === void 0) ? getGlobal() : Object(context);
}
// 严格模式下, 妥协方案
return Object(context);
}
function generateFunctionCode(argsLength){
var code = 'return arguments[0][arguments[1]](';
for(var i = 0; i < argsLength; i++){
if(i > 0){
code += ',';
}
code += 'arguments[2][' + i + ']';
}
code += ')';
// return arguments[0][arguments[1]](arg1, arg2, arg3...)
return code;
}
Function.prototype.call = function (context) {
// 不能够被调用
if (typeof this !== 'function') {
throw new TypeError(this + ' is not a function');
}
// 获取上下文
var ctx = getContext(context);
// 更为稳妥的是建立惟一ID, 以及检查是否有重名
var propertyName = "__fn__" + Math.random() + "_" + new Date().getTime();
var originVal;
var hasOriginVal = isFunction(ctx.hasOwnProperty) ? ctx.hasOwnProperty(propertyName) : false;
if (hasOriginVal) {
originVal = ctx[propertyName]
}
ctx[propertyName] = this;
var argArr = [];
var len = arguments.length;
for (var i = 1; i < len; i++) {
argArr[i - 1] = arguments[i];
}
var r = new Function(generateFunctionCode(argArr.length))(ctx, propertyName, argArr);
// 还原现场
if (hasOriginVal) {
ctx[propertyName] = originVal;
} else {
delete ctx[propertyName]
}
return r;
}
复制代码
评论区最精彩:
Symbol
由于是基于ES5的标准来写,若是使用Symbol
,那拓展运算符也可使用。 考察的知识面天然少不少。
这样子的话,可能真的无能为力了。
感谢虚鲲菜菜子的指正,其文章手写 call 与 原生 Function.prototype.call 的区别 推荐你们细读。
以下的代码,严格模式下会报错,非严格模式复制不成功:
"use strict";
var context = {
a: 1,
log(msg){
console.log("msg:", msg)
}
};
Object.freeze(context);
context.fn = function(){
};
console.log(context.fn);
VM111 call:12 Uncaught TypeError: Cannot add property fn, object is not extensible
at VM49 call:12
复制代码
这种状况怎么办呢,我能想到的是两种方式:
Obect.create
这也算是一种妥协方法,毕竟链路仍是变长了。
"use strict";
var context = {
a: 1,
log(msg){
console.log("msg:", msg)
}
};
Object.freeze(context);
var ctx = Object.create(context);
ctx.fn = function(){
}
console.log("fn:", typeof ctx.fn); // fn: function
console.log("ctx.a", ctx.a); // ctx.a 1
console.log("ctx.fn", ctx.fn); // ctx.fn ƒ (){}
复制代码
回顾一下依旧存在的问题
Object
进行了封装基础数据类型会致使严格模式下传递非对象的时候,this的指向是不许的, 不得以的妥协。 哪位同窗有更好的方案,敬请指导。
虽然说咱们把临时的属性名变得难以重名,可是若是重名,而函数调用中真调用了此方法,可能会致使异常行为
小程序等环境可能禁止使用eval
和new Function
对象被冻结,call
执行函数中的this
不是真正传入的上下文对象。
因此,我仍是修改标题为三千文字,也没写好 Function.prototype.call
一个手写call
涉及到很多的知识点,本人水平有限,若有遗漏,敬请谅解和补充。
当面试官问题的时候,你要清楚本身面试的岗位,是P6,P7仍是P8。
是高级开发仍是前端组长,抑或是前端负责人。
岗位不同,面试官固然指望的答案也不同。
写做不易,您的支持就是我前行的最大动力。
Function.prototype.call() - JavaScript | MDN
Strict mode - JavaScript | MDN
ECMAScript 5 Strict Mode
ES合集
手写call、apply、bind实现及详解
call、apply、bind实现原理
面试官问:可否模拟实现JS的call和apply方法