JavaScript基础专题之手动实现call、apply、bind(六)

实现本身的call

MDN 定义:数组

call() 提供新的 this 值给当前调用的函数/方法。你可使用 call 来实现继承:写一个方法,而后让另一个新的对象来继承它(而不是在新对象中再写一次这个方法)。浏览器

简答的归纳就是:bash

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。闭包

举个例子:app

var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar.call(foo); // 1
复制代码

简单的解析一下call都作了什么:函数

第一步:call 改变了 this 的指向,指向到 foopost

第二步:bar 函数执行测试

函数经过 call 调用后,结构就以下面代码:ui

var foo = {
    value: 1,
    bar: function() {
        console.log(this.value)
    }
};

foo.bar(); // 1
复制代码

这样this 就指向了 foo,可是咱们给foo添加了一个属性,这并不可取。因此咱们还要执行一步删除的动做。this

因此咱们模拟的步骤能够分为:

第一步:将函数设为传入对象的属性

第二步:执行该函数

第三部:删除该函数

以上个例子为例,就是:

// 第一步
foo.fn = bar
// 第二步
foo.fn()
// 第三步
delete foo.fn
复制代码

注意:fn 是对象的临时属性,由于执行事后要删除滴。

根据这个思路,咱们能够尝试着去写一个call

Function.prototype._call = function(context) {
    // 首先要获取调用call的函数,用this能够获取
    context.fn = this;
    context.fn();
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

bar._call(foo); // 1
复制代码

OK,咱们能够在控制台看到结果了,和预想的同样。

这样只是将第一个参数做为上下文进行执行,可是并没用传入参数,下面咱们尝试传入参数执行。

举个例子:

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar.call(foo, 'chris', 10);
// chris
// 10
// 1
复制代码

咱们会发现参数并不固定,因此要在 Arguments 对象的第二个参数截取,传入到数组中。

好比这样:

// 以上个例子为例,此时的arguments为:
// arguments = {
//      0: foo,
//      1: 'kevin',
//      2: 18,
//      length: 3
// }
// 由于arguments是类数组对象,因此能够用for循环
var args = [];
vae len = arguments.length
for(var i = 1,  i < len; i++) {
    args.push('arguments[' + i + ']');
}

// 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"]
复制代码

OK,看到这样操做第一反应会想到 ES6 的方法,不过 call 是 ES3 的方法,因此就麻烦一点吧。因此咱们此次用 eval 方法拼成一个函数,相似于这样:

eval('context.fn(' + args +')')
复制代码

这里 args 会自动调用 Array.toString() 这个方法。

代码以下:

Function.prototype._call = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    eval('context.fn(' + args +')');
    delete context.fn;
}

// 测试一下
var foo = {
    value: 1
};

function bar(name, age) {
    console.log(name)
    console.log(age)
    console.log(this.value);
}

bar._call(foo, 'chris', 10); 
// chris
// 10
// 1
复制代码

OK,这样咱们实现了 80% call的功能。

再看看定义:

根据 MDN 对 call 语法的定义:

第一个参数:

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

执行参数:

使用调用者提供的 this 值和参数调用该函数的返回值。若该方法没有返回值,则返回 undefined

因此咱们还须要注意两个点

1.this 参数能够传 null,当为 null 的时候,视为指向 window

举个例子:

var value = 1;

function bar() {
    console.log(this.value);
}

bar.call(null); // 1
复制代码

虽然这个例子自己不使用 call,结果依然同样。

2.函数是能够有返回值

举个例子:

var obj = {
    value: 1
}

function bar(name, age) {
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar.call(obj, 'chris', 10)
// Object {
//    value: 1,
//    name: 'chris',
//    age: 10
// }
复制代码

不过都很好解决,让咱们直接看第三版也就是最后一版的代码:

Function.prototype._call = function (context = window) {
    var context = context;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar._call(null); // 2

console.log(bar._call(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }
复制代码

这样咱们就成功的完成了一个call函数。

实现本身的apply

apply 的实现跟 call 相似,只是后面传的参数是一个数组或者类数组对象。

Function.prototype.apply = function (context = window, arr) {
    var context = context;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}
复制代码

实现本身的bind

根据 MDN 定义:

bind() 方法会建立一个新函数。当这个新函数被调用时,bind() 的第一个参数将做为它运行时的 this,以后的一序列参数将会在传递的实参前传入做为它的参数。

由此咱们能够首先得出 bind 函数的三个特色:

  1. 改变this指向
  2. 返回一个函数
  3. 能够传入参数
var foo = {
    value: 1
};

function bar() {
    console.log(this.value);
}

var bindFoo = bar.bind(foo); // 返回了一个函数

bindFoo(); // 1
复制代码

关于指定 this 的指向,咱们可使用 call 或者 apply 实现。

Function.prototype._bind = function (context) {
    var self = this;
    return function () {
        return self.apply(context);
    }
}
复制代码

之因此是 return self.apply(context) ,是考虑到绑定函数多是有返回值的,依然是这个例子:

var foo = {
    value: 1
};

function bar() {
	return this.value;
}

var bindFoo = bar.bind(foo);

console.log(bindFoo()); // 1
复制代码

第三点,能够传入参数。这个很困惑是在 bind 时传参仍是在 bind 以后传参。

var foo = {
    value: 1
};

function bar(name, age) {
    console.log(this.value);
    console.log(name);
    console.log(age);
}

var bindFoo = bar.bind(foo, 'chris');
bindFoo('18');
// 1
// chris
// 18
复制代码

经过实例,咱们发现二者参数是能够累加的,就是第一次 bind 时传的参数和能够在调用的时候传入。

因此咱们仍是用 arguments 进行处理:

Function.prototype._bind = function (context) {
    var self = this;
    // 获取_bind函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);

    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(context, args.concat(bindArgs));
    }
}
复制代码

完成了上面三步,其实咱们还有一个问题没有解决。

根据 MDN 定义:

一个绑定函数也能使用new操做符建立对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。

举个例子:

var value = 2;

var foo = {
    value: 1
};

function bar(name, age) {
    this.habit = 'shopping';
    console.log(this.value);
    console.log(name);
    console.log(age);
}

bar.prototype.friend = 'james';

var bindFoo = bar.bind(foo, 'chris');

var obj = new bindFoo('18');
// undefined
// chris
// 18
console.log(obj.habit);
console.log(obj.friend);
// shopping
// james
复制代码

尽管在全局和 foo 中都声明了 value 值,仍是返回了 undefind,说明this已经失效了,若是你们了解 new 的实现,就会知道this是指向 obj 的。

因此咱们能够经过修改返回的函数的原型来实现,让咱们写一下:

Function.prototype.bind2 = function (context) {
    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        // 看成为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可让实例得到来自绑定函数的值
        // 以上面的是 demo 为例,若是改为 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改为 this ,实例会具备 habit 属性
        // 看成为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
    }
    // 修改返回函数的 prototype 为绑定函数的 prototype,实例就能够继承绑定函数的原型中的值
    fBound.prototype = this.prototype;
    return fBound;
}
复制代码

可是在这个写法中,咱们直接将 fBound.prototype = this.prototype,咱们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,咱们能够须要一个空函数来进行中转:

Function.prototype._bind = function (context) {

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
复制代码

还存在一些问题:

1.调用 bind 的不是函数咋办?

作一个类型判断呗

if (typeof this !== "function") {
  throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
复制代码

2.我要在线上用

作一下兼容性测试

Function.prototype.bind = Function.prototype.bind || function () {
    ……
};
复制代码

好了,这样就咱们就完成了一个 bind

Function.prototype._bind = function (context) {

    if (typeof this !== "function") {
      throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
    }

    var self = this;
    var args = Array.prototype.slice.call(arguments, 1);

    var fNOP = function () {};

    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}
复制代码

补充

eval根据 MDN 定义:表示JavaScript表达式,语句或一系列语句的字符串。表达式能够包含变量以及已存在对象的属性。

一个简单的例子:

var x = 2;
var y = 39;
function add(x,y){
	return x + y
}
eval('add('+ ['x','y'] + ')')//等于add(x,y)
复制代码

也就说eavl调用函数后,字符串会被解析出变量,达到去掉字符串调用变量的目的。

JavaScript基础系列目录

JavaScript基础专题之原型与原型链(一)

JavaScript基础专题之执行上下文和执行栈(二)

JavaScript基础专题之深刻执行上下文(三)

JavaScript基础专题之闭包(四)

JavaScript基础专题之参数传递(五)

新手写做,若是有错误或者不严谨的地方,请大伙给予指正。若是这片文章对你有所帮助或者有所启发,还请给一个赞,鼓励一下做者,在此谢过。

相关文章
相关标签/搜索