ES6躬行记(15)——箭头函数和尾调用优化
一、箭头函数
箭头函数(Arrow Function)是ES6提供的一个很实用的新功能,与普通函数相比,不但在语法上更为简洁,而且在使用时也有更多注意点,下面列出了其中的三点:
(1)由于不能作为构造函数,因此也就没有元属性(new.target)和原型(prototype属性)。
(2)函数体内不存在arguments、super和this,即没有为它们绑定值。
(3)当需要包含多个参数时,它们的名称不可重复。
1)语法
箭头函数由三部分组成:箭头(=>)、参数和函数体,省略了名称和function关键字,如下代码所示。注意,参数和箭头要在一行中,不能换行,并且没有箭头函数声明,只有箭头函数表达式。
var arrow1 = (name, age) => {
return name;
};
如果函数体中只包含一个表达式,那么可以像下面这样省略花括号和return关键字。
var arrow2 = (name, age) => name;
当返回值是一个对象字面量时,需要为其包裹圆括号(如下代码所示),以免将花括号解析成代码块而引起语法错误。
var arrow3 = (name, age) => ({name: name, age: age});
如果只有一个参数,那么可以省略包裹参数的圆括号。
var arrow4 = name => name;
在参数缺失的情况下,还是需要一对空的圆括号占位。
var arrow5 = () => "strick";
箭头函数也支持ES6新增的参数解构、剩余参数和默认参数,依次如下所示。
var arrow6 = ({name, age}) => name + age;
var arrow7 = (name, ...args) => name + args[0];
var arrow8 = (name = "strick") => name;
箭头函数很适合那些只需做简短计算的函数,例如数组排序的回调函数。下面代码的第一段采用了普通的匿名函数,第二段采用了箭头函数,可以看出第二段的代码来得清晰简洁,更有吸引力。
var list1 = [1, 2, 3].sort(function(a, b) {
return a < b;
});
var list2 = [1, 2, 3].sort((a, b) => a < b);
虽然箭头不是运算符,但是当把它与其它运算符结合的时候,需要将其用圆括号隔离,否则会引起语法错误。
var operator1 = 0 || () => 2; //语法错误
var operator2 = 0 || (() => 2); //() => 2
var operator3 = 0 || (() => 2)(); //
上面代码的第一句是错误的写法;第二句中的箭头函数默认是不会自动执行的,因此或运算的结果是函数本身;第三句在箭头函数的末尾又加了一对圆括号,从而创建了立即执行函数表达式(IIFE),运算后的结果是2。
普通函数能创建两种即时函数(如下代码所示),区别就是末尾的那对圆括号的位置,相对于函数是在内还是在外,而箭头函数只支持第一种写法的即时函数。
(function() {})(); //第一种
(function() {}()); //第二种
2)this
普通函数都会包含一个this对象,而this绑定的对象只取决于函数的调用方式。在下面的代码中,有两个函数,分别是构造函数people()以及定时器的回调函数。
function people() {
var self = this;
this.age = 28;
setTimeout(function() {
this.age; //undefined
self.age; //
}, 100);
}
new people();
people()中的this通过构造函数绑定,指向了新创建的目标对象。而回调函数中的this通过默认绑定,指向了全局对象(window)。由于age属性只存在于构造函数中,因此在全局对象中是访问不到的。如果要在定时器的回调函数中访问age属性,那么需要为this赋予一个别名(例如代码中的self)。
前面曾提到过,箭头函数中不包含this,不过在其内部还是能使用this的,这个this是从上一层的作用域中继承而来的。下面将构造函数people()改造成animal(),定时器采用箭头函数。
function animal() {
this.age = 28;
setTimeout(() => {
this.age; //
}, 100);
}
new animal();
此时,箭头函数中的this指向的是新创建的目标对象,因此能成功访问到age属性。
普通函数可以通过bind()、apply()或call()方法来更改this的指向,但这不适用于箭头函数,如下代码所示。还是以animal()为例,在箭头函数上调用call(),虽然第一个参数传入了全局对象,但依然无法改变this的指向。
function animal() {
this.age = 28;
setTimeout((() => {
this.age; //
}).call(window), 100);
}
new animal();
二、尾调用优化
尾调用(Tail Call)是指函数中的最后一步会调用一个函数,并将其结果作为返回值,就像下面这两个函数。
function tail1() {
return digit(28);
}
function tail2(name) {
if (name == "strick")
return digit(28);
return digit(30);
}
如果在最后一步调用的是自身,那么这种情况可称为尾递归(Tail Recursion)。下面是个会无限递归的函数,只是为了做演示用。
function recursion() {
return recursion();
}
尾调用不是一个新语法,而是一条组织代码的约束。下面代码里的三种情况就都不符合尾调用的要求。
function error1() {
digit(28);
}
function error2() {
return digit(28) + 1;
}
function error3() {
var number = digit(28);
return number;
}
error1()函数缺少return关键字;error2()函数在调用digit(28)之后还额外多做了一次加法运算;error3()函数没有直接返回digit(28)的结果,而是先赋给一个变量,再返回。
接下来为了能更好的理解尾调用优化,会先讲解调用栈的基础知识。
1)调用栈
简单地说,调用栈(Call Stack)是一种记录函数地址的栈,当调用一个函数时,会在调用栈的顶端添加一个新的栈帧(Stack Frame),即入栈;而当函数执行完毕后,就会将其移出调用栈,即出栈。
正常情况下,调用栈能够保持有进有出的平衡,但当函数的调用次数激增时(例如过多的递归),调用栈就会消耗大量内存,甚至占满整个内存空间,从而导致栈溢出(Stack Overflow)。不过值得庆幸的是,现代浏览器都不会因为栈溢出而奔溃,其内部已做了限制,例如Chrome仅仅会给出“RangeError: Maximum call stack size exceeded”的错误警告。
2)尾调用优化
为了控制栈帧的数量,减少内存空间的使用,引入了尾调用优化(Tail Call Optimization,TCO)。这也不是一个新语法,只是一种空间上的优化,并且所有的工作都由JavaScript引擎(例如V8、SpiderMonkey等)代劳了。
ES6中的TCO只有在严格模式下才能开启,它的原理就是不需要记住尾调用所处函数的地址。下面是一个检测字符串回文的函数,当开启TCO时,由于函数内需要的值已通过参数的形式传递进来(即不依赖函数外部的变量),因此每次递归都会覆盖前一次函数调用的栈帧,即调用栈的数量会保持恒定。
"use strict";
function palindrome(str) {
if (str.length <= 1)
return true;
if (str[0] != str[str.length - 1])
return false;
return palindrome(str.substr(1, str.length - 2));
}
虽然TCO的表现很抢眼,但很遗憾,目前除了Safrai之外,大部分浏览器暂不支持(可参考ES6浏览器兼容性),因此在这些浏览器中还享受不到TCO带来的性能提升。
ES6躬行记(15)——箭头函数和尾调用优化的更多相关文章
- ES6躬行记(1)——let和const
古语云:“纸上得来终觉浅,绝知此事要躬行”.的确,不管看了多少本书,如果自己不实践,那么就很难领会其中的精髓.自己研读过许多ES6相关的书籍和资料,平时工作中也会用到,但在用到时经常需要上搜索引擎中查 ...
- ES6躬行记 笔记
ES6躬行记(18)--迭代器 要实现以下接口## next() ,return,throw 可以用for-of保证迭代对象的正确性 例如 var str = "向
- ES6躬行记(14)——函数
在前面的章节中,已陆陆续续介绍了ES6为改良函数而引入的几个新特性,本章将会继续讲解ES6对函数的其余改进,包括默认参数.元属性.块级函数和箭头函数等. 一.默认参数 在ES5时代,只能在函数体中定义 ...
- ES6躬行记(19)——生成器
根据ES6制订的标准自定义迭代器实现起来比较复杂,因此ES6又引入了生成器的概念,生成器(Generator)是一个能直接创建并返回迭代器的特殊函数,可将其赋给可迭代对象的Symbol.iterato ...
- ES6躬行记(12)——数组
ES6为数组添加了多个新方法,既对它的功能进行了强化,也消除了容易产生歧义的语法. 一.静态方法 1)of() ES6为Array对象新增的第一个静态方法是of(),用于创建数组,它能接收任意个参数, ...
- ES6躬行记(21)——类的继承
ES6的继承依然是基于原型的继承,但语法更为简洁清晰.通过一个extends关键字,就能描述两个类之间的继承关系(如下代码所示),在此关键字之前的Man是子类(即派生类),而在其之后的People是父 ...
- ES6躬行记(3)——解构
解构(destructuring)是一种赋值语法,可从数组中提取元素或从对象中提取属性,将其值赋给对应的变量或另一个对象的属性.解构地目的是简化提取数据的过程,增强代码的可读性.有两种解构语法,分别是 ...
- ES6躬行记(7)——代码模块化
在ES6之前,由于ECMAScript不具备模块化管理的能力,因此往往需要借助第三方类库(例如遵守AMD规范的RequireJS或遵循CMD规范的SeaJS等)才能实现模块加载.而自从ES6引入了模块 ...
- ES6躬行记(4)——模板字面量
模板字面量(Template Literal)是一种能够嵌入表达式的格式化字符串,有别于普通字符串,它使用反引号(`)包裹字符序列,而不是双引号或单引号.模板字面量包含特定形式的占位符(${expre ...
随机推荐
- linux_systemctl介绍
声明:本文转载自:systemd (中文简体) systemd 是 Linux 下的一款系统和服务管理器,兼容 SysV 和 LSB 的启动脚本.systemd 的特性有:支持并行化任务:同一时候採用 ...
- 20164305徐广皓 - Exp1 PC平台逆向破解(5)M
1.逆向及Bof基础实践说明 1.1实践目标 实践对象:pwn1的linux可执行文件 实践目的:使程序执行另一个代码(ShellCode) 实践内容: 手工修改可执行文件,改变程序执行 ...
- 第一章Java学习(查漏补缺)
第一章主要内容: 1.Java的地位:网络地位 语言地位 需求地位 2.Java的特点:①简单 面向对象 平台无关:软件的运行不因操作系统,处理器的变化而无法运行或出现运行错误. ②多线程 动态 3. ...
- c# 序列化效率比拼
前言:作为开发人员,对象的序列化经常用到,特别是在现在前后端分离 采用json 交互 ,就将原来用过的几种方式总结了下,也算是做一个记录,顺便做了下性能测试. 1:内置 JavaScriptSeria ...
- 理解 Linux 的硬链接与软链接【转】
转自:https://www.ibm.com/developerworks/cn/linux/l-cn-hardandsymb-links/index.html 从 inode 了解 Linux 文件 ...
- bently addin 二次开发学习
元素结构: 一些基本元素的添加与绘制: class CreateElement { public static void LineAndLineString() { Application app = ...
- 爬虫 http原理,梨视频,github登陆实例,requests请求参数小总结
回顾:http协议基于请求响应的方式,请求:请求首行 请求头{'keys':vales} 请求体 :响应:响应首行,响应头{'keys':'vales'},响应体. import socket soc ...
- VMware虚拟机配置内网电脑能访问
关键字:内网访问虚拟机.内网访问Linux虚拟机.虚拟机访问外部网络 1.vmware虚拟机网络设置为桥接模式 2.虚拟机配置iP地址,以linux系统为示例. 勾选自动连接,选择手动配置ip,然后配 ...
- python 视频转成代码视频
# -*- coding:utf-8 -*- # coding:utf-8 import os, cv2, subprocess, shutil from cv2 import VideoWriter ...
- GitLab CI/CD 进行持续集成
简介 从 GitLab 8.0 开始,GitLab CI 就已经集成在 GitLab 中,我们只要在项目中添加一个 .gitlab-ci.yml 文件,然后添加一个 Runner,即可进行持续集成. ...