ES6函数扩展

前面的话

  函数是全部编程语言的重要组成部分,在ES6出现前,JS的函数语法一直没有太大的变化,从而遗留了不少问题,致使实现一些基本的功能常常要编写不少代码。ES6大力度地更新了函数特性,在ES5的基础上进行了许多改进,使用JS编程能够更少出错,同时也更加灵活。本文将详细介绍ES6函数扩展html

 

形参默认值

  Javascript函数有一个特别的地方,不管在函数定义中声明了多少形参,均可以传入任意数量的参数,也能够在定义函数时添加针对参数数量的处理逻辑,当已定义的形参无对应的传入参数时为其指定一个默认值编程

【ES5模拟】数组

  在ES5中,通常地,经过下列方式建立函数并为参数设置默认值浏览器

function makeRequest(url, timeout, callback) {
    timeout = timeout || 2000;
    callback = callback || function() {};
    // 函数的剩余部分
}

  在这个示例中,timeout和callback为可选参数,若是不传入相应的参数系统会给它们赋予一个默认值。在含有逻辑或操做符的表达式中,前一个操做数的值为false时,总会返回后一个值。对于函数的命名参数,若是不显式传值,则其值默认为undefined安全

  所以咱们常用逻辑或操做符来为缺失的参数提供默认值闭包

  然而这个方法也有缺陷,若是咱们想给makeRequest函数的第二个形参timeout传入值0,即便这个值是合法的,也会被视为一个false值,并最终将timeout赋值为2000app

  在这种状况下,更安全的选择是经过typeof检查参数类型,以下所示编程语言

function makeRequest(url, timeout, callback) {
    timeout = (typeof timeout !== "undefined") ? timeout : 2000;
    callback = (typeof callback !== "undefined") ? callback : function() {};
    // 函数的剩余部分
}

  虽然这种方法更安全,但依然为实现一个基本需求而书写了额外的代码。它表明了一种常见的模式,而流行的 JS 库中都充斥着相似的模式进行默认补全函数式编程

【ES6默认参数】函数

  ES6简化了为形参提供默认值的过程,若是没为参数传入值则为其提供一个初始值

function makeRequest(url, timeout = 2000, callback = function() {}) {
    // 函数的剩余部分
}

  在这个函数中,只有第一个参数被认为老是要为其传入值的,其余两个参数都有默认值,并且不须要添加任何校验值是否缺失的代码,因此函数代码比较简洁

  若是调用make Request()方法时传入3个参数,则不使用默认值

// 使用默认的 timeout 与 callback
makeRequest("/foo");
// 使用默认的 callback
makeRequest("/foo", 500);
// 不使用默认值
makeRequest("/foo", 500, function(body) {
    doSomething(body);
});

【触发默认值】

  声明函数时,能够为任意参数指定默认值,在已指定默认值的参数后能够继续声明无默认值参数

function makeRequest(url, timeout = 2000, callback) {
    console.log(url);
    console.log(timeout);
    console.log(callback);
}

  在这种状况下,只有当不为第二个参数传入值或主动为第二个参数传入undefined时才会使用timeout的默认值

  [注意]若是传入undefined,将触发该参数等于默认值,null则没有这个效果

function makeRequest(url, timeout = 2000, callback) {
    console.log(timeout);
}
makeRequest("/foo");//2000
makeRequest("/foo", undefined);//2000
makeRequest("/foo", null);//null
makeRequest("/foo", 100);//100

  上面代码中,timeout参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值

  使用参数默认值时,函数不能有同名参数

// SyntaxError: Duplicate parameter name not allowed in this context
function foo(x, x, y = 1) {
  // ...
}

  另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都从新计算默认值表达式的值。也就是说,参数默认值是惰性求值的

let x = 99;
function foo(p = x + 1) {
  console.log(p);
}
foo() // 100
x = 100;
foo() // 101

  上面代码中,参数p的默认值是x+1。这时,每次调用函数foo,都会从新计算x+1,而不是默认p等于100

【length属性】

  指定了默认值之后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真

(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2

  这是由于length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值之后,预期传入的参数个数就不包括这个参数了。同理,rest 参数也不会计入length属性

(function(...args) {}).length // 0

  若是设置了默认值的参数不是尾参数,那么length属性也再也不计入后面的参数了

(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1

【arguments】

  当使用默认参数值时,arguments对象的行为与以往不一样。在ES5非严格模式下,函数命名参数的变化会体如今arguments对象中

function mixArgs(first, second) {
    console.log(first === arguments[0]);//true
    console.log(second === arguments[1]);//true
    first = "c";
    second = "d";
    console.log(first === arguments[0]);//true
    console.log(second === arguments[1]);//true
}
mixArgs("a", "b");

   在非严格模式下,命名参数的变化会同步更新到arguments对象中,因此当first和second被赋予新值时,arguments[0]和arguments[1]相应更新,最终全部===全等比较的结果为true  

  然而,在ES5的严格模式下,取消了arguments对象的这个使人感到困惑的行为,不管参数如何变化,arguments对象再也不随之改变

function mixArgs(first, second) {
    "use strict";
    console.log(first === arguments[0]);//true
    console.log(second === arguments[1]);//true
    first = "c";
    second = "d"
    console.log(first === arguments[0]);//false
    console.log(second === arguments[1]);//false
}
mixArgs("a", "b");

  这一次更改 first 与 second 就不会再影响 arguments 对象,所以输出结果符合一般的指望

  在ES6中,若是一个函数使用了默认参数值,则不管是否显式定义了严格模式,arguments对象的行为都将与ES5严格模式下保持一致。默认参数值的存在使得arguments对象保持与命名参数分离,这个微妙的细节将影响使用arguments对象的方式

// 非严格模式
function mixArgs(first, second = "b") {
    console.log(first);//a
    console.log(second);//b
    console.log(arguments.length);//1
    console.log(arguments[0]);//a
    console.log(arguments[1]);//undefined
    first = 'aa';
    arguments[1] = 'b';
    console.log(first);//aa
    console.log(second);//b
    console.log(arguments.length);//1
    console.log(arguments[0]);//a
    console.log(arguments[1]);//b
}
mixArgs("a");

  在这个示例中,只给mixArgs()方法传入一个参数,arguments. Iength 的值为 1, arguments[1] 的值为 undefined, first与arguments[0]全等,改变first和second并不会影响arguments对象

【默认参数表达式】

  关于默认参数值,最有趣的特性多是非原始值传参了。能够经过函数执行来获得默认参数的值

function getValue() {
    return 5;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6

  在这段代码中,若是不传入最后一个参数,就会调用getvalue()函数来获得正确的默认值。切记,初次解析函数声明时不会调用getvalue()方法,只有当调用add()函数且不传入第二个参数时才会调用

let value = 5;
function getValue() {
    return value++;
}
function add(first, second = getValue()) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 6
console.log(add(1)); // 7

  在此示例中,变量value的初始值为5,每次调用getvalue()时加1。第一次调用add(1)返回6,第二次调用add(1)返回7,由于变量value已经被加了1。由于只要调用add()函数就有可能求second的默认值,因此任什么时候候均可以改变那个值

  正由于默认参数是在函数调用时求值,因此可使用先定义的参数做为后定义参数的默认值

function add(first, second = first) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 2

  在上面这段代码中,参数second的默认值为参数first的值,若是只传入一个参数,则两个参数的值相同,从而add(1,1)返回2,add(1)也返回2

function getValue(value) {
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7

  在上面这个示例中,声明second=getvalue(first),因此尽管add(1,1)仍然返回2,可是add(1)返回的是(1+6)也就是7

  在引用参数默认值的时候,只容许引用前面参数的值,即先定义的参数不能访问后定义的参数

function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误

  调用add(undefined,1)会抛出错误,由于second比first晚定义,所以其不能做为first的默认值

【临时死区】

  在介绍块级做用域时提到过临时死区TDZ,其实默认参数也有一样的临时死区,在这里的参数不可访问。与let声明相似,定义参数时会为每一个参数建立一个新的标识符绑定,该绑定在初始化以前不可被引用,若是试图访问会致使程序抛出错误。当调用函数时,会经过传入的值或参数的默认值初始化该参数

function getValue(value) {
    return value + 5;
}
function add(first, second = getValue(first)) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(1)); // 7

  调用add(1,1)和add(1)时实际上至关于执行如下代码来建立first和second参数值

// JS 调用 add(1, 1) 可表示为
let first = 1;
let second = 1;
// JS 调用 add(1) 可表示为
let first = 1;
let second = getValue(first);

  当初次执行函数add()时,first和second被添加到一个专属于函数参数的临时死区(与let的行为相似)。因为初始化second时first已经被初始化,因此它能够访问first的值,可是反过来就错了

function add(first = second, second) {
    return first + second;
}
console.log(add(1, 1)); // 2
console.log(add(undefined, 1)); // 抛出错误

  在这个示例中,调用add(1,1)和add(undefined,1)至关于在引擎的背后作了以下事情

// JS 调用 add(1, 1) 可表示为
let first = 1;
let second = 1;
// JS 调用 add(1) 可表示为
let first = second;
let second = 1;

  在这个示例中,调用add(undefined,1)函数,由于当first初始化时second还没有初始化,因此会致使程序抛出错误,此时second尚处于临时死区中,全部引用临时死区中绑定的行为都会报错

【形参与自由变量】

  下列代码中,y是形参,须要考虑临时死区的问题;而x是自由变量,不须要考虑。因此调用函数时,因为未传入参数,执行y=x,x是自由变量,经过做用域链,在全局做用域找到x=1,并赋值给y,因而y取值1

let x = 1;
function f(y = x) {}
f() // 1

  下列代码中,x和y是形参,须要考虑临时死区的问题。由于没有自由变量,因此不考虑做用域链寻值的问题。调用函数时,因为未传入参数,执行y=x,因为x正处于临时死区内,全部引用临时死区中绑定的行为都会报错

let x = 1;
function f(y = x,x) {}
f()// ReferenceError: x is not defined

  相似地,下列代码也报错

let x = 1;
function foo(x = x) {}
foo() // ReferenceError: x is not defined

 

不定参数

  不管函数已定义的命名参数有多少,都不限制调用时传入的实际参数数量,调用时老是能够传入任意数量的参数。当传入更少数量的参数时,默认参数值的特性能够有效简化函数声明的代码;当传入更多数量的参数时,ES6一样也提供了更好的方案。

【ES5】

   早先,Javascript提供arguments对象来检查函数的全部参数,从而没必要定义每个要用的参数。尽管arguments对象检査在大多数状况下运行良好,可是实际使用起来却有些笨重

function pick(object) {
    let result = Object.create(null);
    // 从第二个参数开始处理
    for (let i = 1, len = arguments.length; i < len; i++) {
        result[arguments[i]] = object[arguments[i]];
    }
    return result;
}
let book = {
    title: "ES6",
    author: "huochai",
    year: 2017
};
let bookData = pick(book, "author", "year");
console.log(bookData.author); // "huochai"
console.log(bookData.year); // 2017

  这个函数模仿了Underscore.js库中的pick()方法,返回一个给定对象的副本,包含原始对象属性的特定子集。在这个示例中只定义了一个参数,第一个参数传入的是被复制属性的源对象,其余参数为被复制属性的名称

  关于pick()函数应该注意这样几件事情:首先,并不容易发现这个函数能够接受任意数量的参数,固然,能够定义更多的参数,可是怎么也达不到要求;其次,由于第一个参数为命名参数且已被使用,要查找须要拷贝的属性名称时,不得不从索引1而不是索引0开始遍历arguments对象

【ES6】

  在ES6中,经过引入不定参数(rest parameters)的特性能够解决这些问题,不定参数也称为剩余参数或rest参数

  在函数的命名参数前添加三个点(...)就代表这是一个不定参数,该参数为一个数组,包含着自它以后传入的全部参数,经过这个数组名便可逐一访问里面的参数

function pick(object, ...keys) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

  在这个函数中,不定参数keys包含的是object以后传入的全部参数,而arguments对象包含的则是全部传入的参数,包括object。这样一来,就能够放心地遍历keys对象了。这种方法还有另外一个好处,只需看一眼函数就能够知道该函数能够处理的参数数量

【使用限制】

  不定参数有两条使用限制

  一、每一个函数最多只能声明一个不定参数,并且必定要放在全部参数的末尾

// 语法错误:不能在剩余参数后使用具名参数
function pick(object, ...keys, last) {
    let result = Object.create(null);
    for (let i = 0, len = keys.length; i < len; i++) {
        result[keys[i]] = object[keys[i]];
    }
    return result;
}

  二、不定参数不能在对象字面量的 setter 属性中使用

let object = {
    // 语法错误:不能在 setter 中使用剩余参数
    set name(...value) {
        // 一些操做
    }
};

  之因此存在这条限制,是由于对象字面量setter的参数有且只能有一个。而在不定参数的定义中,参数的数量能够无限多,因此在当前上下文中不容许使用不定参数

【arguments】

  不定参数的设计初衷是代替JS的arguments对象。起初,在ES4草案中,arguments对象被移除并添加了不定参数的特性,从而能够传入不限数量的参数。可是ES4从未被标准化,这个想法被搁置下来,直到从新引入了ES6标准,惟一的区别是arguments对象依然存在

function checkArgs(n,...args) {
    console.log(args.length);//2
    console.log(arguments.length);//3
    console.log(args);//['b','c']
    console.log(arguments);//['a','b','c']
}
checkArgs("a", "b", "c");

【应用】

  不定参数中的变量表明一个数组,因此数组特有的方法均可以用于这个变量

// arguments变量的写法
function sortNumbers() {
  return Array.prototype.slice.call(arguments).sort();
}

// 不定参数的写法
const sortNumbers = (...numbers) => numbers.sort();

  上面代码的两种写法,比较后能够发现,不定参数的写法更天然也更简洁

 

展开运算符

  在全部的新功能中,与不定参数最类似的是展开运算符。不定参数能够指定多个各自独立的参数,并经过整合后的数组来访问;而展开运算符能够指定一个数组,将它们打散后做为各自独立的参数传入函数。JS内建的Math.max()方法能够接受任意数量的参数并返回值最大的那一个

let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50

  如上例所示,若是只处理两个值,那么Math.max()很是简单易用。传入两个值后返回更大的那一个。可是若是想从一个数组中挑选出最大的那个值应该怎么作呢?Math.max()方法不容许传入数组,因此在ES5中,可能须要手动实现从数组中遍历取值,或者使用apply()方法

let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values)); // 100

  这个解决方案确实可行,但却让人很难看懂代码的真正意图

  使用ES6中的展开运算符能够简化上述示例,向Math.max()方法传入一个数组,再在数组前添加不定参数中使用的...符号,就无须再调用apply()方法了。JS引擎读取这段程序后会将参数数组分割为各自独立的参数并依次传入

let values = [25, 50, 75, 100]
// 等价于 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100

  使用apply()方法须要手动指定this的绑定,若是使用展开运算符可使这种简单的数学运算看起来更加简洁

  能够将展开运算符与其余正常传入的参数混合使用。假设限定Math.max()返回的最小值为0,能够单独传入限定值,其余的参数仍然使用展开运算符获得

let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0

  在这个示例中,Math.max()函数先用展开运算符传入数组中的值,又传入了参数0

  展开运算符能够简化使用数组给函数传参的编码过程,在大多数使用apply()方法的状况下展开运算符多是一个更合适的方案

 

严格模式

  从 ES5 开始,函数内部能够设定为严格模式

function doSomething(a, b) {
  'use strict';
  // code
}

  ES7作了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,不然会报错

// 报错
function doSomething(a, b = a) {
  'use strict';
  // code
}

// 报错
const doSomething = function ({a, b}) {
  'use strict';
  // code
};

// 报错
const doSomething = (...a) => {
  'use strict';
  // code
};

const obj = {
  // 报错
  doSomething({a, b}) {
    'use strict';
    // code
  }
};

  这样规定的缘由是,函数内部的严格模式,同时适用于函数体和函数参数。可是,函数执行的时候,先执行函数参数,而后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,可是参数却应该先于函数体执行

// 报错
function doSomething(value = 070) {
  'use strict';
  return value;
}

  上面代码中,参数value的默认值是八进制数070,可是严格模式下不能用前缀0表示八进制,因此应该报错。可是实际上,JS引擎会先成功执行value = 070,而后进入函数体内部,发现须要用严格模式执行,这时才会报错

  虽然能够先解析函数体代码,再执行参数代码,可是这样无疑就增长了复杂性。所以,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

  两种方法能够规避这种限制:

  一、设定全局性的严格模式

'use strict';
function doSomething(a, b = a) {
  // code
}

  二、把函数包在一个无参数的当即执行函数里面

const doSomething = (function () {
  'use strict';
  return function(value = 42) {
    return value;
  };
}());

 

构造函数

  Function构造函数是JS语法中不多被用到的一部分,一般咱们用它来动态建立新的函数。这种构造函数接受字符串形式的参数,分别为函数参数及函数体

var add = new Function("first", "second", "return first + second");
console.log(add(1, 1)); // 2

  ES6加强了Function构造函数的功能,支持在建立函数时定义默认参数和不定参数。惟一须要作的是在参数名后添加一个等号及一个默认值

var add = new Function("first", "second = first","return first + second"); console.log(add(1, 1)); // 2 console.log(add(1)); // 2

  在这个示例中,调用add(1)时只传入一个参数,参数second被赋值为first的值。这种语法与不使用Function声明函数很像

  定义不定参数,只需在最后一个参数前添加...

var pickFirst = new Function("...args", "return args[0]");
console.log(pickFirst(1, 2)); // 1

  在这段建立函数的代码中,只定义了一个不定参数,函数返回传入的第一个参数。对于Function构造函数,新增的默认参数和不定参数这两个特性使其具有了与声明式建立函数相同的能力

  

参数尾逗号

  ES8容许函数的最后一个参数有尾逗号(trailing comma)。

  此前,函数定义和调用时,都不容许最后一个参数后面出现逗号

function clownsEverywhere(
  param1,
  param2
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar'
);

  上面代码中,若是在param2bar后面加一个逗号,就会报错。

  若是像上面这样,将参数写成多行(即每一个参数占据一行),之后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来讲,就会显示添加逗号的那一行也发生了变更。这看上去有点冗余,所以新的语法容许定义和调用时,尾部直接有一个逗号

function clownsEverywhere(
  param1,
  param2,
) { /* ... */ }

clownsEverywhere(
  'foo',
  'bar',
);

  这样的规定使得函数参数与数组和对象的尾逗号规则保持一致了

 

name属性

  因为在JS中有多种定义函数的方式,于是辨别函数就是一项具备挑战性的任务。此外,匿名函数表达式的普遍使用更是加大了调试的难度,开发者们常常要追踪难以解读的栈记录。为了解决这些问题,ES6为全部函数新增了name属性

  ES6中全部的函数的name属性都有一个合适的值 

function doSomething() {
    // ...
}
var doAnotherThing = function() {
    // ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"

  在这段代码中,dosomething()函数的name属性值为"dosomething",对应着声明时的函数名称;匿名函数表达式doAnotherThing()的name属性值为"doAnotherThing",对应着被赋值为该匿名函数的变量的名称

【特殊状况】

  尽管肯定函数声明和函数表达式的名称很容易,ES6仍是作了更多的改进来确保全部函数都有合适的名称

var doSomething = function doSomethingElse() {
    // ...
};
var person = {
    get firstName() {
        return "huochai"
    },
    sayName: function() {
        console.log(this.name);
    }
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"

  在这个示例中,dosomething.name的值为"dosomethingElse",是因为函数表达式有一个名字,这个名字比函数自己被赋值的变量的权重高

  person.sayName()的name属性的值为"sayName",由于其值取自对象字面量。与之相似,person.firstName其实是一个getter函数,因此它的名称为"get firstName",setter函数的名称中固然也有前缀"set"

  还有另外两个有关函数名称的特例:经过bind()函数建立的函数,其名称将带有"bound"前缀;经过Function构造函数建立的函数,其名称将带有前缀"anonymous"

var doSomething = function() {
    // ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"

  绑定函数的name属性老是由被绑定函数的name属性及字符串前缀"bound"组成,因此绑定函数dosomething()的name属性值为"bound dosomething"

  [注意]函数name属性的值不必定引用同名变量,它只是协助调试用的额外信息,因此不能使用name属性的值来获取对于函数的引用

 

判断调用

  ES5中的函数结合new使用,函数内的this值将指向一个新对象,函数最终会返回这个新对象

function Person(name) {
    this.name = name;
}
var person = new Person("huochai");
var notAPerson = Person("huochai");
console.log(person); // "[Object object]"
console.log(notAPerson); // "undefined"

  给notAperson变量赋值时,没有经过new关键字来调用person(),最终返回undefined(若是在非严格模式下,还会在全局对象中设置一个name属性)。只有经过new关键字调用person()时才能体现其能力,就像常见的JS程序中显示的那样

  而在ES6中,函数混乱的双重身份终于将有一些改变

  JS函数有两个不一样的内部方法:[[Call]]和[[Construct]]

  当经过new关键字调用函数时,执行的是[[construct]]函数,它负责建立一个一般被称做实例的新对象,而后再执行函数体,将this绑定到实例上

  若是不经过new关键字调用函数,则执行[[call]]函数,从而直接执行代码中的函数体

  具备[[construct]]方法的函数被统称为构造函数

  [注意]不是全部函数都有[[construct]]方法,所以不是全部函数均可以经过new来调用

【ES5判断函数被调用】

  在ES5中,若是想肯定一个函数是否经过new关键字被调用,或者说,判断该函数是否做为构造函数被调用,最经常使用的方式是使用instanceof操做符

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("huochai");
var notAPerson = Person("huochai"); // 抛出错误

  在这段代码中,首先检查this的值,看它是否为构造函数的实例,若是是,则继续正常执行。若是不是,则抛出错误。因为[[construct]]方法会建立一个person的新实例,并将this绑定到新实例上,一般来说这样作是正确的

  但这个方法也不彻底可靠,由于有一种不依赖new关键字的方法也能够将this绑定到person的实例上

function Person(name) {
    if (this instanceof Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("huochai");
var notAPerson = Person.call(person, "huochai"); // 不报错

  调用person.call()时将变量person传入做为第一个参数,至关于在person函数里将this设为了person实例。对于函数自己,没法区分是经过person.call()(或者是person.apply())仍是new关键字调用获得的person的实例

【元属性new.target】

  为了解决判断函数是否经过new关键字调用的问题,ES6引入了new.target这个元属性。元属性是指非对象的属性,其能够提供非对象目标的补充信息(例如new)。当调用函数的[[construct]]方法时,new.target被赋值为new操做符的目标,一般是新建立对象实例,也就是函数体内this的构造函数;若是调用[[call]]方法,则new.target的值为undefined

  有了这个元属性,能够经过检查new.target是否被定义过,检测一个函数是不是经过new关键字调用的

function Person(name) {
    if (typeof new.target !== "undefined") {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
var person = new Person("huochai");
var notAPerson = Person.call(person, "match"); // 出错!

  也能够检查new.target是否被某个特定构造函数所调用

function Person(name) {
    if (new.target === Person) {
        this.name = name; // 使用 new
    } else {
        throw new Error("You must use new with Person.")
    }
}
function AnotherPerson(name) {
    Person.call(this, name);
}
var person = new Person("huochai");
var anotherPerson = new AnotherPerson("huochai"); // 出错!

  在这段代码中,若是要让程序正确运行,new.target必定是person。当调用 new Anotherperson("huochai") 时, 真正的调用Person. call(this,name)没有使用new关键字,所以new.target的值为undefined会抛出错误

  [注意]在函数外使用new.target是一个语法错误

 

块级函数

  在ES3中,在代码块中声明一个函数(即块级函数)严格来讲应当是一个语法错误, 但全部的浏览器都支持该语法。不幸的是,每一个浏览器对这个特性的支持都稍有不一样,因此最好不要在代码块中声明函数,更好的选择是使用函数表达式

   为了遏制这种不兼容行为, ES5的严格模式为代码块内部的函数声明引入了一个错误

"use strict";
if (true) {
    // 在 ES5 会抛出语法错误, ES6 则不会
    function doSomething() {
        // ...
    }
}

  在ES5中,代码会抛出语法错误。而在ES6中,会将dosomething()函数视为一个块级声明,从而能够在定义该函数的代码块内访问和调用它

"use strict";
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething); // "undefined"

  在定义函数的代码块内,块级函数会被提高至顶部,因此typeof dosomething的值为"function",这也佐证了,即便在函数定义的位置前调用它,仍是能返回正确结果。可是一旦if语句代码块结束执行,dosomething()函数将再也不存在

【使用场景】

  块级函数与let函数表达式相似,一旦执行过程流出了代码块,函数定义当即被移除。两者的区别是,在该代码块中,块级函数会被提高至块的顶部,而用let定义的函数表达式不会被提高

"use strict";
if (true) {
    console.log(typeof doSomething); // 抛出错误
    let doSomething = function () {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething);

  在这段代码中,当执行到typeof dosomething时,因为此时还没有执行let声明语句,dosomething()还在当前块做用域的临时死区中,所以程序被迫中断执行

  所以,若是须要函数提高至代码块顶部,则选择块级函数;若是不须要,则选择let表达式

【非严格模式】

  在ES6中,即便处于非严格模式下,也能够声明块级函数,但其行为与严格模式下稍有不一样。这些函数再也不提高到代码块的顶部,而是提高到外围函数或全局做用域的顶部

// ES6 behavior
if (true) {
    console.log(typeof doSomething); // "function"
    function doSomething() {
        // ...
    }
    doSomething();
}
console.log(typeof doSomething); // "function"

  在这个示例中,dosomething()函数被提高至全局做用域,因此在if代码块外也能够访问到。ES6将这个行为标准化了,移除了以前存在于各浏览器间不兼容的行为,因此全部ES6的运行时环境都将执行这一标准

 

箭头函数

  在ES6中,箭头函数是其中最有趣的新增特性。顾名思义,箭头函数是一种使用箭头(=>)定义函数的新语法,可是它与传统的JS函数有些许不一样,主要集中在如下方面 

  一、没有this、super、arguments和new.target

  绑定箭头函数中的this、super、arguments和new.target这些值由外围最近一层非箭头函数决定

  二、不能经过new关键字调用

  箭头函数没有[[construct]]方法,不能被用做构造函数,若是经过new关键字调用箭头函数,程序抛出错误

  三、没有原型

  因为不能够经过new关键字调用箭头函数,于是没有构建原型的需求,因此箭头函数不存在prototype这个属性

  四、不能够改变this绑定

  函数内部的this值不可被改变,在函数的生命周期内始终保持一致

  五、不支持arguments对象

  箭头函数没有arguments绑定,必须经过命名参数和不定参数这两种形式访问函数的参数

  六、不支持重复的命名参数

  不管在严格仍是非严格模式下,箭头函数都不支持重复的命名参数;而在传统函数的规定中,只有在严格模式下才不能有重复的命名参数

  在箭头函数内,其他的差别主要是减小错误以及理清模糊不清的地方。这样一来,JS引擎就能够更好地优化箭头函数的执行过程

  这些差别的产生有以下几个缘由

  一、最重要的是,this绑定是JS程序中一个常见的错误来源,在函数内很容易对this的值失去控制,其常常致使程序出现意想不到的行为,箭头函数消除了这方面的烦恼

  二、若是限制箭头函数的this值,简化代码执行的过程,则JS引擎能够更轻松地优化这些操做,而常规函数每每同时会做为构造函数使用或者以其余方式对其进行修改

  [注意]箭头函数一样也有一个name属性,这与其余函数的规则相同

【语法】

  箭头函数的语法多变,根据实际的使用场景有多种形式。全部变种都由函数参数、箭头、函数体组成,根据使用的需求,参数和函数体能够分别采起多种不一样的形式

var reflect = value => value;
// 有效等价于:
var reflect = function(value) {
    return value;
};

  当箭头函数只有一个参数时,能够直接写参数名,箭头紧随其后,箭头右侧的表达式被求值后便当即返回。即便没有显式的返回语句,这个箭头函数也能够返回传入的第一个参数

  若是要传入两个或两个以上的参数,要在参数的两侧添加一对小括号

var sum = (num1, num2) => num1 + num2;
// 有效等价于:
var sum = function(num1, num2) {
    return num1 + num2;
};

  这里的sum()函数接受两个参数,将它们简单相加后返回最终结果,它与reflect()函数惟一的不一样是,它的参数被包裹在小括号中,而且用逗号进行分隔(相似传统函数)

  若是函数没有参数,也要在声明的时候写一组没有内容的小括号

var getName = () => "huochai";
// 有效等价于:
var getName = function() {
    return "huochai";
};

  若是但愿为函数编写由多个表达式组成的更传统的函数体,那么须要用花括号包裹函数体,并显式地定义一个返回值

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),能够定义一个匿名函数并当即调用,自始至终不保存对该函数的引用。当建立一个与其余程序隔离的做用域时,这种模式很是方便

let person = function(name) {
    return {
        getName: function() {
            return name;
        }
    };
}("huochai");
console.log(person.getName()); // "huochai"

  在这段代码中,IIFE经过getName()方法建立了一个新对象,将参数name做为该对象的一个私有成员返回给函数的调用者

  只要将箭头函数包裹在小括号里,就能够用它实现相同的功能

let person = ((name) => {
    return {
        getName: function() {
            return name;
        }
    };
})("huochai");
console.log(person.getName()); // "huochai"

  [注意]小括号只包裹箭头函数定义,没有包含("huochai"),这一点与正常函数有所不一样,由正常函数定义的当即执行函数表达式既能够用小括号包裹函数体,也能够额外包裹函数调用的部分

【this】

  函数内的this绑定是JS中最常出现错误的因素,函数内的this值能够根据函数调用的上下文而改变,这有可能错误地影响其余对象

var PageHandler = {
    id: "123456",
    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: "123456",
    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: "123456",
    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()这些能够接受回调函数的数组方法,均可以经过箭头函数语法简化编码过程并减小编码量

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

【arguments】

  箭头函数没有本身的arguments对象,且将来不管函数在哪一个上下文中执行,箭头函数始终能够访问外围函数的arguments对象

function createArrowFunctionReturningFirstArg() {
    return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg(5);
console.log(arrowFunction()); // 5

  在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值不会受这些方法的影响

var sum = (num1, num2) => num1 + num2;
console.log(sum.call(null, 1, 2)); // 3
console.log(sum.apply(null, [1, 2])); // 3
var boundSum = sum.bind(null, 1, 2);
console.log(boundSum()); // 3

  包括回调函数在内全部使用匿名函数表达式的地方都适合用箭头函数来改写

【函数柯里化】

  柯里化是一种把接受多个参数的函数变换成接受一个单一参数的函数,而且返回(接受余下的参数并且返回结果的)新函数的技术

  若是使用ES5的语法来写,以下所示

function add(x){
  return function(y){
    return y + x;
  };
}
 
var addTwo = add(2);
addTwo(3);          // => 5
add(10)(11);        // => 21

  使用ES6的语法来写,以下所示

var add = (x) => (y) => x+y

  通常来讲,出现连续地箭头函数调用的状况,就是在使用函数柯里化的技术

 

尾调用优化

  ES6关于函数最有趣的变化多是尾调用系统的引擎优化。尾调用指的是函数做为另外一个函数的最后一条语句被调用

function doSomething() {
    return doSomethingElse(); // 尾调用
}

  尾调用之因此与其余调用不一样,就在于它的特殊的调用位置

  咱们知道,函数调用会在内存造成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。若是在函数A的内部调用函数B,那么在A的调用帧上方,还会造成一个B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。若是函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。全部的调用帧,就造成一个“调用栈”(call stack)

  尾调用因为是函数的最后一步操做,因此不须要保留外层函数的调用帧,由于调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就能够了

  尾调用优化(Tail call optimization),即只保留内层函数的调用帧。若是全部函数都是尾调用,那么彻底能够作到每次执行时,调用帧只有一项,这将大大节省内存

  ES6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),若是知足如下条件,尾调用再也不建立新的栈帧,而是清除并重用当前栈帧

  一、尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)

  二、在函数内部,尾调用是最后一条语句

  三、尾调用的结果做为函数值返回

  如下这段示例代码知足上述的三个条件,能够被JS引擎自动优化

"use strict";
function doSomething() {
    // 被优化
    return doSomethingElse();
}

  在这个函数中,尾调用doSomethingElse()的结果当即返回,不调用任何局部做用域变量。若是作一个小改动,不返回最终结果,那么引擎就没法优化当前函数

"use strict";
function doSomething() {
    // 未被优化:缺乏 return
    doSomethingElse();
}

  一样地,若是定义了一个函数,在尾调用返回后执行其余操做,则函数也没法获得优化

"use strict";
function doSomething() {
    // 未被优化:在返回以后还要执行加法
    return 1 + doSomethingElse();
}

  若是把函数调用的结果存储在一个变量里,最后再返回这个变量,则可能致使引擎没法优化

"use strict";
function doSomething() {
    // 未被优化:调用并不在尾部
    var result = doSomethingElse();
    return result;
}

  可能最难避免的状况是闭包的使用,它能够访问做用域中全部变量,于是致使尾调用优化失效

"use strict";
function doSomething() {
    var num = 1,
    func = () => num;
    // 未被优化:此函数是闭包
    return func();
}

  在示例中,闭包func()能够访问局部变量num,即便调用func()后当即返回结果,也没法对代码进行优化

【应用】

  实际上,尾调用的优化发生在引擎背后,除非尝试优化一个函数,不然无须思考此类问题。递归函数是其最主要的应用场景,此时尾调用优化的效果最显著

function factorial(n) {
    if (n <= 1) {
        return 1;
    } else {
        // 未被优化:在返回以后还要执行乘法
        return n * factorial(n - 1);
    }
}

  因为在递归调用前执行了乘法操做,于是当前版本的阶乘函数没法被引擎优化。若是n是一个很是大的数,则调用栈的尺寸就会不断增加并存在最终致使栈溢出的潜在风险

  优化这个函数,首先要确保乘法不会在函数调用后执行,能够经过默认参数来将乘法操做移出return语句,结果函数能够携带着临时结果进入到下一个迭代中

function factorial(n, p = 1) {
    if (n <= 1) {
        return 1 * p;
    } else {
        let result = n * p;
        // 被优化
        return factorial(n - 1, result);
    }
}

  在这个重写后的factorial()函数中,第一个参数p的默认值为1,用它来保存乘法结果,下一次迭代中能够取出它用于计算,再也不须要额外的函数调用。当n大于1时,先执行一轮乘法计算,而后将结果传给第二次factorial()调用的参数。如今,ES6引擎就能够优化递归调用了

  写递归函数时,最好得用尾递归优化的特性,若是递归函数的计算量足够大,则尾递归优化能够大幅提高程序的性能

  另外一个常见的事例是Fibonacci数列

function Fibonacci (n) {
  if ( n <= 1 ) {return 1};
  return Fibonacci(n - 1) + Fibonacci(n - 2);
}

Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出

  尾递归优化过的 Fibonacci 数列实现以下

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) {return ac2};
  return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

  因而可知,“尾调用优化”对递归操做意义重大,因此一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,全部 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存

相关文章
相关标签/搜索