论普通函数和箭头函数的区别以及箭头函数的注意事项、不适用场景

79次阅读

共计 5736 个字符,预计需要花费 15 分钟才能阅读完成。

箭头函数是 ES6 的 API,相信很多人都知道,因为其语法上相对于普通函数更简洁,深受大家的喜爱。就是这种我们日常开发中一直在使用的 API,大部分同学却对它的了解程度还是不够深 …
普通函数和箭头函数的区别:
箭头函数的 this 指向规则:
1. 箭头函数没有 prototype(原型),所以箭头函数本身没有 this
let a = () =>{};
console.log(a.prototype); // undefined
2. 箭头函数的 this 指向在定义的时候继承自外层第一个普通函数的 this。
下面栗子中在一个函数中定义箭头函数,然后在另一个函数中执行箭头函数。
let a,
barObj = {msg: ‘bar 的 this 指向 ’};
fooObj = {msg: ‘foo 的 this 指向 ’};
bar.call(barObj); // 将 bar 的 this 指向 barObj
foo.call(fooObj); // 将 foo 的 this 指向 fooObj
function foo() {
a(); // 结果:{ msg: ‘bar 的 this 指向 ’}
}
function bar() {
a = () => {
console.log(this, ‘this 指向定义的时候外层第一个普通函数 ’); //
}; // 在 bar 中定义 this 继承于 bar 函数的 this 指向
}
从上面栗子中可以得出两点

箭头函数的 this 指向定义时所在的外层第一个普通函数,跟使用位置没有关系。
被继承的普通函数的 this 指向改变,箭头函数的 this 指向会跟着改变

3. 不能直接修改箭头函数的 this 指向
上个栗子中的 foo 函数修改一下,尝试直接修改箭头函数的 this 指向。
let fnObj = {msg: ‘ 尝试直接修改箭头函数的 this 指向 ’};
function foo() {
a.call(fnObj); // 结果:{msg: ‘bar 的 this 指向 ’}
}
很明显,call 显示绑定 this 指向失败了,包括 aaply、bind 都一样。
它们 (call、aaply、bind) 会默认忽略第一个参数,但是可以正常传参。
然后我又通过隐式绑定来尝试同样也失败了,new 调用会报错,这个稍后再说。
SO,箭头函数不能直接修改它的 this 指向。
幸运的是,我们可以通过间接的形式来修改箭头函数的指向:
去修改被继承的普通函数的 this 指向,然后箭头函数的 this 指向也会跟着改变,这在上一个栗子中有演示。
bar.call(barObj); // 将 bar 普通函数的 this 指向 barObj 然后内部的箭头函数也会指向 barObj
4. 箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)
唔,这个问题实际上是面试官提出来的,当时我认为的箭头函数规则就是:箭头函数的 this 指向继承自外层第一个普通函数的 this,现在看来真是不严谨(少说一个定义的时候),要是面试官问我:定义和执行不在同一个普通函数中,它又指向哪里,肯定歇菜 …
既然箭头函数的 this 指向在定义的时候继承自外层第一个普通函数的 this,那么:
当箭头函数外层没有普通函数,它的 this 会指向哪里?
这里跟我之前写的 this 绑定规则不太一样(不懂的可以点进去看一下), 普通函数的默认绑定规则是:
在非严格模式下,默认绑定的 this 指向全局对象,严格模式下 this 指向 undefined
如果箭头函数外层没有普通函数继承,它 this 指向的规则:
经过测试,箭头函数在全局作用域下,严格模式和非严格模式下它的 this 都会指向 window(全局对象)。
Tip:测试的时候发现严格模式在中途声明无效,必须在全局 / 函数的开头声明才会生效:
a = 1;
‘use strict’; // 严格模式无效 必须在一开始就声明严格模式
b = 2; // 不报错
箭头函数的
箭头函数的 arguments
箭头函数的 this 指向全局,使用 arguments 会报未声明的错误
如果箭头函数的 this 指向 window(全局对象)使用 arguments 会报错,未声明 arguments。
let b = () => {
console.log(arguments);
};
b(1, 2, 3, 4); // Uncaught ReferenceError: arguments is not defined
PS:如果你声明了一个全局变量为 arguments,那就不会报错了,但是你为什么要这么做呢?
箭头函数的 this 指向普通函数时, 它的 argumens 继承于该普通函数
上面是第一种情况:箭头函数的 this 指向全局对象,会报 arguments 未声明的错误。
第二种情况是:箭头函数的 this 如果指向普通函数, 它的 argumens 继承于该普通函数。
function bar() {
console.log(arguments); // [‘ 外层第二个普通函数的参数 ’]
bb(‘ 外层第一个普通函数的参数 ’);
function bb() {
console.log(arguments); // [“ 外层第一个普通函数的参数 ”]
let a = () => {
console.log(arguments, ‘arguments 继承 this 指向的那个普通函数 ’); // [“ 外层第一个普通函数的参数 ”]
};
a(‘ 箭头函数的参数 ’); // this 指向 bb
}
}
bar(‘ 外层第二个普通函数的参数 ’);
那么应该如何来获取箭头函数不定数量的参数呢?答案是:ES6 的 rest 参数(… 扩展符)
rest 参数获取函数的多余参数
这是 ES6 的 API,用于获取函数不定数量的参数数组,这个 API 是用来替代 arguments 的,API 用法如下:
let a = (first, …abc) => {
console.log(first, abc); // 1 [2, 3, 4]
};
a(1, 2, 3, 4);
上面的栗子展示了,获取函数除第一个确定的参数,以及用一个变量接收其他剩余参数的示例。
也可以直接接收函数的所有参数,rest 参数的用法相对于 arguments 的优点:

箭头函数和普通函数都可以使用。
更加灵活,接收参数的数量完全自定义。
可读性更好参数都是在函数括号中定义的,不会突然出现一个 arguments,以前刚见到的时候,真的好奇怪了!

rest 是一个真正的数组,可以使用数组的 API。
因为 arguments 是一个类数组的对象,有些人以为它是真正的数组,所以会出现以下场景:
arguments.push(0); // arguments.push is not a function
如上,如果我们需要使用数组的 API,需要使用扩展符 /Array.from 来将它转换成真正的数组:
arguments = […arguments]; 或者:arguments = Array.from(arguments);

rest 参数有两点需要注意:

rest 必须是函数的最后一位参数:
let a = (first, …rest, three) => {
console.log(first, rest,three); // 报错:Rest parameter must be last formal parameter
};
a(1, 2, 3, 4);

函数的 length 属性,不包括 rest 参数
(function(…a) {}).length // 0
(function(a, …b) {}).length // 1

扩展运算符还可以用于数组,这里是阮一峰老师的文档
PS:感觉这里写多了,但比较喜欢把一个知识点讲清楚 …
使用 new 调用箭头函数会报错
无论箭头函数的 thsi 指向哪里,使用 new 调用箭头函数都会报错,因为箭头函数没有 constructor
let a = () => {};
let b = new a(); // a is not a constructor
箭头函数不支持 new.target:
new.target 是 ES6 新引入的属性,普通函数如果通过 new 调用,new.target 会返回该函数的引用。
此属性主要:用于确定构造函数是否为 new 调用的。

箭头函数的 this 指向全局对象,在箭头函数中使用箭头函数会报错
let a = () => {
console.log(new.target); // 报错:new.target 不允许在这里使用
};
a();

箭头函数的 this 指向普通函数,它的 new.target 就是指向该普通函数的引用。
new bb();
function bb() {
let a = () => {
console.log(new.target); // 指向函数 bb:function bb(){…}
};
a();
}

更多关于 new.target 可以看一下阮一峰老师关于这部分的解释。
箭头函数不支持重命名函数参数, 普通函数的函数参数支持重命名
如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:
function func1(a, a) {
console.log(a, arguments); // 2 [1,2]
}

var func2 = (a,a) => {
console.log(a); // 报错:在此上下文中不允许重复参数名称
};
func1(1, 2); func2(1, 2);
箭头函数相对于普通函数语法更简洁优雅:
讲道理,语法上的不同,也属与它们两个的区别!

箭头函数都是匿名函数,并且都不用写 function

只有一个参数的时候可以省略括号:
var f = a => a; // 传入 a 返回 a

函数只有一条语句时可以省略 {} 和 return
var f = (a,b,c) => a; // 传入 a,b,c 返回 a

简化回调函数,让你的回调函数更优雅:

[1,2,3].map(function (x) {
return x * x;
}); // 普通函数写法
[1,2,3].map(x => x * x); // 箭头函数只需要一行

箭头函数的注意事项及不适用场景
箭头函数的注意事项
一条语句返回对象字面量,需要加括号,或者直接写成多条语句的 return 形式,否则像 func 中演示的一样,花括号会被解析为多条语句的花括号,不能正确解析

var func1 = () => { foo: 1}; // 想返回一个对象, 花括号被当成多条语句来解析,执行后返回 undefined
var func2 = () => ({foo: 1}); // 用圆括号是正确的写法
var func2 = () => {
return {
foo: 1 // 更推荐直接当成多条语句的形式来写,可读性高
};
};
箭头函数在参数和箭头之间不能换行!
var func = ()
=> 1; // 报错:Unexpected token =>
箭头函数的解析顺序相对靠前
MDN: 虽然箭头函数中的箭头不是运算符,但箭头函数具有与常规函数不同的特殊运算符优先级解析规则
let a = false || function() {}; // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok
箭头函数不适用场景:
围绕两点:箭头函数的 this 意外指向和代码的可读性。
定义字面量方法,this 的意外指向。
因为箭头函数的简洁
const obj = {
array: [1, 2, 3],
sum: () => {
// 根据上文学到的:外层没有普通函数 this 会指向全局对象
return this.array.push(‘ 全局对象下没有 array,这里会报错 ’); // 找不到 push 方法
}
};
obj.sum();
上述栗子使用普通函数或者 ES6 中的方法简写的来定义方法,就没有问题了:
// 这两种写法是等价的
sum() {
return this.array.push(‘this 指向 obj’);
}
sum: function() {
return this.array.push(‘this 指向 obj’);
}
还有一种情况是给普通函数的原型定义方法的时候,通常会在普通函数的外部进行定义,比如说继承 / 添加方法的时候。
这时候因为没有在普通函数的内部进行定义,所以 this 会指向其他普通函数,或者全局对象上,导致 bug!
回调函数的动态 this
下文是一个修改 dom 文本的操作,因为 this 指向错误,导致修改失败:
const button = document.getElementById(‘myButton’);
button.addEventListener(‘click’, () => {
this.innerHTML = ‘Clicked button’; // this 又指向了全局
});
相信你也知道了,改成普通函数就成了。

考虑代码的可读性,使用普通函数

函数体复杂:具体表现就是箭头函数中使用多个三元运算符号,就是不换行,非要在一行内写完,非常恶心!

行数较多
函数内部有大量操作

文章内容小结:
普通函数和箭头函数的区别:

箭头函数没有 prototype(原型),所以箭头函数本身没有 this
箭头函数的 this 在定义的时候继承自外层第一个普通函数的 this。
如果箭头函数外层没有普通函数,严格模式和非严格模式下它的 this 都会指向 window(全局对象)
箭头函数本身的 this 指向不能改变,但可以修改它要继承的对象的 this。
箭头函数的 this 指向全局,使用 arguments 会报未声明的错误。
箭头函数的 this 指向普通函数时, 它的 argumens 继承于该普通函数
使用 new 调用箭头函数会报错,因为箭头函数没有 constructor

箭头函数不支持 new.target

箭头函数不支持重命名函数参数, 普通函数的函数参数支持重命名
箭头函数相对于普通函数语法更简洁优雅

箭头函数的注意事项及不适用场景
箭头函数的注意事项:

箭头函数一条语句返回对象字面量,需要加括号
箭头函数在参数和箭头之间不能换行
箭头函数的解析顺序相对 || 靠前

不适用场景:箭头函数的 this 意外指向和代码的可读性。

结语
呕心沥血,可以说是很全了,反正第一次问到我的时候只能想到箭头函数的 this 是继承而来的,以及语法上的简洁性,其他的我都不知道,希望这篇文章能够帮助各位同学学到知识。
PS:目前找工作中,求大佬们内推,中高级前端,偏 JS,Vue,上海杨浦。
博客、前端积累文档、公众号、GitHub、wx:OBkoro1、邮箱:obkoro1@foxmail.com
以上 2019.03.22
参考资料:
MDN 箭头函数
阮一峰 -ES6 入门
什么时候你不能使用箭头函数?

正文完
 0