一、箭头函数
ES6
允许使用“箭头”( =>
)定义函数。
return
语句返回。//例1
var f = v => v;
// 等同于
var f = function (v) {
return v;
};
//例2
var f = () => 5;
// 等同于
var f = function () { return 5 };
//例3
var sum = (num1, num2) => {
let result = num1 + num2;
return result
}
// 等同于
var sum = function(num1, num2) {
let result = num1 + num2;
return num1 + num2;
};
// 报错
let getTempItem = id => { id: id, name: "Temp" };
// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });
const full = ({ first, last }) => first + ' ' + last;
// 等同于
function full(person) {
return person.first + ' ' + person.last;
}
箭头函数使得表达更加简洁.
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);
const numbers = (...nums) => nums;
numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]
const headAndTail = (head, ...tail) => [head, tail];
headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
箭头函数有几个使用注意点。
(1)函数体内的 this
对象,就是定义时所在的对象,而不是使用时所在的对象。
(2)不可以当作构造函数,也就是说,不可以使用 new
命令,否则会抛出一个错误。
(3)不可以使用arguments
对象,该对象在函数体内不存在。如果要用,可以用 rest
参数代替。
(4)不可以使用 yield
命令,因此箭头函数不能用作 Generator
函数。
上面四点中,第一点尤其值得注意。this
对象的指向是可变的,但是在箭头函数中,它是固定的.
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
。
所以,箭头函数转成 ES5 的代码如下。
// ES6
function foo() {
setTimeout(() => {
console.log('id:', this.id);
}, 100);
}
// ES5
function foo() {
var _this = this;
setTimeout(function () {
console.log('id:', _this.id);
}, 100);
}
上面代码中,转换后的 ES5
版本清楚地说明了,箭头函数里面根本没有自己的 this
,而是引用外层的 this
。
箭头函数可以让 this 指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。
var handler = {
id: '123456',
init: function() {
document.addEventListener('click',
event => this.doSomething(event.type), false);
},
doSomething: function(type) {
console.log('Handling ' + type + ' for ' + this.id);
}
};
上面代码的 init
方法中,使用了箭头函数,这导致这个箭头函数里面的 this
,总是指向 handler
对象。
请问下面的代码之中有几个 this
?
function foo() {
return () => {
return () => {
return () => {
console.log('id:', this.id);
};
};
};
}
var f = foo.call({id: 1});
var t1 = f.call({id: 2})()(); // id: 1
var t2 = f().call({id: 3})(); // id: 1
var t3 = f()().call({id: 4}); // id: 1
上面代码之中,只有一个 this
,就是函数 foo
的 this
,所以 t1
、 t2
、 t3
都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的 this
,它们的 this
其实都是最外层 foo
函数的 this
。
除了 this
,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量: arguments
、 super
、 new.target
。
function foo() {
setTimeout(() => {
console.log('args:', arguments);
}, 100);
}
foo(2, 4, 6, 8)
// args: [2, 4, 6, 8]
上面代码中,箭头函数内部的变量 arguments
,其实是函数 foo
的 arguments
变量。
另外,由于箭头函数没有自己的 this
,所以当然也就不能用 call() 、 apply() 、 bind()
这些方法去改变 this
的指向。
(function() {
return [
(() => this.x).bind({ x: 'inner' })()
];
}).call({ x: 'outer' });
// ['outer']
上面代码中,箭头函数没有自己的 this
,所以 bind
方法无效,内部的this
指向外部的 this
。
二、rest 参数
ES6
引入rest
参数(形式为 …变量名 ),用于获取函数的多余参数,这样就不需要使用 arguments
对象了。rest
参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
下面是一个 rest 参数代替 arguments 变量的例子。arguments
对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用 Array.prototype.slice.call
先将其转为数组。rest
参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。
// arguments变量的写法
function sortNumbers() {
return Array.prototype.slice.call(arguments).sort();
}
// rest参数的写法
const sortNumbers = (...numbers) => numbers.sort();
下面是一个利用 rest 参数改写数组 push 方法的例子。
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
三、函数name 属性
函数的name
属性,返回该函数的函数名。这个属性早就被浏览器广泛支持,但是直到 ES6
,才将其写入了标准。
需要注意的是,ES6
对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5
的 name
属性,会返回空字符串,而 ES6
的 name
属性会返回实际的函数名。
function foo() {}
foo.name // "foo"
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 "
四、函数参数的默认值
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
这种写法的缺点在于,如果参数 y
赋值了,但是对应的布尔值为 false
,则该赋值不起作用。就像上面代码的最后一行,参数 y
等于空字符,结果被改为默认值。
为了避免这个问题,通常需要先判断一下参数 y
是否被赋值,如果没有,再等于默认值。
if (typeof y === 'undefined') {
y = 'World';
}
ES6
允许为函数的参数设置默认值,即直接写在参数定义的后面。
function log(x, y = 'World') {
console.log(x, y);
}
log('Hello') // Hello World
log('Hello', 'China') // Hello China
log('Hello', '') // Hello
参数变量是默认声明的,所以不能用 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
一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。
下面代码中,参数 p
的默认值是 x + 1
。这时,每次调用函数 foo
,都会重新计算 x + 1
,而不是默认 p
等于 100
。
let x = 99;
function foo(p = x + 1) {
console.log(p);
}
foo() // 100
x = 100;
foo() // 101
参数默认值可以与解构赋值的默认值,结合起来使用。
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
的参数是一个对象时,变量x
和 y
才会通过解构赋值生成。如果函数 foo
调用时没提供参数,变量 x
和 y
就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
通常情况下,定义了默认值的参数,应该是函数的尾参数
。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
// 例二
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
作为练习,请问下面两种写法有什么差别?
// 写法一
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 和 y 都无值的情况
m1({}) // [0, 0];
m2({}) // [undefined, undefined]
m1({z: 3}) // [0, 0]
m2({z: 3}) // [undefined, undefined]
五、函数的 length 属性
指定了默认值以后,函数的 length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后, length
属性将失真。
length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest
参数也不会计入 length
属性。
(function (a) {}).length // 1
(function (a = 5) {}).length // 0
(function (a, b, c = 5) {}).length // 2
(function(...args) {}).length // 0
如果设置了默认值的参数不是尾参数,那么 length
属性也不再计入后面的参数了。
(function (a = 0, b, c) {}).length // 0
(function (a, b = 1, c) {}).length // 1
六、函数参数作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context
)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
上面代码中,参数 y
的默认值等于变量 x 。调用函数 f
时,参数形成一个单独的作用域。在这个作用域里面,默认值变量 x
指向第一个参数 x
,而不是全局变量 x
,所以输出是 2
。
let x = 1;
function f(y = x) {
let x = 2;
console.log(y);
}
f() // 1
上面代码中,函数 f
调用时,参数 y = x
形成一个单独的作用域。这个作用域里面,变量 x
本身没有定义,所以指向外层的全局变量 x
。函数调用时,函数体内部的局部变量 x
影响不到默认值变量 x
。
如果此时,全局变量 x 不存在,就会报错。
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
下面这样写,也会报错。
var x = 1;
function foo(x = x) {
// ...
}
foo() // ReferenceError: x is not defined
上面代码中,参数 x = x
形成一个单独作用域。实际执行的是 let x = x
,由于暂时性死区的原因,这行代码会报错”x
未定义“。
如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。
let foo = 'outer';
function bar(func = () => foo) {
let foo = 'inner';
console.log(func());
}
bar(); // outer
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 = 3;
y();
console.log(x);
}
foo() // 2
x // 1