JavaScript的this绑定

128 阅读5分钟

JavaScript的this绑定

要了解JavaScript(下称 js)的this绑定机制首先要明白函数调用链。 js中的函数调用链决定了this的绑定。

注:本文代码均运行于浏览器下。

以下代码简单解释了函数的调用栈:

function baz() {
  // baz在bar中调用,此时调用栈为:window -> foo -> bar -> baz
  console.log("baz");
}

function bar() {
  // bar在foo中调用,所以此时调用栈新增bar,为:window -> foo -> bar
  console.log("bar");
  baz(); // baz的调用位置,在bar中调用
}

function foo() {
  // 由全局调用的foo函数,所以此时调用栈是window -> foo(window是浏览器的全局对象)
  console.log("foo");
  bar(); // bar的调用位置,在foo下的作用域调用
}

foo(); // foo的调用位置,在全局作用域调用

es6之前js的this绑定主要有四种情况:默认绑定隐式绑定显式绑定new绑定

箭头函数的this指向比较特殊,他指向声明时语境下的this对象

默认绑定


当函数在全局调用,即上级调用是window对象或是直接不带修饰而调用时,通常应用默认绑定。 **默认绑定机制在非严格模式下将会把this绑定到全局对象上。**在严格模式this将会绑定到undefined

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

var a = 10;
foo(); // 10

代码中foo函数调用栈为window -> foo,所以在foo中的this将会应用默认绑定形式。foo中的this将会绑定为window,实际执行的this.a则等于window.a

另外一种上文提到的直接不带修饰而调用的情况,光这么说可能比较难理解,直接上代码:

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

var a = 10;

var obj = {
  a: 1,
  foo: foo,
};

var obj2 = {
  a: 2,
  cb: cb,
};

function cb(callback) {
  callback();
}

obj2.cb(obj.foo); // 10
// 与cb相似的setTimeout等各种内部没有对回调函数进行bind、call、apply等强绑定的函数也是应用默认绑定

像代码中将obj.foo当作参数传入cb函数中,实际上是将foo函数的引用传入了函数作为参数,在内部调用callback相当于直接调用了foo,即所说的直接不带修饰而调用的情况,因而foo的this被默认绑定为全局对象。 在js中回调函数内this应用默认绑定是一件很常见的事情,这种现象也有人叫this丢失。

隐式绑定


当函数被调用的位置有上下文对象时,则应用隐式绑定

隐式绑定机制在函数调用位置拥有上下文对象时会把this绑定到上一层的上下文对象。

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

var a = 1;

var obj = {
  a: 2,
  foo: foo,
};

obj.foo(); // 2

根据之前所说的调用链分析,fooobj对象调用的,因而foo的上下文对象是obj,此处的this应绑定为obj对象。 需要注意的是调用链中只有最后一层才会在调用位置起作用。

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

var a = 1;

var obj1 = {
  a: 10,
  foo: foo,
};

var obj2 = {
  a: 20,
  obj1: obj1,
};

obj2.obj1.foo(); // 10

如果obj1内没有a也不会找到obj2中的a,只会返回undefined

隐式绑定有一种特殊情况,在《你不知道的JavaScript》中作者叫它隐式丢失。 看如下代码:

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

var a = 1;

var obj = {
  a: 2,
  foo: foo,
};

var bar = obj.foo;

bar(); // 1

此处将obj.foo赋值给了bar,实际上只是将foo函数的引用赋值给了bar,所以此处调用bar相当于不带修饰地直接调用了foo隐式丢失导致此处应用了默认绑定,即此处的this绑定到了全局对象。

显式绑定


显式绑定比较容易理解,就是通过函数原型上内置的callapplybind等将this指明绑定对象。

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

var a = 1;

var obj = {
  a: 2,
};

foo.call(obj); // 2
foo.call(window); // 1

new绑定


在认识这种绑定方式之前,我们首先需要理清js中最常见的函数和对象之间的关系。 在其他传统的面向对象编程语言中,构造函数通常是类中的一些特殊方法,使用new关键字调用类中的构造函数。通常像这样:

something = new MyClass(...)

在es6之前,js的构造函数会直接用来创建对象。像是这样:

function Person(name, age) {
  this.name = name;
  this.age = age;
}

var man = new Person("skyle", 22);

console.log(man); // Person{name: "skyle",age: 22}

但js中的构造函数其实只是一个被new操作符调用的普通函数。 以js中的Number()为例子:

console.log(Number("10")); // 10
console.log(new Number("10")); // Number{10}

我们可以将new看为一个可以改变函数调用方式的操作符关键字,当使用new关键字来调用函数时,会自动执行以下操作: 1.创建一个全新的空对象 2.这个新对象会被执行[[Prototype]]连接 3.这个新对象会绑定到函数调用的this 4.如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象

function Foo(a) {
  this.a = a;
}

var foo = new Foo(1); // 使用new操作符创建并返回了一个对象赋值给foo
console.log(Foo(1)); // 不使用new操作符则正常执行Foo函数,该函数下的this默认绑定全局对象,因而导致在全局中创建了一个a并赋值了1
console.log(a) // 1  全局作用域中的1

console.log(foo.a); // 1

四种绑定方式的优先级


此处直接写出优先级排列,建议自己可以写点demo尝试验证下四种绑定优先级。

1.函数是否使用new关键字调用,如果是new关键字调用则this绑定的是新创建的对象。 2.函数是否通过callapply(显式绑定),如果是,this绑定的是指定的对象。 3.函数是否在某个上下文对象中调用(隐式调用),如果是则this绑定的是那个上下文对象。 4.如果都不是,则使用的是默认绑定,非严格模式下绑定到全局对象window,否则绑定到undefined