ES6
函数
函数形参的默认值
我们看一下以前,处理函数默认参数的代码:
function query(url, timeout, callback){
timeout = timeout || 2000;
callback = callback || function(){}
}
在这个例子中,timeout 和 callback 是非必传参数,代码中对非必传参数进行了处理,赋予默认值.但是这样的处理是不安全的,因为当 timeout 传 0 时,条件为 false ,会赋予默认值,与预期不同.
优化之后的代码:
function query(url, timeout, callback){
timeout = timeout!= 'undefined' ? timeout : 2000;
callback = callback != 'undefined' ? callback : function(){};
}
但是这样虽然安全,但是对于一个简单的参数默认值操作来说还是太冗余.于是在 ES6 中对默认参数的处理做了简化:
function makeRequest(url, timeout = 2000, callback = function(){}){
}
这个例子中,认为 url 是必传参数,其余的为非必传.当没有传参时,使用默认值.如果非必传参数也传参,则不使用默认值.
有一种情况是,我中间的参数不传,但是第一和第三个是传参的,那么当想使用默认参数时,因该:
makeRequest('/data', undefined, function(body){
handleBody(body);
});
中间的参数要主动传 undefined .假设传的是 null 是什么效果呢?
对于默认参数, null 是一个合法值,会被判定为 true ,从而不使用默认参数,所以, null 是不会用默认参数的.
arguments对象
arguments 是一个对应函数入参的 类数组 对象,是所有(非箭头)函数中都可用的对象,可以使用该对象在函数中访问函数的参数.
类数组:类似于
Array,但是除了length属性和索引元素之外没有任何Array属性.可以被转换为一个真正的Array.
箭头函数:箭头函数表达式,相当于匿名函数,简化了函数定义,形如:x => { if (x > 0) { return x * x; } else { return - x * x; } }
arguments 对象在 ES5 中就存在,只是行为和 ES6 不一样.在 ES5 的非严格模式下,函数入参在函数中被修改后, arguments 对象中的参数值也会被修改,代码如下:
function changeArgs(pa1,pa2){
console.log(pa1 === arguments[0]);
console.log(pa2 === arguments[1]);
pa1 = "3";
pa2 = "4";
console.log(pa1 === arguments[0]);
console.log(pa2 === arguments[1]);
}
changeArgs(1,2);
通过打印的结果可以看出, arguments 中的值被同步更新了.
但是在严格模式下, arguments 中的值不会被同步修改. ES6 中 arguments 的行为和 ES5 的严格模式表现一致,代码示例:
function changeArgs(pa1,pa2="2"){
console.log(arguments.length);
console.log(pa1 === arguments[0]);
console.log(pa2 === arguments[1]);
pa1 = "3";
pa2 = "4";
console.log(pa1 === arguments[0]);
console.log(pa2 === arguments[1]);
}
changeArgs(1);
函数只有一个入参, arguments 对象的长度也为 1 ,默认参数的存在并不会影响 arguments 对象的行为,入参的改变也不会影响. arguments 对象的行为更加符合预期,同时也影响了你使用这个对象的方式.
默认参数表达式
默认参数不仅可以传原始值,也可以传表达式,例如:
function getValue(){
return 5;
}
function add(pa1, pa2=getValue()){
return pa1 + pa2;
}
console.log(add(1,2));
console.log(1);
这个例子比较简单,但是包含了两个注意点:
- 初次解析函数的时候,作为默认参数的函数不会被调用,只有当调用
add()并且用到了默认参数的时候,getValue()才会被调用. - 当使用函数作为默认参数的时候,要加
(),否则pa2的值是函数的引用,并不是函数执行的结果.
既然默认参数可以在函数被调用时求值,那么你也可以把第一个参数作为第二个参数的默认值.例如:
function add(pa1, pa2=pa1){
return pa1 + pa2;
}
同理,也可将第一个参数作为第二个参数的默认值(函数)的参数,即:
function add(pa1, pa2=getValue(pa1)){
return pa1 + pa2;
}
但是,反过来是不可以的,因为将第二个参数作为第一个参数的默认值,会出现 ++临时死区++ .
默认参数的临时死区
默认参数的临时死区更好理解一些:
++定义参数时会为每个参数创建一个新的标识符绑定,标识符在初始化之前不可以被引用,因为默认参数是在函数调用时初始化的,所以在此之前如果引用未被初始化的参数,会抛出错误++
例子:
function add(pa1=pa2,pa2){
return pa1+pa2;
}
console.log(add(1,1));
console.log(add(undefined,1));
两次调用 add() 代码时对参数的声明相当于:
// 第一次调用add(1,1);
let pa1 = 1;
let pa2 = 1;
// 第二次调用add(undefined,1);
let pa1 = pa2;
let pa2 = 1;
这时候再根据变量声明时的临时死区来看默认参数的临时死区,这两者实际上还是一样的意思.
注意:
++函数参数有自己的作用域和临时死区,与函数体的作用域是各自独立的,也就是说参数的默认值不可访问函数体内声明的变量++
无名参数
JS的函数在传参时,无论函数本身定义的参数是多少个,都可以在调用时传入更多和更少的参数.
但是,如我们经常在ES5中使用的那样,函数不太容易被发现可以接受人一个参数,并且在我们传入任意个参数之后,想要使用未定义参数,需要循环遍历函数内部的arguments对象,并且遍历的索引从1开始.
在ES6中,引入了 不定参数 来使函数参数的用法更加简单.不定参数形如...args . 熟悉java的同学肯定见过这种形式的入参写法.
...args在此处代表的是包括它自己和之后的参数在内的所有参数集合.与arguments相比最大的区别就是,后者代表所有入参,前者只包括具名参数之外的入参.这样做就消除了循环参数的限制(索引从1开始),更好管理你的无名参数.
不定参数的初衷是代替arguments对象,在ES6中两者共存.无论你是否使用不定参数,arguments对象总是包含函数在被调用时传入的所有参数.
Function构造函数
ES6增强了Function构造函数的能力,使其支持传参时传入默认参数和不定参数.
例子:
let add = new Function("pa1","pa2=pa1","return pa1+pa2");
let pickFirst = new Function("...args","return args[0]");
ES6增强的这两个特性使得Function构造函数具备了和声明式创建函数相同的能立.但是Function在JS中是很少被使用的.
展开运算符
展开运算符形如...,与上面提到的不定参数看起来类似,但是两者作用相反.不定参数作用是将多个各自独立的参数整合数组被访问,展开运算符是将指定的数组打散成独立个体.
例子:
// Math.max不允许传入数组
let val1 = 1,val2 = 2;
console.log(Math.max(val1,val2);
// 如果出现从大量的值中选择最大的那一个,每个参数单独传入很麻烦,可以修改成
let vals = [1,2,3,4,5,6,7,8,9];
console.log(Math.max(...vals));
展开运算符还可以与其他正常参数混合使用.例如:
let vals = [-1,-2,-3,-4,-5];
console.log(...vals,0);
展开运算符可以简化参数的使用过程.
name属性
ES6为每个函数新增了一个name属性,目的是为了更好的辨别函数,同时为了兼容一些特殊情况,ES6做了很多改进来使所有的函数拥有更合适的名称.
例子:
function doSomething(){
};
console.log(doSomething.name);//打印 doSomething
var doAnotherThing = function(){
};
console.log(doAnotherThing.name);//打印 doAnotherThing
/**
这个例子中,函数表达式自己的名字比变量名字的优先级要高,因此函数的名字是函数表达式的名字
*/
var doAnotherThing = function doSomething(){
};
console.log(doAnotherThing.name);
/**
这个例子中,person.firstName是一个getter函数,因此函数名字前有一个get修饰
*/
let person = {
get firstName(){
return 'xlx';
},
sayName:function(){
console.log(this.name);
}
};
console.log(person.sayName.name);//打印sayName
console.log(person.firstName.name);//打印 get firstName
/**
绑定函数的name总是由函数的name和前缀 bound组成
*/
let doSomething = function(){
};
console.log(doSomething.bind().name);//打印 bound doSomething
/**
通过构造函数创建的函数,其名称将是anonymous
*/
console.log((new Function()).name);//打印anonymous
注意:
++name属性只是用来帮助调试,不能用来获取函数的引用++.
函数的双重身份
在ES5及早期版本中,函数有两种调用方式,使用new调用或者直接调用.
JavaScript函数的内部有两个不同的方法,Call和Construct.当执行Construct方法时,会创建函数的新实例,并将函数内部的this绑定到这个实例上,具有Construct的函数被称为 构造函数 .如果是执行Call,则会直接执行函数体.
使用new和直接调用函数的区别就在于,new调用函数执行的是 Construct ,最终返回一个函数的新对象.而不使用这个关键字调用,就是执行 Call ,最终返回的是 undefined.
例子:
function Person(name){
this.name = name;
}
console.log(new Person("xlx"));// 打印 Person {name: "xlx"}
console.log(Person("xlx"));// 打印 undefined
++并不是所有函数都有Construct方法++.
ES5判断一个函数是否作为构造函数被调用,可以使用 instanceof 关键字(意思是"xxx的实例").当使用new调用的时候,会调用 Construct 创建一个新示例并绑定到this,因此this instanceof xxx可以用来判断函数是否是作为构造函数调用.但是这个方法并不安全.因为在下面这种情况下,虽然函数不是被作为构造函数调用的,instanceof也不能判断出来.
function Person(name){
if(this instanceof Person){
this.name = name;
}else {
throw new Error('非new调用');
}
};
let person = new Person('xlx');
let anotherPerson = Person.call(person,'xlx');//不会抛异常
原因在于,其实上面的例子中,先创建了一个Person对象,然后调用call方法将Person对象当成第一个参数传给了Person函数.这相当于将函数体中的this赋值为person,即此时的this确实是一个Person实例.
为了解决函数的这个含糊不清的问题,ES6引入了一个新的属性--new.target元属性.
元属性:非对象属性,其可以以属性访问的方式提供非对象目标的补充信息.
在上面的例子中,当调用 Construct 方法,new.target会被赋值为函数的实例,如果调用的是 Call 方法,new.target为undefined.
例子:
function Person(name){
if(tyoeof new.target != 'undefined'){
this.name = name;
}else {
throw new Error('非new调用!');
}
}
let person = new Person('xlx');//通过
let person2 = Person.call(person,'xlx');//抛异常
块级函数
ES5的严格模式中,在代码块中声明函数,会抛出错误.但是在ES6中,代码块中声明函数被视为块级声明,可以在函数所在的代码块内调用它.
if(true){
console.log(typeof doSomething);//function
function doSomething(){
}
}
console.log(typeof doSomething);//undefined
块级函数和let相似的地方在于,当代码块执行完毕,声明的对象会被立即移除.区别在于,代码块中的函数会被提升至代码块顶部,而let声明的函数不会.是否需要函数提升决定了你的函数声明方式.
++以上函数的行为都是在严格模式下++,在非严格模式中,也可以声明块级函数,但是函数提升的行为表现与严格模式不同,具体为:
++严格模式下的函数提升至代码块的顶级,但是非严格模式下函数提升至外围函数或者全局作用域的顶级++
在ES6之前,每个浏览器对块级函数特性的支持都略有不同,在ES6中,这个行为被标准化,所有ES6运行时环境都执行相同的标准.
箭头函数
形如=>,是一种用箭头定义函数的新语法(在各种支持函数式编程的语言中还是很常见的).与传统的JS函数的区别如下:
- 没有this/super/arguments/new.target绑定
- 不能用
new关键字调用- 没有原型
- 不可以改变
this的绑定- 不支持
arguments对象- 不支持重复的命名函数
语法
/**单参数,单表达式*/
let reflect = value => value;
/**多参数,单表达式*/
let reflect = (val1,val2) => val1+val2;
/**无参*/
let reflect = () =>"1";
/**多表达式,需要{}包裹函数体,并显式返回*/
let reflect = (val1,val2) =>{
console.log(val1+val2);
return val1+val2;
};
/**空函数*/
let reflect = ()=>{};
/**返回对象字面量,需要将返回的字面量包裹在小括号内,目的是为了将字面量的{}和函数体区分开*/
let reflect = id => ({id:id,name:'xlx'});
立即执行的函数表达式
立即调用函数表达式(IIFE)是一个在定义时就会执行的
JavaScript函数.(function(){ statement })();这个表达式包含两个部分,第一个部分是包围在
()内的匿名函数,第二部分再一次用()创建了一个立即执行函数表达式,JavaScript引擎到此将直接执行函数.两个特点:
- 立即执行表达式内部的变量不能从外部调用(作用域独立)
- 将 IIFE 分配给一个变量,不是存储 IIFE 本身,而是存储 IIFE 执行后返回的结果
箭头函数定义立即调用函数表达式如下:
let person = ((name) => {
return {
getName:function(){
return name;
}
};
})('xlx');
console.log(person.getName());//打印 "xlx"
箭头函数的 this
函数内的 this 绑定容易造成不可预期的错误,因为函数内的 this 会根据函数调用的上下文发生改变.
例如:
let PageHandler = {
id:11111,
init:function(){
document.addEventListener('click',function(event){
this.doSomething(event.type);
},false);
},
doSomething:function(type){
console.log('Handling' + type + 'for' + this.id);
}
}
这段代码并不会按照预期在 init 的时候调用 doSomething 方法,而是会抛出异常.因为 this 在 init 中绑定的是事件目标对象,即 document ,而不是 PageHandler .
事件目标对象:触发事件的对象,在这里就是
document.
假设不使用箭头函数,我们可以使用 bind() 方法修正:
let PageHandler = {
id:11111,
init:function(){
document.addEventListener('click',(function(event){
this.doSomething(event.type);
}).bind(this),false);
},
doSomething:function(type){
console.log('Handling' + type + 'for' + this.id);
}
}
这时候this绑定的就是PageHandler对象.
但是这样做还是不合适,因为bind()会创建一个新的函数,将函数的this绑定到当前的this.我们并不想额外创建一个函数解决这个问题
之前我们说过箭头函数的特点之一是没有this绑定,但是它是可以使用this的.箭头函数的this是通过作用域链来决定它的值的,具体规则是:
当箭头函数被非箭头函数包含时,
this的值是最近一层非箭头函数的this,否则this的值就是全局对象.
刚才的例子可以修改为:
let PageHandler = {
id:11111,
init:function(){
document.addEventListener(
'click',
event => this.doSomething(event.type),
false
);
},
doSomething:function(type){
console.log('Handling' + type + 'for' + this.id);
}
}
++箭头函数的设计初衷就是"即用即弃"++
++可以接受回调函数的方法,都可以通过箭头函数语法减少代码量++
ES6尾调用优化
什么是尾调用?
尾调用指的是函数作为零一个函数的最后一条语句被调用.
function doSomething(){ return doAnotherThing();//尾调用 }
尾调用的实现和存在的问题
创建一个新得栈帧,将其推入调用栈表示函数调用,在事件循环中,尾调用会被作为未完成的调用栈帧被保存在内存中,当调用栈变的过大会造成程序问题.
优化方式
在严格模式下,
ES6缩减了尾调用栈的大小(非严格模式不影响),尾调用不创建新的栈帧,而是清除并重用当前栈帧.
优化条件
- 尾调用不访问当前栈帧的变量(函数非闭包)
- 函数内部尾调用是最后一个语句
- 尾调用的结果作为函数返回值
无法优化的情况:
"use static";
/**无返回*/
function doSomething(){
doAnotherThing();
}
/**尾调用返回后执行其他操作*/
function doSomething(){
return 1 + doAnotherThing();
}
/**尾调用结果保存在其他变量中再返回*/
function doSomething(){
let res = doAnotherThing();
return res;
}
/**闭包*/
function doSomething(){
let num = 1;
let func = () => num;
return func();
}
以上情况下,引擎都不能对尾调进行优化.
如何利用尾调优化?
递归函数是尾调优化最主要的应用场景,运用尾调优化的效果显著.
/**优化前,如果n是一个很大的数,则会每次调用都插入栈帧,可能导致栈溢出*/
function factorial(n){
if(n<=1){
return n;
}else{
return n * factorial(n-1);
}
}
/**优化后*/
function factorial(n, p=1){
if(n<=1){
return n * p;
}else{
let res = n * p;
return factorial(n-1, res);
}
}