一、调用位置
在理解this的绑定过程之前,首先要理解调用位置:调用位置就是函数在代码中被调用的位置(而不是申明的位置)。只有仔细分析调用位置才能回答这个问题:这个this到底引用的是什么?
function foo() {console.log('foo')// 当前调用栈是: bar// 因此,当前调用位置在bar中
}function bar() {console.log('bar')// foo当前调用位置foo()// 当前调用栈是: bar// 因此,当前调用位置在bar中
}function baz() {console.log('baz')// bar当前调用位置bar()// 当前调用栈是: baz// 因此,当前调用位置是全局作用域
}// baz当前调用位置
// 全局作用域
baz()
二、绑定规则
1、默认绑定(非严格模式):独立函数调用
const a = 2
function foo() {console.log(this.a)
}
foo() // 2
// foo() 在这里直接食用不带任何修饰的函数引用进行调用,因此只能使用默认绑定;调用位置为全局作用域,因此this默认绑定到全局作用域。
2、隐式绑定:调用位置是否有上下文,或者说是否被某个对象拥有或者包含
- 当函数引用有上下文对象时,隐式绑定规则会把函数调用中this绑定到这个上下文对象上。
function foo() {console.log(this.a)
}
const obj = {a: 2,foo: foo
}
obj.foo() // 2// 注意: 首先需要注意的是foo()的声明方式,及其之后是如何北方做引用属性添加到obj中的。但是无论是直接在obj中定义还是先定义在添加为引用属性,这个函数严格来说都不属于obj对象。
// 然而,调用位置会使用obj上下文来引用函数,因此你可以说函数被调用时obj对象‘拥有’或‘包含’它。// 当函数引用有上下文对象时,隐式绑定规则会把函数调用中this绑定到这个上下文对象上。
对象属性引用链中只有 最顶层 或者说 最后一层 会影响调用位置
function foo() {console.log(this.a)
}
const obj1 = {a: 2,foo: obj2
}
const obj2 = {a: 42,foo: foo
}
obj1.obj2.foo() // 42
隐式丢失:一个最常见的this绑定问题就是 被隐式绑定的函数 会丢失绑定对象,也就是说它会引用默认绑定,从而把this绑定到全局对象或者undefined上,取决于是否是严格模式。
function foo() {console.log(this.a)
}
const obj = {a: 2,foo: foo
}
const bar = obj.foo // 函数别名!
const a = '我是全局对象'
bar() // -> 我是全局对象
// 虽然bar是obj.foo的一个引用,但是实际上,他引用的是foo函数本身,因此此时的 bar() 相当于 foo(), 是一个不带任何修饰的函数调用,因此应用了默认绑定
function foo() {console.log(this.a)
}
function doFoo(fn) {// fn其实引用的是foofn() // <--调用位置!
}
const obj = {a: 2,foo: foo
}
const a = '我是全局对象'
doFoo(foo) // -> 我是全局对象
// 参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样
// 如果把函数传入语言内置的函数而不是自定义的函数,会怎么样呐?结果是一样的,没有区别
function foo() {console.log(this.a)
}
const obj = {a: 2,foo: foo
}
const a = '我是全局对象'
setTimeout(obj.foo, 100) // -> 我是全局对象// JavaScript环境中内置的setTimeout()函数实现和下面的代码类似:
function setTimeout(fn, delay) {// 等待delay毫秒fn() // <--调用位置!
}
3、显式绑定
function foo() {console.log(this.a)
}
const obj = {a: 2,
}
foo.call(obj) // -> 2
// 通过foo.call(...)可以在调用foo时强制把它绑定到obj上// 思考下面代码输出什么
const b = '我是全局对象'
function foo() {console.log(this.b)
}
const obj = {b: 2,foo: foo
}
const bar = function(fn) {fn()
}
bar.call(obj, foo) // -> 我是全局对象// 显然,通过这种方式还是无法解决绑定丢失问题
3.1、硬绑定
function foo() {console.log(this.b)
}
const obj = {b: 2,
}
const bar = function() {foo.call(obj)
}
bar() // -> 2
// 硬绑定的bar不能再修改它的this
bar.call(window) // -> 2
由于硬绑定是一种非常常用的模式。所以在es5中提供了内置的方法Funtion.prototype.bind
bind(…)会返回一个硬编码的新函数,它会吧参数设置为this的上下文并调用原始函数
function foo(something) {console.log(this.a, something)return this.a + something
}
const obj = {a: 2,
}
const bar = foo.bind(obj)
const b = bar(3) // -> 2 3
console.log(b) // -> 5
4、new绑定
使用new来调用函数,或者说发生构造函数调用时,它会自动执行下面的操作:
- 创建(构造)一个全新的对象
- 这个新对象会被执行[[原型]]链接(将这个对象的_proto_指向其构造函数的原型)
- 这个新对象会绑定到函数调用(构造函数)的this
- 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象
function foo(a) {this.a = a
}
const bar = new foo(2)
console.log(bar.a) // -> 2
三、优先级
如果某个调用位置同时使用四条规则,那谁的优先级更高呢?
1、隐式绑定与显式绑定谁的优先级更高?
function foo() {console.log(this.a)
}
const obj1 = {a: 2,foo: foo
}
const obj2 = {a: 3,foo: foo
}
obj1.foo() // -> 2
obj2.foo() // -> 3obj1.foo().call(obj2) // -> 3
obj2.foo().call(obj1) // -> 2// 可以看到,显式绑定优先级高于隐式绑定
2、隐式绑定与new绑定谁的优先级更高?
function foo(something) {this.a = something
}
const obj1 = {foo: foo
}
const obj2 = {}obj1.foo(2)
console.log(obj1.a) // -> 2obj1.foo.call(obj2, 3)
console.log(obj2.a) // -> 3const bar = new obj1.foo(4)
console.log(obj1.a) // -> 2
console.log(bar.a) // -> 4// 可以看到,new绑定的优先级高于隐式绑定
3、new绑定与显式绑定谁的优先级更高?
new和call/apply无法一起使用,因此无法通过 new foo.call(obj1)来直接进行测试,但是我们可以使用硬绑定来进行测试
function foo(something) {this.a = something
}
const obj1 = {}
const bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // -> 2const baz = new bar(3)
console.log(obj1.a) // -> 2
console.log(baz.a) // -> 3// 可以看到,new绑定的优先级高于显式绑定
四、判断this步骤
现在我们可以根据优先级来判断函数在某个调用位置引用的式哪条规则:
- 函数是否在new中调用(new)绑定?如果是的话this绑定的是新创刊的对象。
const bar = new foo() - 函数是否通过call,apply(显示绑定)或者bind(硬绑定)调用?如果是的话,this绑定的式指定对象。
const bar = foo.call(obj2) - 函数是否在某个上下文对象中调用(隐士绑定)?如果是的话,this绑定的是那个上下文对象
const bar = obj1.foo() - 如果都不是的话,使用默认绑定。如果在严格式模式下,就绑定到undefined,否则绑定到全局对象
const bar = foo()