- 函数的扩展
- 基本用法
- 函数参数的默认值
ES6允许为函数的参数设置默认值,即直接写在参数定义的后面。
- 参数变量是默认声明的,所以不能用
let
或const
再次声明。下面代码中,参数变量x
是默认声明的,在函数体中,不能用let
或const
再次声明,否则会报错。function foo(x = 5) { let x = 1; // error const x = 2; // error }
- 函数参数的默认值
- 与解构赋值默认值结合使用
- 参数默认值可以与解构赋值的默认值,结合起来使用。下面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数
foo
的参数是一个对象时,变量x
和y
才会通过解构赋值而生成。如果函数foo
调用时参数不是对象,变量x
和y
就不会生成,从而报错。如果参数对象没有y
属性,y
的默认值5才会生效。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
下面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。
// 写法一 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]
个人理解当m1没有传入参数时,参数设置为空对象,使用对象结构值为{x = 0, y = 0};当m2没有传入参数时,使用设置的默认参数
- 参数默认值可以与解构赋值的默认值,结合起来使用。下面代码使用了对象的解构赋值默认值,而没有使用函数参数的默认值。只有当函数
- 参数默认值的位置
- 通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
// 例一 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
,将触发该参数等于默认值,null
则没有这个效果。function foo(x = 5, y = 6) { console.log(x, y); } foo(undefined, null) // 5 null
- 通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。
- 函数的length属性
- 指定了默认值以后,函数的
length
属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length
属性将失真。(function (a) {}).length // 1 (function (a = 5) {}).length // 0 (function (a, b, c = 5) {}).length // 2
- 因为
length
属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,rest参数也不会计入length
属性。(function(...args) {}).length // 0
- 如果设置了默认值的参数不是尾参数,那么
length
属性也不再计入后面的参数了。(function (a = 0, b, c) {}).length // 0 (function (a, b = 1, c) {}).length // 1
- 指定了默认值以后,函数的
- 作用域
- 下面代码中,函数
foo
的参数x
的默认值也是x
。这时,默认值x
的作用域是函数作用域,而不是全局作用域。由于在函数作用域中,存在变量x
,但是默认值在x
赋值之前先执行了,所以这时属于暂时性死区(参见《let和const命令》一章),任何对x
的操作都会报错。var x = 1; function foo(x = x) { // ... } foo() // ReferenceError: x is not defined
- 下面代码中,函数
- 应用
- 利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
function throwIfMissing() { throw new Error('Missing parameter'); } function foo(mustBeProvided = throwIfMissing()) { return mustBeProvided; } foo() // Error: Missing parameter
上面代码的
foo
函数,如果调用的时候没有参数,就会调用默认值throwIfMissing
函数,从而抛出一个错误。从上面代码还可以看到,参数
mustBeProvided
的默认值等于throwIfMissing
函数的运行结果(即函数名之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行(即如果参数已经赋值,默认值中的函数就不会运行),这与python语言不一样。另外,可以将参数默认值设为
undefined
,表明这个参数是可以省略的。function foo(optional = undefined) { ··· }
- 利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。
- 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参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。
// 报错 function f(a, ...b, c) { // ... }
- 函数的length属性,不包括rest参数。
(function(a) {}).length // 1 (function(...a) {}).length // 0 (function(a, ...b) {}).length // 1
- ES6引入rest参数(形式为“...变量名”),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest参数搭配的变量是一个数组,该变量将多余的参数放入数组中。
- 扩展运算符
- 扩展运算符(spread)是三个点(
...
)。它好比rest参数的逆运算,将一个数组转为用逗号分隔的参数序列。console.log(...[1, 2, 3]) // 1 2 3 console.log(1, ...[2, 3, 4], 5) // 1 2 3 4 5 [...document.querySelectorAll('div')] // [<div>, <div>, <div>]
- 该运算符主要用于函数调用。
function push(array, ...items) { array.push(...items); } function add(x, y) { return x + y; } var numbers = [4, 38]; add(...numbers) // 42
上面代码中,
array.push(...items)
和add(...numbers)
这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。 - 扩展运算符与正常的函数参数可以结合使用,非常灵活。
function f(v, w, x, y, z) { } var args = [0, 1]; f(-1, ...args, 2, ...[3]);
- 扩展运算符(spread)是三个点(
- 扩展运算符的应用
- 合并数组:扩展运算符提供了数组合并的新写法。
// ES5 [1, 2].concat(more) // ES6 [1, 2, ...more] var arr1 = ['a', 'b']; var arr2 = ['c']; var arr3 = ['d', 'e']; // ES5的合并数组 arr1.concat(arr2, arr3); // [ 'a', 'b', 'c', 'd', 'e' ] // ES6的合并数组 [...arr1, ...arr2, ...arr3] // [ 'a', 'b', 'c', 'd', 'e' ]
- 与解构赋值结合:扩展运算符可以与解构赋值结合起来,用于生成数组。
// ES5 a = list[0], rest = list.slice(1) // ES6 [a, ...rest] = list
下面是另外一些例子。
const [first, ...rest] = [1, 2, 3, 4, 5]; first // 1 rest // [2, 3, 4, 5] const [first, ...rest] = []; first // undefined rest // []: const [first, ...rest] = ["foo"]; first // "foo" rest // []
如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。
const [...butLast, last] = [1, 2, 3, 4, 5]; // 报错 const [first, ...middle, last] = [1, 2, 3, 4, 5]; // 报错
-
字符串
-
扩展运算符还可以将字符串转为真正的数组。
[...'hello'] // [ "h", "e", "l", "l", "o" ]
-
正确返回字符串长度的函数,可以像下面这样写。
function length(str) { return [...str].length; }
-
- 实现了Iterator接口的对象
- 任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。下面是一个最特殊的例子。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于
NodeList
对象实现了Iterator接口。var nodeList = document.querySelectorAll('div'); var array = [...nodeList];
- 对于那些没有部署Iterator接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。下面代码中,
arrayLike
是一个类似数组的对象,但是没有部署Iterator接口,扩展运算符就会报错。这时,可以改为使用Array.from
方法将arrayLike
转为真正的数组。let arrayLike = { '0': 'a', '1': 'b', '2': 'c', length: 3 }; // TypeError: Cannot spread non-iterable object. let arr = [...arrayLike];
- 任何Iterator接口的对象,都可以用扩展运算符转为真正的数组。下面是一个最特殊的例子。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于
- 合并数组:扩展运算符提供了数组合并的新写法。
- Map和Set结构,Generator函数
- 扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构。
let map = new Map([ [1, 'one'], [2, 'two'], [3, 'three'], ]); let arr = [...map.keys()]; // [1, 2, 3]
- Generator函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。
var go = function*(){ yield 1; yield 2; yield 3; }; [...go()] // [1, 2, 3]
下面代码中,变量
go
是一个Generator函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。如果对没有
iterator
接口的对象,使用扩展运算符,将会报错。var obj = {a: 1, b: 2}; let arr = [...obj]; // TypeError: Cannot spread non-iterable object
- 扩展运算符内部调用的是数据结构的Iterator接口,因此只要具有Iterator接口的对象,都可以使用扩展运算符,比如Map结构。
- name属性
- 函数的
name
属性,返回该函数的函数名。function foo() {} foo.name // "foo" var func1 = function () {}; // ES5 func1.name // "" // ES6 func1.name // "func1" const bar = function baz() {}; // ES5 bar.name // "baz" // ES6 bar.name // "baz"
- 函数的
- 箭头函数
- 函数体内的
this
对象,就是定义时所在的对象,而不是使用时所在的对象
- 函数体内的
- 基本用法
- 对象的扩展
- 属性的简洁表示法
- ES6允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。下面代码表明,ES6允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。
- 属性名表达式
- ES6 允许字面量定义对象时,用(表达式)作为对象的属性名,即把表达式放在方括号内。
let propKey = 'foo'; let obj = { [propKey]: true, ['a' + 'bc']: 123 };
- 注意,属性名表达式与简洁表示法,不能同时使用,会报错。
// 报错 var foo = 'bar'; var bar = 'abc'; var baz = { [foo] }; // 正确 var foo = 'bar'; var baz = { [foo]: 'abc'};
- 注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串
[object Object]
,这一点要特别小心。下面代码中,[keyA]
和[keyB]
得到的都是[object Object]
,所以[keyB]
会把[keyA]
覆盖掉,而myObject
最后只有一个[object Object]
属性。const keyA = {a: 1}; const keyB = {b: 2}; const myObject = { [keyA]: 'valueA', [keyB]: 'valueB' }; myObject // Object {[object Object]: "valueB"}
- ES6 允许字面量定义对象时,用(表达式)作为对象的属性名,即把表达式放在方括号内。
- Object.is()
- ES5比较两个值是否相等,只有两个运算符:相等运算符(
==
)和严格相等运算符(===
)。它们都有缺点,前者会自动转换数据类型,后者的NaN
不等于自身,以及+0
等于-0
。JavaScript缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。ES6提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is
就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。Object.is('foo', 'foo') // true Object.is({}, {}) // false //不同之处只有两个:一是+0不等于-0,二是NaN等于自身。 +0 === -0 //true NaN === NaN // false Object.is(+0, -0) // false Object.is(NaN, NaN) // true
- ES5比较两个值是否相等,只有两个运算符:相等运算符(
- Object.assign()
Object.assign
方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。Object.assign
方法的第一个参数是目标对象,后面的参数都是源对象。注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。如果只有一个参数,Object.assign
会直接返回该参数。如果该参数不是对象,则会先转成对象,然后返回。由于undefined
和null
无法转成对象,所以如果它们作为参数(仅限第一个参数),就会报错(其它参数无法转成对象,会直接跳过)。var target = { a: 1 }; var source1 = { b: 2 }; var source2 = { c: 3 }; Object.assign(target, source1, source2); target // {a:1, b:2, c:3}
- 其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。
var v1 = 'abc'; var v2 = true; var v3 = 10; var obj = Object.assign({}, v1, v2, v3); console.log(obj); // { "0": "a", "1": "b", "2": "c" }
- 注意点:
Object.assign
方法实行的是浅拷贝,而不是深拷贝。深拷贝实现方式如下Object.assign(obj2,JSON.parse(JSON.stringify(obj1)))
- 常见用途
- 为对象添加属性
class Point { constructor(x, y) { Object.assign(this, {x, y}); } }
- 为对象添加方法
Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { ··· }; SomeClass.prototype.anotherMethod = function () { ··· };
- 克隆对象
function clone(origin) { return Object.assign({}, origin); }
- 合并多个对象
const merge = (target, ...sources) => Object.assign(target, ...sources);
- 为属性指定默认值
const DEFAULTS = { logLevel: 0, outputFormat: 'html' }; function processContent(options) { options = Object.assign({}, DEFAULTS, options); }
- 为对象添加属性
- 属性的可枚举性
- 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。
Object.getOwnPropertyDescriptor
方法可以获取该属性的描述对象。let obj = { foo: 123 }; Object.getOwnPropertyDescriptor(obj, 'foo') // { // value: 123, // writable: true, // enumerable: true, // configurable: true // }
-
描述对象的enumerable属性,称为”可枚举性“,如果该属性为false,就表示某些操作会忽略当前属性。ES5有三个操作会忽略enumerable为false的属性。
for...in循环:只遍历对象自身的和继承的可枚举的属性
Object.keys():返回对象自身的所有可枚举的属性的键名
JSON.stringify():只串行化对象自身的可枚举的属性
ES6新增了一个操作Object.assign(),会忽略enumerable为false的属性,只拷贝对象自身的可枚举的属性。这四个操作之中,只有for...in会返回继承的属性。实际上,引入enumerable的最初目的,就是让某些属性可以规避掉for...in操作。比如,对象原型的toString方法,以及数组的length属性,就通过这种手段,不会被for...in遍历到。
Object.getOwnPropertyDescriptor(Object.prototype, 'toString').enumerable // false Object.getOwnPropertyDescriptor([], 'length').enumerable // false
-
上面代码中,
toString
和length
属性的enumerable
都是false
,因此for...in
不会遍历到这两个继承自原型的属性。另外,ES6规定,所有Class的原型的方法都是不可枚举的。Object.getOwnPropertyDescriptor(class {foo() {}}.prototype, 'foo').enumerable // false
-
总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用
for...in
循环,而使用Object.keys()
代替。
- 对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。
- __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()
- __proto__属性
__proto__
属性(前后各两个下划线),用来读取或设置当前对象的prototype
对象。目前,所有浏览器(包括IE11)都部署了这个属性。// es6的写法 var obj = { method: function() { ... } }; obj.__proto__ = someOtherObj; // es5的写法 var obj = Object.create(someOtherObj); obj.method = function() { ... };
- 该属性没有写入ES6的正文,而是写入了附录,原因是
__proto__
前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的API,只是由于浏览器广泛支持,才被加入了ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)代替。
- Object.setPrototypeOf()
- Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象。它是ES6正式推荐的设置原型对象的方法。
// 格式 Object.setPrototypeOf(object, prototype) // 用法 var o = Object.setPrototypeOf({}, null);
- 该方法等同于下面的函数。
function (obj, proto) { obj.__proto__ = proto; return obj; }
- 下面是一个例子。
let proto = {}; let obj = { x: 10 }; Object.setPrototypeOf(obj, proto); proto.y = 20; proto.z = 40; obj.x // 10 obj.y // 20 obj.z // 40
上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。
- Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象。它是ES6正式推荐的设置原型对象的方法。
- Object.getPrototypeOf()
- 该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象
Object.getPrototypeOf(obj);
- 下面是一个例子。
function Rectangle() { } var rec = new Rectangle(); Object.getPrototypeOf(rec) === Rectangle.prototype // true Object.setPrototypeOf(rec, Object.prototype); Object.getPrototypeOf(rec) === Rectangle.prototype // false
- 该方法与setPrototypeOf方法配套,用于读取一个对象的prototype对象
- __proto__属性
- Object.values()
Object.values
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。var obj = { foo: "bar", baz: 42 }; Object.values(obj) // ["bar", 42]
- 返回数组的成员顺序,与《属性的遍历》介绍的排列规则一致。(首先遍历所有属性名为数值的属性,按照数字排序。其次遍历所有属性名为字符串的属性,按照生成时间排序。最后遍历所有属性名为Symbol值的属性,按照生成时间排序。)
var obj = { 100: 'a', 2: 'b', 7: 'c' }; Object.values(obj) // ["b", "c", "a"]
-
上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是
b
、c
、a
。Object.values
只返回对象自身的可遍历属性。var obj = Object.create({}, {p: {value: 42}}); Object.values(obj) // []
-
上面代码中,
Object.create
方法的第二个参数添加的对象属性(属性p
),如果不显式声明,默认是不可遍历的。Object.values
不会返回这个属性。Object.values
会过滤属性名为Symbol值的属性。Object.values({ [Symbol()]: 123, foo: 'abc' }); // ['abc']
-
如果
Object.values
方法的参数是一个字符串,会返回各个字符组成的一个数组。Object.values('foo') // ['f', 'o', 'o']
-
上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,
Object.values
返回每个属性的键值,就是各个字符组成的一个数组。如果参数不是对象,
Objec
t.values
会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values
会返回空数组。Object.values(42) // [] Object.values(true) // []
- Object.entries()
Object.entries
方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。var obj = { foo: 'bar', baz: 42 }; Object.entries(obj) // [ ["foo", "bar"], ["baz", 42] ]
-
除了返回值不一样,该方法的行为与
Object.values
基本一致。如果原对象的属性名是一个Symbol值,该属性会被省略。
- 对象的扩展运算符
- 解构赋值
- 对象的解构赋值用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 }; x // 1 y // 2 z // { a: 3, b: 4 }
- 扩展运算符
- 扩展运算符(
...
)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。let z = { a: 3, b: 4 }; let n = { ...z }; n // { a: 3, b: 4 }
- 扩展运算符可以用于合并两个对象。
let ab = { ...a, ...b }; // 等同于 let ab = Object.assign({}, a, b);
- 扩展运算符(
- 对象的解构赋值用于从一个对象取值,相当于将所有可遍历的、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。
- 解构赋值
- Object.getOwnPropertyDescriptors()
- ES7有一个提案,提出了
Object.getOwnPropertyDescriptors
方法,返回指定对象所有自身属性(非继承属性)的描述对象。const obj = { foo: 123, get bar() { return 'abc' } }; Object.getOwnPropertyDescriptors(obj) // { foo: // { value: 123, // writable: true, // enumerable: true, // configurable: true }, // bar: // { get: [Function: bar], // set: undefined, // enumerable: true, // configurable: true } }
Object.getOwnPropertyDescriptors
方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。
- ES7有一个提案,提出了
- 属性的简洁表示法