默认参数
如果在调用函数时没有传入参数时,我们希望使用默认值,ES5 的写法如下
function getValue(url, timeout, callback) { |
但是上面的代码有问题,如果 timeout 传入的是 0,那么 timeout 还是会使用默认值,因为 0 对应的 boolean 为 false,所以代码会修改如下
function getValue(url, timeout, callback) { |
现在我们可以在 ES6 通过简单的写法实现上面的效果
function getValue(url, timeout = 2000, callback = function(data) {console.log(data)}) { |
上面的 timeout 和 callback 参数我们称之为默认参数,当调用函数没有传入参数,或者传入的参数为 undefiend 时,就是使用指定的默认参数。
arguments 对象
每一个函数中都会有一个 arguments 对象,其中保存的是传入的参数值。arguments 对象在严格和非严格模式下的行为是不同的。
在非严格模式下,arguments 会受到命名参数的影响,即在函数内对命名参数进行修改,也会相应的对 arguments 对象产生修改
function getName(first, second) { |
在严格模式下,对命名参数的修改不会影响 arguments 对象
function getName(first, second = "hello") { |
对于默认参数,无论是在严格模式还是在非严格模式下,对命名参数的修改都不会影响 arguments 对象
function getName(first, second = "hello") { |
因为只传入了一个参数值,所以 arguments[0] === first
,而 arguments[1] === undefiend
,所以 second 的值与 arguments[1] 不相等。
表达式
默认参数的取值除了原始值以外,还可以是一个表达式,例如
function getValue() { |
甚至你可以将前面的参数作为后面参数的默认值
function add(first, second = first) { |
TDZ(暂时性死区)
所谓的临时死区是指在变量声明之前,不能够访问该变量
function add(first = second, second) { |
上面我们设置 first 的默认值为 second,当我们为 first 传入 udefiend 时会使用默认值,但是此时 second 并未初始化,所以这时访问 second 会发生错误。
匿名参数
当我们调用 JavaScript 的函数时,传入的参数个数可以与定义函数时要求的函数个数不同,可多可少。所以传入的参数都会保存在 arguments 对象中,无论是在函数签名中已经定义了的参数(命名参数)还是未定义的参数(匿名参数)
function print(arg) { |
在 ES6 中引入了剩余参数(Rest Parameters),剩余参数中只保留匿名参数的值
function print(arg, ...values) { |
在上面对函数进行了一点修改,在参数列表中添加了一项 ...values
,values 表示的就是剩余参数,如果调用函数时多传入了参数,那么多余的参数就会放入 values 这个数组中。
剩余参数与 arguments 参数的不同:
- 剩余参数是一个真正的数组,而 arguments 是一个类数组
- 剩余参数中只保存了匿名参数的值,而 arguments则保存了所有传入的值
使用剩余参数有两点限制:
一个函数中只能有一个剩余参数,且剩余参数必须放置在参数列表的最后
剩余参数不能用于对象的 setter 方法中
let obj = {
// SyntaxError: Setter function argument must not be a rest parameter
set name(...values) {
// do something
}
}这是因为 setter 方法被限制为只能有一个参数,但是剩余参数理论上表示的是可以接受无穷多的参数。
无论在函数的参数列表中是否定义了剩余参数,都对 arguments 对象没有影响,它始终保存着传入函数的所有参数。
Function 增强
Function 构造函数一般是用来生成函数实例的,但是我们很少用到它来声明一个函数
let add = new Function("first", "second", "return first + second"); |
在 ES6 中对 Function 构造函数进行了增强,使它能够使用默认参数和剩余参数
let add= new Function("first", "second = first", "return first + second"); |
let print = new Function("...values", "console.log(values[0])"); |
展开运算符
与剩余参数密切相关就是展开运算符,剩余参数是将多个独立的参数合并为一个数组,而展开运算符则相反,向一个数组展开为一个个独立的参数。考虑 Math.max 方法,它接收一个个独立的参数
Math.max(3, 5); |
当参数的个数比较少时可以还算容易使用,当需要比较的参数较多时,一般我们会将参数放置在一个数组中然后传入,但是Math.max 不接收数组,它只接收一个个独立的参数
Math.max([1, 3, 5, 4]); // × |
这个时候我们通过会借助于 apply 方法,因为 apply 方法是将方法的参数合并为数组传入的
Math,max.apply(Math, [1, 3, 5, 4]); |
在 ES6 中提供了展开运算符,可以直接将数组展开为一个个的独立参数进行传入
Math.max(...[1, 3, 5, 4]) |
name 属性
- 由于有多种方式创建函数,所以识别一个函数很难
- 匿名函数的盛行,使得调试十分困难,无法跟踪堆栈信息
出于如上原因,ES6 为所有的函数都添加了一个 name 属性。
function doSomething() { |
函数 doSomething 的 name 属性为 doSomething,因为它的函数签名为 doSomething;匿名函数 doOtherThing 的 name 属性为 doOtherThing,因为它被赋予的变量名为 doOtherThing。anotherThing 的 name 属性还是为 doOtherThing,说明匿名函数的 name 属性并不会更改。
下面看几个特例:
let doSomething= function doSomethingElse() { |
let doSomething = function() { |
console.log((new Function()).name); // anonymous |
函数的多重角色
函数可以通过 new 调用,也可以不通过 new 调用。JavaScript 的函数含有两个内部可见的方法,[[Call]] 和 [[Construct]]。
当一个函数通过 new 调用时,[[Construct]] 方法就会被调用,该方法是用来创建一个新对象的,称之为实例,通过函数体内部的 this 来设置实例。拥有 [[Construct]] 方法的函数叫做构造器,并不是所有的函数都有[[Construct]] 方法,例如后面提及的箭头函数就没有。
当函数不是通过 new 调用时,就是调用函数内部的 [[Call]] 方法,它会执行函数的方法体。
那么如何分辨函数以何种方式调用呢? 在 ES5 中,常常通过 instance 来判别
function Person(name) { |
上面的方法能够有效是因为 [[Construct]] 方法会创建一个 Person 的实例,并将它赋值给 this。但是这个方法并不可靠,因为不使用 new 也可以得到 Person 的实例
function Person(name) { |
在 ES6 中提出了更好的解决办法,那就是 new.target,如果一个函数的 [[Construct]] 方法被调用,new.target 的值就会被赋值为新创建的对象,如果 [[Call]] 被调用,那么 new.target 的值就是 undefiend。所以通过 new.target 可以方便的知道是通过何种方式调用的函数
function Person(name) { |
在函数的外部不能使用 new.target, 否则会报语法错误。
Block-Level Functions
在 ES3 或者更早以前,是不能在块级作用域中定义函数的,但是所有的浏览器都支持这一行为,于是在 ES5 中,为了修复这不兼容的行为,规定在严格模式下载块级作用域中声明函数将会导致错误
|
但是上面的行为在 ES6 中却是可以的,在 ES6 的严格模式下,函数的作用域只在块级作用域中,在块级作用域以外不能访问该函数
|
因为对于函数的声明会被提升到作用域的最前方,所以第二个 typeof doSomething 的结果是function。
上面的行为都是在严格模式下发生的,那么在非严格模式下,函数的声明没有块级作用域的概念,所以函数的声明是在全局作用域中,即在块级作用域之外也可以访问到函数。
箭头函数
语法
箭头函数是 ES6 中的新语法,正如名字所暗示的一样,是使用箭头来声明函数的,例如
let add = (x, y) => { |
上面的函数等价于(当然不是完全等价,具体的区别后文详述)
let add= function(x, y) { |
箭头函数由三部分组成
- 参数列表:使用括号将参数括起,参数之间使用逗号分隔
(x, y)
(x, y = x)
- 箭头:⇒
- 函数体:使用花括号括起
如果参数列表只有一个参数的话,那么可以参数列表不写括号
(x) => { |
如果函数体中只有一条语句的话,那么也可以省略花括号
x => return x; |
进一步,如果这一条语句直接返回一个值的话,return 也可以省略,它会计算这条语句,然后将值返回
x => x; |
如果是直接返回一个字面量对象的话,需要使用括号括起
() => ({x: 1, y: 2}); |
无 this 绑定
在 JavaScript 中,每一个函数都会默认的传入一个 this 参数,它的取值取决于调用方式,而不是所处的位置
function Person(name) { |
不同的调用方式,函数内部 this 的取值也不同,所以很容易就写出错误的代码。但是箭头函数内部没有 this 绑定,这意味着它会在它的作用域链中去寻找 this
function Person(name) { |
上面修改 getName 为箭头函数,因为箭头函数的内部没有 this 绑定,所以它会向它的作用域链中寻找 this,即它使用的是它父级作用域的 this,这意味着箭头函数内部的 this 与调用方式无关,与它所在的位置有关,这样可以获得预期的结果,减少出 bug 的几率。
无 arguments 绑定
同 this 一样,每个函数内部都会自动传入一个 arguments 对象,但是箭头函数内部并没有 arguments 对象,所以想取得箭头函数传入的参数,就得使用剩余参数
(function() { |
箭头函数与一般函数的不同:
- 函数内部没有 this,arguments,new.target 绑定
- 不能通过 new 的方式被调用,上文提及过,箭头函数内部没有 [[Construct]] 方法
- 没有 prototype
尽管箭头函数与一般函数有所不同,但是通过 typeof 以及 instanceof 方法鉴别的行为与一般函数相同
let add = (x, y) => x + y; |
箭头函数也可以调用call,apply,bind 方法,但是箭头函数内部的 this 取值并不会受到影响。
尾调用优化
什么叫尾调用? 尾调用指的是函数的最后一条语句是调用一个函数,例如
function doSomething() { |
我们知道每次调用函数时,都会创建一个栈帧放置到函数栈中,当函数调用较多时,特别是递归调用,可能会使得栈中的栈帧越来越多,当超过栈帧允许的最大数量时,就会报错
function doSomething(){ |
那么所谓的尾调用优化指的是,当函数的最后一条语句是返回另一个函数的调用时,那么调用的函数会复用当前栈帧,而不会创建一个新的栈帧,这样栈中的栈帧的数量就不会增加,也就不会出现超过栈帧允许的最大数量。
要实现尾调用优化,被调用函数要满足以下特点:
- 尾调用没有访问当前栈帧中的变量
- 尾调用是最后一条语句
- 尾调用的返回值作为函数的返回值
function doSomething(){ |
function doSomething() { |
function doSomething() { |
下面我们看一个求阶乘的例子
function factorial(n) { |
上面的函数无法进行尾优化,因为最后一条语句不是尾调用。现在我们换一种思路,将上面的函数转化为可以进行尾调用优化的函数
function factorial(n, result = 1) { |