主要知识点有:函数参数默认值、剩余参数、扩展运算符、new.target属性、块级函数、箭头函数以及尾调用优化
函数的默认参数
在ES5中,我们给函数传参数,然后在函数体内设置默认值,如下面这种方式。
function a(num, callback) {
num = num || 6
callback = callback || function (data) {console.log('ES5: ', data)}
callback(num * num)
}
a() //ES5: 36,不传参输出默认值
//你还可以这样使用callback
a(10, function(data) {
console.log(data * 10) // 1000, 传参输出新数值
})
弊端:此处的 num 的有效值实际上有可能是 0 ,但因为 0 是假值,就会导致 num 的值在这种情况下会被替换为 6;
可以用 typeof 来检测参数的类型:
function a(num, callback) {
num = (typeof num!== "undefined") ? num: 6;
callback = (typeof callback !== "undefined") ? callback : function (data) {console.log('ES5: ', data)};
callback(num * num)
}
虽然这种方法更安全,但依然为实现一个基本需求而书写了过多的代码。它代表了一种公共
模式,而流行的 JS 库中都充斥着类似的模式。
ES6 中的参数默认值
function a(num = 6, callback = function (data) {console.log('ES6: ', data)}) {
callback(num * num)
}
a() //ES6: 36, 不传参输出默认值
a(10, function(data) {
console.log(data * 10) // 1000,传参输出新数值
})
使用ES6的默认值写法可以让函数体内部的代码更加简洁优雅
参数默认值如何影响 arguments 对象
- ES5 的非严格模式下
function mixArgs(first, second) {
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
//输出
true
true
true
true
- ES5 的严格模式下
function mixArgs(first, second) {
"use strict";
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a", "b");
//输出
true
true
false
false
- ES6
arguments 对象的表现总是会与 ES5 的严格模式一致,无论此时函数是否明确运行在严格模式下。
// 非严格模式
function mixArgs(first, second = "b") {
console.log(arguments.length);
console.log(first === arguments[0]);
console.log(second === arguments[1]);
first = "c";
second = "d"
console.log(first === arguments[0]);
console.log(second === arguments[1]);
}
mixArgs("a");
//输出
1
true
false
false
false
此时arguments.length =1 ,因为只给 mixArgs() 传递了一个参数。这也意味着arguments[1] 的值是 undefined ,符合将单个参数传递给函数时的预期;这同时意味着first 与 arguments[0] 是相等的。改变 first 和 second 的值不会对 arguments 对象造成影响,无论是否在严格模式下,所以你可以始终依据 arguments 对象来反映初始调用状态。
默认参数表达式
参数不仅可以设置默认值为字符串,数字,数组或者对象,还可以是一个函数。
function add() {
return 10
}
function a(num = add()){
console.log(num)
}
a() // 10
默认参数的临时死区
第一章我们提到了let和const什么变量的临时死区(TDZ),默认参数既然是参数,那么也同样有临时死区,函数的作用域是独立的,a函数不能共享b函数的作用域参数。
//这是个默认参数临时死区的例子,当初始化a时,b还没有声明,所以第一个参数对b来说就是临时死区。
function add(a = b, b){
console.log(a + b)
}
add(undefined, 2) // b is not define
处理无命名参数
上面说的参数都是命名参数,而无命名参数也是函数传参时经常用到的。当传入的参数是一个对象,不是一个具体的参数名,则是无命名参数。
function add(object){
console.log(object.a + object.b)
}
let obj = {
a: 1,
b: 2
}
add(obj) // 3
不定参数
使用...(展开运算符)的参数就是不定参数,它表示一个数组。
function add(...arr){
console.log(a + b)
}
let a = 1,b = 2
add(a, b) // 3
不定参数的使用限制:
- 函数只能有一个剩余参数,并且它必须被放在最后
//错误的写法1
function add(...arr, c){
console.log(a + b)
}
let a = 1,b = 2,c = 3
add(a, b, c)
- 剩余参数不能在对象字面量的 setter 属性中使用
//错误的写法2
let obj = {
set add(...arr) {
}
}
剩余参数如何影响 arguments 对象
arguments 对象在函数被调用时反映了传入的参数,与剩余参数能协同工作,就像如下程序所演示的:
function checkArgs(...args) {
console.log(args.length);
console.log(arguments.length);
console.log(args[0], arguments[0]);
console.log(args[1], arguments[1]);
}
checkArgs("a", "b");
//输出
2
2
a a
b b
arguments 对象总能正确反映被传入函数的参数,而无视剩余参数的使用。
ES6中的构造函数Function新增了支持默认参数和不定参数。
扩展运算符
考虑一下Math.max()方法,它接受任意数量的参数,并会返回其中的最大值。
//两个值进行比较
let value1 = 25,
value2 = 50;
console.log(Math.max(value1, value2)); // 50
//一个数组中找到最大值(es5)
let values = [25, 50, 75, 100]
console.log(Math.max.apply(Math, values));
//es6
let values = [25, 50, 75, 100]
// 等价于 console.log(Math.max(25, 50, 75, 100));
console.log(Math.max(...values)); // 100
扩展运算符传递参数
//假设你想让 Math.max() 返回的最小值为 0 (以防数组中混入了负值),你可以将参数 0 单独传入,并继续为其他参数使用扩展运算符
let values = [-25, -50, -75, -100]
console.log(Math.max(...values, 0)); // 0
ES6 的名称属性
ES6 给所有函数添加了 name 属性。
选择合适的名称
//函数声明
function doSomething() {
// ...
}
//匿名函数表达式
var doAnotherThing = function() {
// ...
};
console.log(doSomething.name); // "doSomething"
console.log(doAnotherThing.name); // "doAnotherThing"
名称属性的特殊情况
//doSomethingElse的优先级高于doSomething
var doSomething = function doSomethingElse() {
// ...
};
//person.firstName 实际是个 getter 函数,因此它的名称是 "get firstName"
var person = {
get firstName() {
return "Nicholas"
},
sayName: function() {
console.log(this.name);
}
}
console.log(doSomething.name); // "doSomethingElse"
console.log(person.sayName.name); // "sayName"
var descriptor = Object.getOwnPropertyDescriptor(person, "firstName");
console.log(descriptor.get.name); // "get firstName"
另外两个特殊情况
- 使用 bind() 创建的函数会在名称属性值之前带有"bound"前缀
- 使用 Function 构造器创建的函数,其名称属性则会有 "anonymous" 前缀
var doSomething = function() {
// ...
};
console.log(doSomething.bind().name); // "bound doSomething"
console.log((new Function()).name); // "anonymous"
明确函数的双重用途
JS 为函数提供了两个不同的内部方法: [[Call]] 与 [[Construct]] 。当函数未使用 new进行调用时, [[call]] 方法会被执行,运行的是代码中显示的函数体。而当函数使用 new进行调用时, [[Construct]] 方法则会被执行,负责创建一个被称为新目标的新的对象,并
且使用该新目标作为 this 去执行函数体。拥有 [[Construct]] 方法的函数被称为构造器。
在 ES5 中判断函数如何被调用
使用instanceof
function Person(name) {
if (this instanceof Person) {
this.name = name; // 使用 new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person("Nicholas"); // 抛出错误
但这种情况下并不可靠:
function Person(name) {
if (this instanceof Person) {
this.name = name; // 使用 new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 奏效了!
new.target 元属性
通过检查 new.target 是否被定义,这个新的元属性就让你能安全地判断函数是否被使用new进行了调用。
function Person(name) {
if (typeof new.target !== "undefined") {
this.name = name; // 使用 new
} else {
throw new Error("You must use new with Person.")
}
}
var person = new Person("Nicholas");
var notAPerson = Person.call(person, "Michael"); // 出错!
也可以检查 new.target 是否被使用特定构造器进行了调用,例如以下代码:
function Person(name) {
if (new.target === Person) {
this.name = name; // 使用 new
} else {
throw new Error("You must use new with Person.")
}
}
function AnotherPerson(name) {
Person.call(this, name);
}
var person = new Person("Nicholas");
var anotherPerson = new AnotherPerson("Nicholas"); // 出错!
警告:在函数之外使用 new.target 会有语法错误。
块级函数
严格模式的块级函数
"use strict";
if (true) {
// 在 ES5 会抛出语法错误, ES6 则不会
function doSomething() {
// ...
}
}
块级函数会被提升到定义所在的代码块的顶部:
"use strict";
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "undefined"
let 函数表达式:
"use strict";
if (true) {
console.log(typeof doSomething); // 抛出错误
let doSomething = function () {
// ...
}
doSomething();
}
console.log(typeof doSomething);
非严格模式的块级函数
ES6 在非严格模式下同样允许使用块级函数,但行为有细微不同。块级函数的作用域会被提升到所在函数或全局环境的顶部,而不是代码块的顶部。
// ES6 behavior
if (true) {
console.log(typeof doSomething); // "function"
function doSomething() {
// ...
}
doSomething();
}
console.log(typeof doSomething); // "function"
箭头函数
箭头函数与传统的 JS 函数区别:
- 没有 this 、 super 、 arguments ,也没有 new.target 绑定
- 不能被使用 new 调用
- 没有原型: 既然不能对箭头函数使用 new ,那么它也不需要原型,也就是没有prototype 属性。
- 不能更改 this : this 的值在函数内部不能被修改,在函数的整个生命周期内其值会保持不变
- 没有 arguments 对象
- 不允许重复的具名参数
箭头函数语法
- 无参数
var getName = () => "Nicholas";
// 有效等价于:
var getName = function() {
return "Nicholas";
};
- 单个参数
var reflect = value => value;
// 有效等价于:
var reflect = function(value) {
return value;
};
- 多个参数
var sum = (num1, num2) => num1 + num2;
// 有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
- 多个函数语句体
var sum = (num1, num2) => {
return num1 + num2;
};
// 有效等价于:
var sum = function(num1, num2) {
return num1 + num2;
};
//将对象字面量包裹在括号内,标示了括号内是一个字面量而不是函数体。
var getTempItem = id => ({ id: id, name: "Temp" });
// 有效等价于:
var getTempItem = function(id) {
return {
id: id,
name: "Temp"
};
};
创建立即调用函数表达式
- 传统函数
let person = function(name) {
return {
getName: function() {
return name;
}
};
}("Nicholas");
console.log(person.getName()); // "Nicholas"
- 箭头函数
let person = ((name) => {
return {
getName: function() {
return name;
}
};
})("Nicholas");
console.log(person.getName()); // "Nicholas"
译注:使用传统函数时, (function(){/函数体/})(); 与 (function(){/函数体/}());
这两种方式都是可行的。
但若使用箭头函数,则只有下面的写法是有效的: (() => {/函数体/})();
尾调用优化
尾调用是指在函数return的时候调用一个新的函数,由于尾调用的实现需要存储到内存中,在一个循环体中,如果存在函数的尾调用,你的内存可能爆满或溢出。
ES6中,引擎会帮你做好尾调用的优化工作,你不需要自己优化,但需要满足下面3个要求:
1、函数不是闭包
2、尾调用是函数最后一条语句
3、尾调用结果作为函数返回
一个满足以上要求的函数如下所示:
"use strict";
function a() {
return b();
}
下面的都是不满足的写法:
//没有return不优化
"use strict";
function a() {
b();
}
//不是直接返回函数不优化
"use strict";
function a() {
return 1 + b();
}
//尾调用是函数不是最后一条语句不优化
"use strict";
function a() {
const s = b();
return s
}
//闭包不优化
"use strict";
function a() {
const num = 1
function b() {
return num
}
return b
}
尾调用实际用途——递归函数优化
在ES5时代,我们不推荐使用递归,因为递归会影响性能。
但是有了尾调用优化之后,递归函数的性能有了提升。
//新型尾优化写法
"use strict";
function a(n, p = 1) {
if(n <= 1) {
return 1 * p
}
let s = n * p
return a(n - 1, s)
}
//求 1 x 2 x 3的阶乘
let sum = a(3)
console.log(sum) // 6
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。