《深入理解ES6》读书笔记——3.函数

2021-02-08  本文已影响0人  弗利萨mom

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函数不同之处为:

(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.jpeg

ES5中存在的问题:在ECMAScript 5的引擎中,尾调用的实现与其他函数调用的实现类似:创建一个新的栈帧(stack frame),将其推入调用栈来表示函数调用。也就是说,在循环调用中,每一个未用完的栈帧都会被保存在内存中,当调用栈变得过大时会造成程序问题。

(1)ECMAScript 6中的尾调用优化

ECMAScript 6缩减了严格模式下尾调用栈的大小(非严格模式下不受影响),如果满足以下条件,尾调用不再创建新的栈帧,而是清除并重用当前栈帧:

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

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

· 尾调用的结果作为函数值返回。

以下这段示例代码满足上述的三个条件,可以被JavaScript引擎自动优化

可能最难避免的情况是闭包的使用,它可以访问作用域中所有变量,因而导致尾调用优化失效,举个例子:

image.jpeg

在此示例中,闭包func()可以访问局部变量num,即使调用func()后立即返回结果,也无法对这段代码进行优化。

(2)如何利用尾调用优化

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

当你写递归函数的时候,记得使用尾递归优化的特性,如果递归函数的计算量足够大,则尾递归优化可以大幅提升程序的性能。

上一篇下一篇

猜你喜欢

热点阅读