前言
Javascript
中,this
指向一直是非常困扰我们前端小伙伴的一个问题,今天就通过几个角度分享一下我对this指向在不同场景下的理解
全局作用域
在全局作用域中浏览器打印this,this指向的是window
全局对象,并且不管是否为严格模式
console.log(this === window) // true
'use strict'
console.log(this === window) // true
复制代码
默认绑定
当一个普通函数被独立调用时,非严格模式下该函数中的this指向的是window
全局对象,严格模式下this的值为undefined
function fn() {
console.log(this === window) // true
}
fn()
复制代码
'use strict'
function fn() {
console.log(this) // undefined
}
fn()
复制代码
在这里值得注意的是,在非严格模式下,虽然函数独立调用中this指向window,却
不能理解为函数的独立调用等同于window调用
以函数表达式
为例,分别以var
、let
、const
命令声明一个普通函数,并分别进行直接调用和使用window
调用
var命令声明的函数,直接调用和使用window调用都可以正常调用,this指向window
var fn = function() {
console.log(this) // window
}
fn()
window.fn()
复制代码
let命令声明的函数,直接调用,this指向window,使用window调用报错
let fn = function() {
console.log(this)
}
fn() // 指向window
window.fn() // 报错
复制代码
const命令声明的函数,直接调用,this指向window,使用window调用报错
const fn = function() {
console.log(this)
}
fn() // 指向window
window.fn() // 报错
复制代码
var
命令和function
命令声明的全局变量,是顶层对象的属性,所以通过window可以调用,而在ES6中,let
命令、const
命令、class
命令声明的全局变量,不属于顶层对象的属性。也就说明了,通过let、const命令声明的函数并不能通过window调用。这也更加说明了一点,在非严格模式下普通函数独立调用时,其this指向window,但不等同于是window调用
对象成员
对象成员中的方法,由对象调用,this指向该对象,若将某个函数,或另外一个对象的方法,赋值于该对象下成员,该对象进行调用,this指向亦是该对象,默认情况下,对象方法的调用,this指向就是该方法的上下文对象
方法
const james = {
name: 'James',
sayHi: function() {
console.log(this)
}
}
const curry = {
name: 'Curry',
sayHi: function() {
console.log(this)
}
}
james.sayHi() // this指向james对象
curry.sayHi() // this指向curry对象
复制代码
隐式绑定
即函数(方法)调用位置,存在上下文对象
,那么this指向便默认绑定到该上下文对象中
// 全局作用域下声明一个sayHi函数
const sayHi = function() {
console.log(this)
}
const james = {
name: 'James',
// 赋值
sayHi: sayHi,
friend: {
name: 'Curry',
// 赋值
sayHi: sayHi
}
}
sayHi() // this指向window
james.sayHi() // this指向james对象
james.friend.sayHi() // this指向james.firend对象
复制代码
无论事先定义对象的sayHi方法,还是引用sayHi函数作为对象中的方法,默认情况下,只要该函数(方法)调用中存在上下文对象,this则指向该上下文对象
显式绑定
显式绑定指的是通过call
、apply
以及bind
方法改变this的指向,相比于隐式绑定,显式绑定则是主动指明this的绑定关系
const james = {
name: 'James',
sayHi: function() {
console.log(this)
}
}
const curry = {
name: 'Curry'
}
james.sayHi.call(curry) // this指向curry对象
james.sayHi.apply(curry) // this指向curry对象
james.sayHi.bind(curry)() // this指向curry对象
复制代码
上面的例子中,我们分别通过call
、apply
、bind
的方式调用了james.sayHi
方法,并传入curry对象,使this主动指向了curry
对象
传入基本类型之非严格模式
ECMAScript中五种基本类型分别是:undefined
、null
、boolean
、number
和string
在非严格模式
下我们分别将这五种基本类型传入,以call为例,apply、bind同理,看看this指向发生了哪些变化
const james = {
name: 'James',
sayHi: function() {
console.log(this)
}
}
james.sayHi.call(undefined) // this指向window
james.sayHi.call(null) // this指向window
james.sayHi.call(true) // this指向Boolean包装类型
james.sayHi.call(0) // this指向Number包装类型
james.sayHi.call('0') // this指向String包装类型
复制代码
在
非严格模式
下,通过传入五种基本类型进行显式绑定,其中除了传入undefined
和null
,this指向了window
,剩余的boolean
、number
、string
都指向其对应的基本包装类型
传入基本类型之严格模式
在严格模式
下我们分别将这五种基本类型传入,以call为例,apply、bind同理,看看this指向发生了哪些变化
'use strict'
const james = {
name: 'James',
sayHi: function() {
console.log(this)
}
}
james.sayHi.bind(undefined)() // this的值为undefined
james.sayHi.bind(null)() // this的值为null
james.sayHi.bind(true)() // this的值为true
james.sayHi.bind(0)() // this的值为0
james.sayHi.bind('0')() // this的值为'0'
复制代码
在
严格模式
下进行显式绑定,this的值便是对应传入的值
通过new调用
我们先了解使用new
操作符调用函数,内部会有以下几个动作
- 创建一个空对象
- 该对象会执行[[Prototype]]链接
- 生成的空对象会绑定到函数调用的this
- 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
- 如果函数没有返回对象类型Object(包含Array、Functoin、Date、RegExg...),那么new表达式中的函数调用会自动返回
(隐式返回)
这个新的对象
通过以上步骤,我们通过代码写一个方法简单模拟一个new内部执行的过程
function myNew() {
// 创建一个空对象
let target = {}
// constructor则是函数(构造函数),args则是获得其余参数
const [constructor, ...args] = [...arguments]
// 执行[[Prototype]]链接
target.__proto__ = constructor.prototype
let result = constructor.apply(target, args)
// 这里简单判断是否为 object 或 function
if (result && (typeof result == "object" || typeof result == "function")) {
// 如果函数返回对象类型为Object类型,则返回该对象
return result
}
// 如果构造函数返回的不是Object类型,返回我们创建的对象
return target
}
复制代码
new操作符调用时,内部会返回一个新对象, 则this指向new内部生成的这个新对象,所以通常我们会说,
通过构造函数创建一个实例(new),构造函数中的this,指向该实例。
(这个实例便是new内部隐式返回的新对象)
function Persion(name, age) {
this.name = name
this.age = age
console.log(this) // {name: 'James', age: 36}
}
const p = new Persion('James', 36)
复制代码
特别特别特别注意!,
“通过构造函数创建一个实例(new),构造函数中的this,指向该实例”
,前提是函数(构造函数)中,不能显式返回Object类型的值,否则创建出来的实例并不是new内部隐式返回的新对象了,而是构造函数中显式返回的Object类型的值,虽然此时构造函数中的this依然指向new内部生成的新对象,因为没能隐式返回,所以实例已经不是new内部生成的新对象了,而是构造函数中显式返回的Object,此时就不能说this指向该实例
function Persion(name, age) {
this.name = name
this.age = age
// this 指向new内部生成的新对象 {name: 'James', age: 14}
console.log(this) // {name: 'James', age: 14}
// 由于这里显式返回了一个数组,无法隐式返回new内部生成的新对象
return [1, 2, 3, 4]
}
// 通过构造函数创建实例
const p = new Persion('James', 14)
// 创建出来的实例是一个数组
console.log(p) // [1, 2, 3, 4]
复制代码
原型链中的this
很显然,方法的调用,默认情况下,调用位置存在上下文对象,则this指向该上下文对象,我们知道new操作符最后会返回一个新对象。原型中的this便指向由该构造函数创建的实例(new内部生成的新对象),即便你继承自其他对象,也会通过原型链方式进行查找
function Persion(name, age) {
this.name = name
this.age = age
}
Persion.prototype.sayHi = function() {
console.log(this) // {name: 'James', age: 14}
}
const p = new Persion('James', 14)
p.sayHi()
复制代码
DOM事件处理
DOM事件的处理,this默认指向的是事件绑定的DOM元素
const btn = document.getElementById('btn')
btn.addEventListener('click', function(e) {
console.log(e.currentTarget === this) // true
})
复制代码
e.currentTarget
是事件绑定
的DOM元素,e.target
是当前事件促发
的DOM元素
创建 ul > li,点击li标签
<ul id="uu">
<li style="height: 30px; background-color: pink;"></li>
</ul>
复制代码
const el = document.getElementById('uu')
el.addEventListener('click', function(e) {
console.log(e.currentTarget) // 当前事件绑定对象 ul元素
console.log(e.currentTarget === this) // true
console.log(e.target) // 当前事件促发对象 li元素
console.log(e.target === this) // false
})
复制代码
箭头函数
阮一峰老师的ECMAScript 6入门详细介绍到了使用箭头函数的以下几点注意事项
(1)箭头函数没有自己的this
对象。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
// 非严格模式下
console.log(this) // window
const sayHi = () => {
console.log(this) // window
}
sayHi()
复制代码
// 严格模式下
'use strict'
console.log(this) // window
const sayHi = () => {
console.log(this) // window
}
sayHi()
复制代码
window.name = 'Curry'
const james = {
name: 'James',
sayHi: function() {
console.log(this.name) // James
},
sayHello: () => {
console.log(this.name) // Curry
}
}
// james对象中的普通函数调用
james.sayHi() // this指向james对象
// james对象中的箭头函数调用
james.sayHello() // this指向window
复制代码
箭头函数没有自己的this
,而是引用外层的this
// ES6
function foo() {
// 使用箭头函数
setTimeout(() => {
console.log('id:', this.id)
}, 100)
}
// Babel 编译后的ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
复制代码
总结
- 全局作用域下,严格模式和非严格模式下,this 指向全局对象
- 普通函数独立调用,非严格模式中 this 指向 window,严格模式中 this 的值为 undefined,注意非严格模式下普通函数的独立调用 this 指向 window 但不等同于 window 调用
- 函数调用位置存在上下文对象,this 指向该上下文对象(对象调用方法,this指向该对象)
- 通过call、apply、bind方法调用进行显式绑定,严格模式下,this指向的值是传入的第一个参数。非严格模式下,第一个参数传入基本类型,除undefined或null指向window,传入的其余基本类型都指向其对应的基本包装类型
- 通过构造函数创建一个实例(new),构造函数中的this,指向该实例
- DOM事件,this指向事件绑定的DOM元素
- 箭头函数没有自己的this,而是引用外层的this