felix-cao / Blog

A little progress a day makes you a big success!
31 stars 4 forks source link

JavaScript 函数的 call/apply/bind 方法 #45

Open felix-cao opened 6 years ago

felix-cao commented 6 years ago

《JavaScript 函数的属性和方法》 中我们聊到了 JavaScript 中所有的函数都是 Function() 构造器的实例对象,并且所有函数都继承了构造器的三个方法 call() apply() bind(),本篇来聊一聊这三个方法。

一、索源

callapplybind 这三个方法其实都是继承自 Function.prototype 中的,属于实例方法。

console.log(Function.prototype.hasOwnProperty('call')) // true
console.log(Function.prototype.hasOwnProperty('apply')) // true
console.log(Function.prototype.hasOwnProperty('bind')) // true

上面代码中,都返回了 true,表明三种方法都是继承自 Function.prototype 的。当然,普通的对象,函数,数组都继承了 Function.prototype 对象中的三个方法,所以这三个方法都可以在对象,数组,函数中使用。

二、call 方法

函数实例的 call 方法,可以指定该函数内部 this 的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数。并且会立即执行该函数。

var teacher = {
    name: 'up8'
}
var name = 'Felix';
person(); // Felix
person.call(); // Felix
person.call(null); // Felix
person.call(undefined); // Felix
person.call(this); // Felix
person.call(teacher);  // up8

function person() {
  console.log('my name is', this.name);
}

上面代码中,person 函数中的 this 关键字,如果指向全局对象,返回结果为 Felix。可以看到,如果call 方法没有参数,或者参数为 nullundefined 或者 this,则等同于指向全局对象。如果使用 call方法将 this 关键字指向 teacher 对象,也就是将该函数执行时所在的作用域为 teacher 对象,返回结果为 up8

call() 方法可以传递多个参数。第一个参数是指定函数内部中 this 的指向(也就是函数执行时所在的作用域),其余参数都是函数调用时需要传递的参数。

function add(x, y) {
  console.log(x+y);
}
add.call(null, 1, 2) // 3

第一个参数是必须的,可以是 nullundefinedthis,但是不能为空。设置为 nullundefinedthis 表明函数 add 此时处于全局作用域。其余参数中必须一个个添加。而在 apply 中必须以数组的形式添加。

三、apply 方法

apply 方法的作用与 call 方法类似,也是改变 this 指向(函数执行时所在的作用域),然后在指定的作用域中,调用该函数。同时也会立即执行该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数。 apply 方法的第一个参数与 call 的第一个参数一样,也是 this 所要指向的那个对象,如果设为 nullundefined 或者 this,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,在调用时传入原函数。原函数的参数,在 call 方法中必须一个个添加,但是在 apply方法中,必须以数组形式添加。

function add(x, y) {
  console.log(x+y);
}
add.apply(null, [1, 2]) // 3

四、bind方法

bind 方法用于指定函数内部的 this 指向(执行时所在的作用域),然后返回一个新函数。bind 方法并非立即执行一个函数**

var person = {
  name: 'Felix Cao',
  getName: function() {
    console.log('this.name: ', this.name);
  }
}
person.getName();

上面代码中,this.name 指向 person 对象的内部属性 name, 如果把 person 对象的 getName 方法赋值给另外一个变量,调用执行后就不是期望的结果

var person = {
  name: 'Felix Cao',
  getName: function() {
    console.log('this.name: ', this.name);
  }
}
var personName = person.getName;
personName()

为什么呢?在 《JavaScript 之 this绑定/指向》 这篇文章中 提到 隐式绑定中的上下文丢失, 为了解决这个问题,可以使用 bind 方法,将 person 对象里的this 绑定到 person 对象上,或者是直接调用。

var personName = person.getName.bind(person);
personName()

五、实现 bind

我们来实现一个函数的 bind 方法:

Function.prototype.bind = function( context ){
  var that = this; // 保存原函数 
  return function(){ // 返回一个新的函数 
    // 执行新的函数的时候,会把之前传入的 context 对象当作新函数体内的 this
    return that.apply( context, arguments );
  }
};

var obj = { name: 'Felix' };
function person() {
  console.log(this.name);
}
var personFn = person.bind(obj); // person 函数调用 bind 方法将其内部的 this 指向 obj
personFn(); // ‘Felix’

上面的代码,我们在 Function 构造器的原型对象中定义了一个 bind 方法,然后我们定义了一个 person 函数,person 函数本身没有 bind,于是使用了它的构造器的原型对象的 bind 方法。

再来点复杂的:

Function.prototype.bind = function(){
  var that = this, // 保存原函数 
  obj = [].shift.call( arguments ), // this 指向谁 
  args = [].slice.call( arguments ); // 剩余的参数转成数组 
  return function(){ // 返回一个新的函数 
    // 执行新的函数的时候,会把之前传入的 obj 当作新函数体内的 this
    // 并且组合两次分别传入的参数,作为新函数的参数
    that.apply( obj, [].concat.call( args, [].slice.call( arguments ) ) );  
} };

var obj = { name: 'Felix' };
var fun = function( a, b, c, d ){ 
  console.log( this.name ); 
  console.log( [ a, b, c, d ] ) 
}.bind( obj, 1, 2 );

fun( 3, 4 );

六、总结

上面的学习中,可以看出 call, apply, bind 三者的区别:

有时候我们使用 call 或者 apply 的目的不在于指定 this 指向,而是另有用途,比如借用其他对象的方法,那么我们可以传入 null 来代替某个具体的对象:

Math.max.apply( null, [ 1, 5, 8, 2, 9 ])

Reference