文章目录
1. 函数参数的默认值
1.1 基本用法
ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。
function log(x, y) {
y = y || 'World';
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello World // 此处需要输出为 hello
上面代码检查函数log()
的参数y
有没有赋值,如果没有,则指定默认值为World
。
这种写法的缺点在于,如果参数y
赋值了,但是对应的布尔值为false
,则该赋值不起作用。
就像上面代码的最后一行,参数y
等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数y
是否被赋值,如果没有,再等于默认值。
function log(x, y) {
if (typeof y === 'undefined') {
y = 'World';
}
console.log(x, y);
}
ES6 允许为函数的参数设置默认值,即 直接写在参数定义的后面。
// 默认值直接写在参数定义的后面
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
————
可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。
function Point(x = 0, y = 0) {
this.x = x;
this.y = y;
}
const p = new Point();
p // { x: 0, y: 0 }
除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;
其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。
————
参数变量是 默认声明 的,所以不能用let
或const
再次声明,否则会报错。
function foo(x = 5) {
let x = 1; // error 都不能再次声明
const x = 2; // error
}
————
使用参数默认值时,函数不能有同名参数。
// 不报错
function foo(x, x, y) {
// ...
}
// 报错:使用参数默认值时,不能有同名参数
function foo(x, x, y = 1) {
// ...
}
// SyntaxError: Duplicate parameter name not allowed in this context
————
另外,一个容易忽略的地方是,参数默认值不是传值的,而是 每次都重新计算默认值表达式的值。
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
上面代码中,参数p
的默认值是x + 1
。这时,每次调用函数foo()
,都会重新计算x + 1
,而不是默认p
等于 100
。
1.2 与解构赋值结合使用
// 使用的是 对象解构赋值的默认值,不是函数参数的默认值
function foo({x, y = 5}) {
console.log(x, y);
}
foo({}) // undefined 5
foo({x: 1}) // 1 5
foo({x: 1, y: 2}) // 1 2
foo() // TypeError: Cannot read property 'x' of undefined
// 函数参数用 {} 括起来了,就代表使用的是解构赋值,所以参数你得 foo({})
以上代码 只使用了 对象解构赋值的默认值,没有使用 函数参数的默认值。
只有当函数foo()
的参数是一个 对象 时,变量x
和y
才会通过 解构赋值 生成。
如果函数 foo()
调用时没提供参数,变量x
和y
就不会生成,从而报错。
通过提供 函数参数的默认值,就可以避免这种情况。
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
// 即 没有传参时,通过提供函数参数的默认值,来执行解构赋值的默认值,参考《2.2 对象解构默认值》
————
下面是另一个解构赋值默认值的例子。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
上面代码中,如果函数fetch()
的第二个参数是一个对象,就可以为它的三个属性设置默认值。
这种写法不能省略第二个参数,如果结合 函数参数的默认值,就可以省略第二个参数。这时,就出现了 双重默认值。
- 即:在对函数没有传参时,通过
提供函数参数的默认值,来执行解构赋值的默认值。
参考《2.2 对象解构默认值》
function fetch(url, { body = '', method = 'GET', headers = {} } = {}) {
console.log(method);
}
fetch('http://example.com')
// "GET"
// 1、函数没有传参触发:【函数参数的默认值】
// 2、函【数参数的默认值】 触发后,触发 【解构赋值的默认值】
注意,函数参数的默认值生效以后,参数解构赋值依然会进行。
【默认值生效的条件是,对象的属性值严格等于 undefined
】
function f({ a, b = 'world' } = { a: 'hello' }) {
// 此时 b 为 undefined,触发解构赋值的默认值 b = 'world'
console.log(b);
}
f() // world
————
作为练习,大家可以思考一下,下面两种函数写法有什么差别?
// 这两种写法在解构赋值中都有讲述到
// 写法一
function m1({x = 0, y = 0} = {}) {
return [x, y];
}
// 写法二
function m2({x, y} = { x: 0, y: 0 }) {
return [x, y];
}
// 函数没有参数的情况
m1() // [0, 0]
m2() // [0, 0]
// x 和 y 都有值的情况
m1({x: 3, y: 8}) // [3, 8]
m2({x: 3, y: 8}) // [3, 8]
// x 有值,y 无值的情况
m1({x: 3}) // [3, 0]
m2({x: 3}) // [3, undefined] // 直接替换掉 函数参数的默认值:{ x: 0, y: 0 }
// x 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
1.3 参数默认值的位置
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1] 必须写 undefined
// 例二
function f(x, y = 5, z) {
return [x, y, z];
}
f() // [undefined, 5, undefined]
f(1) // [1, 5, undefined]
f(1, ,2) // 报错
f(1, undefined, 2) // [1, 5, 2] // 必须写undefined
如果传入undefined
,将触发该参数等于默认值,null
则没有这个效果。
function foo(x = 5, y = 6) {
console.log(x, y);
}
foo(undefined, null)
// 5 null // 传入null值不能触发默认值
1.5 函数的 length 属性
指定了默认值以后,函数的length
属性,将返回没有指定默认值的参数个数。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
……
1.6 作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) { // 这里面形成了作用域,变量查找机制。
console.log(y);
}
f(2) // 2
在这个作用域里面,默认值变量 x
指向第一个参数 x
,而不是 全局变量 x
,所以输出是2
。
let x = 1;
function f(y = x) { // 实际执行的是 let y = x;
let x = 2;
console.log(y);
}
f() // 1
function f(y = x) { // 形成单独的作用域,不会查找 { } 中的内容
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
// 报错
var x = 1;
function foo(x = x) { // 实际执行的是 let x = x;
// ...
}
foo() // ReferenceError: Cannot access 'x' before initialization
上面代码中,参数x = x
形成一个单独作用域。实际执行的是let x = x
,由于暂时性死区的原因,这行代码会报错。
————
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。
let foo = 'outer';
function bar(func = () => foo) { // 匿名函数: 立即执行函数
let foo = 'inner';
console.log(func());
}
bar(); // outer
上面代码中,函数 bar
的参数 func
的默认值是一个匿名函数,返回值为变量 foo
。函数参数形成的单独作用域里面,并没有定义变量 foo
,所以 foo
指向外层的全局变量 foo
,因此输出 outer
。
// 报错
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar() // ReferenceError: foo is not defined
————
下面是一个更复杂的例子。
var x = 1;
function foo(x, y = function() { x = 2; }) {
var x = 3;
y();
console.log(x);
}
foo() // 3
x // 1
上面代码中,函数foo
的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量y
,y
的默认值是一个匿名函数。
这个匿名函数内部的变量x
,指向同一个作用域的第一个参数x
。函数foo内部又声明了一个内部变量x
,该变量与第一个参数x
由于不是同一个作用域,所以不是同一个变量,因此执行y
后,内部变量x
和外部全局变量x
的值都没变。
如果将var x = 3
的var去除,函数foo
的内部变量 x
就指向第一个参数x
,与匿名函数内部的x
是一致的,所以最后输出的就是2,而外层的全局变量x
依然不受影响。
var x = 1;
function foo(x, y = function() { x = 2; }) {
// 匿名函数中的 x 与 第一个x是同一个变量
x = 3; // x 向外查找,找到第一个x
y();
console.log(x);
}
foo() // 2
x // 1
2. rest 参数:(…变量名)
ES6 引入 rest 参数(形式为 ...变量名
),用于获取函数的多余参数,这样就不需要使用arguments
对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
function add(...values) {
let sum = 0;
for (var val of values) {
sum += val;
}
return sum;
}
add(2, 5, 3) // 10
————
下面是一个 rest 参数代替 arguments
变量的例子。
// arguments变量的写法
function sortNumbers() {
return Array.from(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
arguments
对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用 Array.from
先将其转为数组。
rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
function push(array, ...items) {
items.forEach(function(item) {
array.push(item);
console.log(item);
});
}
var a = [];
push(a, 1, 2, 3)
————
注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// 报错
function f(a, ...b, c) {
// ...
}
————
函数的 length
属性,不包括 rest 参数。
(function(a) {}).length // 1
(function(...a) {}).length // 0
(function(a, ...b) {}).length // 1
3. 严格模式
3.1 只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式。
从 ES5 开始,函数内部可以设定为严格模式。
function doSomething(a, b) {
'use strict';
// code
}
ES2016 做了一点修改,规定:只要 函数参数 使用了默认值、解构赋值、或者扩展运算符
,那么函数内部 就不能显式设定为严格模式
,否则会报错。
// 报错 使用了 "默认值"
function doSomething(a, b = a) {
'use strict';
// code
}
// 报错 使用了 "解构赋值"
const doSomething = function ({a, b}) {
'use strict';
// code
};
// 报错 使用了 "扩展运算符"
const doSomething = (...a) => {
'use strict';
// code
};
const obj = {
// 报错 使用了 "解构赋值"
doSomething({a, b}) {
'use strict';
// code
}
};
这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。
这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。所以这样规定。
// 报错
function doSomething(value = 070) {
'use strict';
return value;
}
上面代码中,参数value的默认值是八进制数070
,但是严格模式下不能用前缀0表示八进制,所以应该报错。
但是实际上,JavaScript 引擎会 先成功执行 value = 070
,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。
虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。
两种方法可以规避这种限制。第一种是 设定全局性的严格模式,这是合法的。
'use strict';
function doSomething(a, b = a) {
// code
}
第二种是把函数包在一个 无参数的立即执行函数里面。
const doSomething = (function () {
'use strict';
return function(value = 42) {
return value;
};
}());
4. 函数的 name 属性(foo.name 返回函数的函数名)
函数的 name
属性,返回该函数的函数名。
function foo() {}
foo.name // "foo"
这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。
需要注意的是,ES6 对这个属性的行为做出了一些修改。如果 将一个匿名函数赋值给一个变量,ES5 的 name
属性,会返回空字符串,而 ES6 的 name
属性会 返回实际的函数名。
// 将匿名函数赋给一个变量
var f = function () {};
// ES5
f.name // ""
// ES6
f.name // "f"
如果将一个 具名函数赋值给一个变量,则 ES5 和 ES6 的 name
属性都返回这个 具名函数原本的名字。
const bar = function baz() {};
// ES5
bar.name // "baz"
// ES6
bar.name // "baz"
————
Function
构造函数返回的函数实例,name
属性的值为 anonymous
。
(new Function).name // "anonymous"
bind
返回的函数,name
属性值会加上 bound
前缀。
function foo() {};
foo.bind({}).name // "bound foo"
(function(){}).bind({}).name // "bound "
5. 箭头函数
5.1 箭头函数
ES6 允许使用 “箭头”(=>
)定义函数。
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
如果箭头函数 不需要参数 或 需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5;
// 等同于
var f = function () { return 5 };
var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
return num1 + num2;
};
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return
语句返回。
var sum = (num1, num2) => { return num1 + num2; }
————
由于 大括号被解释为代码块,所以如果箭头函数 直接返回一个对象,必须 在对象外面加上括号()(不是数组 [] ),否则会报错。
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
下面是一种特殊情况,虽然可以运行,但会得到错误的结果。
let foo = () => { a: 1 };
foo() // undefined
上面代码中,原始意图是返回一个对象 { a: 1 }
,但是由于引擎认为大括号是代码块,所以执行了一行语句 a: 1
。
这时,a
可以被解释为语句的标签,因此实际执行的语句是 1
;,然后函数就结束了,没有返回值。
————
如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。
let fn = () => void doesNotReturn();
————
箭头函数可以与变量解构结合使用。
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
// 确实,因为{ first, last }解构赋值 取得了 person.first、person.first 属性
箭头函数使得表达更加简洁。
const isEven = n => n % 2 === 0;
const square = n => n * n;
————
箭头函数的一个用处是 简化回调函数。
// 普通函数写法
[1,2,3].map(function (x) {
return x * x;
});
// 箭头函数写法
[1,2,3].map(x => x * x);
// 普通函数写法
var result = values.sort(function (a, b) {
return a - b;
});
// 箭头函数写法
var result = values.sort((a, b) => a - b);
rest 参数与箭头函数结合:
// nums 是一个数组
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
// tail 是一个数组,放到一个数组里面
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
5.2 使用注意点
箭头函数有几个使用注意点。
(1)箭头函数没有自己的 this
对象(详见下文)。
(2)不可以当作构造函数,也就是说,不可以对箭头函数使用 new
命令,否则会抛出一个错误。
(3)不可以使用 arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
(4)不可以使用 yield
命令,因此箭头函数不能用作 Generator 函数。
上面四点中,最重要的是第一点。对于普通函数来说,内部的 this
指向函数运行时所在的对象,但是这一点对箭头函数不成立。它没有自己的 this
对象,内部的 this
就是定义时上层作用域中的 this
。也就是说,箭头函数内部的 this
指向是固定的,相比之下,普通函数的this指向是可变的。
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
var id = 21;
foo.call({ id: 42 });
// id: 42
上面代码中,setTimeout()
的参数是一个箭头函数,这个箭头函数的定义生效是在foo
函数生成时,而它的真正执行要等到 100 毫秒后。
如果是普通函数,执行时this
应该指向全局对象 window
,这时应该输出21
。但是,箭头函数导致 this
总是指向 函数定义生效时 所在的对象(本例是 {id: 42}
),所以打印出来的是 42
。
下面例子是回调函数分别为箭头函数和普通函数,对比它们内部的 this
指向。
function Timer() {
this.s1 = 0;
this.s2 = 0;
// 箭头函数: this绑定定义时所在的作用域(Timer函数)
setInterval(() => this.s1++, 1000);
// 普通函数: this指向运行时所在的【对象】(即全局对象)
setInterval(function () {
this.s2++;
}, 1000);
}
var timer = new Timer();
setTimeout(() => console.log('s1: ', timer.s1), 3100);
setTimeout(() => console.log('s2: ', timer.s2), 3100);
// s1: 3
// s2: 0
上面代码中,Timer
函数内部设置了两个定时器,分别使用了箭头函数和普通函数。
前者的this
绑定 定义时所在的作用域 / 对象(即Timer函数),后者的this
指向 运行时所在的作用域 / 对象(即全局对象)。所以,3100 毫秒之后,timer.s1
被更新了 3 次,而 timer.s2
一次都没更新。