实现apply call bind

1,161 阅读4分钟

一:从this指向开始

1: this指向分类

this指向分为默认绑定,隐式绑定,显式绑定,new操作,箭头函数这几类。

(1): 默认绑定:

声明一个函数:

function test1 () {
    console.log(this);     // window
};
test1();

在window的非严格模式下: this指向为window. 在严格模式下为undefined

(2): 隐式绑定:函数引用有上下文的时候, 隐式绑定为这个上下文。

(3): important: 显示绑定:

显示绑定的意思就是可以用户手动的绑定this指向。

经常使用的改变this指向的方法就是call,apply, bind,后面会详细介绍。

(4): new操作, 通过new操作符可以让this指向固化在实例上。

(5): 箭头函数: 箭头函数的this指向它外层普通函数的this指向。

function foo () {
    var vm = () => {
        console.log(this);
    };
    vm();           // Window
};

二:Function.prototype.call

 call方法使用一个指定的this值和单独给出的一个或多个参数来调用一个函数。            ---MDN

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

(1): 常见的使用

通过call方法可以实现继承:

function Father () {
    this.books = ['js', 'vue'];
    this.name = 'yhq';
};
Father.prototype.sayBooks = function () {
    console.log(this.books);
};
function Son () {
    Father.call(this);
};
var son = new Son();
son.books      //  ["js", "vue"]
son.sayBooks    //  Uncaught TypeError: son.sayBooks is not a function 

这种继承只能继承父类上的属性和方法,不能继承父类原型上的。

通过call方法调用匿名函数:

var animals = [
  { species: 'Lion', name: 'King' },
  { species: 'Whale', name: 'Fail' }
];

for (var i = 0; i < animals.length; i++) {
  (function(i) {
    this.print = function() {
      console.log('#' + i + ' ' + this.species
                  + ': ' + this.name);
    }
    this.print();
  }).call(animals[i], i);
}

(2): 实现一个call

fn.call(obj, args1, args2,...);

call方法接受一个参数是obj。让this指向这个obj。我们的想法是把fn已属性的方式加入这个obj中,执行call操作之后delete这个新增的属性。

第一版代码:

Function.prototype.myCallOne = function (ctx) {
    var args = [...arguments].slice(1);  // 这里使用es6的方法,取出了this外的所有参数
    ctx.fn = this;         // 改变this指向
    var result = ctx.fn(...args);     // 执行函数,因为args为数组,所以解构出来
    delete ctx.fn;
    return result;
}

我们来测试一下:

var obj = {
    name: 'yhq'
};
function funcs(age) {
    console.log('name:'+ this.name + 'age:' + age);
};
funcs.myCallOne(obj, 24);           // name:yhq age:24

ok ! 没有问题。

优化一下:

这里有几个小问题: 如果没有call的时候没有传入obj呢? 如果原有的对象上有fn呢?

处理这两个问题: 如果没有传入默认为window, 保证fn的唯一性。

第二个版本:

Function.prototype.myCallTwo = function (ctx) {
    ctx = ctx || window;
    var fn = Symbol();       //  Symbol属性来确定fn唯一。
    var args = [...arguments].slice(1);
    ctx[fn] = this;
    var result = ctx[fn](...args);
    delete ctx[fn];
    return result;
}

测试一下:

var obj2 = {
    name: 'yhq',
    fn: function () {}
};
function practive(age) {
    console.log('name:'+ this.name + 'age:' + age);
};
funcs.myCallTwo(obj, 24);           // name:yhq age:24

ok啦。。

三: Function.prototype.apply

apply() 方法调用一个具有给定this值的函数,以及以一个数组(或类数组对象)的形式提供的参数                                                                                                                     ---MDN

apply()和call()的使用场景基本相同,主要就是接受的参数不一样1,apply接受的是一个数组,call接受的是一个参数列表。

实现一个apply:

fn.apply(this, []); 

思路类似于call,注意一点,可以通过直接传入一个array来接受参数,不需要通过arguments

代码如下:

Function.prototype.myBind = function (ctx, array) {
    ctx = ctx || window;
    var fn = Symbol(); 
    var result;
    if (!array) {    // 判断array是否存在        
        result = ctx[fn]();    
    }    
    else {        
        result = ctx[fn](...array);    
    }    
    delete ctx[fn];    
    return result;
};

测试一下:

var obj2 = {
    name: 'yhq',
    fn: function () {}
};
function practive(age) {
    console.log('name:'+ this.name + 'age:' + age);
};
funcs.myBind(obj, [24]);           // name:yhq age:24

OK!

四: Function.prototype.bind

**bind()** 方法创建一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时使用                                  ---MDN

fn.bind(obj, args1, args2...);  返回的是一个执行上下文函数,需要用户手动调用才能执行。

bind函数有如下特性:

1.  改变this指向

2. 接受参数列表

3. return 一个函数

4. 遇到new这样优先级高于bind方法的时候,bind改变this指向会无效。

ok! 我们来实现一下。

Function.prototyep.myBind = function (ctx) {
    var self = this;   // 因为之后要return一个函数,这里保存一下this防止混乱
    var args = Array.prototype.slice.call(arguments, 1);   // 获取参数
    var fb = function () {
        var bindArgs = Array.prototype.slice.call(arguments, 1);
        return self.apply( this instanceof fb ? this : ctx,
            args.concat(bindArgs))    };
    var fn = function () {};
    fn.prototype = this.prototype;     // 让实例可以继承原型
    fb.prototype = new fn();
    return fb;
}

解析几个问题:

self.apply( fb instance this ? this : ctx, args.concat(bindArgs));

如果是构造函数,this指向的是实例, this instanceof fb === true, 此时不需要修改this,直接取原来的this。 或者为ctx。

new操作的话,需要实例可以继承原型。所以这里有个继承的关系。

通过一个fn的中介来继承原型。

基本实现就是这样子,之后我在慢慢完善。