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
根据之前所说的调用链分析,foo由obj对象调用的,因而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绑定到了全局对象。
显式绑定
显式绑定比较容易理解,就是通过函数原型上内置的call、apply、bind等将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.函数是否通过call、apply(显式绑定),如果是,this绑定的是指定的对象。
3.函数是否在某个上下文对象中调用(隐式调用),如果是则this绑定的是那个上下文对象。
4.如果都不是,则使用的是默认绑定,非严格模式下绑定到全局对象window,否则绑定到undefined。