前言
我相信大多数掘友在平时开发时都和我一样有着相同的困惑,就是始终搞不懂 this
这个关键词在 JavaScript 中的使用方法,也就是 this
指向问题。虽然有时候能碰巧或者自以为的感觉是这样而得出正确的结果,但是还是不知道其背后的原因。
那么接下来我们就好好讨论下 this
指向问题,因为这不仅在面试中会考到,而且在工作中使用的频率也非常高,只有彻底弄懂 this
指向问题,我们才能在开发的道路上一马平川!
什么是 this
《你不知道的JavaScript(上卷)》 这本书中说到,this
关键字是 JavaScript 中最复杂的机制之一。 它是一个很特别的关键字,被自动定义在所有函数的作用域中。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、 函数的调用方法、 传入的参数等信息。 this
就是记录的其中一个属性, 会在函数执行的过程中用到。
我们不妨举一个例子来具体的分析一下:
js
// 首先定义函数
function foo() {
console.log(this)
}
// 我们可以直接调用函数
foo() // Window
// 也可以通过对象调用函数
var obj = {
foo: foo
}
obj.foo() // obj对象
// 也可以通过call/apply调用函数
foo.call("bar") // String {"bar"}
从上面的代码中,我们可以得出一个结论:
1. 当函数被调用时,JavaScript 会默认给 this
绑定一个值
2. this
是在运行时进行绑定的,而不是在编写时进行绑定的
3. this
的绑定和函数声明的位置没有任何关系, 只取决于函数的调用方式,也就是函数在代码中被调用的位置
this 的绑定规则
默认绑定
独立函数调用是最常用的函数调用类型,我们可以把这条规则看作是无法应用其他规则时的默认规则,那么什么是独立函数调用呢?举例:
当我们定义一个普通的函数的时候,并且调用它,就是独立函数调用。
js
// 定义函数
function foo() {
console.log("foo:", this)
}
// 调用函数
foo() // foo: Window
从上面的代码我们可以看出,其运行的结果是 Window
,因为独立函数调用时,this
指向全局对象 Window
。
当我们把函数定义在对象中时,独立的去调用它,也是独立函数调用。
js
var obj = {
name: "lisi",
bar: function() {
console.log("bar:", this)
}
}
var baz = obj.bar
baz() // bar: Window
从上面的代码我们可以看出,其运行结果与函数定义的位置没有关系,而是看它是在什么时候被调用的,obj
是一个对象,obj
对象中 bar
属性的引用被赋值给 baz
变量,baz
独立的去调用函数,所以结果同样指向 Window
。
当我们定义一个高阶函数时,并对它传入一个参数调用,也是独立函数调用。
js
var obj = {
name: "lisi",
bar: function() {
console.log("bar:", this)
}
}
// 定义一个高阶函数
function foo(fn) {
fn()
}
// 调用函数
foo(obj.bar) // bar: Window
从以上的代码分析中我们可以看出,独立函数调用,也称为默认规则,可以理解为函数没有被绑定到某个对象上进行调用 。其结果都是指向 Window
。我们可以把它当作一个小的结论记住,只要遇到类似的场景,就能够明确 this
的指向。注意:如果函数体处于严格模式, this
会被绑定到 undefined
, 否则 this
会被绑定到全局对象。
隐式绑定
隐式绑定要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含。那么什么又是隐式绑定呢?
当我们定义一个函数,把这个函数放入对象中作为其属性值,然后再通过对象调用,这就是隐式绑定。
js
function foo() {
console.log("foo函数:", this)
}
var obj = {
bar: foo
}
obj.bar()
从上面的代码我们可以看出,foo
函数被调用时, obj
对象拥有或者包含它,其落脚点确实是指向 obj
对象的,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的 this
绑定到这个上下文对象上,即调用 foo()
时 this
被绑定到 obj
上。
显式绑定
其实隐式绑定是有前提条件的,那就是必须在调用的对象内部有一个对函数的引用,否则在进行调用时会因找不到函数而报错,也正是因为这个引用,才间接的将 this
绑定到这个对象上。但是我们不想在对象内部包含函数引用,而想在某个对象上强制调用函数,该怎么做呢?
我们可以使用 call(..) 和 apply(..) 方法,它们的第一个参数是一个对象,会把这个对象绑定到 this
,接着在调用函数时指定这个 this
。因为你可以直接指定 this
的绑定对象, 因此我们称之为显式绑定。
js
function foo() {
console.log(this.a)
}
var obj = {
a: 2
}
foo.call(obj) // 2
foo.apply(obj) // 2
从上面的代码我们可以看出,通过 foo.call(..)
函数调用,我们可以在调用 foo
函数时强制把它的 this
绑定到 obj
上。
其实,bind(..) 也是一种显示绑定的方法,它返回一个新的函数,当调用该新函数时,会调用原始函数并将其 this
关键字设置为给定的值,同时,还可以传入一系列指定的参数。
js
function foo(name, age, height, address) {
console.log("foo:", this) // foo: { name: "zhangsan" }
console.log("参数:", name, age, height, address) // 参数: "lisi" 24 1.8 地址
}
var obj = { name: "zhangsan" }
var bar = foo.bind(obj, "lisi", 24, 1.8)
bar("地址")
new 绑定
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作:
1. 创建一个全新的对象
2. 这个新对象会被执行 prototype
连接
3. 这个新对象会绑定到函数调用的 this
上(this
的绑定在这个步骤完成)
4. 如果函数没有返回其他对象,表达式会返回这个新对象
js
function foo() {
this.name = "wangwu"
console.log("foo函数:", this)
}
new foo() // foo函数: foo { name: 'wangwu' }
从上面的代码我们可以看出,使用 new 来调用 foo(..)
时,我们会构造一个新对象并把它绑定到 foo(..)
调用中的 this
上。
内置函数的绑定
在平时的开发中,我们有时候会调用一些 JavaScript 的内置函数,或者第三方库中的内置函数,这些内置函数可能会让我们传入另外一个函数,并通过 JavaScript 内部或者第三方库内部帮助我们执行,那么这些函数中的 this
又是如何进行绑定的?
比如我们常用的定时器函数:
js
setTimeout(function() {
console.log("定时器函数:", this) // 定时器函数: Windows
}, 1000)
从上面的代码我们可以看出,当我们将一个函数传入定时器函数时,其 this
指向的是全局对象 Windows
。
又比如我们常用的 forEach 函数,其实它还有第二个参数,就是执行回调函数时用作 this 的值。
js
var names = ["123", "456", "789"]
names.forEach(function(item) {
console.log("forEach:", this)
}, "000")
从上面的代码我们可以看出,其运行结果则会遍历返回 forEach: String {'000'}
,即通过 forEach 的第二个参数,指定 this
。
所以针对一些特殊的内置函数,要具体问题具体分析,它们都有一套属于自己的绑定规则。
绑定规则的优先级
既然绑定的规则有这么多种,那么如果某个调用位置可以应用多条规则该怎么办?这时我们就要抛出绑定规则优先级这个概念,应用多条规则时,一定是有区分的,接下来我们就来分析下它们的优先级。
情况一:apply/call
的优先级高于默认绑定
js
function foo() {
console.log( this.a )
}
var obj1 = {
a: 2,
foo: foo
}
var obj2 = {
a: 3,
foo: foo
}
obj1.foo()// 2
obj2.foo() // 3
obj1.foo.apply( obj2 ); // 3
obj2.foo.call( obj1 ); // 2
从上面的代码我们可以看出,apply/call
修改了函数中的 this
指向,由此可以得出 apply/call
是高于默认绑定的。
情况二:bind
的优先级高于默认绑定
js
function foo() {
console.log("foo:", this)
}
var bar = foo.bind("aaa")
var obj = {
baz: bar
}
obj.baz() // foo: String {'aaa'}
从上面的代码我们可以看出,其运行结果是 foo: String {'aaa'}
,显然 bind
同样高于默认绑定。
情况三:bind
的优先级高于 apply/call
js
function foo() {
console.log("foo:", this)
}
var bindFn = foo.bind("aaa")
bindFn.call("bbb") // foo: String {'aaa'}
从上面的代码我们可以看出,其运行结果是 foo: String {'aaa'}
,显然 bind
的优先级高于 apply/call
。
情况四:new
绑定优先级高于隐式绑定
js
function foo() {
console.log("foo:", this)
}
var obj = {
foo: function () {
console.log('foo:', this) // foo: foo {}
console.log('foo:', this === obj) // foo: false
}
}
new obj.foo()
从上面的代码我们可以看出,其运行结果分别是 foo: foo {}
和 foo: false
,显然 new
绑定优先级高于隐式绑定。
情况五:new
优先级高于 bind
js
function foo(something) {
this.a = something
}
var obj1 = {}
var bar = foo.bind(obj1)
bar(2)
console.log(obj1.a) // 2
var baz = new bar(3)
console.log(obj1.a) // 2
console.log(baz.a) // 3
从上面的代码我们可以看出,bar
被硬绑定到 obj1
上, 但是 new bar(3)
并没有像我们预计的那样把 obj1.a
修改为 3。 相反,new 修改了硬绑定从而调用 bar(..)
中的 this
。
因为使用了 new 绑定,我们得到了一个名字为 baz
的新对象,并且 baz.a
的值是 3。 由此可以得出 new 优先级高于 bind
。
注意:new 绑定不可以和 apply/call
一起使用,否则会报错。
根据以上示例我们可以得出优先级的结论:new -> bind -> apply/call -> 隐式 -> 默认
this 绑定之外的情况
如果你把 null
或者 undefined
作为 this
的绑定对象传入 call
、apply
或者 bind
,这些值在调用时会被忽略,实际应用的是默认绑定规则。
情况一:显式绑定 null/undefined
,那么使用的规则是默认绑定
js
function foo() {
console.log("foo:", this)
}
foo.apply("abc") // foo: String {'abc'}
foo.apply(null) // foo: Window
foo.apply(undefined) // foo: Window
情况二:间接引用
当创建一个函数的"间接引用"时,调用这个函数会应用默认绑定规则,间接引用通常在赋值时发生。
js
var obj1 = {
name: "obj1",
foo: function() {
console.log("foo:", this)
}
}
var obj2 = {
name: "obj2"
}
obj2.foo = obj1.foo
obj2.foo()
// 间接引用
(obj2.foo = obj1.foo)()
上述代码中,赋值表达式 obj2.foo = obj1.foo
的返回值是目标函数的引用,因此调用位置是 foo()
而不是 obj2.foo()
或者 obj1.foo()
。
箭头函数中的 this
在箭头函数中,无论 this
绑定到什么上,箭头函数都会继承外层函数调用的 this
绑定。这其实和 ES6 之前代码中的 self = this
机制一样。
普通函数中是有 this
的标识符,但是在箭头函数中是没有的,通过 apply
调用时, 也是没有 this
的。
js
var bar = () => {
console.log("bar:", this)
}
bar() // bar: Window
bar.apply("aaaa") // bar: Window
this
在箭头函数中的查找规则,根据外层(函数或者全局) 作用域来决定 this
。
js
var obj = {
name: 'obj',
foo: () => {
var bar = () => {
console.log('bar:', this)
}
return bar
}
}
var fn = obj.foo()
fn() // bar: Window
fn.apply('bbb') // bar: Window
this 指向面试题
面试题一:
js
var name = 'window'
var person1 = {
name: 'person1',
foo1: function () {
console.log(this.name)
},
foo2: () => console.log(this.name),
foo3: function () {
return function () {
console.log(this.name)
}
},
foo4: function () {
return () => {
console.log(this.name)
}
}
}
var person2 = { name: 'person2' }
// 面试题目:
person1.foo1()
person1.foo1.call(person2)
person1.foo2()
person1.foo2.call(person2)
person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)
person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)
面试题二:
js
var name = 'window'
function Person(name) {
this.name = name
this.foo1 = function () {
console.log(this.name)
},
this.foo2 = () => console.log(this.name),
this.foo3 = function () {
return function () {
console.log(this.name)
}
},
this.foo4 = function () {
return () => {
console.log(this.name)
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
// 面试题目:
person1.foo1()
person1.foo1.call(person2)
person1.foo2()
person1.foo2.call(person2)
person1.foo3()()
person1.foo3.call(person2)()
person1.foo3().call(person2)
person1.foo4()()
person1.foo4.call(person2)()
person1.foo4().call(person2)
面试题三:
js
var name = 'window'
function Person(name) {
this.name = name
this.obj = {
name: 'obj',
foo1: function () {
return function () {
console.log(this.name)
}
},
foo2: function () {
return () => {
console.log(this.name)
}
}
}
}
var person1 = new Person('person1')
var person2 = new Person('person2')
// 面试题目
person1.obj.foo1()()
person1.obj.foo1.call(person2)()
person1.obj.foo1().call(person2)
person1.obj.foo2()()
person1.obj.foo2.call(person2)()
person1.obj.foo2().call(person2)
上述就是典型的三道 this 指向面试题,感兴趣的掘友们可以先试着练习下,可以把代码复制到浏览器中,看看结果是否和预期的一样,也可以在下方评论区我们一起讨论。
总结
this 指向一直是我们老生常谈的问题,尤其是在面试中,考察的频率非常高。我们在平时的工作中或者学习中用到它的地方也很多,甚至在一些框架的源码中,都能见到 this 的身影,可谓学好 this 有多么的重要。
在这里我们做一个总结,如果要判断一个运行中函数的 this 绑定,就需要找到这个函数的直接调用位置,然后我们可以根据下面的规则进行判定 this 指向:
首先,函数是否在 new 中调用(new 绑定)?如果是的话 this 绑定的是新创建的对象。
然后,函数是否通过 call、apply(显式绑定)或者硬绑定调用?如果是的话,this 绑定的是指定的对象。
其次,函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this 绑定的是那个上下文对象。
最后,如果都不是的话,使用默认绑定。 如果在严格模式下,就绑定到 undefined, 否则绑定到全局对象。