一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
js中为什么有this
var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(myName)
}
}
var myName = 'geek'
bar.printName() // geek
在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是geek。这是因为 JavaScript 语言的作用域链是由词法作用域决定的,而词法作用域是由代码结构来确定的。
尽管 printName 函数声明在对象 bar 里面,但是仍然看做是全局定义的。
不过按照常理来说,调用bar.printName方法时,该方法内部的变量 myName 应该使用 bar 对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计。
所以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套 this 机制。
所以,在 JavaScript 中可以使用 this 实现在 printName 函数中访问到 bar 对象的 myName 属性了。
具体该怎么操作呢?你可以调整 printName 的代码,如下所示:
printName: function () {
console.log(this.myName)
}
this 指向谁?
一般情况下,this指向调用它的那个对象。
当调用方法没有明确对象时,this 就指向全局对象。
在浏览器中,指向 window;在 Node 中,指向 Global。
this 的指向是在调用时决定的,而不是在书写时决定的。这点和闭包恰恰相反。
声明位置与调用位置
js是词法作用域模型,无论我是一个对象也好,一个方法也好,它的生命周期只和我们声明它的位置有关。我把它写在哪个位置,它就活在哪个位置。
// 声明位置
var me = {
name: 'xiaofang',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var you = {
name: 'xiaoming',
hello: me.hello
}
// 调用位置
me.hello() // xiaofang
you.hello() // xiaoming
可以看到hello在代码中分别被 me 和 you 调用了,因此两次调用的 this 也就分别指向了 me 和 you,这没问题。稍微把这个例子改一下:
// 声明位置
var me = {
name: 'xiaoming',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var name = 'BigBear'
var hello = me.hello
// 调用位置
me.hello() // 你好,我是xiaoming
hello() // 你好, 我是BigBear
这里我们直接调用 hello 的时候,输出了全局的 name 变量。我们可以理解为是因为 name 和 hello 都挂在在全局对象 window 上,所以 hello () 其实等价于 window.hello (),此时 hello 方法内部的 this 自然指向 window,于是 this.name就等价于window.name。这也没问题。再改一下:
// 声明位置
var me = {
name: 'xiaofang',
hello: function() {
console.log(`你好,我是${this.name}`)
}
}
var you = {
name: 'xiaoming',
hello: function() {
var targetFunc = me.hello
targetFunc()
}
}
var name = 'BigBear'
// 调用位置
you.hello()
调用位置输出的结果是 BigBear—— 竟然不是 xiaoming?的确,直觉上肯定会认为是 you 这个对象在调用 hello 方法、进而调用 targetFunc,所以此时 this 肯定指向 you 对象啊!为啥会输出一个 window 上的 name 呢?
我们知道,this指向调用它那个对象。直觉会认为它的 this 应该指向 you 这个对象,其实还是因为把"声明位置"和"调用位置"混淆了。我们看到虽然 targetFunc 是在 you 对象的 hello 方法里声明的,但是在调用它的时候,我们是不是没有给 targetFunc 指明任何一个对象作为它前缀? 所以 you 对象的 this 并不会神奇地自动传入 targetFunc 里,js 引擎仍然会认为 targetFunc 是一个挂载在 window 上的方法,进而把 this 指向 window 对象。
特殊情况下的this指向
在三种特殊情境下,this 会 100% 指向 window:
- 立即执行函数(IIFE)
- setTimeout 中传入的函数
- setInterval 中传入的函数
箭头函数
箭头函数中的this比较特别,它和闭包很相似,都是认死理—— 认词法作用域的家伙。
所以说箭头函数中的 this,和你如何调用它无关,由你书写它的位置决定(和咱们普通函数的 this 规则恰恰相反),例如:
var name = 'BigBear'
var me = {
name: 'xiuyan',
// 声明位置
hello: () => {
console.log(this.name)
}
}
// 调用位置
me.hello() // BigBear
因为 this 在书写的时候,它所在的作用域是全局作用域,于是这个 this 就和全局对象绑在了一起。
箭头函数的缺点
- 没有arguments
- 无法通过apply call等改变this
什么情况下不能使用箭头函数
对象的方法不能使用
const obj = {
name: 'xiaoming',
getName: () => {
return this.name
}
}
原型上增加方法不能使用
var obj = { name: 'xiaoming'}
obj.__proto__.getName = () => {
return this.name
}
当在原型上使用箭头函数后,this就不会指向obj了,而是window。
构造函数不能使用
动态上下文中如果有this,不能使用
btn.addEventListener('click', () => {
this.innerHTML = 'click'
})
vue中的生命周期和method不能使用
因为vue组件本质上就是一个对象,对象的方法是不能用箭头函数的,否则this就不是指向当前对象了。
如何改变this指向
改变 this 的指向,我们主要有两条路:
- 通过改变书写代码的方式(比如箭头函数)。
- 显式地调用一些方法
改变书写代码的方式,进而改变 this 的指向
唱反调的箭头函数
var a = 1
var obj = {
a: 2,
// 声明位置
showA: () => {
console.log(this.a)
}
}
// 调用位置
obj.showA() // 1
当我们将普通函数改写为箭头函数时,箭头函数的 this 会在书写阶段(即声明位置)就绑定到它父作用域的 this 上。无论后续我们如何调用它,都无法再为它指定目标对象 —— 因为箭头函数的 this 指向是静态的,"一次便是一生"。
构造函数里的this
function Person(name) {
this.name = name
console.log(this)
}
var person = new Person('xiaoming')
构造函数里面的 this 会绑定到我们 new 出来的这个对象上。
显式地调用一些方法
改变 this 指向,我们常用的是 call、 apply 和 bind 方法:
- call: fn.call(target, arg1, arg2) 改变后立即执行
- apply: fn.apply(target, [arg1, arg2]) 改变后立即执行
- bind: fn.bind(target, arg1, arg2) 改变后不立即执行
call方法的模拟
先来看一个call的调用示范例:
var me = {
name: 'xiuyan'
}
function showName() {
console.log(this.name)
}
showName.call(me) // xiuyan
首先至少能想到以下两点:
- call 是可以被所有的函数继承的,所以 call 方法应该被定义在 Function.prototype 上
- call 方法做了两件事:1. 改变 this 的指向,将 this 绑到第一个入参指定的的对象上去;2. 根据输入的参数,执行函数。
showName在call方法调用后,表现得就像是me这个对象的一个方法一样。所以我们可以给me增加一个showName方法:
var me = {
name: 'xiuyan',
showName: function() {
console.log(this.name)
}
}
me.showName()
用户在传入 me 这个对象的时候, 想做的仅仅是让 call 把 showName 里的 this 给改掉,而不想给 me 对象新增一个 showName 方法。所以说我们在执行完 me.showName 之后,还要记得把它给删掉。
Function.prototype.myCall = function(context, ...args) {
// step1: 把函数挂到目标对象上(这里的 this 就是我们要改造的的那个函数)
context.func = this
// step2: 执行函数
context.func(...args)
// step3: 删除 step1 中挂到目标对象上的函数,把目标对象”完璧归赵”
delete context.func
}