The this keyword refers to the current object the code is being written inside.
翻译过来就是:在 JavaScript 中,关键字 this 指向了当前代码运行时的对象。
如果我是刚接触 JavaScript 的话,看到这句话应该会有很多问号???
二、为什么需要设计 this?
假设我们有一个 person 对象,该对象有一个 name 属性和 sayHi() 方法。然后我们的需求是,sayHi() 方法要打印出如下信息:
var person = {
name: 'Frankie',
sayHi: function() {
// 若该方法的功能是,打印出:Hi, my name is Frankie.
// 要如何实现?
}
}
方案一
最愚蠢的方案,如下:
var person = {
name: 'Frankie',
sayHi: function(name) {
console.log('Hi, my name is ' + name + '.')
}
}
person.sayHi(person.name) // Hi, my name is Frankie.
方案二
方案一在每次调用方法都需要传入 person.name 参数,还不如传入一个 person 参数。试图优化一下:
var person = {
name: 'Frankie',
sayHi: function(context) {
console.log('Hi, my name is ' + context.name + '.')
}
}
person.sayHi(person) // Hi, my name is Frankie.
var person = {
name: 'Frankie',
sayHi: function() {
console.log('Hi, my name is ' + this.name + '.')
}
}
person.sayHi() // Hi, my name is Frankie.
但是为什么 this.name 的值等于 person.name 的属性值呢?还有 this 哪来的?
原来 this 指向函数[运行时]()所在的环境(执行上下文)。
实际上,this 可以理解为函数中的第一个形参(看不见的形参),在你调用 person.sayHi() 的时候,JavaScript 引擎会自动帮你把 person 绑定到 this 上。所以当你通过 this.name 访问属性时,其实就是 person.name。
三、了解 this
到目前为止,好像 this 也没那么神秘,没那么难理解嘛!
注意,本小节示例均在非严格模式下,除非有特殊说明。
再看:
var person = {
name: 'Frankie',
sayHi: function() {
console.log('Hi, my name is ' + this.name + '.')
}
}
var name = 'Mandy'
var sayHi = person.sayHi
// 写法一
person.sayHi() // Hi, my name is Frankie.
// 写法二,Why???
sayHi() // Hi, my name is Mandy.
原因就是函数内部使用了 this 关键字。上面提到 this 指的是函数运行时所在的环境。对于 person.sayHi() 来说,sayHi 运行在 person 环境,所以 this 指向了 person;对于 sayHi() 来说,sayHi 运行在全局环境,所以 this 指向了全局环境。(可在 sayHi 函数体内打印 this 来对比)
那么,函数的运行环境是怎么决定的呢?
内存的数据结构
下面先了解一下 JavaScript 内存的数据结构。JavaScript 之所以有 this 的设计,跟内存里面的数据结构有关系。
var fn = function() {}
var obj = { fn: fn }
// 单独执行
fn()
// obj 环境执行
obj.fn()
环境变量
JavaScript 允许在函数体内部,引用当前环境的其他变量。
var sayHi = function() {
console.log('Hi, my name is ' + name + '.')
}
上面示例中,函数体里面使用了变量 name。该变量由运行环境提供。
现在问题来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获取当前的运行环境(context)。所以, this 就出现了,它的设计目的就是在函数内部,指代函数当前的运行环境。
var sayHi = function() {
console.log('Hi, my name is ' + this.name + '.')
}
上面的示例中,函数体内部的 this.name 就是指当前运行环境的 。
var sayHi = function() {
console.log('Hi, my name is ' + this.name + '.')
}
var name = 'Mandy'
var person = {
name: 'Frankie',
sayHi: sayHi
}
// 单独执行
sayHi() // Hi, my name is Mandy.
// person 环境执行
person.sayHi() // Hi, my name is Frankie.
上面的示例中,函数 sayHi 在全局环境执行,this.name 指向全局环境的 name。
在 person 环境执行,this.name 指向 person.name。
回到本小节开头的示例中:
var person = {
name: 'Frankie',
sayHi: function() {
console.log('Hi, my name is ' + this.name + '.')
}
}
var name = 'Mandy'
var sayHi = person.sayHi
// 写法一
person.sayHi() // Hi, my name is Frankie.
// 写法二,Why???
sayHi() // Hi, my name is Mandy.
person.sayHi() 是通过 person 找到 sayHi,所以就是在 person 环境执行。一旦 var sayHi = person.sayHi,变量 sayHi 就直接指向函数本身,所以 sayHi() 就变成在全局环境执行。
var person = {
name: 'Frankie',
sayHi: function() {
console.log('Hi, my name is ' + this.name + '.')
}
}
person.sayHi() // Hi, my name is Frankie.
// 等价于
person.sayHi.call(person) // Hi, my name is Frankie.
根据前面讲述的函数在内存中的数据结构,以上示例跟下面动态地将 sayHi 方法绑定到 person 上是一致的。
function sayHi() {
console.log('Hi, my name is ' + this.name + '.')
}
var person = { name: 'Frankie' }
var name = 'Mandy'
// 动态添加到 person 上
person.sayHi = sayHi
person.sayHi() // Hi, my name is Frankie.
sayHi() // Hi, my name is Mandy.
var x = 1
function fn() {
console.log(this.x)
}
fn() // 1
注意,如果使用了 ES6 的 let 或 const 去声明变量 x 时,结果又稍微有点不同了!
let x = 1
function fn() {
// 注意,若严格模式下,this 为 undefined,所以执行 this.x 就会报错
console.log(this.x)
}
fn() // undefined
原因很简单。首先在 fn 运行时,this 仍然指向 window,只不过 window 对象下没有 x 属性,所以打印了 undefined。
为什么会这样呢?
// 以下两种方式,其实都往 window 对象添加了 x、y 属性,并赋值。
x = 1
var y = 2
// 但在 ES6 之后,做出了改变
// 使用了 let、const 或者 class 来定义变量或类,都不会往顶层对象添加对应属性
let x = 1
const y = 2
class Fn {}
我相信很多人会将
this
和作用域混淆在一起理解,其实它们完全是两回事。例如
this.xxx
和console.log(xxx)
有什么不同呢?前者是查找当前this
所指对象上的xxx
属性,后者是在当前作用域链上查找变量xxx
。作用域与函数的调用方式无关,而
this
则与函数调用方式相关。就
this
问题写了两篇文章:一、this 是什么?
在 MDN 上原话是:
翻译过来就是:在 JavaScript 中,关键字
this
指向了当前代码运行时的对象。如果我是刚接触 JavaScript 的话,看到这句话应该会有很多问号???
二、为什么需要设计 this?
假设我们有一个
person
对象,该对象有一个name
属性和sayHi()
方法。然后我们的需求是,sayHi()
方法要打印出如下信息:最愚蠢的方案,如下:
方案一在每次调用方法都需要传入
person.name
参数,还不如传入一个person
参数。试图优化一下:先卖个关子,这种方案看着是不是跟 Function.prototype.call() 有点相似?
在方案二里方法的调用方式,看着还是有点不雅。能不能把形参
context
也省略掉,然后通过person.sayHi()
直接调用方法?当然可以,但此时的形参
context
要换成 JavaScript 中的保留关键字this
。但是为什么
this.name
的值等于person.name
的属性值呢?还有this
哪来的?实际上,
this
可以理解为函数中的第一个形参(看不见的形参),在你调用person.sayHi()
的时候,JavaScript 引擎会自动帮你把person
绑定到this
上。所以当你通过this.name
访问属性时,其实就是person.name
。三、了解 this
到目前为止,好像
this
也没那么神秘,没那么难理解嘛!再看:
上面示例中,
person.sayHi
和sayHi
明明都指向同一个函数,但为什么执行结果不一样呢?原因就是函数内部使用了
this
关键字。上面提到this
指的是函数运行时所在的环境。对于person.sayHi()
来说,sayHi
运行在person
环境,所以this
指向了person
;对于sayHi()
来说,sayHi
运行在全局环境,所以this
指向了全局环境。(可在sayHi
函数体内打印this
来对比)那么,函数的运行环境是怎么决定的呢?
内存的数据结构
下面先了解一下 JavaScript 内存的数据结构。JavaScript 之所以有
this
的设计,跟内存里面的数据结构有关系。上面的示例将一个对象赋值给变量
person
。JavaScript 引擎会先在内存里面,生成一个对象{ name: 'Frankie' }
,然后把这个对象的内存地址赋值给变量person
。也就是说,变量
person
是一个地址(reference)。后面如果要读取person.name
,JavaScript 引擎先从person
拿到内存地址,然后再从该地址读出原始的对象,返回它的name
属性。原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面的例子
name
属性,实际上是以下面的形式保存的。这样的结构是很清晰的,问题在于属性的值可能是一个函数。
这时,JavaScript 引擎会将函数单独保存在内存中,然后再将函数的地址赋值给
sayHi
属性的value
属性。由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行。
环境变量
JavaScript 允许在函数体内部,引用当前环境的其他变量。
上面示例中,函数体里面使用了变量
name
。该变量由运行环境提供。现在问题来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获取当前的运行环境(context)。所以,
this
就出现了,它的设计目的就是在函数内部,指代函数当前的运行环境。上面的示例中,函数体内部的
this.name
就是指当前运行环境的 。上面的示例中,函数
sayHi
在全局环境执行,this.name
指向全局环境的name
。在
person
环境执行,this.name
指向person.name
。回到本小节开头的示例中:
person.sayHi()
是通过person
找到sayHi
,所以就是在person
环境执行。一旦var sayHi = person.sayHi
,变量sayHi
就直接指向函数本身,所以sayHi()
就变成在全局环境执行。在 JavaScript 中,
this
的四种绑定规则,而以上的示例this
均属于默认绑定的。四、函数调用
上面有提到 Function.prototype.call(),下面先聊聊这个方法。
call()
允许为不同的对象分配和调用属于一个对象的函数/方法。1. call 语法
this
值。请注意,this
可能不是该方法看到的实际值。 在非严格模式下,若参数指定为null
或undefined
,this
会自动替换为指向全局对象。2. 使用 call 方法调用函数,且不指定第一参数
此时分为两种情况:严格模式和非严格模式,两者在
this
的绑定上有区别。所以下面示例会报错。
3. 普通函数调用(小结)
在 JavaScript 中,其实所有的函数原始的调用方式是这样的:
fn()
其实就是fn.call()
的语法糖形式。普通函数调用可以总结成这样:
4. 成员函数调用(方法)
这也是一种非常常见的调用方式,又回到开头的那个示例。
根据前面讲述的函数在内存中的数据结构,以上示例跟下面动态地将
sayHi
方法绑定到person
上是一致的。以上两者区别是,前一个例子的
sayHi
函数只能通过变量person
在内存中根据变量存放的对象地址找到对象,该对象下有一个sayHi
属性,该属性的[[value]]
存放的函数地址,所以只能通过person.sayHi()
进行调用。而后一个例子中sayHi
函数除了前面提到的调用方式外,还可以直接通过变量sayHi
去调用该函数(sayHi()
)。四、this 的绑定方式
this
是 JavaScript 的一个关键字,它是函数运行时,在函数体内部自动生成的一个对象,只能在函数体内部使用。上面示例中,函数
fn
运行时,内部会自动有一个this
可以使用。函数的不同使用场景,
this
会有不同的值。总的来说,this
就是函数运行时所在的环境对象。1. 普通函数调用(默认绑定)
这是函数最常用的用法了,属于全局性调用,因此
this
就代表全局对象。注意,如果使用了 ES6 的
let
或const
去声明变量x
时,结果又稍微有点不同了!原因很简单。首先在
fn
运行时,this
仍然指向window
,只不过window
对象下没有x
属性,所以打印了undefined
。为什么会这样呢?
2. 作为对象方法调用(隐式绑定)
函数还作为某个对象的方法调用,这时
this
就指向这个上级对象。3. 通过 call、apply 调用(显式绑定)
这两个方法的参数以及区别不再赘述,上面已经讲过了。
4. 作为构造函数调用(new 绑定)
所谓构造函数,就是通过这个函数,可以生成一个新对象。这时,
this
就指向这个新对象(实例)。为了表明这时
this
不是指向全局对象,我们修改一下代码:根据结果,我们可以看到全局变量
x
没有发生变化。五、其他
1. 箭头函数
箭头函数,没有自己的
this
,arguments
,super
或new.target
,并且它不能用作构造函数。由于没有this
,因此它不能绑定this
。2. 事件处理函数
在事件处理函数中,不同的使用方式,会导致
this
指向不同的对象,你知道吗?六:思考题
抛下两个题,可以分析分析为什么结果不一样。
例一:
例二:
七、参考