箭头函数和普通函数
1、箭头函数
ES6中允许使用箭头 =>
来定义函数。箭头函数相当于匿名函数,并且简化了函数定义。
举个例子🌰:
const add = (x, y) => x + y;
const fn = (name) => ({ name });
箭头函数特性:
- 语法简洁: 箭头函数的语法更为简洁,通常可以使用更少的代码来定义函数。
- 没有自己的
this
: 箭头函数没有自己的this
上下文,它会继承外层作用域的this
值。 - 不能作为构造函数: 箭头函数不能使用
new
关键字创建对象实例,也不能用作构造函数。 - 不能使用 arguments 对象: 箭头函数没有自己的
arguments
对象,它会继承外层作用域的arguments
。 - 适合短小的函数: 由于简洁的语法,箭头函数特别适合定义简短的函数或用作回调函数。
2、普通函数
函数的定义方式通常有三种:函数声明方式
、函数表达式
、 使用 Function
构造函数 。
2.1、函数声明方式
function add(x, y) {
return x + y;
}
2.2、函数表达式
const fn = function (name) {
return { name };
}
2.3、使用Function构造函数
Function
构造函数可以接收任意数量的参数,最后一个参数为函数体,其他的参数则枚举出新函数的参数。
const sum = new Function("num1", "num2", "return num1+num2");
2.4、三种定义方式的区别
三种方式的区别,可以从作用域、效率以及加载顺序来区分。
2.4.1、作用域
函数声明式、函数表达式和 Function()
使用的都是局部变量
。
var name = '我是全局变量 name';
// 声明式
function a() {
var name = '我是函数a中的name';
return name;
}
console.log(a()); // 打印: "我是函数a中的name"
// 表达式
var b = function () {
var name = '我是函数b中的name';
return name; // 打印: "我是函数b中的name"
};
console.log(b());
// Function构造函数
var c = new Function('const name = "我是函数c中的name"; return name;');
console.log(c()); // 打印: "我是函数c中的name"
2.4.2、执行效率
Function()
构造函数的效率要低于其它两种方式,尤其是在循环体中,因为构造函数每执行一次都要重新编译,并且生成新的函数对象。
2.4.3、加载顺序
function
方式(即函数声明式)是在 JavaScript 编译的时候就加载到作用域中,而其他两种方式则是在代码执行的时候加载,如果在定义之前调用它,则会返回 undefined
。
console.log(typeof f); // function
console.log(typeof c); // undefined
console.log(typeof d); // undefined
function f() {
return 'JS 深入浅出';
}
var c = function () {
return 'JS 深入浅出';
};
console.log(typeof c); // function
var d = new Function('return "JS 深入浅出"');
console.log(typeof d); // function
2.5、函数的参数
2.5.1、arguments
arguments
对象的 length
属性显示实参的个数,函数的length
属性显示形参的个数。
function sum(x, y) {
console.log(arguments.length); // 3
return x + 1;
}
sum(1, 2, 3);
console.log(sum.length); // 2
2.5.2、同名参数
在非严格模式下,函数中可以出现同名形参,且只能访问最后出现的该名称的形参。
function sum(x, x, x) {
return x;
}
console.log(sum(1, 2, 3)); // 3
而在严格模式下,出现同名形参会抛出语法错误。
function sum(x, x, x) {
'use strict';
return x;
}
console.log(sum(1, 2, 3)); // SyntaxError: Duplicate parameter name not allowed in this context
2.6、函数的返回值
所有函数都有返回值,没有 return
语句时,默认返回内容为undefined
。
function sum1(x, y) {
var total = x + y;
}
console.log(sum1()); // undefined
function sum2(x, y) {
return x + y;
}
console.log(sum2(1, 2)); // 3
如果函数调用时在前面加上了 new
前缀,且返回值不是一个对象,则返回this
(该新对象)。
function Book() {
this.bookName = 'JS 深入浅出';
}
var book = new Book();
console.log(book); // Book { bookName: 'JS 深入浅出' }
console.log(book.constructor); // [Function: Book]
如果返回值是一个对象,则返回该对象。
function Book() {
return { bookName: 'JS 深入浅出' };
}
var book = new Book();
console.log(book); // { bookName: 'JS 深入浅出' }
console.log(book.constructor); // [Function: Book]
2.7、匿名函数
匿名函数是一种在 JavaScript 中定义函数的方式,它没有给函数起一个具体的名字标识符。匿名函数通常在需要时直接定义并使用,而不需要预先命名。这种函数在语法上与普通函数类似,只是省略了函数名。
匿名函数有多种用途,常见的包括:
2.7.1、函数表达式
将一个匿名函数赋值给变量,可以创建函数表达式,这使得函数能够像其他数据类型一样存储在变量中。
const add = function(x, y) {
return x + y;
};
console.log(add(3, 5)); // 输出 8
2.7.2、作为参数传递
可以将匿名函数作为参数传递给其他函数,用于回调或执行特定操作。
setTimeout(function() {
console.log('Delayed message');
}, 1000);
2.7.3、立即执行函数表达式(IIFE)
这是一种常见的模式,用于在定义后立即执行匿名函数,以创建一个私有作用域。
(function() {
console.log('IIFE executed');
})();
2.7.4、函数属性
将匿名函数分配给对象属性,以创建对象的方法。
const obj = {
sayHello: function() {
console.log('Hello');
}
};
obj.sayHello(); // 输出 'Hello'
需要注意的是,匿名函数在定义后只能通过变量、参数、属性等方式引用,无法直接通过函数名调用。这与具名函数不同,具名函数可以通过函数名直接调用。
总之,匿名函数是一种在需要时临时创建函数的方式,适用于各种场景,特别是在需要传递函数作为参数、创建私有作用域或动态定义函数时。
普通函数特性:
- 拥有自己的
this
: 普通函数在调用时会有自己的this
上下文,this
的值取决于函数被调用的方式。 - 可以作为构造函数: 普通函数可以通过
new
关键字创建对象实例,用作构造函数。 - 可以使用 arguments 对象: 普通函数拥有自己的
arguments
对象,用于获取函数参数。 - 语法相对复杂: 普通函数的语法相对复杂一些,需要使用
function
关键字来定义。
3、箭头函数与普通函数的区别
我们可以通过打印箭头函数和普通函数来看看两者到底有什么区别:
let fn = (name) => {
console.log(name);
};
let fn2 = function (name) {
console.log(name);
};
console.dir(fn);
console.dir(fn2);
从打印结果来看,箭头函数与普通函数相比,缺少了 caller
,arguments
,prototype
。
3.1、声明方式不同
-
声明一个普通函数需要使用关键字
function
来完成,并且使用function
既可以声明成一个具名函数也可以声明成一个匿名函数。 -
声明一个箭头函数则只需要使用箭头就可以,无需使用关键字
function
,比普通函数声明更简洁。 -
箭头函数只能声明成匿名函数,但可以通过表达式的方式让箭头函数具名。
3.2、箭头函数没有prototype
(原型)
const a = () => {};
console.log(a.prototype); // undefined
const b = function () {};
console.log(b.prototype); // {constructor: ƒ}
3.3、箭头函数不能当成一个构造函数
let fn = (value) => value;
const f = new fn('hi'); // Uncaught TypeError: fn is not a constructor
我们知道 new
内部实现原理:
- 创建一个新的空对象。
- 将该对象的原型链链接到构造函数的原型对象上,使其继承构造函数的属性和方法。
- 将构造函数中的
this
指向新创建的对象。 - 执行构造函数内部的代码,给新对象添加属性和方法。
- 如果构造函数没有返回其他对象,则返回新创建的对象;如果构造函数返回了一个非基本类型的值(对象),则返回这个对象,否则还是返回新创建的对象。
function myNew(fn, ...args) {
// 创建一个新的空对象
let target = {};
// 将这个空对象的__proto__指向构造函数的原型
target.__proto__ = fn.prototype;
// 将this指向空对象
let res = fn.apply(target, args);
// 对构造函数返回值做判断,然后返回对应的值
return res instanceof Object ? res : target;
}
因为箭头函数没有自己的 this
,它的 this
其实是继承了外层执行环境中的 this
,且 this
指向永远不会变,并且箭头函数没有原型 prototype
,没法让它的实例的 __proto__
指向箭头函数原型,所以箭头函数也就无法作为构造函数,否则用 new
调用时会报错。
3.4、箭头函数不支持new.target
new
是从构造函数生成实例对象的命令。ES6 为 new
命令引入了一个 new.target
属性,这个属性一般用在构造函数中,返回 new
调用的那个构造函数。如果构造函数不是通过 new
命令或 Reflect.construct()
调用的, new.target
会返回 undefined
,所以这个属性可以用来确定构造函数是怎么调用的。
function fn() {
console.log('fn:', new.target);
}
fn(); // fn: undefined
new fn(); // fn: [Function: fn]
// 箭头函数的this指向全局对象时
let fn2 = () => {
console.log('fn2', new.target);
}
fn2(); // Uncaught SyntaxError: new.target expression is not allowed here
// 箭头函数的this指向普通函时
function func() {
let test = () => {
console.log(new.target); // 指向函数func:[Function: func]
};
test();
}
new func();
new.target
属性一般用在构造函数中,返回new
调用的那个构造函数。- 箭头函数的
this
指向全局对象,在箭头函数中使用new.target
会报错。 - 箭头函数的
this
指向普通函数,它的new.target
就是指向该普通函数的引用。
3.5、this
指向规则
对于普通函数来说,内部的this
指向函数运行时所在的对象
,但是这一点对箭头函数不成立。它没有自己的this
对象,内部的this
就是定义时上层作用域中的this
。也就是说,箭头函数内部的this
指向是固定的,相比之下,普通函数的this
指向是可变的。
const name = 'hello';
const person = {
name: 'hi',
say: function () {
console.log('say:', this.name);
},
say2: () => {
console.log('say2:', this.name);
},
};
person.say(); // say: hi
person.say2(); // say2: hello
这里第一个 say
定义的是一个普通函数,并且它是作为对象 person
的方法来进行调用的,所以它的 this
指向的就是 person
,所以它应该会输出 say: hi
。
而第二个 say2
定义的是一个箭头函数,我们知道箭头函数本身没有 this
,它的 this
永远指向它定义时所在的上层作用域,所以 say2
的 this
应该指向的是 全局window
,所以它会输出 say2: hello
。
我们也可以通过Babel
转箭头函数产生的 ES5
代码来证明箭头函数没有自己的this
,而是引用的上层作用域中this
。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
转换后的 ES5
版本清楚地说明了,箭头函数里面根本没有自己的 this
,而是引用的上层作用域中this
。
3.5.1、箭头函数的this指向
箭头函数的 this
指向在定义的时候继承自外层第一个普通函数的 this
。而普通函数它的 this
是在运行时动态绑定的,根据函数的调用方式和上下文而定。
下面例子中在一个函数中定义箭头函数,然后在另一个函数中执行箭头函数。
let a;
let barObj = { msg: 'bar的this指向' };
let fooObj = { msg: 'foo的this指向' };
bar.call(barObj); // 将bar的this指向barObj
foo.call(fooObj); // 将foo的this指向fooObj
function foo() {
a(); // 结果:{ msg: 'bar的this指向' }
}
function bar() {
// 在bar中定义 this继承于bar函数的this指向
a = () => {
console.log(this, 'this指向定义的时候外层第一个普通函数'); // { msg: 'bar的this指向' } this指向定义的时候外层第一个普通函数
};
}
从上面例子中可以得出两点:
- 箭头函数的
this
指向定义时所在的外层第一个普通函数,跟使用位置没有关系。 - 被继承的普通函数的
this
指向改变,箭头函数的this
指向会跟着改变。
3.5.2、不能直接修改箭头函数的this指向
把上个例子中的 foo
函数修改一下,尝试直接修改箭头函数的 this
指向。
let a;
let barObj = { msg: 'bar的this指向' };
let fooObj = { msg: 'foo的this指向' };
let fnObj = { msg: '尝试直接修改箭头函数的this指向' };
bar.call(barObj);
foo.call(fooObj);
function foo() {
a.call(fnObj); // 结果:{ msg: 'bar的this指向' }
}
function bar() {
a = () => {
console.log(this);
};
}
很明显,call
显示绑定 this
指向失败了,包括 aaply
、bind
都一样。它们(call
、aaply
、bind
)会默认忽略第一个参数,但是可以正常传参。
所以箭头函数不能直接修改它的 this
指向。幸运的是,我们可以通过间接的形式来修改箭头函数的指向:去修改被继承的普通函数的this指向,然后箭头函数的this指向也会跟着改变
,这在上一个例子中有演示。
bar.call(barObj); // 将bar普通函数的this指向barObj 然后内部的箭头函数也会指向barObj
3.5.3、函数外层有没有对象
箭头函数外层没有普通函数,严格模式和非严格模式下它的 this
都会指向 window(全局对象)
。
普通函数的默认绑定规则是:在非严格模式下,默认绑定的 this
指向全局对象
,严格模式下 this
指向 undefined
。
3.6、箭头函数的 arguments
箭头函数处于全局作用域中
,则没有 arguments
,使用 arguments
会报未声明的错误。
let fn = () => {
console.log(arguments);
};
fn(); // Uncaught ReferenceError: arguments is not defined
let fn2 = function () {
console.log(arguments);
};
fn2(); // Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
还是用这两个函数来比较,普通函数能够打印出 arguments
,箭头函数使用 arguments
则会报错,因为箭头函数自身是没有 arguments
的,然后它会往上层作用域中去查找 arguments
,由于全局作用域中并没有定义 arguments
,所以会报错。
箭头函数处于普通函数的作用域中
,arguments
则是上层普通函数的 arguments
。
let fn2 = function () {
console.log('fn2:', arguments); // fn2: Arguments ['hi', callee: ƒ, Symbol(Symbol.iterator): ƒ]
let fn = () => {
console.log('fn:', arguments); // fn: Arguments ['hi', callee: ƒ, Symbol(Symbol.iterator): ƒ]
};
fn();
};
fn2();
这里两个函数的 arguments
相同,都是 fn2
函数的 arguments
。
普通函数拥有自己的
arguments
对象,可以访问函数传入的参数。箭头函数没有自己的arguments
对象,会共享外层作用域的arguments
。普通函数拥有自己的独立作用域,而箭头函数没有独立的作用域,会共享定义时的外层作用域。
3.7、rest
参数获取函数的多余参数
ES6 引入 rest
参数,用于获取函数不定数量的参数数组,这个API是用来替代 arguments
的,形式为 ...变量名
, rest
参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
let fn = (a, ...args) => {
console.log(a); // 1
console.log(args); // [2,3,4,5,6]
};
fn(1, 2, 3, 4, 5, 6);
需要⚠️注意的是:
rest
参数只能作为函数的最后一个参数
let a = (a, ...rest, b) => {
console.log(a, rest, b); // Uncaught SyntaxError: Rest parameter must be last formal parameter
};
a(1, 2, 3, 4);
- 函数的
length
属性,不包括rest
参数
const fn = function (...a) {};
console.log(fn.length); // 0
const fn2 = function (a, ...b) {};
console.log(fn2.length); // 1
rest参数与arguments的比较:
- 箭头函数和普通函数都可以使用
rest
参数,而arguments
只能普通函数使用。 - 接受参数
rest
比arguments
更加灵活。 rest
参数是一个真正的数组,而arguments
是一个类数组对象,不能直接使用数组方法,需要使用扩展符
([...arguments]
)或者Array.from
(Array.from(arguments)
) 来将它转换成真正的数组。
3.8、箭头函数不能重复函数参数名称
箭头函数不支持重命名函数参数,而普通函数的函数参数支持重命名。
如下示例,普通函数的函数参数支持重命名,后面出现的会覆盖前面的,箭头函数会抛出错误:
function func1(a, a) {
console.log(a, arguments); // 2 Arguments(2) [1, 2, callee: ƒ, Symbol(Symbol.iterator): ƒ]
}
func1(1, 2);
var func2 = (a, a) => {
console.log(a); // Uncaught SyntaxError: Duplicate parameter name not allowed in this context
};
func2(1, 2);
3.9、箭头函数不可以使用 yield
命令
不可以使用yield
命令,因此箭头函数不能用作 Generator 函数。
这个可能是由于历史原因哈,TC39 在 2013 年和 2016 年分别讨论过两次,从 *()
、*=>
、=*>
、=>*
中选出了 =>*
,勉强进入了 stage 1。而且因为有了异步生成器(async generator) ,所以还得同时考虑**异步箭头生成器(async arrow generator)**的东西,之前生成器 99.999% 的用途都是拿它来实现异步编程
,并不是真的需要生成器本来的用途,自从有了 async/await
,generator
生成器越来越没人用了。猜测可能是因为这个原因添加一个使用频率不高的语法,给规范带来较大的复杂度可能不值当。
4、箭头函数的注意事项
4.1、一条语句返回对象时需要加括号
一条语句返回对象字面量,需要加括号,或者直接写成多条语句的 return
形式,否则像 func
中演示的一样,花括号会被解析为多条语句的花括号,不能正确解析。
var func1 = () => { foo: 1 }; // 想返回一个对象,花括号被当成多条语句来解析,执行后返回undefined
var func2 = () => ({foo: 1}); // 用圆括号是正确的写法
var func2 = () => {
return {
foo: 1 // 更推荐直接当成多条语句的形式来写,可读性高
};
};
4.2、箭头函数在参数和箭头之间不能换行
var func = ()
=> 1; // 报错: Unexpected token =>
4.3、箭头函数的解析顺序相对 ||
靠前
let a = false || function() {}; // ok
let b = false || () => {}; // Malformed arrow function parameter list
let c = false || (() => {}); // ok
在上面的例子中,c
被赋值为一个逻辑表达式 false || (() => true)
。逻辑或运算符 ||
是短路运算,如果左侧表达式的值为真(truthy),则整个表达式的结果为真,不会再计算右侧的表达式。因此,当左侧的表达式 false
时,右侧的箭头函数 () => true
并没有被执行,而是被当作整个表达式的值。
5、箭头函数不适用场景
5.1、定义字面量方法,this
的意外指向。
var name = 'hello';
var person = {
name: 'hi',
say: function () {
console.log('say:', this.name); // say: hi
},
say2: () => {
console.log('say2:', this.name); //say2: hello
},
};
person.say();
person.say2();
上面代码中,person.say2()
方法是一个箭头函数,调用 person.say2()
时,使得 this
指向全局对象,因此不会得到预期结果。这是因为对象不构成单独的作用域,导致 say2()
箭头函数定义时的作用域就是全局作用域
。而 say()
定义的是一个普通函数,它内部的 this
就指向调用它的那个对象,所以使用普通函数符合预期。
5.2、当函数需要动态this时
下面是一个修改 dom
文本的操作,因为 this
指向错误,导致修改失败:
var button = document.querySelector('.btn');
button.addEventListener('click', () => {
this.innerHTML = 'Clicked button'; // this又指向了全局
});
这里很显然会报错,因为按钮点击的回调是一个箭头函数,而箭头函数内部的 this
永远都是指向它的上层作用域中的 this
,在这里就是 window
,所以会报错。这里只需要将箭头函数改成普通函数就能正常调用了。
5.3、考虑代码的可读性,使用普通函数
-
函数体复杂(具体表现就是箭头函数中使用多个三元运算符号,就是不换行,非要在一行内写完,非常恶心)。
-
行数较多。
-
函数内部有大量操作。
6、面试题
var name = 'global_name';
function Person(name) {
this.name = name;
this.foo1 = function () {
console.log(this.name);
};
this.foo2 = () => console.log(this.name);
this.foo3 = function () {
return function () {
console.log(this.name);
};
};
this.foo4 = function () {
return () => {
console.log(this.name);
};
};
}
var person1 = new Person('person1_name');
var person2 = new Person('person2_name');
person1.foo1();
person1.foo1.call(person2);
person1.foo2();
person1.foo2.call(person2);
person1.foo3()();
person1.foo3.call(person2)();
person1.foo3().call(person2);
person1.foo4()();
person1.foo4.call(person2)();
person1.foo4().call(person2);
// 揭晓答案:
// person1.foo1(); => person1_name
// person1.foo1.call(person2); => person2_name
// person1.foo2(); => person1_name
// person1.foo2.call(person2); => person1_name
// person1.foo3()(); => global_name
// person1.foo3.call(person2)(); => global_name
// person1.foo3().call(person2); => person2_name
// person1.foo4()(); => person1_name
// person1.foo4.call(person2)(); => person2_name
// person1.foo4().call(person2); => person1_name
解析:
全局代码执行,person1 = new Person('person1_name'); person2 = new Person('person2_name')
执行完, person1
中的 this.name
为 person1_name
,person2
中的 this.name
为 person2_name
,OK这一点清楚后,继续往下看:
- 执行
person1.foo1()
,foo1
为普通函数,所以this
应该指向person1
,打印出person1_name
。 - 执行
person1.foo1.call(person2)
,foo1
为普通函数,并且用call
改变了this
指向,所以它里面的this
应该指向person2
,打印出person2_name
。 - 执行
person1.foo2()
,foo2
为箭头函数,它的this
指向上层作用域,也就是person1
,所以打印出person1_name
。 - 执行
person1.foo2.call(person2)
,箭头函数的this
指向无法使用call
改变,所以它的this
还是指向person1
,打印出person1_name
。 - 执行
person1.foo3()()
,这里先执行person1.foo3()
,它返回了一个普通函数,接着再执行这个函数,此时就相当于在全局作用域中执行了一个普通函数,所以它的this
指向window
,打印出global_name
。 - 执行
person1.foo3.call(person2)()
这个与上面类似,也是返回了一个普通函数再执行,其实前面的执行都不用关心,它也是相当于在全局作用域中执行了一个普通函数,所以它的this
指向window
,打印出global_name
。 - 执行
person1.foo3().call(person2)
这里就是把foo3
返回的普通函数的this
绑定到person2
上,所以打印出person2_name
。 - 执行
person1.foo4()()
,先执行person1.foo4()
返回了一个箭头函数,再执行这个箭头函数,由于箭头函数的this
始终指向它的上层作用域,所以打印出person1_name
。 - 执行
person1.foo4.call(person2)()
,与上面类似只不过使用call
把上层作用域的this
改成了person2
,所以打印出person2_name
。 - 执行
person1.foo4().call(person2)
,这里是先执行了person1.foo4()
,返回了箭头函数,再试图通过call
改变改变该箭头函数的this
指向,上面我们说到箭头函数的this始终指向它的上层作用域,所以打印出person1_name
。
7、总结
-
箭头函数没有
prototype
(原型),所以箭头函数本身没有this
。 -
箭头函数的
this
在定义的时候继承自外层第一个普通函数的this
。 -
如果箭头函数外层没有普通函数,严格模式和非严格模式下它的
this
都会指向window
(全局对象)。 -
箭头函数本身的
this
指向不能改变,但可以修改它要继承的对象的this
。 -
箭头函数的
this
指向全局,使用arguments
会报未声明的错误。 -
箭头函数的
this
指向普通函数时,它的arguments
继承于该普通函数。 -
使用
new
调用箭头函数会报错,因为箭头函数没有constructor
。 -
箭头函数不支持
new.target
。 -
箭头函数不支持重命名函数参数,普通函数的函数参数支持重命名。
-
箭头函数相对于普通函数语法更简洁优雅。
转载自:https://juejin.cn/post/7265695216695672832