面试官问:可否模仿完成JS的call和apply要领

之前写过两篇《面试官问:可否模仿完成JSnew操纵符》《面试官问:可否模仿完成JSbind要领》

个中模仿bind要领时是运用的callapply修改this指向。但面试官能够问:可否不必callapply来完成呢。意义也就是须要模仿完成callapply的了。

附上之前写文章写过的一段话:已经有许多模仿完成
call
apply的文章,为何本身还要写一遍呢。进修就好比是座大山,人们沿着差别的路爬山,分享着本身看到的景致。你不一定能看到他人看到的景致,体味到他人的心境。只要本身去爬山,才看到不一样的景致,体味才越发深切。

先经由过程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)

当以
thisArg
argArray 为参数在一个
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://segmentfault.com/a/1190000017206223
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞