作者:Dmitri Pavlutin
翻译:疯狂的技术宅
原文:https://dmitripavlutin.com/di…
在 JavaScript 中,你可以通过多种方式去定义函数。
第一种常用的方法是使用关键字 function
:
// 函数声明
function greet(who) {return `Hello, ${who}!`;
}
// 函数表达式
const greet = function(who) {return `Hello, ${who}`;
}
代码中的函数声明和函数表达式被称为“常规函数”。
从 ES2015 开始,第二种可用的方法是 箭头函数 语法:
const greet = (who) => {return `Hello, ${who}!`;
}
虽然两者的语法都能够定义函数,但是在开发时该怎么选择呢?这是个好问题。
在本文中,我将展示两者之间的主要区别,以供你能够根据需要选择正确的语法。
1. this 值
1.1 常规函数
在常规 JavaScript 函数内部,this
值(即执行上下文)是动态的。
动态上下文意味着 this
的值取决于 如何 调用函数。在 JavaScript 中,有 4 种调用常规函数的方式。
在 简单调用 过程中,this
的值等于全局对象(如果函数在严格模式下运行,则为 undefined
):
function myFunction() {console.log(this);
}
// 简单调用
myFunction(); // logs global object (window)
在 方法调用 过程中,this
的值是拥有该方法的对象:
const myObject = {method() {console.log(this);
}
};
// 方法调用
myObject.method(); // logs "myObject"
在使用 myFunc.call(context, arg1, ..., argN)
或 myFunc.apply(context, [arg1, ..., argN])
的间接调用中,this
的值等于第一个参数:
function myFunction() {console.log(this);
}
const myContext = {value: 'A'};
myFunction.call(myContext); // logs {value: 'A'}
myFunction.apply(myContext); // logs {value: 'A'}
在使用关键字 new
的构造函数调用期间,this
等于新创建的实例:
function MyFunction() {console.log(this);
}
new MyFunction(); // logs an instance of MyFunction
1.2 箭头函数
箭头函数中 this
的行为与常规函数的 this
行为有很大不同。
无论如何执行或在何处执行,箭头函数内部的 this
值始终等于外部函数的 this
值。换句话说,箭头函数可按词法解析 this
,箭头函数没有定义自己的执行上下文。
在以下示例中,myMethod()
是箭头函数 callback()
的外部函数:
const myObject = {myMethod(items) {console.log(this); // logs "myObject"
const callback = () => {console.log(this); // logs "myObject"
};
items.forEach(callback);
}
};
myObject.myMethod([1, 2, 3]);
箭头函数 callback()
中的 this
值等于外部函数 myMethod()
的 this
。
this
词法解析是箭头函数的重要功能之一。在方法内部使用回调时,要确保箭头函数没有定义自己的 this
:不再有 const self = this
或者 callback.bind(this)
这种解决方法。
2. 构造函数
2.1 常规函数
如上一节所述,常规函数可以轻松的构造对象。
例如用 Car()
函数创建汽车的实例:
function Car(color) {this.color = color;}
const redCar = new Car('red');
redCar instanceof Car; // => true
Car
是常规函数,使用关键字 new
调用时会创建 Car
类型的新实例。
2.2 箭头函数
this
词法解决了箭头函数不能用作构造函数。
如果你尝试调用带有 new
关键字前缀的箭头函数,则 JavaScript 会引发错误:
const Car = (color) => {this.color = color;};
const redCar = new Car('red'); // TypeError: Car is not a constructor
调用 new Car('red')
(其中 Car
是箭头函数)会抛出 TypeError: Car is not a constructor
。
3. arguments 对象
3.1 常规函数
在常规函数的主体内部,arguments
是一个特殊的类似于数组的对象,其中包含被调用函数的参数列表。
让我们用 3 个参数调用 myFunction
函数:
function myFunction() {console.log(arguments);
}
myFunction('a', 'b'); // logs {0: 'a', 1: 'b'}
类似于数组对象的 arguments
中包含调用参数:'a'
和 'b'
。
3.2 箭头函数
另一方面,箭头函数内部未定义 arguments
特殊关键字。
用词法解析 arguments
对象:箭头函数从外部函数访问 arguments
。
让我们试着在箭头函数内部访问 arguments
:
function myRegularFunction() {const myArrowFunction = () => {console.log(arguments);
}
myArrowFunction('c', 'd');
}
myRegularFunction('a', 'b'); // logs {0: 'a', 1: 'b'}
箭头函数 myArrowFunction()
由参数 'c'
, 'd'
调用。在其主体内部,arguments
对象等于调用 myRegularFunction()
的参数:'a'
, 'b'
。
如果你想访问箭头函数的直接参数,可以使用剩余参数 ...args
:
function myRegularFunction() {const myArrowFunction = (...args) => {console.log(args);
}
myArrowFunction('c', 'd');
}
myRegularFunction('a', 'b'); // logs {0: 'c', 1: 'd'}
剩余参数 ... args
接受箭头函数的执行参数:{0: 'c', 1: 'd'}
。
4. 隐式返回
4.1 常规函数
使用 return expression
语句从函数返回结果:
function myFunction() {return 42;}
myFunction(); // => 42
如果缺少 return
语句,或者 return 语句后面没有表达式,则常规函数隐式返回 undefined
:
function myEmptyFunction() {42;}
function myEmptyFunction2() {
42;
return;
}
myEmptyFunction(); // => undefined
myEmptyFunction2(); // => undefined
4.2 箭头函数
可以用与常规函数相同的方式从箭头函数返回值,但有一个有用的例外。
如果箭头函数包含一个表达式,而你省略了该函数的花括号,则将显式返回该表达式。这些是内联箭头函数
const increment = (num) => num + 1;
increment(41); // => 42
increment()
仅包含一个表达式:num + 1
。该表达式由箭头函数隐式返回,而无需使用 return
关键字。
5. 方法
5.1 常规函数
常规函数是在类上定义方法的常用方式。
在下面 Hero
类中,用了常规函数定义方法 logName()
:
class Hero {constructor(heroName) {this.heroName = heroName;}
logName() { console.log(this.heroName); }}
const batman = new Hero('Batman');
通常把常规函数用作方法。
有时你需要把该方法作为回调提供给 setTimeout()
或事件监听器。在这种情况下,你可能会很难以访问 this
的值。
例如用 logName()
方法作为 setTimeout()
的回调:
setTimeout(batman.logName, 1000);
// after 1 second logs "undefined"
1 秒钟后,undefined
会输出到控制台。setTimeout()
执行 logName
的简单调用(其中 this
是全局对象)。这时方法会与对象分离。
让我们手动把 this
值绑定到正确的上下文:
setTimeout(batman.logName.bind(batman), 1000);
// after 1 second logs "Batman"
batman.logName.bind(batman)
将 this
值绑定到 batman
实例。现在,你可以确定该方法不会丢失上下文。
手动绑定 this
需要样板代码,尤其是在你有很多方法的情况下。有一种更好的方法:把箭头函数作为类字段。
5.2 箭头函数
感谢类字段提案(目前在第 3 阶段),你可以将箭头函数用作类中的方法。
与常规函数相反,现在用箭头定义的方法能够把 this
词法绑定到类实例。
让我们把箭头函数作为字段:
class Hero {constructor(heroName) {this.heroName = heroName;}
logName = () => {console.log(this.heroName);
}
}
const batman = new Hero('Batman');
现在,你可以把 batman.logName
用于回调而无需手动绑定 this
。logName()
方法中 this
的值始终是类实例:
setTimeout(batman.logName, 1000);
// after 1 second logs "Batman"
6. 总结
了解常规函数和箭头函数之间的差异有助于为特定需求选择正确的语法。
常规函数中的 this
值是动态的,并取决于调用方式。是箭头函数中的 this
在词法上是绑定的,等于外部函数的 this
。
常规函数中的 arguments
对象包含参数列表。相反,箭头函数未定义 arguments
(但是你可以用剩余参数 ...args
轻松访问箭头函数参数)。
如果箭头函数有一个表达式,则即使不用 return
关键字也将隐式返回该表达式。
最后一点,你可以在类内部使用箭头函数语法定义去方法。粗箭头方法将 this
值绑定到类实例。
不管怎样调用胖箭头方法,this
始终等于类实例,在回调这些方法用时非常有用。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解 Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13 个帮你提高开发效率的现代 CSS 框架
- 快速上手 BootstrapVue
- JavaScript 引擎是如何工作的?从调用栈到 Promise 你需要知道的一切
- WebSocket 实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30 分钟用 Node.js 构建一个 API 服务器
- Javascript 的对象拷贝
- 程序员 30 岁前月薪达不到 30K,该何去何从
- 14 个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把 HTML 转成 PDF 的 4 个方案及实现
- 更多文章 …