ES6里关于函数的拓展(三)
一、箭头函数
在ES6中,箭头函数是其中最有趣的新增特性。顾名思义,箭头函数是一种使用箭头(=>)定义函数的新语法,但是它与传统的JS函数有些许不同,主要集中在以下方面:
1、没有this、super、arguments和new.target
绑定箭头函数中的this、super、arguments和new.target这些值由外围最近一层非箭头函数决定
2、不能通过new关键字调用
箭头函数没有[[construct]]方法,不能被用作构造函数,如果通过new关键字调用箭头函数,程序抛出错误
3、没有原型
由于不可以通过new关键字调用箭头函数,因而没有构建原型的需求,所以箭头函数不存在prototype这个属性
4、不可以改变this绑定
函数内部的this值不可被改变,在函数的生命周期内始终保持一致
5、不支持arguments对象
箭头函数没有arguments绑定,必须通过命名参数和不定参数这两种形式访问函数的参数
6、不支持重复的命名参数
无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才不能有重复的命名参数
在箭头函数内,其余的差异主要是减少错误以及理清模糊不清的地方。这样一来,JS引擎就可以更好地优化箭头函数的执行过程。这些差异的产生有如下几个原因:
1、最重要的是,this绑定是JS程序中一个常见的错误来源,在函数内很容易对this的值失去控制,其经常导致程序出现意想不到的行为,箭头函数消除了这方面的烦恼
2、如果限制箭头函数的this值,简化代码执行的过程,则JS引擎可以更轻松地优化这些操作,而常规函数往往同时会作为构造函数使用或者以其他方式对其进行修改
注意:箭头函数同样也有一个name属性,这与其他函数的规则相同
【语法】
箭头函数的语法多变,根据实际的使用场景有多种形式。所有变种都由函数参数、箭头、函数体组成,根据使用的需求,参数和函数体可以分别采取多种不同的形式
var reflect = value => value;
// 有效等价于:
var reflect = function(value) {
return value;
};
(1)1个参数:当箭头函数只有一个参数时,可以直接写参数名,箭头紧随其后,箭头右侧的表达式被求值后便立即返回。即使没有显式的返回语句,这个箭头函数也可以返回传入的第一个参数
(2)多个参数:如果要传入两个或两个以上的参数,要在参数的两侧添加一对小括号
var sum = (num1, num2) => num1 + num2;
// 有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
这里的sum()函数接受两个参数,将它们简单相加后返回最终结果,它与reflect()函数唯一的不同是,它的参数被包裹在小括号中,并且用逗号进行分隔(类似传统函数)
(3)无参数:如果函数没有参数,也要在声明的时候写一组没有内容的小括号
var getName = () => "huochai";
// 有效等价于:
var getName = function() {
return "huochai";
};
(4)花括号函数体:如果希望为函数编写由多个表达式组成的更传统的函数体,那么需要用花括号包裹函数体,并显式地定义一个返回值
var sum = (num1, num2) => {
return num1 + num2;
};
// 有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
除了arguments对象不可用以外,某种程度上都可以将花括号里的代码视作传统的函数体定义
如果想创建一个空函数,需要写一对没有内容的花括号
var doNothing = () => {};
// 有效等价于:
var doNothing = function() {};
花括号代表函数体的部分,但是如果想在箭头函数外返回一个对象字面量,则需要将该字面量包裹在小括号里。将对象字面量包裹在小括号中是为了将其与函数体区分开来。
var getTempItem = id => ({ id: id, name: "Temp" });//代表返回的是个对象
// 有效等价于:
var getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};
【IIFE】
JS函数的一个流行的使用方式是创建立即执行函数表达式(IIFE),可以定义一个匿名函数并立即调用,自始至终不保存对该函数的引用。当创建一个与其他程序隔离的作用域时,这种模式非常方便。
只要将箭头函数包裹在小括号里,就可以用它实现同IIFE相同的功能
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("huochai");
console.log(person.getName()); // "huochai"
//在这段代码中,IIFE通过getName()方法创建了一个新对象,将参数name作为该对象的一个私有成员返回给函数的调用者
注意:小括号只包裹箭头函数定义,没有包含("huochai"),这一点与正常函数有所不同,由正常函数定义的立即执行函数表达式既可以用小括号包裹函数体,也可以额外包裹函数调用的部分
【this】
函数内的this绑定是JS中最常出现错误的因素,函数内的this值可以根据函数调用的上下文而改变,这有可能错误地影响其他对象
var PageHandler = {
id: "",
init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type); // 错误
}, false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
在这段代码中,对象pageHandler的设计初衷是用来处理页面上的交互,通过调用init()方法设置交互,依次分配事件处理程序来调用this.dosomething()。然而,这段代码并没有如预期的正常运行。实际上,因为this绑定的是事件目标对象的引用(在这段代码中引用的是document),而没有绑定pageHandler,且由于this.dosonething()在目标document中不存在,所以无法正常执行,尝试运行这段代码只会使程序在触发事件处理程序时抛出错误。
可以使用bind()方法显式地将this绑定到pageHandler函数上来修正这个问题
var PageHandler = {
id: "",
init: function() {
document.addEventListener("click", (function(event) {
this.doSomething(event.type); // 错误
}).bind(this), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
现在代码如预期的运行,但可能看起来仍然有点奇怪。调用bind(this)后,事实上创建了一个新函数,它的this被绑定到当前的this,也就是page Handler
可以通过一个更好的方式来修正这段代码:使用箭头函数
箭头函数中没有this绑定,必须通过查找作用城链来决定其值。如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined
var PageHandler = {
id: "",
init: function() {
document.addEventListener("click",
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handling " + type + " for " + this.id);
}
};
这个示例中的事件处理程序是一个调用了this.doSomething()的箭头函数,此处的this与init()函数里的this一致,所以此版本代码的运行结果与使用bind(this)一致。虽然doSomething()方法不返回值,但是它仍是函数体内唯一的一条执行语句,所以不必用花括号将它包裹起来
箭头函数缺少正常函数所拥有的prototype属性,它的设计初衷是即用即弃,所以不能用它来定义新的类型。如果尝试通过new关键字调用一个箭头函数,会导致程序抛出错误
var MyType = () => {},
object = new MyType(); // 错误:不能对箭头函数使用 'new'
在这段代码中,MyType是一个没有[[Construct]]方法的箭头函数,所以不能正常执行new MyType()。也正因为箭头函数不能与new关键字混用,所以JS引擎可以进一步优化它们的行为。同样,箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call()、apply()或bind()方法来改变this的值
【数组】
箭头函数的语法简洁,非常适用于数组处理。如果想给数组排序,通常需要写一个自定义的比较器
var result = values.sort(function(a, b) {
return a - b;
});
//只想实现一个简单功能,但这些代码实在太多了。用箭头函数简化如下
var result = values.sort((a, b) => a - b);
诸如sort()、map()及reduce()这些可以接受回调函数的数组方法,都可以通过箭头函数语法简化编码过程并减少编码量
// 正常函数写法
[,,].map(function (x) {
return x * x;
}); // 箭头函数写法
[,,].map(x => x * x);
【arguments】
箭头函数没有自己的arguments对象,且未来无论函数在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象
function createArrowFunctionReturningFirstArg() {
return () => arguments[];
}
var arrowFunction = createArrowFunctionReturningFirstArg();
console.log(arrowFunction()); //
在createArrowFunctionReturningFirstArg()中,箭头函数引用了外围函数传入的第一个参数arguments[0],也就是后续执行过程中传入的数字5。即使函数箭头此时已不再处于创建它的函数的作用域中,却依然可以访问当时的arguments对象,这是arguments标识符的作用域链解决方案所规定的
【辨识方法】
尽管箭头函数与传统函数的语法不同,但它同样可以被识别出来
var comparator = (a, b) => a - b;
console.log(typeof comparator); // "function"
console.log(comparator instanceof Function); // true
同样地,仍然可以在箭头函数上调用call()、apply()及bind()方法,但与其他函数不同的是,箭头函数的this值不会受这些方法的影响
包括回调函数在内所有使用匿名函数表达式的地方都适合用箭头函数来改写
【函数柯里化】
柯里化是一种把接受多个参数的函数变换成接受一个单一参数的函数,并且返回(接受余下的参数而且返回结果的)新函数的技术
如果使用ES5的语法来写,如下所示
function add(x){
return function(y){
return y + x;
};
} var addTwo = add();
addTwo(); // => 5
add()(); // => 21
使用ES6的语法来写,如下所示
var add = (x) => (y) => x+y
一般来说,出现连续地箭头函数调用的情况,就是在使用函数柯里化的技术
二、尾调用优化
ES6关于函数最有趣的变化可能是尾调用系统的引擎优化。尾调用指的是函数作为另一个函数的最后一条语句被调用
function doSomething() {
return doSomethingElse(); // 尾调用
}
尾调用之所以与其他调用不同,就在于它的特殊的调用位置
我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A
的内部调用函数B
,那么在A
的调用帧上方,还会形成一个B
的调用帧。等到B
运行结束,将结果返回到A
,B
的调用帧才会消失。如果函数B
内部还调用函数C
,那就还有一个C
的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了
尾调用优化(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存
ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧
1、尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)
2、在函数内部,尾调用是最后一条语句
3、尾调用的结果作为函数值返回
以下这段示例代码满足上述的三个条件,可以被JS引擎自动优化
//以下这段示例代码满足上述的三个条件,可以被JS引擎自动优化
"use strict";
function doSomething() {
// 被优化
return doSomethingElse();
}
在这个函数中,尾调用doSomethingElse()的结果立即返回,不调用任何局部作用域变量。如果做一个小改动,不返回最终结果,那么引擎就无法优化当前函数
"use strict";
function doSomething() {
// 未被优化:缺少 return
doSomethingElse();
} //同样地,如果定义了一个函数,在尾调用返回后执行其他操作,则函数也无法得到优化
"use strict";
function doSomething() {
// 未被优化:在返回之后还要执行加法
return + doSomethingElse();
}
如果把函数调用的结果存储在一个变量里,最后再返回这个变量,则可能导致引擎无法优化
"use strict";
function doSomething() {
// 未被优化:调用并不在尾部
var result = doSomethingElse();
return result;
}
可能最难避免的情况是闭包的使用,它可以访问作用域中所有变量,因而导致尾调用优化失效
"use strict";
function doSomething() {
var num = ,
func = () => num;
// 未被优化:此函数是闭包
return func();
}
//闭包func()可以访问局部变量num,即使调用func()后立即返回结果,也无法对代码进行优化
【应用】
实际上,尾调用的优化发生在引擎背后,除非尝试优化一个函数,否则无须思考此类问题。递归函数是其最主要的应用场景,此时尾调用优化的效果最显著
function factorial(n) {
if (n <= ) {
return ;
} else {
// 未被优化:在返回之后还要执行乘法
return n * factorial(n - );
}
}
由于在递归调用前执行了乘法操作,因而当前版本的阶乘函数无法被引擎优化。如果n是一个非常大的数,则调用栈的尺寸就会不断增长并存在最终导致栈溢出的潜在风险
优化这个函数,首先要确保乘法不会在函数调用后执行,可以通过默认参数来将乘法操作移出return语句,结果函数可以携带着临时结果进入到下一个迭代中
function factorial(n, p = 1) {
if (n <= ) {
return * p;
} else {
let result = n * p;
// 被优化
return factorial(n - , result);
}
}
在这个重写后的factorial()函数中,第一个参数p的默认值为1,用它来保存乘法结果,下一次迭代中可以取出它用于计算,不再需要额外的函数调用。当n大于1时,先执行一轮乘法计算,然后将结果传给第二次factorial()调用的参数。现在,ES6引擎就可以优化递归调用了。
写递归函数时,最好得用尾递归优化的特性,如果递归函数的计算量足够大,则尾递归优化可以大幅提升程序的性能。
另一个常见的事例是Fibonacci数列
function Fibonacci (n) {
if ( n <= ) {return };
return Fibonacci(n - ) + Fibonacci(n - );
} Fibonacci() //
Fibonacci() // 堆栈溢出
Fibonacci() // 堆栈溢出
尾递归优化过的 Fibonacci 数列实现如下
function Fibonacci2 (n , ac1 = , ac2 = ) {
if( n <= ) {return ac2};
return Fibonacci2 (n - , ac2, ac1 + ac2);
} Fibonacci2() //
Fibonacci2() // 7.0330367711422765e+208
Fibonacci2() // Infinity
由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。
ES6里关于函数的拓展(三)的更多相关文章
- ES6里关于函数的拓展(二)
一.构造函数 Function构造函数是JS语法中很少被用到的一部分,通常我们用它来动态创建新的函数.这种构造函数接受字符串形式的参数,分别为函数参数及函数体 var add = new Functi ...
- ES6里关于函数的拓展(一)
一.形参默认值 Javascript函数有一个特别的地方,无论在函数定义中声明了多少形参,都可以传入任意数量的参数,也可以在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指 ...
- 进阶路上有你我-相互相持篇之ES6里箭头函数里的this指向问题
首先复习下普通函数里的this指向: function test(){ console.log(this) } test() 你会秒杀的毫无疑问的回答:window,针对普通函数:谁调用了函数 函数 ...
- ES6里关于类的拓展(一)
大多数面向对象的编程语言都支持类和类继承的特性,而JS却不支持这些特性,只能通过其他方法定义并关联多个相似的对象,这种状态一直延续到了ES5.由于类似的库层出不穷,最终还是在ECMAScript 6中 ...
- ES6里关于作用域的拓展:块级作用域
过去,javascript缺乏块级作用域,var声明时的声明提升.属性变量等行为让人困惑.ES6的新语法可以帮助我们更好地控制作用域. 一.var声明 1.变量提升:var声明会发生“变量提升”现象, ...
- ES6里关于数字的拓展
一.指数运算符 ES6引入的唯一一个JS语法变化是求幂运算符,它是一种将指数应用于基数的数学运算.JS已有的Math.pow()方法可以执行求幂运算,但它也是为数不多的需要通过方法而不是正式的运算符来 ...
- ES6里关于类的拓展(二):继承与派生类
继承与派生类 在ES6之前,实现继承与自定义类型是一个不小的工作.严格意义上的继承需要多个步骤实现 function Rectangle(length, width) { this.length = ...
- ES6里关于字符串的拓展
一.子串识别 自从 JS 引入了 indexOf() 方法,开发者们就使用它来识别字符串是否存在于其它字符串中.ES6 包含了以下三个方法来满足这类需求: 1.includes():该方法在给定文本存 ...
- ES6里箭头函数的陷阱
ECMAScript 6新增了箭头函数 原来的匿名函数 function(){},现在可以简化成()=>{} 看起来高大上,像C#什么的语法. 但是箭头函数的this对象,不能更改,总是指向函数 ...
随机推荐
- 【距离GDOI:130天】 AC自动机ing
弄完后缀数组,终于能安心来复习AC自动机了..其实当时学的很不好,非常不好..模版都是有问题的...今天花了第一节晚修和一节自习算是把AC自动机的基础弄好了...切掉3道基础题,然后就被某道坑爹题坑掉 ...
- [bzoj3218] a+b problem [最小割+数据结构优化建图]
题面 传送门 思路 最小割 我们首先忽略掉那个奇♂怪的限制,就有一个比较显然的最小割模型: 建立源点$S$和汇点$T$ 对于每个元素$i$建立一个点$i$,连边$<S,i,w[i]>$和$ ...
- 导入android源码中的APP源码到eclipse
导入android源码中的APP源码到eclipse 一般最简单的办法就是创建新的android工程,选择create project from existing source选项,直接导入源码就OK ...
- shell脚本——项目1
案例名称:系统初始化 背景:10台已装有linux系统的服务器 需求: 1.设置时区同步 2.禁用selinux 3.清空防火墙策略 4.历史命令显示操作时间 5.禁止root远程登录 6.禁止定时任 ...
- centos7配置环境LNMP
#yum安装epel-release第三方软件包 yum install epel-release #要验证EPEL仓库是否建立成功 yum repolist xshell访问系统出现中文乱码的解决方 ...
- ZOJ 3822 Domination (三维概率DP)
E - Domination Time Limit:8000MS Memory Limit:131072KB 64bit IO Format:%lld & %llu Submi ...
- JUnit--BeforeClass、AfterClass、Before、After示例
JUnit 4 使用 Java 5 中的注解(annotation),以下是JUnit 4 常用的几个 annotation 介绍@Before:初始化方法@After:释放资源@Test:测试方法, ...
- 在Laravel中使用Middleware进行身份验证
新建一个中间件: 方法写在handle中 判断用户是否登录而且是否是管理员,不是的话返回到主页 新建判断是否为管理员的方法 在kernel定义一个中间件,key是admin 注册群组路由:prefix ...
- Codeforces Beta Round #91 (Div. 2 Only) A. Lucky Division【暴力/判断是不是幸运数字4,7的倍数】
A. Lucky Division time limit per test 2 seconds memory limit per test 256 megabytes input standard i ...
- HDU1016 Prime Ring Problem (回溯 + 剪枝)
本文链接:http://www.cnblogs.com/Ash-ly/p/5398684.html 题意: 给你一个数字N(N <= 20),要求你把这N个数组成一个环,环内的数字不能重复,左右 ...