3-1.参数默认值
1.默认参数值
参数默认值指的是当参数是 undefined
时,参数默认采取的一个值。注意, null
是一个实际的值。
在 Es5
中可利用下例方式实现:
function getSomething(a, b) {
a = typeof a === 'undefined' ? 'hello' : a
b = typeof b === 'undefined' ? 'world' : b
return a + ' ' + b
}
console.log(getSomething())
在 Es6
中有了更为简洁明了的方式:
function getSomething(a = 'hello', b = 'world') {
return a + ' ' + b
}
console.log(getSomething())
2.默认参数表达式
参数默认值也可以使用表达式:
function getValue(value) {
return value
}
function getExpression(a, b = getValue('world')) {
return a + ' ' + b
}
console.log(getExpression('hello'))
3.默认参数的临时性死区
当把参数默认值设置为形参时,要注意下面的情况:
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 来获取。
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
,虽然不太麻烦,但还是需要捋一遍逻辑之后,才会知道为什么设置为1
。语义表达不够直观。
2.Es6中的不定参数
Es6
中的不定参数,在某种程度上代替 arguments
的使用。而且表达更加简洁明了。使得代码的可读性大大提高。
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'))
使用不定参数有一些限制条件:
- 每个函数最多只能声明一个不定参数。
- 不定参数一定要放在形参的末尾。
- 不定参数不能用于对象字面量setter中。
3-3.扩展运算符
在上一节中,我们使用了 ...keys
这种形式来表示不定参数。其中的 ...
就是扩展运算符。
它的作用可以看做将数组或对象转化为可枚举形式。
var list = [1, 2, 3]
console.log([...list])
var map = {
id: 19,
name: 'yxp'
}
console.log({
...map
})
3-3-1.使用便捷
扩展运算符极大的简便了代码书写方式 在某些场景下可以发挥极大的威力 譬如:
var numList = [27,12,71,54,29]
console.log(Math.max.apply(Math, numList))
console.log(Math.max(...numList))
3-3-2.浅拷贝
要注意的是,利用扩展符可以实现浅拷贝:
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
属性主要针对的是后者。
// 函数声明 name指向函数名
function doSomething() {
console.log(doSomething.name)
}
doSomething()
// 匿名函数表达式 name指向变量名
var doOtherthing = function() {
console.log(doOtherthing.name)
}
doOtherthing()
另外,函数名的优先级高于变量名。
var doSomething = function doSomethingElse() {}
console.log(doSomething.name) // doSomethingElse
此外,函数的调用可能还有其他方式:
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.函数的两种调用方式
;(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中判断函数如何被调用
// 在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
会导致语法错误。
;(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.特点
- 没有
this
、super
、arguments
以及new.target
。这些值由外围最近一层非箭头函数决定。 - 不能通过
new
关键字调用。因为箭头函数不是构造函数(即constructor
)。 - 没有原型。 即箭头函数的
prototype
属性是undefined
。 - 不可以改变
this
的绑定。 - 不支持
arguments
对象。
arguments is not defined
- 不支持
new.target
。
new.target expression is not allowed here
- 不支持重复的命名入参。譬如:
const fn = (name, name) => {}
// error. Duplicate parameter name not allowed in this context
3-6-2.IIFE
IIFE
意为 Immediately Invoked Function Expression
,翻译成中文的话,就是立即执行函数表达式。
普通函数创建 IIFE
的形式有多种,这里记录下较普遍的两种:
;(function (name) {
console.log(name)
})('yxp')
;(function (name) {
console.log(name)
}('yxp'))
与普通函数创建 IIFE
有所不同的是,箭头函数创建 IIFE
时,小括号只能包裹箭头函数的函数体。
const getName = ((name) => name)('yxp') // work
const getValue = ((value) => { return value }('123')) // error
3-6-3.call、apply与bind
其实箭头函数依然能调用 call
apply
bind
这些方法。但实际执行时 this
指向不受影响。
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.尾调用优化
尾调用优化通常可用于递归函数的优化处理。譬如有递归函数如下:
function fn(n) {
if (n > 0) {
fn(n - 1)
}
}
fn(5)
TIP
首先要知道,浏览器在执行函数时会创建栈。如果是递归函数 则会不断的创建栈。
而尾调用优化 则是让函数在执行过程中,不再创建新的栈,而是清除并重用当前栈。
这样就减小了栈溢出的可能性。对性能和内存方面都有益处。
尾调用优化是浏览器本身处理代码的一种优化手段,它的触发需要满足一下条件:
- 尾调用不访问当前栈的变量,也就是说函数不是一个闭包。
- 在函数内部,尾调用是最后一条语句。
- 尾调用的结果作为函数值返回。
改写下上述的递归函数:
function fn(n) {
if (n > 0) {
return fn(n - 1)
// 如果语句是下面这样 则违反了上述第二条细则。因为在尾调用后 进行了加法操作。
// return 1 + fn(n - 1)
}
}
fn(5)