翻过JS的大山——this指向

459 阅读4分钟

前言

在this之前,js当中没有一个机制可以让对象中的函数去访问对象自己的属性,如下面这段代码所示:调用对象obj中的方法bar访问obj内部属性,直接打印myname会报错,倘若打印this.myname就不会。所以,为了让对象中的函数有能力访问对象自己的属性,我们需要this这样一个机制。

let obj = {
    myname: 'qing',
    bar: function(){ //在对象内部方法使用对象内部的属性
        console.log(myname);
    }
}
obj.bar()

this的指向

this关键字能在全局、函数体内和欺骗词法作用域内被使用,不过欺骗词法用的太少,本文暂且先不讨论。

全局的this,在浏览器上代指Window对象,在node上代指global对象。那么在函数体内的this指向,有这么几个规则:

this的绑定规则

  1. 默认绑定:当一个函数独立调用,即不带任何修饰符的时候,函数在哪个词法作用域下生效,函数中的this就指向哪里。通俗点说,只要是默认绑定,this就指向window(因为一个函数不带任何前缀被独立调用的时候,就说明它被声明在了全局中)
  2. 隐式绑定:当函数的引用有上下文对象的时候(当函数被某个对象所拥有时),函数的this指向引用它的对象
  3. 隐式丢失:当一个函数被多个对象链式调用时,函数的this指向就近的那个对象
  4. 显式绑定:调用方法call apply bind强行掰弯this指向我们想要的地方去
  5. new绑定:当使用构造函数创建对象时,构造函数内部的this会绑定到新创建的对象实例上

词法作用域就是定义在词法阶段的作用域,函数被声明在哪里,哪里就是它的词法作用域。词法作用域是一生下来就决定了,并且一般不会再改变

请看以下例子来一一解释上面的规则:

  1. 函数在全局中被独立调用时,this指向window
function foo() {
    console.log(this);  // window
}
foo() 
  1. 尽管foo函数在bar函数中被调用,foo函数的this还是指向window对象
function foo() {
    console.log(this);  // window 
}
function bar() {
    foo() 
}
bar()
  1. 当foo函数被obj对象所拥有时,foo的this指向obj
var obj = {
    a:1,
    foo: function(){
        console.log(this);  // { a: 1, foo: [Function: foo] }
    }
}
obj.foo()
  1. 函数obj2引用了函数obj,函数obj又引用了函数foo,foo被链式调用,this指向离它最近的那个对象obj
var obj = {
    a: 1,
    foo: foo   //引用foo函数体 
}

var obj2 = {
    a: 2,
    bar: obj
}

function foo() {
    console.log(this.a);  // 1
}

obj2.bar.foo()
  1. call方法将函数bar的this强行指向obj,它是Function构造函数的显式原型上的方法
var obj = {
    a:1
}

function bar() {
    console.log(this.a);   // 1
}

bar.call(obj) 

② 若函数bar本身就有参数,就这样给call方法传参:

var obj = {
    a:1
}

function foo(x,y) {
    console.log(this.a, x + y);  // 1 5 
}

foo.call(obj, 2, 3)
  1. apply方法与call方法的作用一样:将函数foo的this指向强行变成函数obj,只不过接收参数的方式不一样,call方法是零散地接收参数,而apply方法会把参数用数组装起来
var obj = {
    a:1
}

function foo(x,y) {
    console.log(this.a, x + y);  // 1 5 
}

foo.apply(obj, [2, 3])
  1. bind方法也是将函数foo的this指向强行变成函数obj,但是调用bind方法后默认返回一个函数体
var obj = {
    a:1
}

function foo(x, y) {
    console.log(this.a, x + y);  // 1 5 
}

var bar = foo.bind(obj, 2, 3)
bar();

bind方法接收参数同样是零散的,但是不仅可以由bind()自己接收,可以由返回的那个函数体bar()接收,还可以两个混合帮忙接收;当混合接收了多余的参数时,就近算bind()接收的参数为有效参数

// 参数由 bind 调用后返回的函数体接收
var bar = foo.bind(obj)
bar(2, 3)
// 由返回的函数体和 bind 混合接收
var bar = foo.bind(obj, 2)
bar(3)
// 接收了多余的参数,返回的函数体bar接收的参数就作废,最终打印出 1 6
var bar = foo.bind(obj, 2, 4)
bar(3) 
  1. Person构造函数内部的“this”指向新创建的john对象。this.namethis.greet都被绑定到了john实例上
function Person(name) {
    this.name = name;
    this.greet = function() {
        console.log("Hello, my name is " + this.name);
    };
}

var john = new Person("John");
john.greet();  // 输出: "Hello, my name is John"

箭头函数

ES6起引入了一种函数的新写法————箭头函数,箭头函数没有this这个机制,但这并不是说箭头函数中不能写this,而是写在箭头函数中的this只能是它外层函数的this而非箭头函数自己的

function foo() {
    var bar = () => {
        console.log(this); // 是外层函数foo的this,指向window
    }
    bar()
}
foo()

总结

this只在全局和函数体中作用,关于this的指向问题应该重点记函数体内的情况:独立调用时指向window、被拥有时指向引用它的对象、链式调用时就近指向、call(),apply(),bind()可强行掰弯指向、构造函数内部this指向实例对象。