三千文字,也没写好 Function.prototype.call

前言

Function.prototype.call,手写系列,万文面试系列,必会系列必包含的内容,足见其在前端的份量。
本文基于MDNECMA 标准,和你们一块儿重新认识calljavascript

涉及知识点:前端

  1. undefined
  2. void 一元运算符
  3. 严格模式和非严格模式
  4. 浏览器和nodejs环境识别
  5. 函数反作用 (纯函数)
  6. eval
  7. Content-Security-Policy
  8. delete
  9. new Function
  10. Object.freeze
  11. 对象属性检查
  12. 面试现场
  13. ECMA规范和浏览器厂商之间的爱恨情仇

掘金流行的版本

面试官的问题:
麻烦你手写一下Function.prototype.calljava

基于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;
}
复制代码

这个版本值得完善的地方面试

  1. this 是否是函数没有进行判断
  2. 使用undefined进行判断,安全不安全
    undefined 可能被改写,(高版本浏览器已作限制)。
  3. 直接使用window做为默认上下文,过于武断。
    脚本运行环境,浏览器? nodejs?
    函数运行模式,严格模式,非严格模式?
  4. eval 必定会被容许执行吗
  5. delete context.fn 有没有产生反作用
    context上要是原来有fn属性呢

在咱们真正开始写Function.prototype.call以前,仍是先来看看MDN和 ECMA是怎么定义她的。算法

MDN call 的说明

语法

function.call(thisArg, arg1, arg2, ...)
复制代码

参数

thisArg编程

可选的。在 function 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:若是这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动替换为指向全局对象原始值会被包装
arg1, arg2, ...
指定的参数列表。小程序

透露的信息

这里透露了几个信息,我已经加粗标注:浏览器

  1. 非严格模式,对应的有严格模式
  2. 这里说的是指向 全局对象,没有说是window。固然MDN这里说是window也没太大问题。我想补充的是 nodejs 也实现了 ES标准。因此咱们实现的时候,是否是要考虑到 nodejs环境呢。
  3. 原始值会被包装。怎么个包装呢,Object(val),即完成了对原始值val的包装。

ES标准

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下能涉及更多的知识点。

不可靠的undefined

(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被改写的状况,没事,来一张图:

image.png

void 这个一元运算法除了这个 准备返回 undefined外, 还有另外两件常见的用途:

  1. a标签的href,就是什么都不作
    <a href="javascript:void(0);">

  2. IIFE当即执行

;void function(msg){
    console.log(msg)
}("你好啊");

复制代码

固然更直接的方式是:

;(function(msg){
    console.log(msg)
})("你好啊");
复制代码

浏览器和nodejs环境识别

浏览器环境:

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里面执行:

image.png

在非严格模式下,函数的调用上下文(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属性已经被干掉了,是破坏了入参,产生了不应产生的反作用。
与反作用对应的是函数式编程中的 纯函数

对应的咱们要采起行动,基本两种思路:

  1. 造一个不会重名的属性
  2. 保留现场而后还原现场

均可以,不过以为 方案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.
复制代码

对咱们比较重要的是 1Note:

看看咱们的基础实现

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;
}

复制代码

当前版依旧存在问题,

  1. 严格模式下,咱们用依然用Obeject进行了封装。

会致使严格模式下传递非对象的时候,this的指向是不许的, 不得以的妥协。 哪位同窗有更好的方案,敬请指导。

  1. 虽然说咱们把临时的属性名变得难以重名,可是若是重名,而函数调用中真调用了此方法,可能会致使异常行为。

因此完美的解决方法,就是产生一个UID.

  1. 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 ......... 复制代码

image.png

前面两条都应该还能接受,至于第三条,咱们不能妥协。

这就得请出下一位嘉宾, new Function

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;
}

复制代码

基于 new Function的实现

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;
}

复制代码

评论区问题收集

评论区最精彩:

  1. 为何不用 Symbol

由于是基于ES5的标准来写,若是使用Symbol,那拓展运算符也可使用。 考察的知识面天然少不少。

  1. 支付宝小程序evel、new Function都是不给用的

这样子的话,可能真的无能为力了。

  1. Object.freeze后的对象是不能够添加属性的

感谢虚鲲菜菜子的指正,其文章手写 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
复制代码

这种状况怎么办呢,我能想到的是两种方式:

  1. 复制对象
  2. 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 ƒ (){}
复制代码

小结

回顾一下依旧存在的问题

  1. 严格模式下,咱们用依然须要用Object进行了封装基础数据类型

会致使严格模式下传递非对象的时候,this的指向是不许的, 不得以的妥协。 哪位同窗有更好的方案,敬请指导。

  1. 虽然说咱们把临时的属性名变得难以重名,可是若是重名,而函数调用中真调用了此方法,可能会致使异常行为

  2. 小程序等环境可能禁止使用evalnew Function

  3. 对象被冻结,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方法

相关文章
相关标签/搜索