《深入理解ES6》读书笔记——3.函数
1)函数形参的默认值
(1)默认参数值
如果没味参数传入值,则为其提供一个初始值。在已经指定默认值的参数后可以继续声明无默认值参数。
function makeRequest(url, timeout = 2000, callback) {
console.log(url,timeout, callback) // undefined 2000 undefined 不会抛出错误
}
// timeout不传值或者传undefined即使用2000这个默认值
makeRequest() // undefined 2000 undefined
makeRequest(undefined, undefined) // undefined 2000 undefined
// null是合法值
makeRequest(undefined, null) // undefined null undefined
优点:不需要添加任何校验值是否缺失的代码
(2)默认参数值对arguments对象的影响
默认参数值的存在使得arguments对象保持与命名参数分离。这种方式确保了arguments对象获取到的一定为初始的传入参数值。
(3)默认参数表达式
特性:非原始值传参
因为默认值是函数调用时求值,所以可以使用先定义的参数作为后定义参数的默认值
let first = 1
function (first, second = first) {
return first + second
}
console.log(add(1)) // 2
let value = 5
function getValue() {
return value++
}
function add(first, second= getValue(first)){
return first + second
}
console.log(add(1)) // 6
但先定义的值不能访问后定义的值
function add(first = second, second){
return first + second
}
add(undefined, 6) // 报错:Uncaught ReferenceError: Cannot access 'second' before initialization
(4)默认参数的临时死区
默认参数也有临时死区,与let类似
function add(first = second, second){
return first + second
}
add(undefined, 1)
相当于
let first = second
let second = 1
2)处理无命名参数
(1)ES5中的无名参数
function pick(object) {
let result = Object.create(null)
for(let i = 1,len = arguments.length;i<len;i++){
console.log(arguments[i]) // 两次分别输出author year
result[arguments[i]] = object[arguments[i]]
}
return result
}
let book = {
title: '深入理解ES6',
author: 'Nicholas C. Zakas',
year: 2016
}
let bookData = pick(book, 'author', 'year')
console.log(bookData) // { author: "Nicholas C. Zakas", year: 2016 }
模仿了Underscore.js库中的pick()方法,返回一个给定对象的副本。
两个缺点:第一,不容易发现这个函数可以接受任意数量的参数。第二,因为第一个参数已经被使用,当要查找需要被拷贝的属性名称时,索引从1开始,而不是0。
(2)不定参数
在函数参数前加... 就表明是不定参数
ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function pick(object) {
let result = Object.create(null)
for(let i = 1,len = arguments.length;i<len;i++){
console.log(arguments[i]) // 两次分别输出author year
result[arguments[i]] = object[arguments[i]]
}
return result
}
let book = {
title: '深入理解ES6',
author: 'Nicholas C. Zakas',
year: 2016
}
let bookData = pick(book, 'author', 'title')
console.log(bookData) // {author: "Nicholas C. Zakas", title: "深入理解ES6"}
// 使用不定参数重写pick()函数
function pick (object, ...keys) { // 参数keys包含的object之后的所有参数(而arguments包含的是所以传入的参数,包括object)
let result = Object.create(null)
for(let i = 0,len=keys.length;i<len;i++){
result[keys[i]] = object[keys[i]]
}
return result
}
(3)不定参数的使用限制
有两条:
第一,每个函数最多只能声明一个不定参数,并且只能放在所有参数的末尾。(如果在声明不定参数后又声明了参数,则导致报错)
function pick(object, ...keys, last){
let result = Object.create(null)
for(var i =0,len=keys.length;i<len;i++){
result[keys[i]] = object[keys[i]]
}
return result
}
console.log(pick({name: 'jim', age: 21}, 'age', 'name', 8)) // Uncaught SyntaxError: Rest parameter must be last formal parameter
第二,不定参数不能用于对象字面量setter中(否则也会大致程序抛出语法错误)之所以有这条限制是因为对象字面量setter的参数有且只能有一个。
let object = {
set name(...value){ // Uncaught SyntaxError: Setter function argument must not be a rest parameter
}
}
(4)不定参数对arguments对象的影响
function checkArgs(...args) {
console.log(args.length) // 2
console.log(arguments.length) // 2
console.log(args[0], arguments[0]) // a a
console.log(args[1], arguments[1]) // b b
}
checkArgs('a', 'b')
无论是否使用不定参数,arguments对象总是包含所有传入函数的参数。
3)增强的Function构造函数
ES6增强了Function构造函数的功能,支持在创建函数时定义默认参数和不定参数
定义默认参数:
var add = new Function("first","second=first","return first + second")
console.log(add(2)) // 4
console.log(add(2,5)) // 7
定义不定参数:
var pickFirst = new Function("...args", "return args[0]")
console.log(pickFirst(2)) // 2
console.log(pickFirst(2,5)) // 2
默认参数和不定参数这两个特性,使其具备了与声明式创建函数形同的能力。
4)展开运算符
不定参数可以让你指定多个参数,并通过整合后的数组类访问,
而展开运算符可以让你指定一个数组,将它们打散后作为各自独立的参数传入函数。
let values = [-1, -5, -4] Math.max(...values, 0)
Math.max() 函数返回一组数中的最大值。参数为一组数值,本身不接受数组。
如果想接受数组为参数,有两种方法
方法1:
Math.max.apply(Math, values) 这种方法可行,但是让人很难看懂代码的真正意图。
方法2:
Math.max(...values)
在ECMAScript 6以前,如果要将数组中的元素作为独立参数传递给函数,只有以下两种方式:手动指定每一个参数或使用apply()方法。只要使用展开运算符,就可以轻松地将数组传入到任何函数中,且由于不再使用apply()方法,也就不需要担心函数的this绑定问题。
5)name属性
函数中新增的name属性,有助于通过函数名称来对其进行调试或评估。
js中有很多种定义函数的方式,比那别函数比较麻烦。匿名函数的广泛使用更加大了调试难度。为了解决这些问题,es6中为所有函数新增了name属性。
(1)如何选择合适的名称
function doSomething () {}
var doAnotherthing = function () {}
console.log(doSomething.name) // doSomething 声明时的函数名称
console.log(doAnotherthing.name) // doAnotherthing 被复制为该匿名函数的变量的名称
var doSomething = function doSomethingElse () {}
var person = {
firstName () {},
sayName: function () {}
}
console.log(doSomething.name) // doSomethingElse
console.log(person.firstName.name) // firstName
console.log(person.sayName.name) // sayName
var doSomething = function () {}
console.log(doSomething.bind().name) // bound doSomething 绑定函数的name属性总是由被绑定函数的name属性及字符串前缀"bound"组成
console.log(new Function().name) // anonymous
切记:函数name属性的值不一定引用同名变量,它只是协助调试用的额外信息,所以不能使用name属性的值来获取对于函数的引用
6)明确函数的多重用途
(1)
function Person(name) { this.name = name }
var person = new Person('jim')
console.log(person) // Person {name: "jim"}
var notAperson = Person('jim')
console.log(notAperson) // undefined
js函数有两个不同的内部方法:[[Call]] 和 [[Construct]]
当通过new关键字调用函数时,执行的是[[Construct]]函数(它负责创建一个通常被称作实例的新对象,然后再执行函数体,将this绑定到实例上)
不通过new,则执行[[Call]]
具有[[Construct]]方法的函数被称为构造函数(不是所有的函数都有[[Construct]]方法,因此不是所有的函数都可以通过new来调用)
(2)在ECMAScript5中判断函数被调用的方法
在ECMAScript5中判断函数是否通过new关键字被调用(或者说,判断函数是否作为构造函数被调用)最流行的方式是使用instanceof
function Person(name) {
if (this instanceof Person) {
this.name = name
} else {
throw new Error('必须通过new关键字来调用Person') }
}
var person = new Person('jim')
var notAperson = Person('jim') // Uncaught Error: 必须通过new关键字来调用Person
但这种方法不完全可靠,因为还有一种方法可以不依赖new关键字也可以将this绑定到Person的实例上
使用Person.call(person, 'jim')
(3)元属性(Metaproperty)new.target
元属性是指非对象的属性,其可以提供非对象目标的补充信息(如new)
有了这个元属性,可以通过检查new.target是否被定义过来安全地检测一个函数是否是通过new关键字调用的
在放弃使用this instanceof Person的方法且改为检测new.target后,我们已经可以在Person构造函数中正确地进行判断,当未通过new关键字调用时抛出错误。
也可以检查new.target是否被某个特定构造函数所调用
new target元属性主要判断是否是new创建的新对象,且只能在函数内调用。
添加new.target后,ECMAScript 6解决了函数调用的一些模棱两可的问题。紧接着,ECMAScript 6还解决了另外一个模糊不清的问题:在代码块中声明函数。
使用场景: 如果一个构造函数不通过 new 命令生成实例, 就报错提醒或自身返回new 实例。
ES6引入了new.target属性,用于确认当前作用的在哪个构造函数上。若没有通过new命令调用构造函数。则new.target会返回undefined,否则返回当前构造函数。
7)块级函数
ECMAScript 5的严格模式中引入了一个错误提示,当在代码块内部声明函数时程序会抛出错误:在ECMAScript 5中,代码会抛出语法错误;在ECMAScript 6中,会将doSomething()函数视作一个块级声明,从而可以在定义该函数的代码块内访问和调用它。
ECMAScript 6也正式定义了块级函数的行为,即使在严格模式下块级函数也不再是一个语法错误了。
(1)使用场景
块级函数与let函数表达式类似,一旦执行过程流出了代码块,函数定义立即被移除。
二者的区别是,在该代码块中,块级函数会被提升至块的顶部,而用let定义的函数表达式不会被提升。
了解二者间的异同后,你可以考虑一个问题:如果需要函数提升至代码块顶部,则选择块级函数;如果不需要,则选择let表达式。
8)箭头函数
箭头函数的设计目标是用来替代匿名函数表达式。它的语法更简洁,具有词法级的this绑定,没有arguments对象,函数内部的this值不可被改变,因而不能作为构造函数使用。
(1)与传统的js函数不同之处为:
- 没有this、super、arguments 和 new.target绑定(箭头函数中的这些值由外围最近一层非箭头函数决定)
- 不能通过new关键字调用(箭头函数没有[[Construct]]方法)
- 没有原型
- 不可改变this的绑定(函数内部的this值不可改变,在函数周期内始终保持一致)
- 不支持arguments对象(箭头函数没有arguments绑定,所以你必须通过函数参数和不定参数这两种形式访问函数的参数)
- 不支持重复的命名参数
(2)箭头函数的语法
第一种,只有一个参数,可以直接写参数名,箭头紧随其后,箭头右侧的白哦大师求值后立即返回
第二种,传两个或者两个以上的参数,需要加小括号
第三章,没有参数,也要写()小括号
如果函数编写有多个表达式,则加{},并显示的返回return。否则直接写表达式。
但是,如果想在箭头函数外返回一个对象字面量,则需要将字面量包裹在小括号里。
(3)创建立即执行函数表达式(IIFE)
可以创建一个匿名函数并立即调用,自始至终都不保存对函数的引用。当你想创建一个与其他程序隔离的作用域时,这种模式非常方便。
let person = function (name) {
return { getName: function() { return name } }
}('jim')
console.log(person.getName()) // jim
只要将箭头函数包裹在小括号里,就可以实现相同的功能
let person = ((name) => {
return { getName: function () { return name } }
})('jim') console.log(person.getName()) // jim
(3)箭头函数没有this绑定
箭头函数没有this绑定,必须通过查找作用域链来决定其值。
如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined。
箭头函数缺少正常函数所拥有的prototype属性,它的设计初衷是“即用即弃”,所以不能用它来定义新的类型。
image.jpeg箭头函数中的this值取决于该函数外部非箭头函数的this值,且不能通过call()、apply()或bind()方法来改变this的值。(但可以调用call,apply或bind方法,只是this值不会受改变)
(4)箭头函数没有arguments绑定
箭头函数没有自己的arguments对象,且未来无论函数在哪个上下文中执行,箭头函数始终可以访问外围函数的arguments对象。
9)尾调用优化
尾调用指的是函数作为另一个函数的最后一条语句被调用
image.jpegES5中存在的问题:在ECMAScript 5的引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。也就是说,在循环调用中,每一个未用完的栈帧都会被保存在内存中,当调用栈变得过大时会造成程序问题。
(1)ECMAScript 6中的尾调用优化
ECMAScript 6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧:
· 尾调用不访问当前栈帧的变量(也就是说函数不是一个闭包)。
· 在函数内部,尾调用是最后一条语句。
· 尾调用的结果作为函数值返回。
以下这段示例代码满足上述的三个条件,可以被JavaScript引擎自动优化
可能最难避免的情况是闭包的使用,它可以访问作用域中所有变量,因而导致尾调用优化失效,举个例子:
image.jpeg在此示例中,闭包func()可以访问局部变量num,即使调用func()后立即返回结果,也无法对这段代码进行优化。
(2)如何利用尾调用优化
实际上,尾调用的优化发生在引擎背后,除非你尝试优化一个函数,否则无须思考此类问题。递归函数是其最主要的应用场景。
当你写递归函数的时候,记得使用尾递归优化的特性,如果递归函数的计算量足够大,则尾递归优化可以大幅提升程序的性能。