this指向与箭头函数
this指向
引言
要理解this
的设计,我们需要先了解Javascript
中内存的数据结构;
Javascript
内置七种数据类型,可以分为基本数据类型和对象数据类型,在这里我们主要讲解一下对象数据类型在内存中的存储方式;
1 | var obj = { name: 'Heternally'}; |
Javascript
引擎在处理上面代码时,会在堆内存中,生成一个对象{ name: 'Heternally'}
,然后把这个对象在内存中的地址赋值给变量obj
。所以在读取obj.name
时,需要先从变量obj
拿到地址,然后再从对应地址中拿到对象,再返回它的name
属性。
对象的属性可能是一个函数,当引擎遇到对象属性是函数的情况,会将函数单独保存在堆中,然后再将函数的地址赋值给对象属性;而Javascript
是允许在函数体内引用当前环境的其他变量,那么问题来了,函数可以在不同的运行环境执行,所以我们就需要一种机制,能够在函数内获得当前运行环境,由此诞生了this
,它的设计目的就是指向函数运行时所在的环境。
如何正确判定this指向
总结了this
的绑定规则总共是有下面5种:请记住一点:==谁调用这个函数或方法,this关键字就指向谁。==
- 1、默认绑定(严格/非严格模式)
- 2、隐式绑定
- 3、显式绑定
- 4、new绑定
- 5、ES6箭头函数绑定
默认绑定
全局上下文
无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this
都指向全局对象。
1 | // 在浏览器中, window 对象同时也是全局对象: |
备注:你可以使用 globalThis
获取全局对象,无论你的代码是否在当前上下文运行。
函数上下文
函数内部,this
的值取决于函数被调用的方式。
因为下面的代码不在严格模式下,且 this
的值不是由该调用设置的,所以 this
的值默认指向全局对象,浏览器中就是 window
。
1 | function f1(){ |
然而,在严格模式下,如果进入执行环境时没有设置 this
的值,this
会保持为 undefined
,如下:
1 | function f2(){ |
类上下文
this
在类中的表现与在函数中类似,因为类本质上也是函数,但也有一些区别和注意事项。
在类的构造函数中,this
是一个常规对象。类中所有非静态的方法都会被添加到 this
的原型中:
1 | class Example { |
备注:静态方法不是 this 的属性,它们只是类自身的属性。
派生类
不像基类的构造函数,派生类的构造函数没有初始的 this
绑定。在构造函数中调用 super()
会生成一个 this
绑定,并相当于执行如下代码,Base为基类:
1 | this = new Base(); |
警告:在调用 super()
之前引用 this
会抛出错误。
派生类不能在调用 super()
之前返回,除非其构造函数返回的是一个对象,或者根本没有构造函数。
1 | class Base {} |
隐式绑定
当函数作为对象的属性存在,通过对象属性执行函数时,此时隐式绑定规则会将this
绑定到对象上;
1 | var name = 'Heternally'; |
注意上面代码中函数执行方式是通过对象属性执行
1 | var name = 'Heternally'; |
由上面代码可以发现,通过赋值操作后执行函数,会应用默认绑定,此时在非严格模式下this
会指向全局对象。
同样的,函数传参也是一种隐式赋值,此时在回调函数中会丢失this
绑定。
1 | function foo() { |
显式绑定
通过 call
apply
bind
绑定
1 | function add(c, d) { |
在非严格模式下使用 call
和 apply
时,如果用作 this
的值不是对象,则会被尝试转换为对象。null
和 undefined
被转换为全局对象。原始值如 7
或 'foo'
会使用相应构造函数转换为对象。因此 7
会被转换为 new Number(7)
生成的对象,字符串 'foo'
会转换为 new String('foo')
生成的对象。严格模式下不会进行转换。
1 | function bar() { |
ECMAScript 5 引入了 Function.prototype.bind()
。调用f.bind(someObject)
会创建一个与f
具有相同函数体和作用域的函数,但是在这个新函数中,this
将永久地被绑定到了bind
的第一个参数,无论这个函数是如何被调用的。
1 | function f(){ |
通过new绑定
我们先看看构造函数在使用new
后,执行了什么操作:
- 它创建(构造)了一个全新的对象
- 它会被执行[[Prototype]](也就是
__proto__
)链接 - 它使this指向新创建的对象
- 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上
- 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用将返回该对象引用
当一个函数用作构造函数时(使用new
关键字),它的this
被绑定到正在构造的新对象。
1 | var name = 'zl'; |
当构造函数返回值为对象时,直接返回这个对象;否则返回new创建的对象
所以使用new
绑定时,需要判断函数返回的值是否为一个对象,如果是对象,那么this
会绑定到返回的对象上。
ES6箭头函数绑定
它的 this 是通过作用域链查到外层作用域的 this ,且指向函数定义时的 this 而非执行时。
1 | function foo() { |
规则优先级
1 | 1、new绑定 |
箭头函数
箭头函数表达式的语法比函数表达式更简洁,并且没有自己的this
,arguments
,super
或new.target
。
箭头函数表达式更适用于那些本来需要匿名函数的地方,并且它不能用作构造函数。
1 | (param1, param2, …, paramN) => { statements } |
引入箭头函数有两个方面的作用:更简短的函数并且不绑定this
。
没有单独的this
在箭头函数出现之前,每一个新函数根据它是被如何调用的来定义这个函数的this值:
- 如果该函数是一个构造函数,this指针指向一个新的对象
- 在严格模式下的函数调用下,this指向
undefined
- 如果该函数是一个对象的方法,则它的this指针指向这个对象
- 等等
This
被证明是令人厌烦的面向对象风格的编程。
1 | function Person() { |
在ECMAScript 3/5中,通过将this
值分配给封闭的变量,可以解决this
问题。
1 | function Person() { |
或者,可以创建绑定函数,以便将预先分配的this
值传递到绑定的目标函数(上述示例中的growUp()
函数)。
箭头函数不会创建自己的this
,它只会从自己的作用域链的上一层继承this。因此,在下面的代码中,传递给setInterval
的函数内的this
与封闭函数中的this
值相同:
1 | function Person(){ |
与严格模式的关系
鉴于 this
是词法层面上的,严格模式中与 this
相关的规则都将被忽略。
普通函数严格模式下this指向undefined。
1 | var f = () => { 'use strict'; return this; }; |
严格模式的其他规则依然不变。
通过call、apply或bind调用不能绑定this
由于箭头函数没有自己的this指针,通过 call()
、 apply()
或bind()
方法调用一个函数时,只能传递参数(不能绑定this),他们的第一个参数会被忽略。
1 | var adder = { |
不绑定arguments
箭头函数不绑定Arguments 对象。因此,在本示例中,arguments
只是引用了封闭作用域内的arguments:
1 | var arguments = [1, 2, 3]; |
在大多数情况下,使用剩余参数是相较使用arguments
对象的更好选择。
1 | function foo(arg) { |
使用箭头函数作为方法
如上所述,箭头函数表达式对非方法函数是最合适的。让我们看看当我们试着把它们作为方法时发生了什么。
1 | ; |
箭头函数没有定义this绑定。
不能使用 new
操作符
箭头函数不能用作构造器,和 new
一起用会抛出错误。
1 | var Foo = () => {}; |
没有prototype
属性
箭头函数没有prototype
属性。
1 | var Foo = () => {}; |
不能使用 yield
关键字
yield
关键字通常不能在箭头函数中使用(除非是嵌套在允许使用的函数内)。因此,箭头函数不能用作函数生成器。
函数体
箭头函数可以有一个“简写体”或常见的“块体”。
在一个简写体中,只需要一个表达式,并附加一个隐式的返回值。在块体中,必须使用明确的return
语句。
1 | var func = x => x * x; |
返回对象字面量
记住用params => {object:literal}
这种简单的语法返回对象字面量是行不通的。
1 | var func = () => { foo: 1 }; |
这是因为花括号({}
)里面的代码被解析为一系列语句(即 foo
被认为是一个标签,而非对象字面量的组成部分)。
所以,记得用圆括号把对象字面量包起来:
1 | var func = () => ({foo: 1}); |
换行
箭头函数在参数和箭头之间不能换行。
1 | var func = () |
但是,可以通过在 ‘=>’ 之后换行,或者用 ‘( )’、’{ }’来实现换行,如下:
1 | var func = (a, b, c) => |
解析顺序
虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则。
1 | let callback; |