JavaScript 在 ES6 语法中新增了箭头函数, 相较于传统函数, 箭头函数不仅更加简洁, 而且在 this 方面进行了改进 this 作为 JavaScript 中比较诡异的存在, 许多文章对于 this 的解释也不尽相同, 本篇文章试图厘清 JS 中函数与 this 的关系
一 JS 中函数的写法
1. 常规函数的写法
在 ES6 语法之前, JS 中的函数由 function 关键字 params 参数和被花括号包裹的函数体组成为了与后面说到的箭头函数相区别, 我们先把这样的函数叫做常规函数, 常规函数既可以用声明式写法也可以用赋值式写法例子:
- function test(name) { // 声明式写法
- console.log(name)
- }
- test('Jerry')
- let test2 = function(name) { // 赋值式写法
- console.log(name)
- }
- test2('Tom')
2. 箭头函数的写法
ES6 箭头函数的引入, 使函数的写法变的更加简洁, 但在书写上要遵循一定的规则
规则一: 箭头函数只能用赋值式写法, 不能用声明式写法
例子:
- const test = (name) => {
- console.log(name)
- }
- test('Jerry')
规则二: 如果参数只有一个, 可以不加括号, 如果没有参数或者参数多于一个就需要加括号
例子:
- const test = name => {
- console.log(name)
- }
- test('Jerry')
- const test2 = (name1, name2) => {
- console.log(name1 + 'and' + name2)
- }
- test2('Tom', 'Jerry')
规则三: 如果函数体只有一句话, 可以不加花括号
例子:
const test = name => console.log(name)
规则四: 如果函数体没有括号, 可以不写 return, 箭头函数会帮你 return
例子:
- const add = (p1, p2) => p1 + p2
- add(10, 25)
记住: 函数体的花括号与 return 关键字同在
从以上的例子我们可以看出, 箭头函数对常规函数的圆括号和花括号都进行了简化除了这些简化, 箭头函数对于常规函数最大的优化之处在于 this
二理解常规函数中 this
在探讨箭头函数对于 this 的优化之前, 我们先得明白 this 究竟是什么, 以及它是如何使用的 this 是使用 call 方法调用函数时传递的第一个参数, 它可以在函数调用时修改, 在函数没有调用的时候, this 的值是无法确定
如果没有使用过 call 方法来调用函数的话, 上面的对于 this 的定义可能不太明白那么我们需要先理解函数调用的两种方法
1. 纯粹的函数调用
第一种方法最常见, 例子如下:
- function test(name) {
- console.log(name)
- console.log(this)
- }
- test('Jerry') // 调用函数
这种方法我们使用最多, 但是这种函数调用方法只是一种简写, 它完整的写法是下面这样的:
- function test(name) {
- console.log(name)
- console.log(this)
- }
- test.call(undefined, 'Tom')
注意到上面调用函数的 call 方法了吗? call 方法接收的第一个参数就是 this, 这里我们传了一个 undefined 那么, 依据定义, 函数执行了之后打出来的 this 会是 undefined 吗? 也不是
如果你传的 context 就 null 或者 undefined, 那么 window 对象就是默认的 context(严格模式下默认 context 是 undefined)
所以这里我们打出来的 this 是 Window 对象
2. 对象中函数的调用
直接看例子:
- const obj = {
- name: 'Jerry',
- greet: function() {
- console.log(this.name)
- }
- }
- obj.greet() // 第一种调用方法
- obj.greet.call(obj) // 第二种调用方法
例子里第一种调用方法只是第二种调用方法的语法糖, 第二种才是完整的调用方法, 而且第二种方法厉害的地方在于它可以手动指定 this
手动指定 this 的例子:
- const obj = {
- name: 'Jerry',
- greet: function() {
- console.log(this.name)
- }
- }
- obj.greet.call({name: 'Spike'}) // 打出来的是 Spike
从上面的例子我们看到 greet 函数执行时 this, 已经被我们改过了
3. 构造函数中 this
构造函数里的 this 稍微有点特殊, 每个构造函数在 new 之后都会返回一个对象, 这个对象就是 this, 也就是 context 上下文
例子:
- function Test() {
- this.name = 'Tom'
- }
- let p = new Test()
- console.log(typeof p) //object
- console.log(p.name) // Tom
4. window.setTimeout() 和 window.setInterval() 中函数的调用
window.setTimeout() 和 window.setInterval() 的函数中的 this 有些特殊, 里面的 this 默认是 window 对象
简单总结一下: 函数完整的调用方法是使用 call 方法, 包括
test.call(context, name)
和
obj.greet.call(context,name)
, 这里的 context 就是函数调用时的上下文, 也就是 this, 只不过这个 this 是可以通过 call 方法来修改的; 构造函数稍微特殊一点, 它的 this 直接指向 new 之后返回的对象;
window.setTimeout()
和
window.setInterval()
默认的是 this 是 window 对象
三理解箭头函数中的 this
上面关于 this 讲了很多, this 是函数用 call 方法调用时传递的第一个参数, 而且它还可以手动更改, 这样要确定 this 的值就太麻烦了不过, 箭头函数的出现给我们确定 this 帮了一些忙
1. 箭头函数的特性一: 默认绑定外层 this
上面提到: this 的值是可以用 call 方法修改的, 而且只有在调用的时候我们才能确定 this 的值而当我们使用箭头函数的时候, 箭头函数会默认帮我们绑定外层 this 的值, 所以在箭头函数中 this 的值和外层的 this 是一样的
不使用箭头函数例子:
- const obj = {
- a: function() { console.log(this) }
- }
- obj.a() // 打出的是 obj 对象
使用箭头函数的例子:
- const obj = {
- a: () => {
- console.log(this)
- }
- }
- obj.a() // 打出来的是 window
在使用箭头函数的例子里, 因为箭头函数默认不会使用自己的 this, 而是会和外层的 this 保持一致, 最外层的 this 就是 window 对象
2. 箭头函数的特性二: 不能用 call 方法修改里面的 this
这个也很好理解, 我们之前一直在说, 函数的 this 可以用 call 方法来手动指定, 而为了减少 this 的复杂性, 箭头函数无法用 call 方法来指定 this
例子:
- const obj = {
- a: () => {
- console.log(this)
- }
- }
- obj.a.call('123') // 打出来的结果依然是 window 对象
因为上文我们说到 window.setTimeout() 中函数里的 this 默认是 window, 我们也可以通过箭头函数使它的 this 和外层的 this 保持一致:
window.setTimeout() 的例子:
- const obj = {
- a: function() {
- console.log(this)
- window.setTimeout(() => {
- console.log(this)
- }, 1000)
- }
- }
- obj.a.call(obj) // 第一个 this 是 obj 对象, 第二个 this 还是 obj 对象
想必大家明白了, 函数 obj.a 没有使用箭头函数, 因为它的 this 还是 obj, 而 setTimeout 里的函数使用了箭头函数, 所以它会和外层的 this 保持一致, 也是 obj; 如果 setTimeout 里的函数没有使用箭头函数, 那么它打出来的应该是 window 对象
四多层对象嵌套里函数的 this
这里是笔者在学习时遇到的一点疑惑箭头函数里的 this 是和外层保持一致的, 但是如果这个外层有好多层, 那它是和哪层保持一致呢?
直接上例子:
- const obj = {
- a: function() { console.log(this) },
- b: {
- c: function() {console.log(this)}
- }
- }
- obj.a() // 打出的是 obj 对象, 相当于 obj.a.call(obj)
- obj.b.c() // 打出的是 obj.b 对象, 相当于 obj.b.c.call(obj.b)
上面的代码都符合直觉, 接下来把 obj.b.c 对应的函数换成箭头函数, 结果如下:
- const obj = {
- a: function() { console.log(this) },
- b: {
- c: () => {console.log(this)}
- }
- }
- obj.a() // 没有使用箭头函数打出的是 obj
- obj.b.c() // 打出的是 window 对象!!
obj.a 调用后打出来的是 obj 对象, 而 obj.b.c 调用后打出的是 window 对象而非 obj, 这表示多层对象嵌套里箭头函数里 this 是和最最外层保持一致的
来源: https://juejin.im/post/5aa1eb056fb9a028b77a66fd