this到底是个什么东西(js基础提升之三)

背景知识

从编程入门开始学习java,就一直觉得this是一个神奇的机制,包括到现在为止,自己觉得对js中的this的理解也没到轻车熟路的地步。所以就用这篇文章跟着大家一起理解一下this它到底是个什么东西。


this的定义

this的绑定跟函数声明的位置没有任何的关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在那里被调用(调用栈)、调用函数的方法、传入的参数等信息。在函数执行的过程中,this就是记录其中的一个属性。(转自你不知道的javascript上卷)


为什么要使用this

先贴上两个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
let willBuy = {name: 'Macan'}
let bought = {name: 'Bora'}

//例子一
function carName() {
return this.name.toUpperCase()
}

function buy() {
let buy = "I bought " + carName.call(this)
console.log(buy)
}

//例子二
function carNameWithoutThis(context) {
return context.name.toUpperCase()
}

function buyWithoutThis(context) {
let greeting = "I bought " + carNameWithoutThis(context)
console.log(greeting)
}

buy.call(willBuy)
buy.call(bought)

buyWithoutThis(willBuy)
buyWithoutThis(bought)

输出结果都是:

I bought MACAN
I bought BORA

我们列出来的只是一个简简单单买车的例子,然而在工作中不可能只传递如此简单的对象,所以当我们对象体量提升的时候使用this传递参数会比直接传递参数要爽的多。
例子一中通过this来隐式传递一个对象的引用,而例子二中就需要通过显式来传入对象。如果使用this,那么我们所写的方法的api将更加简洁以及利于复用。

说到这里我们得先说几个方法,call、apply、bind。


call、apply、bind

这三个方法都是Function的内置方法,也就是说我们定义函数的时候函数会继承这三个方法。他们的作用是重新定义该方法的this对象!

1
2
3
4
5
6
7
8
9
10
11
12
let obj = {
name: 'wx',
job: 'teacher',
say: function (age, sex) {
console.log(this.name + ' ' + this.job + ' ' + age + ' ' + sex)
},
}

obj.say('27', 'female') //wx teacher 27 female
obj.say.call({name:'wxy', job: 'programmer'}, '26', 'male') //wxy programmer 26 male
obj.say.apply({name:'wxy', job: 'programmer'}, ['26', 'male']) //wxy programmer 26 male
obj.say.bind({name:'wxy', job: 'programmer'}, '26', 'male')() //wxy programmer 26 male

call和apply的区别在于传入参数形式不通,call以函数参数形式传递参数,第一参数为this,从第二个参数开始对应say型参。而apply将say的型参作为一个数组形式传入。而bind和call的区别在于call返回函数执行结果而bind返回绑定this的函数。


默认绑定

如果说call,apply,bind是显示绑定this,或者说硬绑定this的话。哪我们执行方法时默认绑定的this就是默认绑定。

1
2
3
4
5
6
7
8
9
10
function foo() {
console.log(this)
}

function fooStrict() {
'use strict'
console.log(this)
}
foo() // Window
fooStrict() //undefined

在非严格模式下foo调用的时候应用了this的默认绑定,所以this指向了全局即Window,而在严格模式下禁止this关键字指向全局对象,所以fooStrict的this为undefined。


隐式绑定

前文说过,this取决于函数的调用方式所以当我们调用一个函数时,通过不同的方式调用,即使该函数在堆内存中地址没有改变,this也是不同的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function foo() {
console.log(this.a)
}

function doFoo(fn) {
fn() //<--注释,调用位置
}

var a = 'window' //<--注意这里必须用var,否则当this指向window时this.a会undefined

let obj2 = {
a: 'obj2',
foo: foo
}

let obj1 = {
a: 'obj1',
foo: foo,
obj2: obj2
}

let bar = obj1.foo

foo() //window
obj1.foo() //obj1
bar() //window
obj1.obj2.foo() //obj2
doFoo(obj1.foo) //window
setTimeout(obj1.foo) //window

看看我们的例子我们一个一个分析:

1.foo(),非严格模式下this指向window,没有异议好理解。
2.obj1.foo(),我们在调用foo()时,obj1为函数foo的引用函数,所以说在调用obj的时候foo被其拥有或者包含,因此foo的this被绑定到了obj上面。所以this.a = obj1.a。
3.bar(),虽然bar = obj1.foo,但是我们知道对于引用数据类型来说,任何的赋值都是对堆内存地址的指向,所以bar引用的是foo的地址,只是把地址传递给bar的不是foo而是obj1.foo而已。
4.obj1.obj2.foo(),链式引用中最后一层会覆盖调用位置所以这里的this为obj2
5.doFoo(obj1.foo),回调引用时obj1.foo的调用位置为doFoo中的fn()如代码段中注释位置,则foo的this为doFoo的this。而doFoo的this在非严格模式下指向了window。
6.setTimeout(obj1.foo),原理和5其实是一样的,我这里单独把setTimeout拿出来说就是强调一下,为何我们在使用setTimeout方法中我们经常会去bind(this)。

var、let和const

在写this的示例的时候有个小插曲,在示例里面在定义全局a的时候注释里面强调必须用var,而不是let。因为使用let时,this.a会undefined。这里我们来顺道解释下let、this和const的区别。

很多人都说const是常量,其实这个定义严格来说并不正确对于赋值类型数据来说const就是一个常量,不可以被更改。但是对于引用数据类型来说const不能被更改的仅仅是引用的地址,即使引用地址里面的值是否改变const并不能控制。

1
2
3
4
5
const obj = {a:1}
obj.a=2
obj.b=2

console.log(obj) //{a: 2, b: 2}

现在我们在来说let和var,先简单的用let和var定义两个变量然后打印。

哎呦,var定义的变量直接挂在了window下,而let定义的变量并没有存在window下。哪let定义的a去了那里呢?

我们给上面的代码加一个debugger来看看情况

我们看到了一个Scope,这个Scope可以理解成angular中的Scope即作用域,看到浏览器解析的a和b,我们就会发现a存在了一个叫做Script的域中,而这个Script域竟然跟Global即window是平级的。

接着我们继续输入

我们发现被let声明过的变量无法进行二次声明,且被var声明过的变量也无法用let进行声明,只有var能再次声明var声明过的变量。

关于变量提升及取值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var a = [], b = [];
for (var i = 0; i < 3; i++) {
a[i] = function () {
return i
};
}

for (let j = 0; j< 3; j++) {
b[j] = function () {
return j
}
}

a.map((value) => {
console.log(value(), 'a')
})

b.map((value) => {
console.log(value(), 'b')
})

首先先说说我们的示例,这个示例是我在网上看的,觉得这个非常的鸡贼。数组里面存的不是值而是一个function,通过function来获取变量的值。按理说我们通过function值获取的变量应该是我们在执行function的时候拿到的变量的值,正如我们this一样指向取决于执行的环境。

当然用var定义的变量说明了这个思路是正确的,然而let定义的变量却让我们的思路无法自圆其说。于是无比尴尬的我通过浏览器的解析结果找到了其中的猫腻。正如上图,a数组的函数在执行的时候,只有一个作用域即global所以a数组的函数执行的结果都是3,而b数组的函数在执行的时候有两个作用域,Block作用域每次的j值都会重新初始化一次。

代码理解如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var a = [], b = [];
for (var i = 0; i < 3; i++) {
let c = i
a[i] = function () {
return c
};
}

for (let j = 0; j< 3; j++) {
let c = j
b[j] = function () {
return c
}
}

a.map((value) => {
console.log(value(), 'a')
})

b.map((value) => {
console.log(value(), 'b')
})

总的来说let相当于加持了执行环境的var,可以类比于this,在隐式绑定中,obj1.obj2.foo()结果为obj2的情况我们就知晓,其实foo中的this其实也是被obj2所加持的,所以this指向了obj2。let也是如此,也会被自己声明以及定义的环境影响套上一个运行环境的作用域。


new绑定

首先我们说一下什么是new,mdn说明如下:

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。

也就是说我们通过new可以获取一个实例,这个实例可能是内置对象的实例或者自定义对象的实例。哪我们来看看用new和不用new有什么区别。

1
2
3
4
5
6
7
8
9
10
function foo(a) {
this.a = a
return this
}

var bar = new foo(2)
console.log(bar)

var test = foo(2)
console.log(test)

咦,这是什么鬼,为啥就差3个字母,能差出着么多。首先我们来分析一下通过new调用和直接调用的this是什么,直接调用的话this指向window所以test是window这个很好解释。而new创建的是一个自定义对象的实例,也就是说bar是foo的实例。所以我们拿到了一个foo的obj。既然这样哪我们强制给test绑定一个空对象,那么test就应该是一个对象而不是window了吧。

1
2
3
4
5
6
7
8
9
10
function foo(a) {
this.a = a
return this
}

var bar = new foo(2)
console.log(bar)

var test = foo.call({},2)
console.log(test)

再看下去发现bar出来的是继承foo的实例,而test仅仅是一个对象。那么这时候我们再让test继承一下foo看看。

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo(a) {
this.a = a
return this
}

var bar = new foo(2)
console.log(bar)

var test = {}
test.__proto__ = foo.prototype
test = foo.call(test,2)

console.log(test)

这时候我们在看test和bar。他们就是一样的两个实例了。所以我们总结一下:
在使用new关键字来实例化对象时,会构造一个新的对象,然后该对象继承foo,并且把this绑定到该对象,。正如我们直接执行foo时所做的三件事。


绑定优先级

我们一共说了4种this的绑定方式分别为默认绑定,隐式绑定,显示绑定以及new绑定。哪当多种绑定同时存在时我们的this该何去何从,我们写出了如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let foo = function () {
return this
}

let obj = {
a: '隐式绑定',
foo: foo
}

let bar = foo.bind({a:'显示绑定'})

console.log(obj.foo()) //隐式绑定覆盖默认绑定
console.log(obj.foo.bind({a:'显示绑定'})()) //显示绑定覆盖隐式绑定
console.log(bar()) //显示绑定覆盖默认绑定
console.log(new bar()) //new绑定覆盖显示绑定

this绑定优先级 new > bind,call,apply > 隐式绑定 > 默认绑定


箭头函数的this

什么是箭头函数,箭头函数对this有什么影响?

箭头函数表达式的语法比函数表达式更短,并且不绑定自己的this,arguments,super或new.target。这些函数表达式最适合用于非方法函数,并且它们不能用作构造函数。

从该定义中我们可以提取出以下几个观点:

1.不能用作构造函数,无法用new关键字来初始化对象。
2.this不可通过call、apply改变,且this的值取决于箭头函数定义位置,而非上下文环境。
3.没有prototype属性,arguments,super或new.target。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function foo() {
return this
}

let foo1 = () => {
return this
}

var obj = {
a: 1,
foo: foo,
foo1: foo1
}

console.log(foo.bind(obj)(), foo.call(obj)) //{a: 1, foo: ƒ, foo1: ƒ} {a: 1, foo: ƒ, foo1: ƒ}
console.log(foo1.bind(obj)(), foo1.call(obj)) //window window
console.log(obj.foo(), obj.foo1()) //{a: 1, foo: ƒ, foo1: ƒ} window

可见箭头函数的this为默认绑定的this,不存在隐式绑定,显示绑定,因为无法使用new关键字初始化,所以更不存在new绑定。


总结一下

合理使用this可以让你的代码更简洁,更优雅但是隐式绑定中有很多坑得注意。

1.对象中的函数会影响函数this指向。
2.调用函数方式会影响函数this指向。
3.回调函数会影响this指向。

总的来说存在即合理,如果不合理的话js中也不会存在this这个关键字,重要的是理解this它到底是个什么东西。