资讯专栏INFORMATION COLUMN

面试官问:能否模拟实现JS的call和apply方法

wuyangnju / 3032人阅读

摘要:之前写过两篇面试官问能否模拟实现的操作符和面试官问能否模拟实现的方法其中模拟方法时是使用的和修改指向。但面试官可能问能否不用和来实现呢。使用模拟实现的浏览器环境非严格模式方法的属性是。

之前写过两篇《面试官问:能否模拟实现JSnew操作符》和《面试官问:能否模拟实现JSbind方法》

其中模拟bind方法时是使用的callapply修改this指向。但面试官可能问:能否不用callapply来实现呢。意思也就是需要模拟实现callapply的了。

附上之前写文章写过的一段话:已经有很多模拟实现callapply的文章,为什么自己还要写一遍呢。学习就好比是座大山,人们沿着不同的路登山,分享着自己看到的风景。你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,体会才更加深刻。
先通过MDN认识下callapply

MDN 文档:Function.prototype.call()

语法

fun.call(thisArg, arg1, arg2, ...)

thisArg

fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为nullundefinedthis值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。

arg1, arg2, ...

指定的参数列表

返回值

返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined

MDN 文档:Function.prototype.apply()

func.apply(thisArg, [argsArray])

thisArg

可选的。在 func 函数运行时使用的 this 值。请注意,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

argsArray

可选的。一个数组或者类数组对象,其中的数组元素将作为多带带的参数传给 func 函数。如果该参数的值为 nullundefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。

返回值

调用有指定this值和参数的函数的结果。
直接先看例子1

callapply 的异同

相同点:

1、callapply的第一个参数thisArg,都是func运行时指定的this。而且,this可能不是该方法看到的实际值:如果这个函数处于非严格模式下,则指定为 nullundefined 时会自动替换为指向全局对象,原始值会被包装。

2、都可以只传递一个参数。

不同点: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]

typeof7种类型(undefined number string boolean symbol object function),笔者都验证了一遍:更加验证了相同点第一点,严格模式下,函数的this值就是callapply的第一个参数thisArg,非严格模式下,thisArg值被指定为 nullundefinedthis值会自动替换为指向全局对象,原始值则会被自动包装,也就是new Object()

重新认识了callapply会发现:它们作用都是一样的,改变函数里的this指向为第一个参数thisArg,如果明确有多少参数,那可以用call,不明确则可以使用apply。也就是说完全可以不使用call,而使用apply代替。

也就是说,我们只需要模拟实现applycall可以根据参数个数都放在一个数组中,给到apply即可。

模拟实现 apply

既然准备模拟实现apply,那先得看看ES5规范。ES5规范 英文版ES5规范 中文版apply的规范下一个就是call的规范,可以点击打开新标签页去查看,这里摘抄一部分。

Function.prototype.apply (thisArg, argArray)

当以 thisArgargArray 为参数在一个 func 对象上调用 apply 方法,采用如下步骤:

1.如果 IsCallable(func)false, 则抛出一个 TypeError 异常。

2.如果 argArraynullundefined, 则返回提供 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 值。thisArgundefinednull 时它会被替换成全局对象,所有其他值会被应用 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;
};
实现第一版后,很容易找出两个问题:

[ ] 1.__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" }

[ ] 2.使用了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;
}
你可能不知道在ES3、ES5undefined 是能修改的

可能大部分人不知道。ES5中虽然在全局作用域下不能修改,但在局部作用域中也是能修改的,不信可以复制以下测试代码在控制台执行下。虽然一般情况下是不会的去修改它。

function test(){
    var undefined = 3;
    console.log(undefined); // chrome下也是 3
}
test();

所以判断一个变量a是不是undefined,更严谨的方案是typeof a === "undefined"或者a === void 0;
这里面用的是voidvoid的作用是计算表达式,始终返回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认识callapply,阅读ES5规范,到模拟实现apply,再实现call

就是使用在对象上添加调用apply的函数执行,这时的调用函数的this就指向了这个thisArg,再返回结果。引出了ES6 SymbolES6的扩展符...evalnew Function(),严格模式等。

事实上,现实业务场景不需要去模拟实现callapply,毕竟是ES3就提供的方法。但面试官可以通过这个面试题考察候选人很多基础知识。如:callapply的使用。ES6 SymbolES6的扩展符...evalnew 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~

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/99597.html

相关文章

  • 面试官问能否模拟实现JSbind方法

    摘要:点击那么面试官可能会问是否想过到底做了什么,怎么模拟实现呢。另外前不久写过一篇文章面试官问能否模拟实现的操作符。所以相当于调用时,的返回值函数内部要模拟实现实现的操作。文章中的例子和测试代码放在中模拟实现。 前言 用过React的同学都知道,经常会使用bind来绑定this。 import React, { Component } from react; class TodoItem ...

    Julylovin 评论0 收藏0
  • 面试官问JSthis指向

    摘要:之前写过一篇文章面试官问能否模拟实现的和方法就是利用对象上的函数指向这个对象,来模拟实现和的。虽然实际使用时不会显示返回,但面试官会问到。非严格模式下,和,指向全局对象 前言 面试官出很多考题,基本都会变着方式来考察this指向,看候选人对JS基础知识是否扎实。读者可以先拉到底部看总结,再谷歌(或各技术平台)搜索几篇类似文章,看笔者写的文章和别人有什么不同(欢迎在评论区评论不同之处),...

    warnerwu 评论0 收藏0
  • 面试官问能否模拟实现JSnew操作符

    摘要:接下来继续看升级版例子例子轩辕轩辕轩辕是浏览器实现的查看原型方案。模拟实现知道了这些现象,我们就可以模拟实现操作符。 前言 用过Vuejs的同学都知道,需要用new操作符来实例化。 new Vue({ el: #app, mounted(){}, }); 那么面试官可能会问是否想过new到底做了什么,怎么模拟实现呢。 附上之前写文章写过的一段话:已经有很多模拟实现new...

    shenhualong 评论0 收藏0
  • 学习 underscore 源码整体架构,打造属于自己函数式编程类库

    摘要:译立即执行函数表达式处理支持浏览器环境微信小程序。学习整体架构,利于打造属于自己的函数式编程类库。下一篇文章可能是学习的源码整体架构。也可以加微信,注明来源,拉您进前端视野交流群。 前言 上一篇文章写了jQuery整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 虽然看过挺多underscore.js分析类的文章,但总感觉少点什么。这也许就是纸上得来终觉浅,绝知此...

    junnplus 评论0 收藏0
  • 面试官问JS继承

    摘要:用过的读者知道,经常用继承。部分源码使用点击这里查看源码面试官可以顺着这个问继承的相关问题,比如的继承用如何实现。主要就是三点子类构造函数的指向父类构造器,继承父类的静态方法子类构造函数的的指向父类构造器的,继承父类的方法。 用过React的读者知道,经常用extends继承React.Component。 // 部分源码 function Component(props, conte...

    stonezhu 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<