ES6学习笔记2--函数

  |  

前言

函数


3



参数默认值

JS函数最独特的一点就是可以接受任意数量的参数,而无视函数声明处的参数数量。这让定义的函数可以使用不同的参数数量调用,调用时未提供的参数会使用默认值来代替。

ES5模拟参数默认值

1
2
3
4
function makeRequest(url, timeout, callback) {
timeout = (typeof timeout !== "undefined") ? timeout : 2000;
callback = callback || function() {};
}

ES6中的参数默认值

ES6使用初始化的形式,以便在参数未被正式传递进函数时使用。

1
2
3
function makeRequest(url, timeout = 2000, callback = function() {}){

}

未被设置默认值的参数被认为是必须传递的。当设置默认值的参数未被传递时使用默认值

在函数声明中可以指定任意一个参数的默认值,即使该参数排在未指定默认值的参数之前也是可以的,这是如果需要使用默认值,则在参数位置传递undefined即可。

参数默认值与arguments对象

1
2
3
4
5
6
7
8
9
10
11
12
13
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]) ;
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
//输出
true
true
true
true

在ES5非严格模式下,arguments对象会反映出具名参数的变化,因此当first和second变量被赋予新值时arguments中的值也相应变化了

在严格模式下这种情况被消除了,具名参数的变化并不会引起arguments的变化

在使用ES6参数默认值的函数中,arguments对象跟ES5严格模式一样,且arguments中不包含使用默认值的参数。无论是否在严格模式下,都可以通过arguments对象来反映初始调用状态。

参数默认值表达式

在ES6中,参数默认值并不要求一定是基本类型的值。可以通过函数来产生一个参数默认值

1
2
3
4
5
6
7
let value = 5
function getValue() {
return value++;
}
function add(first, second = getValue()) {
return first + second;
}

甚至可以使用前面的参数作为后面参数的默认值

1
2
3
function add(frist, second = first){
return first + second
}

或者将前面的参数作为后面参数表达式的参数传入

1
2
3
function add(frist, second = getValue(first)){

}

参数默认值的暂时性死区

参数默认值同样有着无法访问特定参数的暂时性死区。与let声明相似,函数每个参数都会创建一个新的标识符绑定,它在初始化之前不允许被访问,否则会抛出错误。参数初始化会在函数被调用时进行,无论使给参数传递一个值还是使用参数的默认值。

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

这段示例很好的说明了暂时性死区导致的函数默认值问题,在不使用默认值的时候,这个函数调用不会出现问题,但是当我们使用默认值时,由于first被初始化时second尚未被初始化,此时second存在于暂时性死区内,对于second的引用就会导致错误发生。

不具名参数

由于JS的函数并不强求参数的数量要等于已定义具名参数的数量,因此可能会导致调用函数时会有不具有参数名的多余参数

ES5的不具名参数

JS早就提供了arguments对象来查看传递给函数的所有的参数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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: "Understanding ES6",
author: "Nicholas C.Zakas",
year: 2015
};

let bookData = pick(book, "author", "year");
console.log(bookData.author); // "Nicholas C.Zakas"

我们用这个函数模拟了Underscore.js代码库中的Pick方法,能够返回包含原有对象特定属性的子集副本。由于我们在定义函数时,并不知道要复制的对象有几个属性,属性名具体为什么,所以只能使用不具名参数。但是这样的操作有些麻烦,我们需要从arguments中避开具名参数。所以ES6引入了剩余参数的概念

剩余参数

剩余参数有三个点(…)与一个紧跟着的具名参数指定,它回包含传递给函数的其余参数的一个数组。我们用剩余参数来改写一下Pick方法

1
2
3
4
5
6
7
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最大的不同在于,剩余参数不会包含具名参数)

剩余参数存在两个限制:

一是一个函数只用有一个剩余参数,且必须放在参数最后

二是不能在对象字面量的setter属性中使用

1
2
3
4
5
let obj = {
set name(...value) {

}
}

存在此限制的原因是因为对象字面两的setter被限制只能使用单个参数

函数构造器的增强

Function构造器允许动态创建一个函数,但在JS中并不常用

1
var add = new Function("first", "second", "return first+second");

ES6增强了Function构造器的能力,允许在构造器中使用默认参数以及剩余参数,使用方法与函数声明形式相同

扩展运算符

扩展运算符允许将一个数组分割,并将各个项作为分离的参数传给函数。使用方法与剩余参数一样,在数组前添加...即可

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

扩展运算符可以与其它参数混用。

使用扩展运算符传递参数,使得更容易将数组作为函数参数来使用,在大多数场景中扩展运算符都是apply()方法的合适替代品。

ES6的名称属性

ES6给所有函数添加了name属性

选择合适的名称

ES6中所有函数都有适当的name属性值。

1
2
3
4
5
6
function doSomething() {
}
var doAnotherthing = function() {
}
console.log(doSomething.name) //"doSomething"
console.log(doAnotherthing.name) //"doAnotherthing"

名称属性的特殊情况

函数声明和函数表达式的名称易于查找,ES6为了更进一步的确保所有函数都拥有合适的名称,为一些特殊的函数的name属性添加了一些特殊的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var doSomething = function doSomethingElse() { 
};
var person = {
get firstName() {
return "Nicholas";
}
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并不是doSomething,而是doSomething,因为函数表达式自己拥有一个名称,且这个名称的优先级高于赋值目标的变量名。person.sayName的name属性是sayName,这个跟对象字面量指定的一样。而person.firstName是一个getter函数,因此他的名称是”get firstName”,以标明它的特征。同样的,setter函数也会带有set前缀。

除此之外使用bind()创建的函数会在名称属性值之前带有”bound”前缀;而是用Function构造器创建的函数,其名称属性则会有”anonymous”前缀

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

函数的name属性未必会关联到同名变量。name属性是为了在调试时获得有用的相关信息,所以不能用name属性去获取对函数的引用。

明确函数的双重用途

在ES5及以前,函数根据是否使用new来调用而有双重用途。当使用new时,函数作为构造器使用,函数内部的this是一个新对象,并作为函数的返回值。

ES6为函数提供了两个不同的内部方法:[[Call]][[Construct]]。当函数未使用new进行调用时,Call方法会被执行,运行的是代码中显示的函数体。当函数使用new进行调用时,Construct方法则会被执行,负责创建要给被称为新目标的新的对象,并且使用该新目标作为this去执行函数体。拥有Construct方法的函数被称为构造器

注意:并不是所有函数都可以拥有C[[Construct]]方法,因此并不是所有函数都已用new来调用

在ES5中如何判断函数如何被调用

最流行的方法是使用instanceof:

1
2
3
4
5
6
7
8
9
10
function Person(name) {
if(this instanceof Person) {
this.name = name;
} else {
throw new Error("you must use new with Person.");
}
}

var person = new Person("Nicholas");
var notAPerson = Person("nicholas"); //抛出错误

通过对this的检查,检查this是否为一个构造器实例。这种方法之所以能奏效是因为Construct方法创建了Person的一个新实例并将其赋值给this。但是这种方法并不绝对可靠,当我们使用call或者apply来将函数调用的this绑定为一个Person实例时,我们还是能绕过这种判断

1
2
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael");

new.target元属性

为了更好的解决函数是被怎样调用的问题,ES6引进了new.target元属性。

元属性:非对象(例如new)上的一个属性,并提供关联到它的目标的附加信息。

当函数的[[Consturct]]方法被调用时,new.target会被填入new运算符的作用目标,而若[[Call]]被执行,new.target的值则会是undefined

通过检查new.target是否被定义或者是否为具体的值,来判断函数使用使用new进行(特定的构造器)调用

1
2
3
4
5
6
7
8
9
function Person(name) {
if(new.target === Person) {
// (new.target !== undefined)
this.name = name;
} else {
throw new Error("You must use new with Person.");
}
}
var person = new Person("Nicholas");

警告:在函数之外使用new.target会有语法错误

块级函数

在ES3或更早版本中,在代码块中声明函数(即块级函数)严格来说应当是一个语法错误,但所有的浏览器都支持该语法,但是支持都有轻微差异。为了控制这种不兼容行为,ES5的严格模式为代码块内部的函数声明引入一个错误。

1
2
3
4
5
6
"use strict"
if(true) {
function doSomething() {
}
doSomething();
}

ES6并不会报错,他会将doSomething()函数视为块级声明,并允许它在定义所在的代码块内部被访问。

决定何时使用块级函数

块级函数与let关键字相似,在执行流跳出定义所在的代码块之后,函数定义就会被移除。关键区别在于:块级函数会被提升到所在代码块的顶部;而是用let的函数表达式则不会

1
2
3
4
5
6
7
if(true) {
console.log(typeof doSomething);
let doSomething = function() {
} //Cannot access 'doSomething' before initialization
function doSomething() {
} //输出function
}

这里解释一下,我看书的时候它在这里说let的函数表达式不会被提升到代码快顶部,我又查了很多资料有些也说let声明并不会提升。所以这里解释一下let到底会不会提升

let定义其实会提升,但它与var的提升又有点不同,var在提升的时候会将变量初始化undefined,而let并不初始化,所以变现的跟没有提升一样,但是它确实提升了。所以这其中会出现暂时性死区的问题,这也是我们之前说过,在let声明并赋值之前调用变量会报错

非严格模式的块级函数

ES6在非严格模式下也是可以使用块级函数的,但是块级函数的作用域会被提升到所在函数或全局环境的顶部,而不是代码块的的顶部。这也就意味着,在代码块之外,我们同样可以访问这个块级函数

箭头函数

箭头函数与传统的函数不同点:

  • 没有this、super、arguments,也没有new.target绑定:这些值都有箭头函数所在的、最靠近的非箭头函数来决定
  • 不能使用new调用
  • 没有原型:prototype属性
  • 不能更改this:this的值在整个函数的生命周期内保持不变
  • 没有arguments对象:函数必须依赖具名参数或剩余参数
  • 不允许重复的具名参数

箭头函数使用单一的this值来执行代码,使得JS引擎可以更容易对代码的操作进行优化,其余差异也聚集在减少箭头函数内部的错误与不确定性

注意:箭头函数也拥有name属性,并且遵循与其它函数相同的规则

箭头函数的语法

箭头函数的语法有很多写法,取决与你要完成的目标。

但是所有的变体都一函数参数开头,紧跟着箭头,再接下来是函数体。

1
2
3
4
5
var reflect = value => value;
// 等效于
var reflect = function(value) {
return value;
}

如果需要传入多个参数,则需要将参数用括号包裹

1
2
3
4
5
var sum = (num1, num2) => num1 + num2;
//等效于
var sum = function(num1, num2) {
return num1 + num2;
}

如果函数没有任何参数,那么声明时必须使用一对括号

1
2
3
4
5
var getName = () => "Nicholas";
// 等效于
var getName = function() {
return "Nocholas";
}

当想使用更多传统的函数体、也就是可能包含多个语句时,需要将函数体统一用一对换括号进行包裹,并明确定义一个返回值

1
2
3
var sum = (num1, num2) => {
return num1 + num2;
}

花括号被用于表示函数的主体,但是若箭头函数想要从函数体内向外返回一个对象字面量,就必须将该字面量包裹在圆括号内

1
var getTempItem = id => ({id: id, name: "Temp"});

将对象字面量包裹在括号内,标识括号内的是一个字面量而不是一个函数体。

创建立即执行函数

在ES5中有一种流行的函数调用方法,即IIFE,可以用来创建一个闭包作用域,将一部分变量隔离于其它程序作用域

1
2
3
4
5
6
7
8
let person = function(name) {
return {
getName: function() {
return name;
}
}
}("Nicholas");
console.log(person.getName()) // "Nicholas"

我们同样可以使用的ES6中的箭头函数来完成这一个IIFE,将name包裹到闭包中

1
2
3
4
5
6
7
let person = ((name) => {
reutrn {
getName: function() {
return name;
}
}
})("Nocholas");

只不过箭头函数需要在被立即调用之前包裹在括号中,这与传统的IIFE不同,传统的IIFE可以使用两种方式即

(function(value){//函数体})(value)function(value){//函数体}(value)

没有this绑定

在JS编程中的常见错误就是this的指向在编程中出现混淆,导致程序出错或者出现非预期情况,例如

1
2
3
4
5
6
7
8
9
10
11
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", function(event) {
this.doSomething(event.type);
}, false);
},
doSomething: function(type) {
console.log("Handing" + type + " for " + this.id);
}
}

这段代码乍一看并没有什么问题,我们闯进啊了一个PageHandler对象,如果调用它的init函数,会为当前文档界面创建一个点击事件的监听,当点击事件触发,调用doSomething函数。但是真正的问题是,当点击事件触发时,我们调用不到doSomething函数。这是因为在事件的回调函数中的this并不是指向PageHandler对象,而是指向了事件触发对象,这里就是document对象。对于这个问题,我们可以使用bind函数来强制绑定函数执行时的this指向

1
2
3
4
5
6
7
8
9
10
11
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", (function(event){
this.doSomething(event.type);
}).bind(this), false);
},
doSomething: function(type) {
console.log("Handing " + type +" for " + this.id);
}
}

我还考虑到了一种解决方法,但是这种方法会额外扩大对象所占用的内存,并不推荐

1
2
3
4
5
6
7
8
9
10
11
12
var PageHandler = {
id: "123456",
self: this,
init: function() {
document.addEventListener("click", function(event){
self.doSomething(event.type);
}, false);
},
doSomething: function(type) {
console.log("Handing " + type + " for " + this.id);
}
}

其实修正这种问题的最好方法就是使用箭头函数,我们利用箭头函数并没有自己的this这个特定,箭头函数的this继承自包含箭头函数最近一层的非箭头函数

1
2
3
4
5
6
7
8
9
var PageHandler = {
id: "123456",
init: function() {
document.addEventListener("click", event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log("Handing " + type + " for " + this.id);
}
}

箭头函数没有this绑定,箭头函数内部的this的值只能通过查找作用域链来继承,在这个例子中箭头函数的this继承字init函数,所以完美的解决了this指向的问题。

箭头函数被设计成抛弃型函数,因此它不具备[[Consturctor]]方法,无法使用new调用,而且不发通过call()、apply()或bind()方法来改变它的this值

箭头函数与数组

箭头函数的便利性使得它成为了对数组操作的理想选择。当我们需要使用自定义比较器对数组进行排序时

1
2
3
4
5
var result = value.sort(function(a, b) {
return a-b;
});
//可以简写为
var result = value.sort((a, b) => a-b);

能够使用回调函数的数组方法(譬如sort()、map()与reduce方法),都能通过箭头函数简化它的返回方法。

没有arguments绑定

尽管箭头函数没有自己的arguments对象,但是它可以从作用域链中继承包含它的函数的arguments对象

1
2
3
4
5
function createArrowFunctionReturningFirstArg() {
return () => arguments[0];
}
var arrowFunction = createArrowFunctionReturningFirstArg();
arrowFunction(); // 5

外层函数返回了一个箭头函数,箭头函数形成闭包,包裹了从作用域链中继承的arguments对象的首个参数,尽管arrowFunction并不存在与外层函数中,仍然能访问闭包中的值

尾调用优化

ES6对函数进行了一项引擎优化,它改变了尾部调用系统。

尾调用:指的是调用函数的语句是另一个函数的最后语句

1
2
3
function doSomething() {
return doSomethingElse();
}

ES6在严格模式下力图为特定的尾调用减少调用栈的大小(非严格模式不变)。在一下条件下,尾调用优化会清除当前栈帧并再次利用它,而不是为尾调用创建新的栈帧:

  • 尾调用不能引用当前栈帧中的变量(意味着尾调用函数不能形成闭包)
  • 进行尾调用的函数在尾调用返回结果后不能有额外的操作
  • 尾调用的结果作为当前函数的返回值

当满足这三个条件的尾调用就会被轻易的优化了

下面举对应的反例

1
2
3
4
5
6
"use strict" 
function doSomething() {
var num = 1,
func = () => num;
return func;
}
1
2
3
4
"use strict" 
function doSomething() {
return 1 + doSomethingElse();
}
1
2
3
4
5
"use strict"
function doSomething() {
var result = doSomethingElse();
return result;
}
文章目录
  1. 1. 参数默认值
    1. 1.1. ES5模拟参数默认值
    2. 1.2. ES6中的参数默认值
    3. 1.3. 参数默认值与arguments对象
    4. 1.4. 参数默认值表达式
    5. 1.5. 参数默认值的暂时性死区
  2. 2. 不具名参数
    1. 2.1. ES5的不具名参数
    2. 2.2. 剩余参数
    3. 2.3. 函数构造器的增强
    4. 2.4. 扩展运算符
  3. 3. ES6的名称属性
    1. 3.1. 选择合适的名称
    2. 3.2. 名称属性的特殊情况
  4. 4. 明确函数的双重用途
    1. 4.1. 在ES5中如何判断函数如何被调用
    2. 4.2. new.target元属性
  5. 5. 块级函数
    1. 5.1. 决定何时使用块级函数
    2. 5.2. 非严格模式的块级函数
  6. 6. 箭头函数
    1. 6.1. 箭头函数的语法
    2. 6.2. 创建立即执行函数
    3. 6.3. 没有this绑定
    4. 6.4. 箭头函数与数组
    5. 6.5. 没有arguments绑定
  7. 7. 尾调用优化