本文对 JavaScript 中的 this 关键字进行全方位的解析,看完本篇文章,心愿读者们可能齐全了解 this 的绑定问题。
开篇:对于那些没有投入工夫去学习 this 机制的 JavaScript 开发者来说,this 的绑定是一件令人困惑的事。(包含已经的本人)。
误区:学习 this 的第一步是明确this 既不指向函数自身也不指向函数的词法作用域,你是否被相似这样的解释所误导?但其实这种说法都是谬误的。
概括:this 理论是在函数被调用时产生的绑定,它所指向的地位齐全取决于函数被调用的地位。
一、调用地位
在了解 this 的绑定过程之前,首先要了解调用地位:调用地位就是函数在代码中被调用的地位(而不是申明的地位)。
所以说,寻找调用地位就是寻找“函数被调用的地位”,这里最重要的点是要剖析 调用栈(寄存以后正在执行的函数的地位)。
什么是调用栈和调用地位?
关系:调用地位就在以后正在执行的函数(调用栈)的前一个地位。
function func1() {
// 以后调用栈:func1
// 以后调用地位是全局作用域(调用栈的前一个地位)console.log('func1')
func2() // 这里是:func2 的调用地位}
function func2() {
// 以后调用栈:func1 -> func2
// 以后调用地位是在 func1(调用栈的前一个地位)console.log('func2')
func3() // 这里是:func3 的调用地位}
function func3() {
// 以后调用栈:func1 -> func2 -> func3
// 以后调用地位是在 func2(调用栈的前一个地位)console.log('func3')
}
func1() // 这里是:func1 的调用地位
关注点:咱们是如何从调用栈中剖析出真正的调用地位的,因为这决定了 this 的绑定。
二、绑定规定
- 默认绑定
最罕用的函数调用类型:独立函数调用
function getName() {console.log(this.name)
}
var name = 'kyrie'
getName() // 'kyrie'
当调用 getName()时,this.name 拿到了全局对象的 name。因为 getName()是间接调用的,不带任何修饰符,应用的是 默认绑定 ,因而 this 指向全局对象( 非严格模式)。
如果应用 严格模式(’strict mode’)呢?
function getName() {
'use strict';
console.log(this.name)
}
var name = 'kyrie'
getName() // 'TypeError: this is undefined'
那么全局对象无奈应用默认绑定,因而 this 会绑定到 undefined。
- 隐式绑定
调用地位是否有 上下文对象
function getName() {console.log(this.name)
}
var person = {
name: 'kyrie',
getName: getName
}
person.getName() // 'kyrie'
当 getName()被调用时,它的落脚点指向 person 对象,当函数援用有 上下文对象 时,隐式绑定 会把函数调用中的 this 绑定到这个上下文对象,因而调用 getName()时 this 被绑定到 person,因而 this.name 跟 person.name 是一样的
常见问题:隐式失落?
function getName() {console.log(this.name)
}
var person = {
name: 'kyrie',
getName: getName
}
var getName2 = person.getName() // 函数别名
var name = 'wen' // name 是全局对象的属性
getName2() // 'wen' 这里拿到的是全局对象的 name
解释:尽管 getName2 是 person.getName 的一个 函数援用 ,但它援用的 getName 函数的自身,因而 getName2() 调用时不带任何修饰符,应用的是 默认绑定 ,因而 this 绑定了 全局对象。
- 显式绑定
应用call() / apply() / bind() 指定 this 的绑定对象
function getName() {console.log(this.name)
}
var person = {name: 'kyrie'}
getName.call(person) // 'kyrie'
getName.apply(person) // 'kyrie'
通过 getName.call()/ getName.apply() 调用强制把它的 this 绑定到 person 上。
- new 绑定
所有函数都能够用 new 来调用,这种函数调用称为 结构函数调用。
重点 :实际上并不存在所谓的“构造函数”,只有对于 函数的“结构调用”。
应用 new 来调用函数,或者说产生结构函数调用时,会主动执行以下的四步操作:
- 创立(或者结构)一个 新的对象
- 这个新对象会被执行 [[原型]] 连贯(临时疏忽,属于原型内容,前面再介绍它)
- 这个新对象会 绑定到函数调用的 this
- 如果函数没有返回其余对象,则 new 表达式中的函数会 主动返回这个新的对象
function setName(name) {this.name = name}
var person = new setName('kyrie')
console.log(person.name) // 'kyrie'
应用 new 调用 setName()时,会创立 一个新对象 并把这个新对象 绑定到 setName()调用的 this 上 ,并把这个对象 返回。
三、优先级
毫无疑问,默认绑定的优先级是四条规定中最低的,所以暂不思考它。
- 隐式绑定和显式绑定哪个优先级高?
function getName() {console.log(this.name)
}
var p1 = {
name: 'kyrie',
getName: getName
}
var p2 = {
name: 'wen',
getName: getName
}
p1.getName() // 'kyrie'
p2.getName() // 'wen'
p1.getName.call(p2) // 'wen'
p2.getName.call(p1) // 'kyrie'
后果,显式绑定的优先级比隐式绑定高。
- 隐式绑定和 new 绑定哪个优先级高?
function setName(name) {this.name = name}
var p1 = {setName: setName}
var p2 = {}
p1.setName('kyrie')
console.log(p1.name) // 'kyrie'
p1.setName.call(p2, 'wen')
console.log(p2.name) // 'wen'
var p3 = new p1.setName('zbw')
console.log(p1.name) // 'kyrie'
console.log(p3.name) // 'zbw'
后果,new 绑定的优先级比隐式绑定高
- 显式绑定和 new 绑定的哪个优先级高?
function setName(name) {this.name = name}
var p1 = {}
// bind 会返回一个新的函数
var setP1Name = setName.bind(p1)
setP1Name('kyrie')
console.log(p1.name) // 'kyrie'
var p2 = new setP1Name('wen')
console.log(p1.name) // 'kyrie'
console.log(p2.name) // 'wen'
后果,new 绑定的优先级比显示绑定高
综上,优先级的正确排序:
从高到低:new > 显示 > 隐式 > 默认
- 判断 this 的指向
当初咱们能够依据优先级来判断函数在某个地位调用 this 的指向。
- 函数是否通过 new 来调用(new 绑定)?如果是,则 this 指向新创建的对象
var p1 = new Person()
- 函数是否通过 call/apply/bind 调用(显式绑定)?如果是,则 this 指向第一个参数
var p1 = setName.call(p2)
- 函数是否在某个上下文对象中调用(隐式绑定)?如果是,则 this 指向该上下文对象
var p2 = p1.setName()
- 如果以上三个条件都不满足,则应用默认绑定。如果是在严格模式中,this 指向 undefined,否则指向全局对象。
var p1 = setName()
四、箭头函数的 this
以上上提到判断 this 指向的四条规定蕴含所有失常的函数,除了 ES6 中的 箭头函数。
概括:箭头函数不像一般函数那样应用 function 关键字定义,而是用 “胖箭头”=> 定义 。而且箭头函数并不实用以上的四条规定,它的 this 绑定齐全是依据 外层作用域(函数或者全局) 来决定的。
function getName() {
// 箭头函数的 this 指向外层作用域
return (name) => {console.log(this.name)
}
}
var p1 = {name: 'kyrie'}
var p2 = {name: 'wen'}
var func = getName.call(p1)
func.call(p2) // 'kyrie'
getName()外部创立的箭头函数会 捕捉调用时外层作用域(getName)的 this,因为 getName 的 this 通过显示绑定到 p1 上,所以 getName 里创立的箭头函数也会指向 p1,最重要的一点:箭头函数的 this 无奈被批改 (即便是 优先级最高的 new 绑定也不行)
总结
要判断一个运行中的函数的 this 绑定,须要找到该函数的调用地位(联合调用栈),接着依据优先级得出的四条规定来判断 this 的绑定对象。
- 函数由 new 调用?绑定到新创建的对象
- 由 call/apply/bind 调用?绑定到指定对象
- 由上下文对象调用?绑定到上下文对象
- 默认:严格模式下绑定到 undefined,否则绑定到全局对象
ES6 的箭头函数不实用以上四条规定,而是 依据以后的词法作用域来决定 this 绑定 ,也就是说,箭头函数会 继承外层函数调用的 this 绑定 (无论绑定到什么),而且 箭头函数的 this 绑定无奈被批改。