JavaScript 中的 this,apply,call

本文主要是对 JavaScript 中常用的 this,apply 和 call 的使用方法和区别进行整理和解析,主要参考了曾探的《JavaScript 设计模式》中的讲解过程并添加了一些实际使用的细节,以方便使用有遗漏时查阅。

JavaScript 中的 this 解析

JavaScript 中的 this 总是指向一个对象,具体指向哪个对象是在运行时基于函数的执行环境动态绑定的

this 的指向

在实际应用中,this 的指向大致可以分为以下四种:

以上四种基本囊括了 JavaScript 中所有 this 的用法。

this 语义的丢失

以下代码经常出现:

let obj = {
    MyName:'seven',
    getName: function(){
        return this.MyName;
    }
};

console.log(obj.getName()); // 输出:'seven'
let getName2 = obj.getName;
console.log(getName2); // 输出: undefined

当调用 obj.getName() 时,getName 方法是作为 obj 对象的属性被调用的,此时的this指向 obj 对象,所以obj.getName()输出’seven’ 当使用变量 getName2 来引用 obj.getName 时,并调用 getName2 时,此时普通函数调用方式。this 是指向全局 window 的,所以程序的执行结果是 undefined。

平时使用时,可以结合apply 来修正 this。

document.getElementById = (function(func){
    return function(){
        return func.apply(document, arguments);
    }
})(document.getElementById);

let getId = document.getElementById;
let div = getId('app');
console.log(div.id);

ES6 中被影响的this

在 ES6 中引入了箭头函数,箭头函数中的 this 指向定义时所在的对象,而不是运行时所在的对象。
箭头函数可以让 this指向固定化,此特性非常有利于封装回调函数。
this 指向的固定化并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的 this。因为箭头函数没有 this(也没有自己的 arguments 对象),所以箭头函数不能作为构造函数。

let handler = {
    id: '12345',
    init: function(){
        document.addEventListener('click',
            event => this.doSomething(event.type),false
        );
    },
    doSomething:function(type){
        console.log('Handling '+type+' for '+this.id);
    }
}

上述代码中的 this 一直指向定义时所在的对象 handler,所以才能回调执行 this.doSomething 不报错。
ES6 转换成 ES5 就可看出 this 在编译解析时的指向:

function foo(){
    setTimeout(()=>{
        console.log('id:',this.id);
    },100);
}

// 使用 Babel 转换成 ES5 后
function foo(){
    var _this = this;
    setTimeout(function(){
        console.log('id:',_this.id);
    },100)
}


JavaScript 中的 call 和 apply

call 和 apply 方法可以很好地体现JavaScript 的函数式语言特性,在诸多设计模式中,也会用到call 和 apply。

call 和 apply的区别

apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数一般为集合(数组),apply 将这个集合的元素作为参数传递给被调用的函数。

call 传入的参数数量不固定,与 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数。

fn.call(obj,arg1,arg2,arg3);
fn.apply(obj,[arg1,arg2,arg3]);

call 和 apply 的区别在于传入参数的形式,apply是所有的参数封装成数组传入需要调用的函数,call是所有参数还是以独立的形式作为多个参数传入待执行的函数。

call 是包装在 apply 上面的一个语法糖。

在使用 call 和 apply 时,如果第一个参数为 null,函数体内的 this 会指向默认的宿主对象,在浏览器中则是 window。若是在 严格模式下,函数体内的 this 还是为 null.

有时使用 call 或者 apply 目的不在于使用 this 的指向,可能是借用其他对象的方法,可传入 null 用于代替某个具体的对象。

使用原生的JavaScript 模拟 call 和 apply

因为 call 的作用是转换执行对象的环境,所以模拟 call 的整体思路是:

示例代码如下:

// 模拟 call 方法
Function.prototype.defineCall = function(context){
  context = context || window;
  context.fn = this;
  let args = [];
  for (let i = 1; i < arguments.length; i++) {
    args.push(arguments[i]);
  }
  let result = context.fn(args.join(','));
  delete context.fn;
  return result;
}

let sayName = function(age){
  console.log('current name: '+ this.name,"current age: "+age);
}
let obj = {
  name:"obj's name"
}
sayName.defineCall(obj,22);
// current name: obj's name current age: 22

使用同样的思路模拟 apply 方法,代码如下:

// 模拟 call 方法
// 模拟 apply 方法
Function.prototype.defineApply = function(context,arr){
  context = context || window;
  context.fn = this;
  let result;
  if(!arr){
    result = context.fn();
  }else {
    let args = [];
    for (let i = 0; i < arr.length; i++) {
      args.push(arr[i]);  
    }
    result = context.fn(args.join(','));
  }
  delete context.fn;
  return result;
}

let obj2 = {
  name: ['Tom','Johy','Joe','David']
}
sayName.defineApply(obj2,[3,4,5,6,7]);
// current name: Tom,Johy,Joe,David current age: 3,4,5,6,7

apply中如果不需要处理传入的数组,也可以直接传入数组。

call 和 apply 的用途

1. 改变 this 指向

call 和 apply 最常见的用途是改变函数内部的 this 指向:

let obj1 = {
    name: 'John'
}

let obj2 = {
    name: 'Steve'
}

window.name = 'window';
let getName = function(){
    console.log(this.name);
}

getName();// window
getName.call(obj1);// John
getName.call(obj2);// Steve

以上代码中,getName.call(obj1) 执行时,getName 函数体内的 this 就指向 obj1 对象。
在实际使用中,常使用 call 和 apply 来修正 this 的指向。


// 用 call 修正 func 函数内的 this,使其仍指向 div

document.getElementById('div1').onclick = function(){
    let func = function(){
        console.log(this.id);
    }
    func.call(this); // 若没有此句,func 输出 undefined
}

document.getElementById = (function(func){
    return function(){
        return func.apply(document, arguments);
    }
})(document.getElementById);

let getId = document.getElementById;
let div = getId('content');
console.log(div.id); // content

上述代码在实现使用 getId 代替系统的 document.getElementById 时,就使用 的 apply 改变 this 的指向到 document。

2. 模拟 Function.prototype.bind
Function.prototype.bind 函数用来指定函数内部的 this 指向,可以用以下代码来模拟:

Function.prototype.bind = function( context ){
    let self = this; // 保存函数的引用
    return function(){ // 返回一个新的函数
        return self.apply(context, arguments);
    }
};

let obj = {
    name: 'seven'
}

let func = function(){
    alert(this.name)
}.bind(obj);
func();

上述代码中,传入的 context 对象是参数,该 context 对象就是要修正的 this 对象。 实际使用过程中如果需要预存参数,并且可以处理后续传入的参数,可以做以下改动:

Function.prototype.bind = function(){
    let self = this,
        context = [].shift.call(arguments),
        args = [].slice.call(arguments);
   return function(){
       return self.apply(context, [].concat.call(args, [].slice.call(arguments)));
   }
};
let obj = {
    name: 'sven';
}
let func = function(a,b,c,d){
    console.log(this.name);
    console.log([a,b,c,d]); // 1,5,7,8
}.bind(obj,1,5); // 可做到预存参数

func(7,8); // 后续传入参数

3. 借用其他对象的方法
借用其他对象的方法,第一种可以使用 apply 或 call 借用构造函数,以实现类似继承的效果,这种继承方法类似 JavaScript 继承 中的 构造函数式继承。

let A = function(name){
    this.name = name;
}
let B = function(){
    A.apply(this, arguments);
}

B.prototype.getName = function(){
    return this.name;
}
let b = new B('Mack');
console.log(b.getName()); // output: Mack

这种方式,在子类的构造函数中执行父类构造函数。

第二种应用场景常用于借用原型的方法实现相关的操作,例如常使用 Array.prototype 对象的方法完成数组的处理:

(function(){
    Array.prototype.push.call(arguments, 3);
    console.log(arguments); // 1,2,3
})(1,2);

还有就是常用来判断变量的数据类型是否是数组的方法:

let arr = [1,2,3];
Object.prototype.toString.call(arr) === "[object Array]"

以上这几种 都是 apply 或者 call 用来借用其他对象的方法,都比较常用。

参考资料