likes
comments
collection
share

手写源码系列:原生js实现call方法

作者站长头像
站长
· 阅读数 8

call语法

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

参数

  • thiaArg

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

  • arg1, arg2, ...

    指定的参数列表。

返回值

  • func函数的返回值,如果func没有返回值,则返回undefined

描述

1. call() 允许为不同的对象分配和调用属于一个对象的函数/方法 (func)
2. call() 提供新的 this 值给当前调用的函数/方法 (func)

ECMAScript5.1

15.3.4.4 Function.prototype.call (thisArg [ , arg1 [ , arg2, … ] ] )

当以 thisArg 和可选的 arg1, arg2 等等作为参数在一个 func 对象上调用 call 方法,采用如下步骤:

1. 如果 IsCallable(func) 是 false, 则抛出一个 TypeError 异常。
2. 令 argList 为一个空列表。
3. 如果调用这个方法的参数多余一个,则从 arg1 开始以从左到右的顺序将每个参数
    插入为 argList 的最后一个元素。
4. 提供 thisArg 作为 this 值并以 argList 作为参数列表,调用 func 的 [[Call]] 内部方法
    ,返回结果。

call 方法的 length 属性是 1

注:在外面传入的 thisArg 值会修改并成为 this 值。
thisArg 是 undefined 或 null 时它会被替换成全局对象,
所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。

从最简单的例子出发

var tObj = {
    name:'tino'
}
function func(){
    console.log('this:',this)
    console.log('name:',this.name)
}

func.call(tObj);
// this: {name: "tino"}
// name: tino

此时的func绑定函数做了执行操作,并且内部this指向了tObj对象

问题一:怎么通过call函数执行绑定函数func,并且将func内部this指向绑定对象

var tObj = {
    name:'tino',
    func:function(){
        console.log('this:',this)
    }
}
tObj.func()
//this: {name: "tino", func: ƒ}

有没有发现,此时的func的this指向的是tObj对象; 也就是说,在绑定对象上创建一个内部函数,这个内部函数的执行会将this指向绑定对象tObj,那也就是我们可以将绑定函数func设置为绑定对象的内部函数,即: tObj.__func = func;

那现在我们来用callFunc函数做实现:

Function.prototype.callFunc = function(target) {
    target.__func = this; //this为绑定函数
    target.__func();
    //不能给绑定对象本身添加属性,可以delete删除这个添加的属性
    delete target.__func;
}

//测试
var tObj = {
    name: 'tino'
}

function func() {
    console.log('this:', this)
    console.log('name:', this.name)
}

func.callFunc(tObj)

// this: {name: "tino", __func: ƒ}
// name: tino

问题二:怎么通过call传递给定参数给绑定函数并执行

var tObj = {
    name: 'tino'
}

function func(arg1, arg2) {
    console.log('this:', this)
    console.log('name:', this.name)
    console.log('arg1:', arg1)
    console.log('arg2:', arg2)
}

func.call(tObj, 'arg1', 'arg2');
//this: {name: "tino"}
//name: tino
//arg1: arg1
//arg2: arg2

绑定函数func调用call函数,传入tObj,arg1,arg2三个参数,执行时可在func参数列表中获取到,但是不能确保参数的数量,可以通过函数的Arguments对象获取到所有的参数即Arguments[0]、Arguments[1]、Arguments[2]...;

var argList = [];
for(var i = 0;i<arguments.length;i++>){
    argList[i] = 'arguments['+(i + 1)+']';
}

argList参数数组有了,现在我们应该怎么去传入__func中执行呢?

target.__func(...argList);

好像是可以的哦,但是是es6的方法,还是尽量模拟es3的吧,

eval('target.__func(' + argList + ')')

Function.prototype.callFunc = function(target) {
    target.__func = this; //this为绑定函数
    var argList = [];
    for (var i = 0, len = arguments.length; i < len; i++) {
        argList[i] = 'arguments[' + (i + 1) + ']';
    }
    // target.__func(...argList);
    eval('target.__func(' + argList + ')')
    delete target.__func
}
//测试
var tObj = {
    name: 'tino'
}

function func(arg1, arg2) {
    console.log('this:', this)
    console.log('name:', this.name)
    console.log('arg1:', arg1)
    console.log('arg2:', arg2)
}

func.callFunc(tObj, "tinolee", 2);
//this: {name: "tino", __func: ƒ}
//name: tino
//arg1: arg1
//arg2: arg2

OK,参数传入测试通过,但是eval似乎不太受欢迎哪,但是还有一个Function构造函数可以来生成执行函数

new Function ([arg1[, arg2[, ...argN]],] functionBody)

// MDN的例子是这样的:
const sum = new Function('a', 'b', 'return a + b');
console.log(sum(2, 6));

那我们应该怎么去设计一个tObj绑定对象执行内部函数__func,并传入参数的执行函数呢?

以上面测试例子为例:我们传入了arg1,arg2两个参数,那么在tObj的__func下怎么传入执行呢?Function构造函数如下:

new Function([[Expression]])(tObj,__func,argList)

构造函数表达式应该是这样

[[Expression]]
{
    return tObj.__func(argList[0],argList[1],argList[2]...)
}

通过argments对象来解析不定参数

argment[0] -> tObj

argment[1] -> __func

argment[2] -> argList

那么构造函数表达式应该是这样

[[Expression]]
{
    return argment[0][argment[1]](argment[2][0],argment[2][1],argment[2][2]...)
}

封装出一个生成表达式函数

function callExecuteExpression(argList) {
    var expression = 'return arguments[0][arguments[1]]('
    for (var i = 0, len = argList.length; i < len; i++) {
        i > 0 ?
            expression += ',arguments[2][' + i + ']' :
            expression += 'arguments[2][' + i + ']'
    }
    expression += ')'
    return expression
}

现在构造函数可以这样书写

var expression = callExecuteExpression(argList)
new Function(expression)(tObj,__func,argList)
Function.prototype.callFunc = function(target) {
    target.__func = this; //this为绑定函数
    var argList = [];
    for (var i = 0, len = arguments.length; i < len; i++) {
        argList[i] = arguments[i + 1];
    }
    // target.__func(...argList);
    //eval('target.__func(' + argList + ')')
    var expression = callExecuteExpression(argList)
    new Function(expression)(target, '__func', argList)
    delete target.__func
}
//测试
var tObj = {
    name: 'tino'
}

function func(arg1, arg2) {
    console.log('this:', this)
    console.log('name:', this.name)
    console.log('arg1:', arg1)
    console.log('arg2:', arg2)
}

func.callFunc(tObj, "tinolee", 2);
//this: {name: "tino", __func: ƒ}
//name: tino
//arg1: tinolee
//arg2: 2

但是,还是有问题,也就是创建的内部函数__func可能在tObj中已经存在,所以需要做进一步处理

最简单的就是通过时间戳(new Date().getTime())作为唯一标识来创建内部函数__func,当然Sybmol()的唯一性也是可以的,只是还是上面说的尽量用es3来折磨自己吧。

var __func = '__'+new Date().getTime()

Function.prototype.callFunc = function(target) {
    var argList = [];
    for (var i = 0, len = arguments.length; i < len; i++) {
        argList[i] = arguments[i + 1];
    }

    var __func = '__' + new Date().getTime();
    //还可以进一步做暂存
    var ownFunc = target[__func];
    var hasOwnFunc = target.hasOwnProperty(__func);
    target[__func] = this; //this为绑定函数
    // target.__func(...argList);
    //eval('target.__func(' + argList + ')')
    var expression = callExecuteExpression(argList)
    new Function(expression)(target, __func, argList)
    delete target[__func]
    if (hasOwnFunc) {
        target[__func] = ownFunc;
    }
}
var tObj = {
    name: 'tino'
}

function func(arg1, arg2) {
    console.log('this:', this)
    console.log('name:', this.name)
    console.log('arg1:', arg1)
    console.log('arg2:', arg2)
}

func.callFunc(tObj, "tinolee", 2);
//this: {name: "tino", __1585152978563: ƒ}
//name: tino
//arg1: tinolee
//arg2: 2

OK,测试通过; 接下来我们来依照ECMAScript5.1规范来完善下封装的函数

最终代码

function globalThis() {
    return this;
}

function callExecuteExpression(argList) {
    var expression = 'return arguments[0][arguments[1]]('
    for (var i = 0, len = argList.length; i < len; i++) {
        i > 0 ?
            expression += ',arguments[2][' + i + ']' :
            expression += 'arguments[2][' + i + ']'
    }
    expression += ')'
    return expression
}
Function.prototype.callFunc = function(target) {
    //this为绑定函数func
    //15.3.4.4.1
    if (typeof this !== 'function') {
        throw new TypeError(this + ' is not a function');
    }
    //thisArg 是 undefined 或 null 时它会被替换成全局对象
    if (typeof target === 'undefined' || target === null) {
        target = globalThis();
    }
    //所有其他值会被应用 ToObject 并将结果作为 this 值
    target = new Object(target);
    var argList = [];
    for (var i = 0, len = arguments.length; i < len; i++) {
        argList[i] = arguments[i + 1];
    }
    var __func = '__' + new Date().getTime();
    //还可以进一步做暂存
    var ownFunc = target[__func];
    var hasOwnFunc = target.hasOwnProperty(__func);
    target[__func] = this; //this为绑定函数
    // target.__func(...argList);
    //eval('target.__func(' + argList + ')')
    var expression = callExecuteExpression(argList)
    new Function(expression)(target, __func, argList)
    delete target[__func]
    if (hasOwnFunc) {
        target[__func] = ownFunc;
    }
}

console.log(Function.prototype.callFunc.length)
// 1
var tObj = {
    name: 'tino'
}

function func(arg1, arg2, arg3) {
    console.log('this:', this)
    console.log('name:', this.name)
    console.log('arg1:', arg1)
    console.log('arg2:', arg2)
}
func.callFunc(tObj, "tinolee", 2, 4);
//原生call此处的this打印是没有__1585152978563这个属性的,还没思考好这步改怎么解决
//this: {name: "tino", __1585152978563: ƒ}
//name: tino
//arg1: tinolee
//arg2: 2

附apply实现代码

apply的实现其实和call区别不大,主要是call方法的length为1,apply方法的length为2, 所以做了apply的第二个传入数组的处理

Function.prototype.applyFunc = function(target, argsArray) {
    //15.3.4.3
    // 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')
    }
    // thisArg 是 undefined 或 null 时它会被替换成全局对象,所有其他值会被应用 ToObject 并将结果作为 this 值,这是第三版引入的更改。
    if (typeof target === 'undefined' || target === null) {
        target = globalThis();
    }
    target = new Object(target);

    var __func = '__' + new Date().getTime();
    var ownFunc = target[__func];
    var hasOwnFunc = target.hasOwnProperty(__func);
    target[__func] = this; //this为绑定函数
    var expression = callExecuteExpression(argsArray);
    var result = (new Function(expression))(target, __func, argsArray);
    delete target[__func];
    if (hasOwnFunc) {
        target[__func] = ownFunc;
    }
    return result;
}

学习借鉴了大佬们的思路,记录下自己学习的过程,希望能帮到需要的人, 最后感谢大佬们的分享

参考链接

ECMAScript5.1中文版 + ECMAScript3 + ECMAScript(合集)

Function - JavaScript | MDN

JavaScript深入之call和apply的模拟实现