理解并实现call、apply、bind

做用

改变函数执行时的上下文(改变函数运行时的this指向)javascript

引子

// 写一个构造函数类
function Person(name){
  this.name = name;
}
Person.prototype = {
  constructor: Person,
  showName: function(){
    console.log(this.name);
  }
}
// 用该类new出的新对象拥有此类的属性和方法
var person = new Person('wuqinhao');
person.showName();
复制代码

而后咱们遇到个小需求,有一个对象不是基于此类new出,但想要showName方法,改如何处理呢java

// 这个对象只有一个name属性,此时它想拥有一个showName方法
var animal = {
  name: 'cat'
}
// 虽然它能够再写一遍showName方法,可从代码设计角度来讲不太优雅,可重用的方法写了两遍。
// 因而创始者发明了call、apply、bind三个方法来解决此问题

// 1 call
person.showName.call(animal);
// 2 apply
person.showName.apply(animal);
// 3 bind
person.showName.bind(animal)();
复制代码

call的定义

apply的定义

咱们发现apply和call差很少,差异是一个传数组,一个传多个参数。这是创始者为了开发者在不一样的语境方便调用不一样的方法。(就好比你设计一个类库时可能也会暴露两个不一样的API,方便开发者调用)git

bind的定义

call的应用

// 1.使用 call 方法调用父构造函数
// 通常用call,能够明确的看到传递的参数
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call(this, name, price);
  this.category = 'food';
}

function Toy(name, price) {
  Product.call(this, name, price);
  this.category = 'toy';
}

var cheese = new Food('feta', 5);
var fun = new Toy('robot', 40);
console.log(cheese);
console.log(fun);
复制代码

apply的应用

// 1.数组合并
// 通常用apply,传递数组方便
var arr1 = [1, 2, 3];
var arr2 = [4, 5, 6];
[].push.apply(arr1, arr2);
console.log(arr1);
console.log(arr2);
// arr1 [1, 2, 3, 4, 5, 6]
// arr2 [4,5,6]
复制代码
// 2.调用封装好的内置函数
/* 找出数组中最大/小的数字 */
var numbers = [5, 6, 2, 3, 7];

/* 应用(apply) Math.min/Math.max 内置函数完成 */
var max = Math.max.apply(null, numbers); /* 基本等同于 Math.max(numbers[0], ...) 或 Math.max(5, 6, ..) */
var min = Math.min.apply(null, numbers);

// 若是咱们不用apply,那只能用通常的for循环逐一查找
var max = -Infinity;
var min = +Infinity;
for (var i = 0; i < numbers.length; i++) {
    if (numbers[i] > max) { max = numbers[i]; }
    if (numbers[i] < min) { min = numbers[i]; }
}
复制代码

bind的应用

// 1.配合 setTimeout 绑定this到当前实例,而不是window
function Person(name, age){
  this.name = name;
  this.age = age
}
Person.prototype = {
  constructor: Person,
  showName: function(){
    console.log(this.name);
  },
  showAge: function(){
    setTimeout(function () {
        console.log(this.age)
    }.bind(this), 1000)
    
    // setTimeout(function () {
    // console.log(this.age)
    // }, 1000)
    // 若是不bind,这里的setTimeOut是window下面的方法,因此this指向会指到window,而window下没有age,因此会输出undefined
    
    // setTimeout( () => {
    // console.log(this.age)
    // }, 1000)
    // 因为es6里出现的箭头函数,他能将this指向当前调用的实例上,因此用此方法也是可行的
  }
}
// 用该类new出的新对象拥有此类的属性和方法
var person = new Person('wuqinhao', 26);
person.showAge();
// 一秒中后打印 26
复制代码
// 2.偏函数(使一个函数拥有预设的初始参数)
function addArguments(arg1, arg2) {
    return arg1 + arg2
}
var result1 = addArguments(1, 2); // 3

// 建立一个函数,它拥有预设的第一个参数
var addThirtySeven = addArguments.bind(null, 37); 
var result2 = addThirtySeven(5); 
// 37 + 5 = 42 
var result3 = addThirtySeven(5, 10);
// 37 + 5 = 42 ,第二个参数被忽略

/* 只要将这些参数(若是有的话)做为bind()的参数写在this后面。 当绑定函数被调用时,这些参数会被插入到目标函数的参数列表的开始位置, 传递给绑定函数的参数会跟在它们后面。*/
复制代码

call模拟实现

1.模拟实现基本语法

咱们看前面的引子示例,animal想执行showName方法,但又不想从新写一遍就调用了call方法。若是咱们写一遍showName方法,执行完,而后再删除这个方法,而把这个过程封装成一个函数,这个函数不就有点像call嘛。es6

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

  • 1.将函数设为对象的属性
  • 2.执行该函数
  • 3.删除该函数
// 初版
Function.prototype.call2 = function(context) {
    // 首先要获取调用call的函数,用this能够获取
    context.fn = this;
    context.fn();
    // fn这个名字能够随便取,由于后面会delete掉
    delete context.fn;
}

// 测试一下(此代码的前提是要有引子里的代码)
person.showName.call2(animal); // cat
复制代码

2.模拟实现传递参数

注意:传入的参数并不肯定,咱们能够从 Arguments 对象中取值,取出第二个到最后一个参数,而后放到一个数组里。数组

// 第二版
Function.prototype.call2 = function(context) {
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    console.log(args); // ["arguments[1]", "arguments[2]"]
    eval('context.fn(' + args +')'); // 至关于eval('context.fn(arguments[1],arguments[2])')
    // args会自动调用 Array.toString()
    delete context.fn;
}

// 测试一下
// 1.使用 call 方法调用父构造函数
// 通常用call,能够明确的看到传递的参数
function Product(name, price) {
  this.name = name;
  this.price = price;
}

function Food(name, price) {
  Product.call2(this, name, price);
  this.category = 'food';
}

var cheese = new Food('feta', 5);
console.log(cheese);
复制代码

eval语法请看 developer.mozilla.org/zh-CN/docs/… Arguments语法请看 developer.mozilla.org/zh-CN/docs/…app

3.模拟实现this绑定null和函数返回值

1.this 参数能够传 null,当为 null 的时候,视为指向 window(当不绑定this时,能够指向window)函数

2.函数是能够有返回值(retrun 出eval的结果便可)测试

// 第三版
Function.prototype.call2 = function(context) {
    context = context || window;
    context.fn = this;
    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }
    console.log(args); // ["arguments[1]", "arguments[2]"]
    var result = eval('context.fn(' + args +')'); // 至关于eval('context.fn(arguments[1],arguments[2])')
    // args会自动调用 Array.toString()
    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.call2(null); // 2

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

apply模拟实现

apply的实现与call类型,区别在与apply传入数组。因此arguments获取改为arr数组。优化

Function.prototype.apply2 = function(context, arr) {
    context = context || window;
    context.fn = this;
    
    var result;
    if (!arr) {
        result = context.fn()
    } else {
        var args = [];
        for(var i = 1, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        console.log(args); // ["arr[1]", "arr[2]"]
        result = eval('context.fn(' + args +')'); // 至关于eval('context.fn(arr[1],arr[2])')
        // args会自动调用 Array.toString()
    }
    delete context.fn;
    return result;
}
复制代码

bind模拟实现

1.返回一个函数。2.能够传入参数。3.一个绑定函数也能使用new操做符建立对象。

1.模拟返回函数

Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
    // 应用apply来指定this的指向
        self.apply(context);
    }
}
复制代码

有时绑定的函数可能也会有返回值,因此绑定的函数return了一个内容,若是再也不加个return,内容是返回不出来的。

Function.prototype.bind2 = function (context) {
    var self = this;
    return function () {
    // 应用apply来指定this的指向
        return self.apply(context); // 再加一个return,将绑定函数的返回值return出来。
    }
}
复制代码

2.模拟传参

先看一个例子(传参还能在返回的函数里继续传)

var foo = {
    value: 1
};

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

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

解决办法:仍是使用arguments,只不过在最后将参数合并到一块儿再使用

Function.prototype.bind2 = function (context) {
    var self = this;
    
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);
    
    return function () {
        // 这个时候的arguments是指bind返回的函数传入的参数
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 应用apply来指定this的指向
        return self.apply(context, args.concat(bindArgs)); // 再加一个return,将绑定函数的返回值return出来。
    }
}
复制代码

3.模拟bind返回的函数做为构造函数调用

看个例子

var value = 2;

var foo = {
    value: 1
};

function bar (name, age) {
    this.a = 'aaa';
    console.log(this.value);
    console.log(name);
    console.log(age);
}
bar.prototype.b = 'bbb';

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

var obj = new bindFoo('18');
// undefined
// daisy
// 18
console.log(obj.a);
console.log(obj.b);
// shopping
// kevin
复制代码

咱们看到现象:当 bind 返回的函数做为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。this指向了obj,然而obj上没有value属性,因此是undefined

实现

Function.prototype.bind2 = function (context) {
    var self = this;
    
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);
    
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 看成为构造函数时,this指向实例,此时结果为true,将绑定函数的this指向该实例,可让实例得到来自绑定函数的值
        return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs));
        // 看成为普通函数时,this指向window,此时结果未false,将绑定函数的this指向context
        // 以上代码若是改为`this instanceof fBound ? null : context`实例只是一个空对象,将null改成this,实例会具备绑定函数的属性
    }
    fBound.prototype = this.prototype;
    
    return fBound;
}
复制代码

优化一波

1.在这个写法中,咱们直接将 fBound.prototype = this.prototype,咱们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,咱们能够经过一个空函数来进行中转 2.当调用bind的不是函数时,咱们要抛出异常

Function.prototype.bind2 = function (context) {
    if (typeof this !== 'function') {
        throw new Error('Function.prototype.bind - 试图绑定的内容不可调用');
    }
    
    var self = this;
    
    // 获取bind2函数从第二个参数到最后一个参数
    var args = Array.prototype.slice.call(arguments, 1);
    
    var f = function () {};
    
    var fBound = function () {
        var bindArgs = Array.prototype.slice.call(arguments);
        
        // 看成为构造函数时,this指向实例,此时结果为true,将绑定函数的this指向该实例,可让实例得到来自绑定函数的值
        return self.apply(this instanceof f ? this : context, args.concat(bindArgs));
        // 看成为普通函数时,this指向window,此时结果未false,将绑定函数的this指向context
        // 以上代码若是改为`this instanceof f ? null : context`实例只是一个空对象,将null改成this,实例会具备绑定函数的属性
    }
    f.prototype = this.prototype;
    fBound.prototype = new f();
    
    return fBound;
}
复制代码

参考至 github.com/mqyqingfeng…