Skip to content

3-1.参数默认值

1.默认参数值

参数默认值指的是当参数是 undefined 时,参数默认采取的一个值。注意, null 是一个实际的值。

Es5 中可利用下例方式实现:

js
function getSomething(a, b) {
  a = typeof a === 'undefined' ? 'hello' : a
  b = typeof b === 'undefined' ? 'world' : b
  return a + ' ' + b
}
console.log(getSomething())

Es6 中有了更为简洁明了的方式:

js
function getSomething(a = 'hello', b = 'world') {
  return a + ' ' + b
}
console.log(getSomething())

2.默认参数表达式

参数默认值也可以使用表达式:

js
function getValue(value) {
  return value
}
function getExpression(a, b = getValue('world')) {
  return a + ' ' + b
}
console.log(getExpression('hello'))

3.默认参数的临时性死区

当把参数默认值设置为形参时,要注意下面的情况:

js
function getFirstTDZ(a, b = a) {
  console.log(a + ' ' + b)
}
function getSecondTDZ(a = b, b) {
  console.log(a + ' ' + b)
}
getFirstTDZ('HA') // works
getSecondTDZ('HA') // works

getFirstTDZ(undefined, 'HA') // works
getSecondTDZ(undefined, 'HA') // Uncaught ReferenceError: Cannot access 'b' before initialization

3-2.不定参数

1.ES5中的arguments

因为函数定义的形参是有限的,而实参的数量却是不受限的。实际调用函数时,传入的参数数量无法控制。

如果我们要获取实参的话,在Es6之前的版本,可以通过 arguments 来获取。

js
function pick(object) {
  var o = Object.create(null)
  for (var i = 1; i < arguments.length; i++) {
    o[arguments[i]] = object[arguments[i]]
  }
  return o
}
var obj = {
  name: 'yxp',
  age: 19,
  gender: 'male'
}
console.log(pick(obj, 'name', 'age'))

上例不太优雅的地方:

  1. 函数声明不能直观的表达出使用方式。譬如这个函数明显是要传入一个对象和一些不定参数。但是函数声明的形参中只有一个对象参数。
  2. 遍历索引需要设置为 1,虽然不太麻烦,但还是需要捋一遍逻辑之后,才会知道为什么设置为 1。语义表达不够直观。

2.Es6中的不定参数

Es6 中的不定参数,在某种程度上代替 arguments 的使用。而且表达更加简洁明了。使得代码的可读性大大提高。

js
function pick(object, ...keys) {
  var o = Object.create(null)
  for(var i = 0; i < keys.length; i++) {
    o[keys[i]] = object[keys[i]]
  }
  return o
}
var obj = {
  name: 'yxp',
  age: 19,
  gender: 'male'
}
console.log(pick(obj, 'name', 'age'))

使用不定参数有一些限制条件:

  1. 每个函数最多只能声明一个不定参数。
  2. 不定参数一定要放在形参的末尾。
  3. 不定参数不能用于对象字面量setter中。

3-3.扩展运算符

在上一节中,我们使用了 ...keys 这种形式来表示不定参数。其中的 ... 就是扩展运算符。

它的作用可以看做将数组或对象转化为可枚举形式

js
var list = [1, 2, 3]
console.log([...list])

var map = {
  id: 19,
  name: 'yxp'
}
console.log({
  ...map
})

3-3-1.使用便捷

扩展运算符极大的简便了代码书写方式 在某些场景下可以发挥极大的威力 譬如:

js
var numList = [27,12,71,54,29]
console.log(Math.max.apply(Math, numList))
console.log(Math.max(...numList))

3-3-2.浅拷贝

要注意的是,利用扩展符可以实现浅拷贝:

js
var targetMap = {
  mapName: 'target map',
  obj: {
    id: 19,
    name: 'yxp'
  }
}

var copyMap = {
  ...targetMap
}

// 因为mapName是值 所以浅拷贝后并不影响原map的属性
copyMap.mapName = 'copy map'
// 而obj是对象 它的内存地址与原map的对应属性指向同一块区域 所以会受影响
copyMap.obj.id = 20

console.log(targetMap)
console.log(copyMap)

3-4.name属性

首先要确定的一点是,函数有两种方式定义:函数声明匿名函数表达式

函数的 name 属性主要针对的是后者。

js
// 函数声明 name指向函数名
function doSomething() {
  console.log(doSomething.name)
}
doSomething()
// 匿名函数表达式 name指向变量名
var doOtherthing = function() {
  console.log(doOtherthing.name)
}
doOtherthing()

另外,函数名的优先级高于变量名

js
var doSomething = function doSomethingElse() {}
console.log(doSomething.name) // doSomethingElse

此外,函数的调用可能还有其他方式:

js
var person = {
  get firstName() {
    return 'yang'
  },
  sayName: function() {
    console.log(this.name)
  }
}

// 1.getter 与 setter 函数都必须用 Object.getOwnPropertyDescriptor() 来检 索
console.log(person.firstName.name) // undefined
var descriptor = Object.getOwnPropertyDescriptor(person, 'firstName')
console.log(descriptor.get.name) // get firstName
// 2.对象属性
console.log(person.sayName.name) // sayName
// 3.bind函数
console.log(person.sayName.bind().name) // bound
// 4.Function构造函数的实例
console.log(new Function().name) // anonymous

3-5.new.target

3-5-1.函数的两种调用方式

js
;(function() {
  function Person(name) {
    this.name = name
  }
  // 1.构造函数
  new Person('yxp')
  
  // 2.普通函数 
  Person('yxp')
}())

JS 为函数提供了两个不同的内部方法: [[Call]][[Construct]]

当未使用 new 进行 函数调用时, [[Call]] 方法会被执行,运行的是代码中的函数体。

而当使用 new 进行函数 调用时, [[Construct]] 方法则会被执行,负责创建一个被称为新目标的新对象,并且将该新目标作为 this 去执行函数体。

拥有 [[Construct]] 方法的函数被称为构造器。

TIP

切记并非所有函数都拥有 [[Construct]] 方法,因此不是所有函数都可以用 new 去调用。譬如Arrow Function

3-5-2.Es5中判断函数如何被调用

js
// 在Es5中使用 instanceof
;(function() {
  function Person(name) {
    if (this instanceof Person) {
      this.name = name
      console.log(this.name)
    } else {
      console.error('You must use new with Person.')
    }
  }
  // ①
  var p = new Person('yxp') // yxp
  // ②
  Person('yxp') // error

  // 但是这种方式并不能完全正确的判断该函数是否由new运算符调用
  Person.call(p, 'ljm') // ljm
}())

3-5-3.Es6中判断函数如何被调用

es6 中提供了 new.target 属性来代替 this instanceof constructor 这种方式。

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

js
;(function() {
  function Person(name) {
    if (new.target !== undefined) {
      this.name = name
      console.log(this.name)
    } else {
      console.error('You must use new with Person', new.target)
    }
  }
  function AnotherPerson(name) {
    Person.call(this, name)
  }
  var p = new Person('yxp') // work
  Person.call(p, 'Tom') // error
  new AnotherPerson('Jerry') // error 因为new.target是undefined
}())

TIP

new.target 就是用来判断函数是否由 new 来调用的。

任何绑定 this 的操作都不能影响到 new.target 的值。

3-6.箭头函数

3-6-1.特点

  1. 没有 thissuperarguments 以及 new.target。这些值由外围最近一层非箭头函数决定
  2. 不能通过 new 关键字调用。因为箭头函数不是构造函数(即 constructor)。
  3. 没有原型。 即箭头函数的 prototype 属性是 undefined
  4. 不可以改变 this 的绑定
  5. 不支持 arguments 对象
shell
arguments is not defined
  1. 不支持 new.target
shell
new.target expression is not allowed here
  1. 不支持重复的命名入参。譬如:
js
const fn = (name, name) => {}
// error. Duplicate parameter name not allowed in this context

3-6-2.IIFE

IIFE 意为 Immediately Invoked Function Expression,翻译成中文的话,就是立即执行函数表达式

普通函数创建 IIFE 的形式有多种,这里记录下较普遍的两种:

js
;(function (name) {
  console.log(name)
})('yxp')

;(function (name) {
  console.log(name)
}('yxp'))

与普通函数创建 IIFE 有所不同的是,箭头函数创建 IIFE 时,小括号只能包裹箭头函数的函数体。

js
const getName = ((name) => name)('yxp') // work

const getValue = ((value) => { return value }('123')) // error

3-6-3.call、apply与bind

其实箭头函数依然能调用 call apply bind 这些方法。但实际执行时 this 指向不受影响。

js
const obj = {
  result: 'result'
}

const getResult = (value) => {
  console.log(this.result, value)
}
// call
getResult.call(obj, 'value')
// apply
getResult.apply(obj, ['value'])
// bind 结合 IIFE
;(getResult.bind(obj))('value')

3-7.尾调用优化

尾调用优化通常可用于递归函数的优化处理。譬如有递归函数如下:

js
function fn(n) {
  if (n > 0) {
    fn(n - 1)
  }
}
fn(5)

TIP

首先要知道,浏览器在执行函数时会创建栈。如果是递归函数 则会不断的创建栈。

而尾调用优化 则是让函数在执行过程中,不再创建新的栈,而是清除并重用当前栈。

这样就减小了栈溢出的可能性。对性能和内存方面都有益处。

尾调用优化是浏览器本身处理代码的一种优化手段,它的触发需要满足一下条件:

  1. 尾调用不访问当前栈的变量,也就是说函数不是一个闭包。
  2. 在函数内部,尾调用是最后一条语句。
  3. 尾调用的结果作为函数值返回。

改写下上述的递归函数:

js
function fn(n) {
  if (n > 0) {
    return fn(n - 1)
    // 如果语句是下面这样 则违反了上述第二条细则。因为在尾调用后 进行了加法操作。
    // return 1 + fn(n - 1)
  }
}
fn(5)