形参默认值
JS中的函数,无论在函数定义时声明了多少形参,在函数调用时都可以传入任意数量的参数。
通常定义函数时会为可选的参数定义默认值,这样可以更方便的针对参数数量添加处理逻辑。
ES6为函数形参定义默认值很简单,直接在形参后面添加默认值即可
function foo(url, timeout = 3000, callback = function(){}) {
// doSomething
}
触发默认值
除了不传参数可以触发默认值外,当参数值是undefined时也可以触发默认值,但是null没有这个效果。
function foo(url, timeout = 3000, callback = function(){}) {
console.log(timeout)
}
foo('/test') // 3000
foo('/test', undefined) // 3000
foo('/test', null) // null
foo('/test', 5000) // 5000
注意: 每次调用函数,默认参数值都会重新计算
let n = 1;
function foo(x = n + 1) {
console.log(x)
}
foo() // 2
n = 11
foo() // 12
length属性
形参指定默认值后,函数的length属性返回没有指定默认值的参数个数。
(function (a, b, c = 3) {}).length // 2
rest 参数也不会计入length属性
(function (a, b, ...args) {}).length // 2
如果默认值的参数不是尾参数,那么length属性不再计入后面的参数
(function (a, b = 3, c) {}).length // 1
arguments
ES6中,如果函数使用了参数默认值,arguments的对象行为同ES5严格模式下保持一致,即arguments对象中保存的是函数调用时传入的参数值。
// ES5 非严格模式
function foo(a) {
console.log(arguments[0]) // 1
a=2
console.log(arguments[0]) // 2
}
foo(1)
// ES5 严格模式
function foo2(a) {
'use strict'
console.log(arguments[0]) // 1
a=2
console.log(arguments[0]) // 1
}
foo2(1)
// ES6默认参
function foo3(a = 1) {
console.log(arguments[0]) // 1
a=2
console.log(arguments[0]) // 1
}
foo3(1)
// ES6默认参
function foo4(a = 1) {
console.log(arguments.length) // 0
a=2
console.log(arguments[0]) // undefined
}
foo4()
arguments.length等于传入参数的数量,所以foo4函数的arguments.length等于0,arguments[0]等于undefined
默认参数表达式
默认参数值可以是一个函数调用,参数值等于函数执行的返回值
function test() {
return 1
}
function foo(a, b=test()) {
console.log(a + b)
}
foo(1,3) // 4
foo(1) // 2
注意: 当第一次调用foo函数时,由于传入了两个值,所以不会触发test函数的执行。
临时死区
function foo(a = b, b) {
console.log(a + b)
}
foo(1,1) // 2
foo(undefined, 1) // b is not defined
在这个示例中,调用foo(undefined,1)函数,由于a初始化时b尚未初始化,所以会导致程序抛出错误,此时b尚处于临时死区中,所有引用临时死区中绑定的行为都会报错
不定参数
不定参数也称剩余参数或者rest参数,它的表示形式是在命名参数前加三个点...
,这个参数在函数内部是一个数组,可以通过数组名访问里面的参数。
function foo(a,b,...c) {
console.log(c)
}
foo(1,2,3,4,5) // [3, 4, 5]
function pick(object, ...keys) {
let result = Object.create(null);
for (let i = 0, len = keys.length; i < len; i++) {
result[keys[i]] = object[keys[i]];
}
return result;
}
let person = {
name: 'wmui',
age: 10,
sex: 'boy'
}
console.log(pick(person,'name','sex')) // {name: "wmui", sex: "boy"}
使用限制
- 每个函数最多声明一个不定参数,并且必须放到所有参数的末尾。
function foo(a,b,...c,d) {
console.log(c)
}
foo(1,2,3,4,5) // Uncaught SyntaxError: Rest parameter must be last formal parameter
- 不定参数不能在对象字面量的setter属性中使用
let o = {
set name(...val) {
// doSomething
}
}
// Uncaught SyntaxError: Setter function argument must not be a rest parameter
有这条限制也很好理解,因为本身对象字面量中setter的参数有且只能有一个,而不定参数的定义中,参数的数量可以无限多,所以在当前上下文中不允许使用不定参数
arguments
虽然有了不定参数,但是在ES6中arguments对象也是可以正常使用的,它并没有被不定参数取代。
function foo(a,b,...c) {
console.log(c.length) // 3
console.log(arguments.length) // 5
}
foo(1,2,3,4,5)
应用
由于不定参数是一个数组,所以数组特有的方法都可以应用于该变量
// arguments变量写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort()
}
// 不定参数写法
let sortNumbers = (...args) => args.sort()
展开运算符
展开运算符和不定参数很相似。展开运算符可以把指定的数组,打散成各自独立的参数,然后传入函数;而不定参数是把各自独立的参数,整合成一个数组,然后在函数内部被访问。
let arr = [1,2,3]
console.log(...arr) // 1 2 3
展开运算符通常用于需要传入多个独立参数的函数,比如用Math.max()方法获取一组数的最大值。
Math.max()方法不能直接获取数组中的最大值,所有参数要以独立参数的形式传入
Math.max(3,2,1) // 3
// 利用apply()改变this,获取数组中元素最大值
let arr = [1,2,3]
Math.max.apply(Math, arr) // 3
虽然可以借助apply()方法实现获取数组中元素最大值,但是第一眼很难看懂代码的真正意图,而利用展开运算符就要好很多。
let arr = [1,2,3]
Math.max(...arr) // 3
展开运算符还可以和正常传入的参数混合使用,比如设置Math.max()返回值最小为0
let arr = [-1,-2,-3]
Math.max(...arr, 0) // 0
展开运算符可以简化使用数组给函数传参的编码过程,在大多数需要使用apply()方法的情况下展开运算符可能是一个更合适的方案
严格模式
从ES5开始,函数内部可以设置为严格模式。ES7对严格模式做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错
之所以这样规定,是因为函数内部的严格模式同时适用于函数参数和函数体,但函数执行的顺序是先执行函数参数再执行函数体,这样就会导致可能你的函数参数是不符合严格模式的,但要到执行函数体时才能被检测到,这无疑是不合理的。
function doSomething(value = 070) {
'use strict';
return value;
}
严格模式下是不允许用前缀0
代表八进制的,如果ES7不修改严格模式,那么JS引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错
参数尾逗号
ES8允许函数的最后一个参数有尾逗号(trailing comma)。
function fn(
param1,
param2,
) { /* ... */ }
fn(
'foo',
'bar',
);
这样的规定使得函数参数与数组和对象的尾逗号规则保持一致
name属性
JS中有多种定义函数的方式,因而辨别函数就是一项具有挑战性的任务,ES6为所有函数新增了name属性,方便开发者们追踪函数调用记录
// 示例1 函数名字是声明时函数的名称
function foo(){}
console.log(foo.name) // foo
// 示例2 函数名字是匿名函数变量的名称
let foo2 = function(){}
console.log(foo2.name) // foo2
// 示例3 函数名字是函数表达式自身的名称
let foo3 = function test(){}
console.log(foo3.name) // test
// 示例4 函数名字带有bound前缀
let foo4 = function (){}
console.log(foo4.bind().name) // bound foo4
// 示例5 函数名字带有anonymous前缀
let foo5 = new Function()
console.log(foo5.name) // anonymous
// 示例6
let obj = {
get getName() {
return 'wmui'
},
set setName(v) {
this.name = v
},
sayName() {
return this.name
}
}
let descriptor = Object.getOwnPropertyDescriptor(obj, 'getName');
let descriptor2 = Object.getOwnPropertyDescriptor(obj, 'setName');
// getter函数带有get前缀,setter函数带有set前缀
console.log(obj.sayName.name) // sayName
console.log(descriptor.get.name) // get getName
console.log(descriptor2.set.name) // set getName
判断调用
JS函数内部有两个内部方法:[[call]]和[[construct]]
当通过new关键字调用函数时,执行的是[[construct]]函数,它会创建一个新的实例对象,函数体执行时会把this绑定到实例上。
如果不使用new关键字调用函数,则执行[[call]]函数,直接执行代码中的函数体
注意: 不是所有函数都有[[construct]]方法,所以不是所有函数都可以通过new来调用。具有[[construct]]方法的函数被统称为构造函数
ES5判断函数调用
在ES5中判断一个函数是否通过new关键字调用,最常用的方法是使用instanceof操作符
function Person(name) {
if(this instanceof Person) {
this.name = name
} else {
throw new Error('You must use new with Person')
}
}
let p1 = new Person('wmui')
let p2 = Person('wmui') // 报错
这种做法是正确的,但是并不完全靠得住,因为当使用call()或apply()方法强制把this绑定到Person实例上时,它是检测不会出来的。
function Person(name) {
if(this instanceof Person) {
this.name = name
} else {
throw new Error('You must use new with Person')
}
}
let instance = new Person()
let p3 = Person.call(instance, 'wmui') // 不报错
ES6判断函数调用
ES6引入了new.target
这个元属性解决判断函数是否通过new关键字调用的问题。元属性就是非对象的属性。当使用new关键字调用函数时,执行的是[[construct]]函数,new.target被赋值为新创建的实例对象;如果不通过new关键字调用,new.target的值为undefined。
function Person(name) {
if(typeof new.target !== "undefined") {
this.name = name
} else {
throw new Error('You must use new with Person')
}
}
let p1 = new Person('wmui')
let p2 = Person('wmui') // 报错
let instance = new Person()
let p3 = Person.call(instance, 'wmui') // 报错
块级函数
在代码块中声明的函数就是块级函数。在ES6之前定义块级函数严格来说是一个语法错误,虽然浏览器也支持,但是表现行为不完全一致。而ES6会把函数视为一个块级声明,从而可以在代码块中声明和访问该函数。
if(true) {
function foo() {
console.log('hello')
}
foo()
}
foo()
// 'hello'
// 'hello'
非严格模式下,代码块内定义的函数,在代码块外仍然可以访问到,这是因为函数声明被提升到了外围函数或全局作用域的顶部
严格模式下,代码块内定义的函数,在代码块外访问不到,这是因为if语句代码块结束执行后,语句内的函数也不存在了。
'use strict';
if(true) {
function foo() {
console.log('hello')
}
foo()
}
foo()
// 'hello'
// Uncaught ReferenceError: foo is not defined
箭头函数
箭头函数(=>)是一种使用箭头定义函数的新语法,它与传统的JS函数有一些不同:
-
没有this、super、arguments、new.target
箭头函数中的这些值由外围最近一层的非箭头函数决定 -
不能通过new关键字调用
因为箭头函数没有[[construct]]方法 -
没有原型
由于不能通过new关键字调用箭头函数,因而没有构建原型的需求,所以没有prototype属性 -
不能改变this绑定
箭头函数内部的this值不可以被改变,在函数声明周期内始终保持一致 -
不支持arguments对象
箭头函数没有arguments对象,必须通过命名参数和不定参数这两种形式来访问其参数 -
不支持重复的命名参数
无论在严格还是非严格模式下,箭头函数都不支持重复的命名参数;而传统函数只有在严格模式下,才不能有重复的命名参数
语法
箭头函数有多种不同的表现形式,但都有参数、箭头和函数体组成。
let foo = num => num + 1;
// 有效等价于
let foo = function(num) {
return num + 1;
}
如果只有一个参数,可以直接写参数名,然后是箭头,箭头右侧的表达式被求值后会立即返回。
如果有两个或两个以上参数,要在参数两侧加上一对小括号。
let foo = (num1, num2) => num1 + num2;
// 有效等价于
let foo = function(num1, num2) {
return num1 + num2;
}
如果函数没有参数,要在声明的时候写一组没有内容的小括号
let foo = () => 1;
// 有效等价于
let foo = function() {
return 1;
}
如果希望为函数编写由多个表达式组成的更传统的函数体,那么需要用花括号包裹函数体,并显式地定义一个返回值
let foo = (num1, num2) => {
return num1 + num2;
}
// 有效等价于
let foo = function(num1, num2) {
return num1 + num2;
}
this
箭头函数中没有this绑定,必须通过查找作用城链来决定其值。
如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this;否则,this的值会被设置为undefined
如果对象的方法中包含了另外一个函数,并且这个函数引用了this,为了不让this指向window对象,我们通常会使用bind()显示的为函数绑定this值
let obj = {
name: 'wmui',
init: function() {
document.addEventListener('click', (function(e){
// 方法内部的函数引用了this,使用bind()改变this指向
this.test(e.type)
}).bind(this), false)
},
test: function(type) {
console.log(this.name,type)
}
}
obj.init() // wmui click
如果用箭头函数重写上面的示例,不仅使代码更精简,而且更加容易理解
let obj = {
name: 'wmui',
init: function() {
document.addEventListener('click', (e) => {
this.test(e.type)
}, false)
},
test: function(type) {
console.log(this.name,type)
}
}
obj.init() // wmui click
辨识方法
尽管箭头函数与传统函数的语法不同,但它同样可以被识别出来
let foo = (num1, num2) => num1 - num2;
console.log(typeof foo); // "function"
console.log(foo instanceof Function); // true
箭头函数上可以调用call()、apply()及bind()方法,但箭头函数的this值不会受这些方法的影响
let foo = (num1, num2) => num1 + num2;
console.log(foo.call(null, 1, 2)); // 3
console.log(foo.apply(null, [1, 2])); // 3
let boundFoo = foo.bind(null, 1, 2);
console.log(boundFoo()); // 3
函数柯里化
柯里化是一种可以把多参函数转变成单参函数,并且调用后返回一个新函数的技术,这个新函数可以接收剩余参数而且有返回结果
使用ES5的语法写一个柯里化函数
function foo(x) {
return function (y) {
return y + x
}
}
foo(1)(2) // 3
使用ES6的语法写一个柯里化函数
let foo (x) => (y) => y + x;
foo(1)(2) // 3
一般来说,出现连续地箭头函数调用的情况,就是在使用函数柯里化的技术
尾调用优化
尾调用是指函数作为另一个函数的最后一条语句被调用。
关于函数的调用这里简单说一下,函数调用会在内存中形成一个调用记录,称作“调用帧”(call frame),用来保存调用位置和内部变量等信息。如果在函数A的内部调用了函数B,那么在A的调用帧上方就会形成一个B的调用帧,等到函数B运行结束并且将结果返回到A,B的调用帧才会消失。同理,如果函数B的内部调用了函数C,那么在B的调用帧上方会有一个C的调用帧,以此类推,所有的调用帧就会形成一个调用栈(call stack)
尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,于是JS引擎就可以在背后对尾调用进行优化。如果所有函数都是尾调用,将会大大节省内存开销。
ES6缩减了严格模式下尾调用栈的大小,如果满足以下三个条件,尾调用不再创建新的栈帧,并且可以被JS引擎自动优化:
- 尾调用不是闭包
- 尾调用是函数内部的最后一条语句
- 尾调用的结果作为返回值被返回
'use strict';
function foo() {
// 被优化
return foo2()
}
下面这几种情况不会被优化:
// 示例1 缺少return语句
'use strict';
function foo() {
foo2()
}
// 示例2 尾调用返回后执行其他操作
'use strict';
function foo() {
return foo2() + 1
}
// 示例3 不是尾调用
'use strict';
function foo() {
let t = foo2()
return t
}
// 示例4 尾调用是闭包
'use strict';
function foo() {
let num = 1
let t = () => num
return t()
}
应用
尾调用优化常被用于递归函数
应用一:计算阶乘
function factorial(n) {
if (n <= 1) {
return 1;
} else {
// 未被优化
return n * factorial(n - 1);
}
}
function factorial(n, p = 1) {
if (n <= 1) {
return 1 * p;
} else {
let result = n * p;
// 被优化
return factorial(n - 1, result);
}
}
应用二:计算Fibonacci数列
function Fibonacci (n) {
if ( n <= 1 ) {return 1};
return Fibonacci(n - 1) + Fibonacci(n - 2);
}
Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
if( n <= 1 ) {return ac2};
return Fibonacci2 (n - 1, ac2, ac1 + ac2);
}
Fibonacci2(100) // 573147844013817200000
ES6 明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存