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

简介: Function.prototype.call,手写系列,万文面试系列,必会系列必包含的内容,足见其在前端的分量。本文基于MDN 和 ECMA 标准,和大家一起从新认识call。

前言


Function.prototype.call,手写系列,万文面试系列,必会系列必包含的内容,足见其在前端的分量。


本文基于MDNECMA 标准,和大家一起从新认识call


涉及知识点:


  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.call


基于ES6的拓展运算符版本


Function.prototype.call = function(context) {
    context = context || window;
    context["fn"] = this;
    let arg = [...arguments].slice(1); 
    context["fn"](...arg);
    delete context["fn"];
}
复制代码


这个版本,应该不是面试官想要的真正答案。不做太多解析。


基于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的版本不同,实现的细节可能不一样,实现的环境也不一样。



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


1.JPG


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


2.JPG


在非严格模式下,函数的调用上下文(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 
.........
复制代码

3.JPG


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

这就得请出下一位嘉宾, 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. 虽说我们把临时的属性名变得难以重名,但是如果重名,而函数调用中真调用了此方法,可能会导致异常行为


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


  1. 对象被冻结,call执行函数中的this不是真正传入的上下文对象。


所以,我还是修改标题为三千文字,也没写好 Function.prototype.call


面试现场


一个手写call涉及到不少的知识点,本人水平有限,如有遗漏,敬请谅解和补充。

当面试官问题的时候,你要清楚自己面试的岗位,是P6,P7还是P8。


是高级开发还是前端组长,抑或是前端负责人。

岗位不一样,面试官当然期望的答案也不一样。


写在最后


写作不易,您的支持就是我前行的最大动力。



目录
打赏
0
0
0
0
4
分享
相关文章
Hive教程(08)- JDBC操作Hive
Hive教程(08)- JDBC操作Hive
1381 0
淘宝商品评论数据API接口详解及JSON示例返回
淘宝商品评论数据API接口是淘宝开放平台提供的一项服务,旨在帮助开发者通过编程方式获取淘宝商品的评论数据。这些数据包括评论内容、评论时间、评论者信息、评分等,对于电商分析、用户行为研究、竞品分析等领域都具有极高的价值。
挑战杯丨2025年度中国青年科技创新“揭榜挂帅”擂台赛阿里云榜题发布!用AI助力乡村振兴丨云工开物
第十九届“挑战杯”竞赛2025年度中国青年科技创新“揭榜挂帅”擂台赛,由阿里巴巴公益、阿里云等主办。赛事以AI技术助力乡村振兴为主题,鼓励高校师生设计长虹乡特色文创产品、农特产品包装等。作品需紧扣开化特色。评选标准涵盖创意、文化呈现和技术应用等方面。比赛设擂主奖及多项奖项。报名截止至2025年6月30日,作品提交截止至8月15日。
淘宝天猫商品评论数据接口丨淘宝 API 实时接口指南
淘宝天猫商品评论数据接口(Taobao.item_review)提供全面的评论信息,包括文字、图片、视频评论、评分、追评等,支持实时更新和高效筛选。用户可基于此接口进行数据分析,支持情感分析、用户画像构建等,同时确保数据使用的合规性和安全性。使用步骤包括注册开发者账号、创建应用获取 API 密钥、发送 API 请求并解析返回数据。适用于电商商家、市场分析人员和消费者。
ERP系统中的订单管理与供应链协作解析
【7月更文挑战第25天】 ERP系统中的订单管理与供应链协作解析
814 6
JavaScript逆向爬虫——无限debugger的原理与绕过
JavaScript逆向爬虫——无限debugger的原理与绕过
416 2
芝麻代理、快代理、神龙代理、小象代理…如何挑选适合的代理IP?
本文介绍了如何选择适合项目需求的代理IP服务。首先,需明确具体应用场景和需求,不同场景对代理IP的要求各异。其次,选择合适的代理类型,如HTTP、HTTPS或SOCKS5。稳定性和速度是核心要素,需关注代理IP的稳定性指标和网络延迟。成本方面,应综合考量性价比,并进行实际测试。最后,选择提供优质服务支持的供应商,以确保问题能够及时解决。通过这些步骤,可以找到最适合项目的代理IP服务。
深入剖析 Swagger enum:实际案例详解
enum 是 Swagger 规范中用来定义枚举类型的一种方式。它允许开发者在 API 文档中明确列出该接口的参数、返回值或请求体中可接受的枚举值。通过使用 Swagger enum,开发者可以更清晰地描述 API 的输入和输出,提高 API 文档的可读性和可维护性。
Charles抓包神器的使用,完美解决抓取HTTPS请求unknown问题
本文介绍了在 Mac 上使用的 HTTP 和 HTTPS 抓包工具 Charles 的配置方法。首先,强调了安装证书对于抓取 HTTPS 请求的重要性,涉及 PC 和手机端。在 PC 端,需通过 Charles 软件安装证书,然后在钥匙串访问中设置为始终信任。对于 iOS 设备,需设置 HTTP 代理,通过电脑上的 IP 和端口访问特定网址下载并安装证书,同时在设置中信任该证书。配置 Charles 包括设置代理端口和启用 SSL 代理。完成这些步骤后,即可开始抓包。文章还提及 Android 7.0 以上版本可能存在不信任用户添加 CA 证书的问题,但未提供解决办法。
3238 0
Charles抓包神器的使用,完美解决抓取HTTPS请求unknown问题
AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等

登录插画

登录以查看您的控制台资源

管理云资源
状态一览
快捷访问