ECMAScript 6 简介
ECMAScript 和 JavaScript 的关系
前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。
ES6 与 ECMAScript 2015 的关系
- ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
- ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简 称 ES2015)。
- ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵 盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标 准。
ECMAScript 的历史
ES6 从开始制定到最后发布,整整用了 15 年。
- ECMAScript 1.0 (1997)
- ECMAScript 2.0(1998年 6月)
- ECMAScript 3.0(1999年12月)
- 2000 年,ECMAScript 4.0 开始酝酿,这个版本最后没有通过,2007 年 10 月,ECMAScript 4.0 版草案发布
- 2009 年 12 月,ECMAScript 5.0 版正式发布
- 2011 年 6 月,ECMAScript 5.1 版发布
- 2013 年 3 月,ECMAScript 6 草案冻结
- 2013 年 12 月,ECMAScript 6 草案发布
- 2015 年 6 月,ECMAScript 6 正式通过
Babel 转码器
Babel是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在老版本的浏览器执行。这意味着,你可以用 ES6 的方 式编写程序,又不用担心现有环境是否支持。
// 转码前
input.map(item => item + 1);
// 转码后
input.map(function (item) {
return item + 1;
});
下面的命令在项目目录中,安装 Babel:
$ npm install --save-dev @babel/core
配置文件.babelrc
Babel 的配置文件是 .babelrc ,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。 该文件用来设置转码规则和插件,基本格式如下:
{
"presets": [],
"plugins": []
}
presets 字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。
# 最新转码规则
$ npm install --save-dev @babel/preset-env
# react 转码规则
$ npm install --save-dev @babel/preset-react
然后,将这些规则加入 .babelrc :
{
"presets": [
"@babel/env",
"@babel/preset-react"
],
"plugins": []
}
命令行转码
Babel 提供命令行工具 @babel/cli ,用于命令行转码。 它的安装命令如下:
$ npm install --save-dev @babel/cli
基本用法如下:
# 转码结果输出到标准输出
$ npx babel example.js
# 转码结果写入一个文件
# --out-file 或 -o 参数指定输出文件
$ npx babel example.js --out-file compiled.js
# 或者
$ npx babel example.js -o compiled.js
# 整个目录转码
# --out-dir 或 -d 参数指定输出目录
$ npx babel src --out-dir lib
# 或者
$ npx babel src -d lib
# -s 参数生成source map文件
$ npx babel src -d lib -s
polyfill
Let和Const命令
let命令
所声明的变量,只在 let 命令所在的代码块内有效。
//表明, let 声明的变量只在它所在的代码块有效。
{
let a = 10;
var b = 1;
}
console.log(a); // ReferenceError: i is not defined
console.log(b);
for 循环的计数器,就很合适使用 let 命令。计数器 i 只在 for 循环体内有效,在循环体外引用就会报错。
var a = [];
for (var i = 0; i < 10; i++) {//变量 i 是 var 命令声明的,在全局范围内都有效
a[i] = function () {//循环内被赋给数组 a 的函数内部的 console.log(i),里面的i指向的就是全局的i
console.log(i);
};
}
a[6](); // 10
使用 let ,声明的变量仅在块级作用域内有效,最后输出的是 6。
var a = [];
for (let i = 0; i < 10; i++) {
a[i] = function () {
console.log(i);
};
}
a[6](); // 6
//表明函数内部的变量 i 与循环变量 i 不在同一个作用域,有各自单独的作用域(同一个作用域不可使用 let 重复声明同一个变量)
for (let i = 0; i < 3; i++) {
let i = 'abc';
console.log(i);
}
// abc
// abc
// abc
不存在变量提升
- var 命令会发生“变量提升”现象,在声明之前使用,值为 undefined
- let 命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。
// var 的情况
console.log(foo); // 输出undefined
var foo = 2;
// let 的情况
console.log(bar); // 报错ReferenceError
let bar = 2;
暂时性死区
只要块级作用域内存在 let 命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。
var tmp = 123;
if (true) {
tmp = 'abc'; // ReferenceError,声明前调用报错
let tmp;
}
-
如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。
-
在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性 死区”(temporal dead zone,简称 TDZ)。
-
在 let 命令声明变量 tmp 之前,都属于变量 tmp 的“死区”
typeof x; // ReferenceError
let x;
- 变量 x 使用 let 命令声明,所以在声明之前,都属于 x 的“死区”,只要用到该变量就会报错。
- 如果一个变量根本没有被声明,使用 typeof 反而不会报错。
typeof undeclared_variable // "undefined"
- 有些“死区”比较隐蔽,不太容易发现
function bar(x = y, y = 2) {//参数 x 默认值等于另一个参数y,而此时y还没有声明,属于“死区”。
return [x, y];
}
bar(); // 报错
function bar(x = 2, y = x) {
return [x, y];
}
bar(); // [2, 2]
// 不报错
var x = x;
// 报错
let x = x;
// ReferenceError: x is not defined
- 在变量 x 的声明语句还没有执行完成前,就去取 x 的值,导致报 错”x 未定义“。
不允许重复声明
- let 不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
let a = 10;
var a = 1;
}
- 不能在函数内部重新声明参数。
function func(arg) {
let arg;
}
func() // 报错
function func(arg) {
{
let arg;
}
}
func() // 不报错
const命令
- const 声明一个只读的常量。一旦声明,常量的值就不能改变。
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
- const 声明的变量不得改变值, const 一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;// SyntaxError: Missing initializer in const declaration
- const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。
- const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用,不可重复声明
本质
const 实际上保证的是变量指向的那个内存地址所保存的数据不得改动。
-
对于简单类型的数据,值就保存在变量指向的那个内存地址
-
对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向 实际数据的指针,保证这个指针是固定的
const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only
ES6 声明变量的六种方法
ES5 只有两种声明变量的方法:var
命令和 function
命令
ES6 除了添加let
和const
命令,还有import
命令和class
命令
顶层对象的属性
- 浏览器环境:指的是 window 对象
- Node: global 对象
- ES5:顶层对象的属性与全局变量是等价的
- var 命令和 function 命令声明的全局变 量,依旧是顶层对象的属性
let
命令、const
命令、class
命令声明的全局变量,不 属于顶层对象的属性
变量的解构赋值
数组的解构赋值
ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。
- 只要等号两边的模式相同,左边的变量就会被赋予对应的值
let [foo, [[bar], baz]] = [1, [[2], 3]];
console.log(foo); // 1
console.log(bar); // 2
console.log(baz); // 3
let [ , , third] = ["foo", "bar", "baz"];
console.log(third); // "baz"
let [x, , y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 3
let [head, ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [2, 3, 4]
- 不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组
let [x, y] = [1, 2, 3];
console.log(x); // 1
console.log(y); // 2
let [a, [b], d] = [1, [2, 3], 4];
console.log(a); // 1
console.log(b); // 2
console.log(d); // 4
- 等号的右边不是数组(不 是可遍历的结构),报错
// 报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)
let [foo] = 1;
let [foo] = false;
let [foo] = NaN;
let [foo] = undefined;
let [foo] = null;
let [foo] = {};
默认值
解构赋值允许指定默认值
let [foo = true] = [];
console.log(foo); // true
let [x, y = 'b'] = ['a'];//a b
// let [x, y = 'b'] = ['a', undefined];//ab
console.log(x);
console.log(y);
ES6 内部使用严格相等运算符( === ),判断一个位置是否有值。所以,只有当一个数组成员严格等于 undefined ,默认值才会生效。
let [x = 1] = [undefined];
console.log(x); // 1
let [x = 1] = [null];
console.log(x); // null
对象的解构赋值
解构不仅可以用于数组,还可以用于对象。
对象解构与数组的不同
- 数组的元素是按次序排列的,变量的取值由它的位置决定
- 对象的属性没有次序,变量必须与属性同名,才能取到正确的值
let { foo, bar } = { foo: 'aaa', bar: 'bbb' };
console.log(foo); // "aaa"
console.log(bar); // "bbb"
let { baz } = { foo: 'aaa', bar: 'bbb' };
console.log(baz); // undefined,变量没有对应的同名属性,导致取不到值
解构失败,变量的值等于 undefined
let {foo} = {bar: 'baz'};
console.log(foo); // undefined
let obj = {};
let arr = [];
({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });
console.log(obj); // {prop:123}
console.log(arr0; // [true]
默认值
默认值生效的条件是,对象的属性值严格等于 undefined 。
var {x = 3} = {x: undefined};
x // 3
var {x = 3} = {x: null};
x // null,因为 null 与 undefined 不严格相等
var {x = 3} = {};
console.log(x); // 3
var {x, y = 5} = {x: 1};
console.log(x); // 1
console.log(y); // 5
var {x: y = 3} = {};
console.log(y); // 3
var {x: y = 3} = {x: 5};
console.log(y); // 5
注意点
如果要将一个已经声明的变量用于解构赋值,必须非常小心(引擎会将 {x} 理解成一个代码块),应该在外层加一个括号
// 正确的写法
let x;
({x} = {x: 1});
字符串的解构赋值
const [abcde] = 'hello';
console.log(abcde); // "h"
const [a, b, c, d, e] = 'hello';
console.log(a); // "h"
console.log(b); // "e"
console.log(c); // "l"
console.log(d); // "l"
console.log(e); // "o"
类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
console.log(len); // 5
函数参数的解构赋值
函数的参数也可以使用解构赋值
//数组参数就被解构成变量x和y
function add([x, y]){
return x + y;
}
add([1, 2]); // 3
圆括号问题
- 一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到等号才能知道
- ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。
- 建议只要有可能,就不要在模式中放置圆括号。
不能使用圆括号的情况
-
变量声明语句
// 全部报错,都是·变量声明·语句,模式不能使用圆括号。 let [(a)] = [1]; let {x: (c)} = {}; let ({x: c}) = {}; let {(x: c)} = {}; let {(x): c} = {};
-
函数参数:也属于变量声明语句,因此不能带有圆括号
//整个模式放在圆括号之中,报错 // 报错 function f([(z)]) { return z; } // 报错 function f([z,(x)]) { return x; } //一部分模式放在圆括号之中,报错 [({ p: a }), { x: c }] = [{}, {}];
-
赋值语句的模式
// 全部报错 ({ p: a }) = { p: 42 }; ([a]) = [5];
可以使用圆括号的情况
可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。
//是赋值语句,不是声明语句,圆括号都不属于模式的一部分
[(b)] = [3];//正确,模式是取数组的第一个成员,跟圆括号无关
({ p: (d) } = {}); // 正确,模式是p,而不是d
[(parseInt.prop)] = [3]; // 正确
常见用法
-
交换变量的值
let x = 1; let y = 2; [x, y] = [y, x];
-
从函数返回多个值
// 返回一个数组 function example() { return [1, 2, 3]; } let [a, b, c] = example(); console.log(a); //1 console.log(example()); //[ 1, 2, 3 ] // 返回一个对象 function example() { return { foo: 1, bar: 2 }; } let { foo, bar } = example();
-
函数参数的定义
方便地将一组参数与变量名对应起来
// 参数是一组有次序的值 function f([x, y, z]) { ... } f([1, 2, 3]); // 参数是一组无次序的值 function f({x, y, z}) { ... } f({z: 3, y: 2, x: 1});
-
提取 JSON 数据
let jsonData = { id: 42, status: "OK", data: [867, 5309] }; let { id, status, data: number } = jsonData; console.log(id, status, number);//42 OK [ 867, 5309 ]
-
函数参数的默认值
jQuery.ajax = function (url, { async = true, beforeSend = function () {}, cache = true, complete = function () {}, crossDomain = false, global = true, // ... more config } = {}) { // ... do stuff };
-
输入模块的指定方法
const { SourceMapConsumer, SourceNode } = require("source-map");
扩展运算符和rest运算符
解决函数参数和数组元素长度未知情况下的编码问题
扩展运算符
- 用3个点表示(…)
- 用于将一个数组或类数组对象转换为用逗号分隔的值序列(单独的值的序列)
- 基本用法是拆解数组和字符串。
const array = [1,2,3,4];
console.log(...array);//1 2 3 4
const str = 'string';
console.log(...str);//s t r i n g
扩展运算符代替apply()函数
将数组转换为函数参数
let arr = [1,4,6,8,2];
console.log(Math.max.apply(null,arr));//8
let arr = [1,4,6,8,2];
console.log(Math.max(...arr));//8
扩展运算符代替concat()函数合并数组
在ES5中,合并数组时,我们会使用concat()函数
let arr1 = [1,2,3];
let arr2 = [4,5,6];
console.log(arr1.concat(arr2));//[ 1, 2, 3, 4, 5, 6 ]
console.log([...arr1,...arr2]);//[ 1, 2, 3, 4, 5, 6 ]
rest运算符
其作用与扩展运算符相反,用于将以逗号分隔的值序列转换成数组。
rest运算符与解构组合使用
将其中的一部分值统一赋值给一个 变量时,可以使用rest运算符
let arr = ['one','two','three','four'];
let[arg1,...arg2] = arr;
//let[...arg1,arg2] = arr;//抛出异常
console.log(arg1);//one
console.log(arg2);//[ 'two', 'three', 'four' ]
rest运算符代替arguments处理函数参数
在ES6之前,如果我们不确定传入的参数长度,可以统一使用arguments来获取所有传递的参数
function foo() {
for (let arg of arguments) {
console.log(arg);
}
}
foo('one', 'two', 'three', 'four');
//one
//two
//three
//four
参数是使用逗号分隔的值序列,可以使用rest运算符处理成一个数组
function foo(...args) {
for (let arg of args) {
console.log(arg);
}
}
foo('one', 'two', 'three', 'four');
//one
//two
//three
//four
扩展运算符和rest运算符的区别
- 两者是互为逆运算的。扩展运算符是将数组分割成独立的序列,而rest运算符是将独立的序列合并成一个数组。
- 当三个点出现在函数的形参上或者出现在赋值等号的左侧,则表示它为rest运算符。
- 当三个点出现在函数的实参上或者出现在赋值等号的右侧,则表示它为扩展运算符。
字符串的新增方法
模板字符串
模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串 使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。
<div id="result">
</div>
<script src="https://libs.baidu.com/jquery/1.11.1/jquery.min.js"></script>
<script>
var basket = {
count: 5,
onSale: '小鸟'
}
$('#result').append(
`
There are <b>${basket.count}</b> items
in your basket, <em>${basket.onSale}</em>
are on sale!
`
);
</script>
-
模板字符串中需要使用反引号,则前面要用反斜杠转义。
let greeting = `\`Yo\` World!`;
-
模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。
$('#result').html(` <ul> <li>first</li> <li>second</li> </ul> `);
-
可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。
let x = 1; let y = 2; `${x} + ${y} = ${x + y}` // "1 + 2 = 3" `${x} + ${y * 2} = ${x + y * 2}` // "1 + 4 = 5" let obj = {x: 1, y: 2}; `${obj.x + obj.y}` // "3"
-
模板字符串之中还能调用函数。
function fn() { return "Hello World"; } console.log(`foo ${fn()} bar`); // foo Hello World bar
-
如果大括号内部是一个字符串,将会原样输出。
`Hello ${'World'}` // "Hello World"
includes(), startsWith(), endsWith()
JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。
- includes():返回布尔值,表示是否找到了参数字符串。
- startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
- endsWith():返回布尔值,表示参数字符串是否在原字符串的尾部。
let s = 'Hello world!';
console.log(s.startsWith('Hello')); // true
console.log(s.endsWith('!')); // true,它针对结尾字符
console.log(s.includes('o')); // true
let s = 'Hello world!';
console.log(s.startsWith('world', 6)); // true,从第6个开始匹配
console.log(s.endsWith('Hello wo',8)); // true,它针对前 n 个字符
console.log(s.includes('Hello', 6)); // false
padStart(),padEnd()
ES2017 引入了字符串补全长度的功能,padStart() 用于头部补全, padEnd() 用于尾部补全。
语法:
-
第一个参数是字符串补全生效的最大长度
-
第二个参数是用来补全的字符串。
-
如果原字符串的长度,等于或大于最大长度,则字符串补全不生效
console.log('x'.padStart(5, 'ab')); // 'ababx'
console.log('x'.padStart(4, 'ab')); // 'abax'
console.log('x'.padEnd(5, 'ab')); // 'xabab'
console.log('x'.padEnd(4, 'ab')); // 'xaba'
- 等于或大于最大长度,则字符串补全不生效
console.log('xxx'.padStart(2, 'ab')); // 'xxx'
console.log('xxx'.padEnd(2, 'ab')); // 'xxx'
- 两者的长度之和超过了最大长度,则会截去超出位数的补全字符 串。
console.log('abc'.padStart(10, '0123456789'));// '0123456abc'
- 省略第二个参数,默认使用空格补全长度。
console.log('x'.padStart(4)); // ' x'
console.log('x'.padEnd(4)); // 'x '
用途:
-
为数值补全指定位数。
console.log('1'.padStart(10, '0')); // "0000000001" console.log('12'.padStart(10, '0')); // "0000000012" console.log('123456'.padStart(10, '0')); // "0000123456"
-
提示字符串格式。
console.log('12'.padStart(10, 'YYYY-MM-DD')); // "YYYY-MM-12" console.log('09-12'.padStart(10, 'YYYY-MM-DD')); // "YYYY-09-12"
trimStart(),trimEnd()
trimStart() 消除字符串头部的空格, trimEnd() 消除尾部的空格。它们返回的都是新字符串, 不会修改原始字符串。
const s = ' abc ';
console.log(s.trim()); // "abc"
console.log(s.trimStart()); // "abc "
console.log(s.trimEnd()); // " abc"
replaceAll()
replace() 只能替换第一个匹配。
console.log('aabbcc'.replace('b', '_'));// 'aa_bcc'
console.log('aabbcc'.replace(/b/g, '_'));// 'aa__cc'
replaceAll() 方法,可以一次性替换所有匹配。
console.log('aabbcc'.replaceAll('b', '_'));// 'aa__cc'
数值的新增方法
Number.isInteger()
Number.isInteger() 用来判断一个数值是否为整数。
console.log(Number.isInteger(25)); // true
console.log(Number.isInteger(25.1)); // false
console.log(Number.isInteger(25.0)); // true
如果参数不是数值, Number.isInteger 返回 false 。
console.log(Number.isInteger()); // false
console.log(Number.isInteger(null)); // false
console.log(Number.isInteger('15')); // false
console.log(Number.isInteger(true)); // false
//如果数值的精度超过这个限度,第54位及后面的位就会被丢弃
console.log(Number.isInteger(3.0000000000000002)); // true
Math.trunc()
Math.trunc 方法用于去除一个数的小数部分,返回整数部分。
console.log(Math.trunc(4.1)); // 4
console.log(Math.trunc(4.9)); // 4
console.log(Math.trunc(-4.1)); // -4
console.log(Math.trunc(-4.9)); // -4
console.log(Math.trunc(-0.1234)); // -0
对于非数值, Math.trunc 内部使用 Number 方法将其先转为数值。
console.log(Math.trunc('123.456')); // 123
console.log(Math.trunc(true)); //1
console.log(Math.trunc(false)); // 0
console.log(Math.trunc(null)); // 0
对于空值和无法截取整数的值,返回 NaN 。
console.log(Math.trunc(NaN)); // NaN
console.log(Math.trunc('foo')); // NaN
console.log(Math.trunc()); // NaN
console.log(Math.trunc(undefined)); // NaN
Math.sign()
Math.sign 方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。 它会返回五种值。
- 参数为正数,返回 +1 ;
- 参数为负数,返回 -1 ;
- 参数为 0,返回 0 ;
- 参数为-0,返回 -0 ;
- 其他值,返回 NaN 。
console.log(Math.sign(-5)); // -1
console.log(Math.sign(5)); // +1
console.log(Math.sign(0)); // +0
console.log(Math.sign(-0)); // -0
console.log(Math.sign(NaN)); // NaN
如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回 NaN 。
console.log(Math.sign('')); // 0
console.log(Math.sign(true)); // +1
console.log(Math.sign(false)); // 0
console.log(Math.sign(null)); // 0
console.log(Math.sign('9')); // +1
console.log(Math.sign('foo')); // NaN
console.log(Math.sign()); // NaN
console.log(Math.sign(undefined)); // NaN
函数的扩展
函数参数的默认值
基本用法
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
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
function foo({x, y = 5} = {}) {
console.log(x, y);
}
foo() // undefined 5
如果函数 fetch 的第二个参数是一个对象,就可以为它的三个属性设置默认值。
function fetch(url, { body = '', method = 'GET', headers = {} }) {
console.log(method);
}
fetch('http://example.com', {})
// "GET"
fetch('http://example.com')
// 报错
参数默认值的位置
定义了默认值的参数,应该是函数的尾参数(有默认值的参数都不是尾参数)。
如果传入 undefined ,将触发该参数等于默认值, null 则没有这个效果。
function f(x = 1, y) {
return [x, y];
}
f() // [1, undefined]
f(2) // [2, undefined]
f(, 1) // 报错
f(undefined, 1) // [1, 1]
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
作用域
一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等 到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。
var x = 1;
function f(x, y = x) {
console.log(y);
}
f(2) // 2
函数 f 调用时,参数 y = x 形成一个单独的作用域。这个作用域里面,变量 x 本身没 有定义,所以指向外层的全局变量 x 。函数调用时,函数体内部的局部变量 x 影响不到默认值变量 x 。 如果此时,全局变量 x 不存在,就会报错
function f(y = x) {
let x = 2;
console.log(y);
}
f() // ReferenceError: x is not defined
箭头函数
-
ES6 允许使用“箭头”( => )定义函数。
-
如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。
var f = () => 5; // 等同于 var f = function () { return 5 }; var sum = (num1, num2) => num1 + num2; // 等同于 var sum = function(num1, num2) { return num1 + num2; };
-
如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返 回。
// 报错 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; }
使用注意点
- 箭头函数没有自己的 this 对象(详见下文)。(内部的 this 就是定义时上层作用域中的 this 。 也就是说,箭头函数内部的 this 指向是固定的)
- 不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误。
- 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
- 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。
不适用场合
-
第一个场合是定义对象的方法,且该方法内部包括 this 。
const cat = { lives: 9, jumps: () => { this.lives--;//this 指向全局对象 } }
-
第二个场合是需 要动态 this 的时候,也不应使用箭头函数。
var button = document.getElementById('press'); button.addEventListener('click', () => { this.classList.toggle('on'); });//button 的监听函数是一个箭头函数,导致里面的 this 就是全局对象。如果改成普通函数, this 就会动态指向被点击的按钮对象。
嵌套的箭头函数
function insert(value) { return { into: function (array) { return { after: function (afterValue) { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; } }; } }; } insert(2).into([1, 3]).after(1); //[1, 2, 3] //用箭头函数 let insert = (value) => ({ into: (array) => ({ after: (afterValue) => { array.splice(array.indexOf(afterValue) + 1, 0, value); return array; } }) }); insert(2).into([1, 3]).after(1); //[1, 2, 3]
数组的扩展
Array.from()
只要是部署了 Iterator 接口的数据结构, Array.from 都能将其转为数组。
用于将两类对象转为真正的数组:
- 类似数组的对象(array-like object)
- 可遍历 (iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。
let arrayLike = {
'0': 'a',
'1': 'b',
'2': 'c',
length: 3
};
// ES5的写法
var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']
// ES6的写法
let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
Array.from('hello')
// ['h', 'e', 'l', 'l', 'o']
let namesSet = new Set(['a', 'b'])
Array.from(namesSet) // ['a', 'b']
Array.of()
用于将一组值,转换为数组
Array.of(3, 11, 8) // [3,11,8]
Array.of(3) // [3]
Array.of(3).length // 1
主要目的,是弥补数组构造函数 Array() 的不足。因为参数个数的不同,会导致 Array() 的行为有差异。
只有当参数 个数不少于 2 个时, Array() 才会返回由参数组成的新数组
Array() // []
Array(3) // [, , ,]
Array(3, 11, 8) // [3, 11, 8]
Array.of() 基本上可以用来替代 Array() 或 new Array() ,并且不存在由于参数不同而导致的 重载。
Array.of() // []
Array.of(undefined) // [undefined]
Array.of(1) // [1]
Array.of(1, 2) // [1, 2]
数组实例的 fill()
fill 方法使用给定值,填充一个数组。
['a', 'b', 'c'].fill(7)
// [7, 7, 7]
new Array(3).fill(7)
// [7, 7, 7]
fill 方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。
['a', 'b', 'c'].fill(7, 1, 2)
// ['a', 7, 'c']
注意,如果填充 的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。
let arr = new Array(3).fill({name: "Mike"});
arr[0].name = "Ben";
console.log(arr);
// [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]
let arr = new Array(3).fill([]);
arr[0].push(5);
console.log(arr);
// [[5], [5], [5]]
数组实例的 flat(),flatMap()
数组的成员有时还是数组, Array.prototype.flat() 用于将嵌套的数组“拉平”,变成一维的数 组。该方法返回一个新数组,对原数据没有影响,
[1, 2, [3, 4]].flat()
// [1, 2, 3, 4]
[1, 2, [3, [4, 5]]].flat(2)
// [1, 2, 3, 4, 5]
如果不管有多少层嵌套,都要转成一维数组,可以用 Infinity 关键字作为参数。
[1, [2, [3]]].flat(Infinity)
// [1, 2, 3]
如果原数组有空位, flat() 方法会跳过空位。
[1, 2, , 4, 5].flat()
// [1, 2, 4, 5]
flatMap() 方法对原数组的每个成员执行一个函数(相当于执行 Array.prototype.map() ),然 后对返回值组成的数组执行 flat() 方法。该方法返回一个新数组,不改变原数组。
flatMap() 只能展开一层数组。
// 相当于 [[2, 4], [3, 6], [4, 8]].flat()
[2, 3, 4].flatMap((x) => [x, x * 2])
// [2, 4, 3, 6, 4, 8]
// 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
[1, 2, 3, 4].flatMap(x => [[x * 2]])
// [[2], [4], [6], [8]]
对象的扩展
属性的简洁表示法
对象都会采用**{key:value}的写法,但是在ES6中,可以直接在对象中写入变 量**,key相当于变量名,value相当于变量值,并且可以直接省略value,通过key表示一个对象的完整属性。
const name = 'cao teacher';
const age = 18;
const obj = {
name,
age
};
// 等同于
const obj = {
name: 'caoteacher',
age: 18
}
console.log(age);
在定义obj对象时,变量名name
作为了对象的属性名,它的值作为了属性值,一 次只需要写一个{name}
就可以表示{name:name}
的含义。
除了属性可以简写,函数也可以简写,即省略掉关键字function
。
const obj = {
method: function () {
return 'hello';
}
};
//等同于
const obj = {
method() {
return 'hello';
}
};
按照commonJS写法,当需要输出一组模块变量时,对象简写的方法就非常合 适。
let obj = {};
//获取元素
function getItem(key) {
return key in obj ? obj[key] : null;
}
//增加元素
function setItem(key, value) {
obj[key] = value;
}
//清空对象
function clear() {
obj = {};
}
module.exports = {
getItem,
setItem,
clear
};
//等同于
module.exports = {
getItem: getItem,
setItem: setItem,
clear: clear
}
属性遍历
到ES6为止,一共有五种方法可以实现对象属性的变量
//定义父类
function Animal(name, type) {
this.name = name;
this.type = type;
}
//定义子类
function Cat(age, weight) {
this.age = age;
this.weight = weight;
this[Symbol('one')] = 'one';
}
//子类继承父类
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
//生成子类的实例
let cat = new Cat(12, '10kg');
//实例增加可枚举属性
Object.defineProperty(cat, 'color', {
configurable: true,
enumerable: true,
value: 'blue',
writable: true
});
//实例增加不可枚举属性
Object.defineProperty(cat, 'height', {
configurable: true,
enumerable: false,
value: '20cm',
writable: true
});
实例cat
具有的属性如下所示:
-
实例属性:
age,weight,Symbol('one'),color
-
继承属性:
name,type
-
可枚举属性:
age,weigth,color
-
不可枚举属性:
height Symbol
-
属性:
Symbol('one')
-
for...in
遍历对象和自身和继承的可枚举属性(不包含Symbol属性)for(let key in cat){ console.log(key); } //age weight color name type constructor
-
Object.keys(obj)
返回一个数组,包含可枚举属性,不包含继承属性和Symbol属 性。console.log(Object.keys(cat)); //[ 'age', 'weight', 'color' ]
-
Object.getOwnPropertyNames(obj)
返回一个数组,包含可枚举属性和不可枚举 属性,不包含继承属性和Symbol属性。
console.log(Object.getOwnPropertyNames(cat));
//[ 'age', 'weight', 'color', 'height' ]
Object.getOwnPropertySymbols(obj)
返回一个数组,包含对象自身所有Symbol属性,不包 含其他属性。
console.log(Object.getOwnPropertySymbols(cat));
//[ Symbol(one) ]
Reflect.ownKeys(obj)
返回一个数组,可包含枚举属性,不可枚举属性以及Symbol属性,不包 含继承属性。
console.log(Reflect.ownKeys(cat))
//[ 'age', 'weight', 'color', 'height', Symbol(one) ]
Object.is()
ES5 只有两个运算符:**相等运算符( == )
和严格相等运算符( === )
**缺点:前者会自动转换数据类型,后者的 NaN 不等于自身,以及 +0 等于 -0
Object.is
用来比较两个值是否严格相等,与严格比较运算符(===)
的行为基本一致。
Object.is('foo', 'foo')
// true
Object.is({}, {})
// false
不同之处只有两个:一是 +0 不等于 -0 ,二是 NaN 等于自身。
console.log(+0 === -0); //true
console.log(NaN === NaN); // false
console.log(Object.is(+0, -0)); // false
console.log(Object.is(NaN, NaN)); // true
Object.assign()
基本用法
用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象 (target)。如果目标对象 与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。
const target = { a: 1, b: 1 };
const source1 = { b: 2, c: 2 };
const source2 = { c: 3 };
Object.assign(target, source1, source2);
target // {a:1, b:2, c:3}
如果只有一个参数, Object.assign() 会直接返回该参数。
const obj = {a: 1};
Object.assign(obj) === obj // true
如果该参数不是对象,则会先转成对象,然后返回。
console.log(typeof Object.assign(2));// "object"
由于 undefined 和 null 无法转成对象,所以如果它们作为参数,就会报错。
Object.assign(undefined) // 报错
Object.assign(null) // 报错
let obj = {a: 1};
Object.assign(obj, undefined) === obj // true
Object.assign(obj, null) === obj // true
其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。除了字符串会以数组形 式,拷贝入目标对象,其他值都不会产生效果。
const v1 = 'abc';
const v2 = true;
const v3 = 10;
const obj = Object.assign({}, v1, v2, v3);
console.log(obj); // { "0": "a", "1": "b", "2": "c" }
只有字符串的包装对象,会产生可枚举属性。
注意点
-
浅拷贝
Object.assign()
方法实行的是浅拷贝,而不是深拷贝。 -
同名属性的替换
一旦遇到同名属性, Object.assign() 的处理方法是替换,而不是添加。
const target = { a: { b: 'c', d: 'e' } } const source = { a: { b: 'hello' } } Object.assign(target, source) // { a: { b: 'hello' } }
-
数组的处理
可以用来处理数组,但是会把数组视为对象
Object.assign([1, 2, 3], [4, 5]) // [4, 5, 3]
-
取值函数的处理
Object.assign()
只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。const source = { get foo() { return 1 } }; const target = {}; Object.assign(target, source) // { foo: 1 }
常见用途
-
为对象添加属性
class Point { constructor(x, y) { Object.assign(this, {x,y}); } }
通过 Object.assign() 方法,将 x 属性和 y 属性添加到 Point 类的对象实例。
-
为对象添加方法
Object.assign(SomeClass.prototype, { someMethod(arg1, arg2) { ··· }, anotherMethod() { ··· } }); // 等同于下面的写法 SomeClass.prototype.someMethod = function (arg1, arg2) { ··· }; SomeClass.prototype.anotherMethod = function () { ··· };
直接将两个函数放在大括号中,再使用 assign() 方法添 加到 SomeClass.prototype 之中。
-
克隆对象
function clone(origin) { return Object.assign({}, origin); }
代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承 链,可以采用下面的代码。
function clone(origin) { let originProto = Object.getPrototypeOf(origin); return Object.assign(Object.create(originProto), origin); }
-
合并多个对象
将多个对象合并到某个对象。
const merge =(target, ...sources) => Object.assign(target, ...sources);
合并后返回一个新对象,可以改写上面函数,对一个空对象合并。
const merge =(...sources) => Object.assign({}, ...sources);
-
为属性指定默认值
const DEFAULTS = { logLevel: 0, outputFormat: 'html' }; function processContent(options) { options = Object.assign({}, DEFAULTS, options); console.log(options); // ... }
Symbol
防止属性名的冲突。凡是属性名属于 Symbol 类型,就都是独一无二的,可以 保证不会与其他属性名产生冲突。
ES6 引入了一种新的原始数据类型 Symbol,它是 JavaScript 语言的第七种数 据类型,前六种是: undefined 、 null 、布尔值(Boolean)、字符串(String)、数值 (Number)、对象(Object)。
let s = Symbol();
console.log(typeof s);
// "symbol"
注意: Symbol 函数前不能使用 new 命令,否则会报错。
生成的 Symbol 是一个原始类型 的值,不是对象。
基本上,它是一种类似于 字符串的数据类型。
let s1 = Symbol('foo');
let s2 = Symbol('bar');
console.log(s1); // Symbol(foo)
console.log(s2); // Symbol(bar)
console.log(s1.toString()); // "Symbol(foo)"
console.log(s2.toString()); // "Symbol(bar)"
Symbol 函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的 Symbol 函数的返回 值是不相等的。Symbol 值不能与其他类型的值进行运算,会报错。
// 没有参数的情况
let s1 = Symbol();
let s2 = Symbol();
s1 === s2 // false
// 有参数的情况
let s1 = Symbol('foo');
let s2 = Symbol('foo');
s1 === s2 // false
Symbol 值可以显式转为字符串。
let sym = Symbol('My symbol');
String(sym) // 'Symbol(My symbol)'
sym.toString() // 'Symbol(My symbol)'
Symbol 值也可以转为布尔值,但是不能转为数值.
let sym = Symbol();
Boolean(sym) // true
!sym // false
if (sym) {
// ...
}
Number(sym) // TypeError
sym + 2 // TypeError
作为属性名的 Symbol
Symbol 值可以作为标识符,用于对象的属性名, 就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不 小心改写或覆盖。
let mySymbol = Symbol();
// 第一种写法
let a = {};
a[mySymbol] = 'Hello!';
// 第二种写法
let a = {
[mySymbol]: 'Hello!'
};
// 第三种写法
let a = {};
Object.defineProperty(a, mySymbol, { value: 'Hello!' });
// 以上写法都得到同样结果
a[mySymbol] // "Hello!"
通过方括号结构和 Object.defineProperty ,将对象的属性名指定为一个 Symbol 值。 注意,Symbol 值作为对象属性名时,不能用点运算符。(点运算符后面总是字符串)
const mySymbol = Symbol();
const a = {};
a.mySymbol = 'Hello!';
a[mySymbol] // undefined
a['mySymbol'] // "Hello!"
在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。
实例:消除魔术字符串
魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值(不利于将 来的修改和维护)。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。
const shapeType = {
triangle: 'Triangle'
};
function getArea(shape, options) {
let area = 0;
switch (shape) {
case shapeType.triangle:
area = .5 * options.width * options.height;
break;
}
return area;
}
getArea(shapeType.triangle, {
width: 100,
height: 100
});
属性名的遍历
Symbol 作为属性名,遍历对象的时候,该属性不会出现在 for...in 、 for...of
循环中,也不会 被 Object.keys() 、 Object.getOwnPropertyNames() 、 JSON.stringify()
返回。
它也不是私有属性,有一个Object.getOwnPropertySymbols()
方法,可以获取指定对象 的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。
const obj = {};
let a = Symbol('a');
let b = Symbol('b');
obj[a] = 'Hello';
obj[b] = 'World';
const objectSymbols = Object.getOwnPropertySymbols(obj);
console.log(objectSymbols);
// [Symbol(a), Symbol(b)]
由于以 Symbol 值作为键名,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些 非私有的、但又希望只用于内部的方法。
Set 和 Map 数据结构
Set用法
它类似于数组,但是成员的值都是唯一的,没有重复的值。 Set 本 身是一个构造函数,用来生成 Set 数据结构。
const s = new Set();
[2, 3, 5, 4, 5, 2, 2].forEach(x => s.add(x));
for (let i of s) {
console.log(i);
}
// 2 3 5 4
Set 函数可 以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。
// 例一
const set = new Set([1, 2, 3, 4, 4]);
[...set]
// [1, 2, 3, 4]
// 例二
const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
items.size // 5
// 例三
const set = new Set(document.querySelectorAll('div'));
set.size // 56
// 类似于
const set = new Set();
document
.querySelectorAll('div')
.forEach(div => set.add(div));
set.size // 56
// 去除数组的重复成员
[...new Set(array)]
//去除字符串里面的重复字符
[...new Set('ababbc')].join('')
// "abc"
向 Set 实例添加了两次 NaN ,但是只会加入一个。这表明,在 Set 内部,两个 NaN 是相等的。 另外,两个对象总是不相等的。
let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}
Set 实例的属性和方法
Set 结构的实例有以下属性。
Set.prototype.constructor
:构造函数,默认就是 Set 函数。Set.prototype.size
:返回 Set 实例的成员总数。
Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。
Set.prototype.add(value)
:添加某个值,返回 Set 结构本身。Set.prototype.delete(value)
:删除某个值,返回一个布尔值,表示删除是否成功。Set.prototype.has(value)
:返回一个布尔值,表示该值是否为 Set 的成员。Set.prototype.clear()
:清除所有成员,没有返回值。
Array.from 方法可以将 Set 结构转为数组。
这就提供了去除数组重复成员的另一种方法。
function dedupe(array) {
return Array.from(new Set(array));
}
console.log(dedupe([1, 1, 2, 3]));// [1, 2, 3]
Set常见用法
-
单一数组的去重
由于set成员值具有唯一性,因此可以使用Set来进行数组的去重。
let arr = [1,2,2,3,3,3,4,4,5,5,6,6]; console.log(new Set(arr));
-
多个数组的合并去重
Set可以用于单个数组的去重,也可以用于多个数组的合并去重。
实现方法是先使用扩展运算符将多个数组处理成一个数组,然后将合并后得到的数组传递给Set构 造函数。
let arr1 = [1,2,3,4]; let arr2 = [1,2,3,4,5,6]; let set1 = new Set([...arr1,...arr2]); console.log(set1);
-
Set与数组的转换
将数组转换为Set时,只需要通过Set的构造函数即可,将Set转换为数组时,通过Array.from() 函数或者扩展运算符即可。
let arr = [1, 3, 5, 7]; //将数组转换为Set let set = new Set(arr); console.log(set);//Set(4) { 1, 3, 5, 7 } let set = new Set(); set.add('a'); set.add('b'); //将Set转换为数组通过Array.from()函数 let arr = Array.from(set); console.log(arr);//[ 'a', 'b' ] //将Set转换为数组通过扩展运算符 let arr2 = [...set]; console.log(arr2);//[ 'a', 'b' ]
Set的遍历
针对Set数据结构,我们可以使用传统的forEach()函数进行遍历,
forEach()
函数的第一个参数表示 的是Set中的每个元素,第二个参数表示的元素的索引,
let set = new Set([4,5,'hello']);
set.forEach((item,index)=>{
console.log(item,index);
});
除了forEach()
函数外,我们还可以使用以下3种函数对Set实例进行遍历。因为Set实例的键和值是相等的,所以keys()函数和values()函数实际返回的是相同的值。
keys()
:返回键名的遍历器。values()
:返回键值的遍历器。entries()
:返回键值对的遍历器。
let set = new Set(['red', 'blue', 'yellow']);
for (let item of set.keys()) {
console.log(item);//red blue yellow
}
for (let item of set.values()) {
console.log(item);//red blue yellow
}
for (let item of set.entries()) {
console.log(item);
}
//[ 'red', 'red' ]
//[ 'blue', 'blue' ]
//[ 'yellow', 'yellow' ]
Map用法
JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当 作键。与传统的对象字面量类 似,它的本质是一种键值对的组合,但是与对象字面量不同的是,对象字面量的键只能是字符串,对于 非字符串类型的值会采用强制类型转换为字符串,而Map的键却可以由各种类型的值组成。
const m = new Map();
const o = {p: 'Hello World'};
m.set(o, 'content')
console.log(m.get(o));// "content"
console.log(m.has(o)); // true
console.log(m.delete(o));// true
console.log(m.has(o)); // false
const map = new Map([
['name', '张三'],
['title', 'Author']
]);
console.log(map.size);// 2
console.log(map.has('name')) // true
console.log(map.get('name'))// "张三"
console.log(map.has('title')) // true
console.log(map.get('title')) // "Author"
不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都 可以当作 Map 构造函数的参数。这就是说, Set 和 Map 都可以用来生成新的 Map。
let map = new Map();
map.set(-0, 123);
map.get(+0) // 123
map.set(true, 1);
map.set('true', 2);
map.get(true) // 1
map.set(undefined, 3);
map.set(null, 4);
map.get(undefined) // 3
map.set(NaN, 123);
map.get(NaN) // 123
实例的属性和操作方法
-
size 属性
返回 Map 结构的成员总数
const map = new Map(); map.set('foo', true); map.set('bar', false); console.log(map.size); // 2
-
Map.prototype.set(key, value)
设置键名 key 对应的键值为 value ,然后返回整个 Map 结构。如果 key 已经有值,则键值会被更新,否则就新生成该键。
const m = new Map(); m.set('edition', 6) // 键是字符串 m.set(262, 'standard') // 键是数值 m.set(undefined, 'nah') // 键是 undefined
set 方法返回的是当前的 Map 对象,因此可以采用链式写法。
let map = new Map() .set(1, 'a') .set(2, 'b') .set(3, 'c');
-
Map.prototype.get(key)
get 方法读取 key 对应的键值,如果找不到 key ,返回 undefined
const m = new Map(); const hello = function() {console.log('hello');}; m.set(hello, 'Hello ES6!') // 键是函数 console.log(m.get(hello)); // Hello ES6!
-
Map.prototype.has(key)
has 方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。
const m = new Map(); m.set('edition', 6); m.set(262, 'standard'); m.set(undefined, 'nah'); m.has('edition') // true m.has('years') // false m.has(262) // true m.has(undefined) // true
-
Map.prototype.delete(key)
delete 方法删除某个键,返回 true 。如果删除失败,返回 false
const m = new Map(); m.set(undefined, 'nah'); console.log(m.has(undefined)); // true m.delete(undefined) console.log(m.has(undefined)); // false
-
Map.prototype.clear()
clear 方法清除所有成员,没有返回值。
let map = new Map(); map.set('foo', true); map.set('bar', false); console.log(map.size); // 2 map.clear() console.log(map.size); // 0
遍历方法
Map 结构原生提供三个遍历器生成函数和一个遍历方法。
- Map.prototype.keys() :返回键名的遍历器。
- Map.prototype.values() :返回键值的遍历器
- Map.prototype.entries() :返回所有成员的遍历器。
- Map.prototype.forEach() :遍历 Map 的所有成员。
Map 结构转为数组结构,比较快速的方法是使用扩展运算符( … )。
结合数组的 map 方法、 filter 方法,可以实现 Map 的遍历和过滤(Map 本身没有 map 和 filter 方 法)。
与其他数据结构的互相转换
-
Map 转为数组
使用扩展运算符( … )。
-
数组 转为 Map
将数组传入 Map 构造函数,就可以转为 Map。
-
Map 转为对象
将数组传入 Map 构造函数,就可以转为 Map。
-
对象转为 Map
对象转为 Map 可以通过 Object.entries() 。
-
Map 转为 JSON
Map 的键名都是字符串,这时可以选择转为对象 JSON。
function mapToArrayJson(map) { return JSON.stringify([...map]); } let myMap = new Map().set(true, 7).set({foo: 3},['abc']); mapToArrayJson(myMap); // '[[true,7],[{"foo":3},["abc"]]]'
-
JSON 转为 Map
正常情况下,所有键名都是字符串
function objToStrMap(obj) { let strMap = new Map(); for (let k of Object.keys(obj)) { strMap.set(k, obj[k]); } return strMap; } function jsonToStrMap(jsonStr) { return objToStrMap(JSON.parse(jsonStr)); } console.log(jsonToStrMap('{"yes": true, "no": false}')); // Map {'yes' => true, 'no' => false}
Proxy
可以理解为代理器,主要用于改变对象的默认访问行为,实际 表现是在访问对象之前增加一层拦截,任何对对象的访问行为都会通过这层拦截。在拦截中,我们可以 增加自定义的行为。
基本语法:const proxy = new Proxy(target,handler);
一个参数是目标对象target,另一个是配置对象handler,
Proxy,target和handler之间的关系是什么样的呢?
通过Proxy构造函数可以生成实例proxy,任何对proxy实例的属性的访问都会自动转发值target对 象上,我们可以针对访问的行为配置自定义的handler对象,因此外界通过proxy访问target对象的属性 时,都会执行hanlder对象自定义的拦截操作。
//定义目标对象
const person = {
name: 'caoteacehr',
age: 22
};
//定义配置对象
let handler = {
get: function (target, prop, receiver) {
console.log('你访问了person的属性');
return target[prop];
}
};
//生成Proxy的实例
const p = new Proxy(person, handler);
//执行结果
console.log(p.name);
//你访问了person的属性
//caoteacher
使用Proxy时,应注意:
-
必须通过代理实例访问
如果直接通过目标对象 person 访问name 属性,则不会触发栏截行为。
-
配置对象不能为空对象
如果为空对象,则代表没有设置任何拦截,实际是对目标对象的访问。另外配置对象不能为null,否则会抛出异常。
Proxy实例函数及其基本使用
通过访问代理对象的属性来触发自定义配置对象的get()函数而get()函数只是 Proxy 实例支持的总共 13种函数中的一种,这13种函数汇总如下。
-
get(target,propKey,receiver)。
拦截对象属性的读取操作,例如调用proxy.name或者proxy[name],其中target表示是目标对 象,propKey表示的是读取的属性值,receiver表示的是配置对象。
-
set(target,propKey,value,receiver)。
拦截对象属性的写操作,即设置属性值,例如proxy.name='kingx’或者proxy[name] = ‘kingx’,其 中target表示目标对象,propKey表示的是将要设置的属性,value表示将要的属性的值,receiver 表示的是配置对象。
-
has(target.propKey)。
拦截hasProperty的操作,返回一个布尔值,最典型的表现形式是执行propKey in target,其中 target 表示目标对象,propKey表示判断的属性。
-
deleteProperty(target,propKey)。
拦截delete proxy[popKey]的操作,返回一个布尔值,表示是否执行成功,其中target表示目标对 象,propKey表示将要删除的属性。
-
ownKeys(target)
拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、 Object.keys(proxy)、for…in循环等操作,其中target表示的是获取对象自身所有的属性名。
-
getownPropertyDescriptor(target,propKey)
-
definePropertyitarget,propKey,propDesc)
-
preventExtensions(target)
-
getPrototypeOf(target)
-
isExtensible(target)
-
setPrototypeOf(target, proto)
-
apply(target, object,args)。
-
construct(target,args)。
读取不存在属性
在正常情况下,读取一个对象不存在属性时,会返回undefined,通过Proxy的get()函数可以设置读取不存在的属性时抛出异常,从而避免对undefined值得兼容性处理。
let person = {
name: 'cao teacher'
}
const proxy = new Proxy(person, {
get: function (target, propKey) {
if (propKey in target) {
return target[propKey];
} else {
throw new ReferenceError(`访问的属性${propKey}不存在`);
}
}
});
console.log(proxy.name);
console.log(proxy.age);
读取负索引的值
数组的索引值是从0开始依次递增的,正常情况下我们无法读取负索引的值,但是通过Proxy()的 get()函数可以做到这一点。
const arr = [1, 4, 9, 16, 25];
const proxy = new Proxy(arr, {
get: function (target, index) {
index = Number(index);
if (index > 0) {
return target[index];
} else {
//索引值为负值,则从尾部元素开始计算索引
return target[target.length + index];
}
}
});
console.log(proxy[2]);//9
console.log(proxy[-2]);//16
禁止访问私有属性
在一些约定俗成的写法中,私有属性都会以下划线(_)开头,事实上我们并不希望用户能访问到私有 属性,这可以通过设置Proxy的get()函数来实现
const person = {
name: 'cao teacher',
_pwd: '123456'
}
const proxy = new Proxy(person, {
get: function (target, prop) {
if (prop.indexOf('_') === 0) {
throw new ReferenceError('不能直接访问私有属性');
} else {
return target[prop];
}
}
});
console.log(proxy.name);
console.log(proxy._pwd);
实现真正的私有
通过Proxy处 理下划线写法来实现真正的私有。
真正的私有要达到的目标有以下几个。
- 不能访问到私有属性,如果访问到私有属性则返回"undefined"。
- 不能直接修改私有属性,即是设置了也无效。
- 不能遍历出私有属性,遍历出来的属性中不会包含私有属性。
const apis = {
_apiKey: '12ab34cd56ef',
getAllUsers: function () {
console.log('这是查询全部用户的函数');
},
getUserById: function (userId) {
console.log('这是根据用户ID查询用户的函数');
},
saveUser: function (user) {
console.log('这是保存用户的函数');
}
};
const proxy = new Proxy(apis, {
get: function (target, prop) {
if (prop[0] === '_') {
return undefined;
}
return target[prop];
},
set: function (target, prop, value) {
if (prop[0] !== '_') {
target[prop] = value;
}
},
has: function (target, prop) {
if (prop[0] === '_') {
return false;
}
return prop in target;
}
});
console.log(proxy.apiKey);//undefined
console.log(proxy.getAllUsers());//这是查询全部用户的函数
proxy._apiKey = '123456789';//undefined
console.log('getUserById' in proxy);//true
console.log('_apiKey' in proxy);//false