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 [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
增加日志记录
可以通过Proxy作为中间件增加日志记录。
需要使用Proxy进行拦截,首先通过get()函数拦截调用的函数名,然后 通过apply()函数进行函数的调用。
在实现上,get()函数会返回一个函数,在这个函数内通过apply()函数调用原始函数,然后调用记录操作日志的函数。
const apis = {
_apiKey: '12ab34cd56ef',
getAllUsers: function () {
console.log('这是查询全部用户的函数');
},
getUserById: function (userId) {
console.log('这是根据用户ID查询用户的函数');
},
saveUser: function (user) {
console.log('这是保存用户的函数');
}
}; //记录日志的方法
function recordLog(){
console.log('这是记录日志的函数');
};
const proxy = new Proxy(apis, {
get: function (target, prop) {
const value = target[prop];
return function (...args) {
//此处调用记录日志的函数
// recordLog();
//调用真实的函数
return value.apply(null,args);
}
}
});
proxy.getAllUsers();
proxy.getUserById();
proxy.saveUser();
不影响原应用正常运行情况下增加日志记录,如果我们只想要对特定的某些函数增加日志,那么可以在get()函数中进行特殊的处理,对函数名进行判断。
Reflect
Reflect概述
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法.Reflect
不是一个函数对象,因此它是不可构造的。
描述
Reflect
不是一个构造函数,所以不能通过new运算符对其进行调用,或者将Reflect
对象作为一个函数来调用。Reflect
的所有属性和方法都是静态的(就像Math
对象)。
增加reflect对象的原因
- 更合理地规划与Object对象相关的API。
- 用一个单一的全局对象去存储这些函数,能够保持其他的JavaScript代码的整洁、干净
- 为了让代码更好维护,更 容易向下兼容
- 修改Object对象的某些函数的返回结果,可以让其变得更合理、使得代码更好维护。
Reflect静态函数
-
Reflect.apply(target, thisArgument, argumentsList)
对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
-
Reflect.construct(target, argumentsList[, newTarget])
对构造函数进行 new 操作,相当于执行 new target(…args)。
-
Reflect.defineProperty(target, propertyKey, attributes)
和 Object.defineProperty() 类似。如果设置成功就会返回 true
-
Reflect.deleteProperty(target, propertyKey)
作为函数的delete操作符,相当于执行 delete target[name]。
-
Reflect.get(target, propertyKey[, receiver])
获取对象身上某个属性的值,类似于 target[name]。
-
Reflect.getOwnPropertyDescriptor(target, propertyKey)
类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined.
-
Reflect.getPrototypeOf(target)
类似于 Object.getPrototypeOf()。
-
Reflect.has(target, propertyKey)
判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
-
Reflect.isExtensible(target)
类似于 Object.isExtensible().
-
Reflect.ownKeys(target)
返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).
-
Reflect.preventExtensions(target)
类似于 Object.preventExtensions()。返回一个Boolean。
-
Reflect.set(target, propertyKey, value[, receiver])
将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
-
Reflect.setPrototypeOf(target, prototype)
设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
Reflect.has(obj, name)
语法:Reflect.has(target, propertyKey)
描述:用于检查一个对象是否拥有某个属性
var myObject={
foo:1,
}
//旧写法
console.log('foo' in myObject);//true
//新写法
console.log(Reflect.has(myObject,'foo'));//true
Reflect.deleteProperty(obj, name)
语法:Reflect.deleteProperty(target, propertyKey)
描述:允许你删除一个对象上的属性。返回一个 Boolean
值表示该属性是否被成功删除。
**注意:**第一个参数不是对象,会报错。
const myObj = { foo: 'bar' };
// 旧写法
console.log(delete myObj.foo);//true
// 新写法
console.log(Reflect.deleteProperty(myObj, 'foo'));//true
Reflect.construct(target, args)
语法:Reflect.construct(target, argumentsList[, newTarget])
描述:允许你使用可变的参数来调用构造函数
注意:第一个参数不是函数,会报错。
function Greeting(name) {
this.name = name;
}
// new 的写法
const instance = new Greeting('张三');
// Reflect.construct 的写法
const instance = Reflect.construct(Greeting, ['张三']);
console.log(instance);
Reflect.apply(func, thisArg, args)
语法:Reflect.apply(func, thisArg, args)
描述:调用一个方法并且显式地指定 this
变量和参数列表(arguments) ,参数列表可以是数组,或类似数组的对象。
const ages = [11, 33, 12, 54, 18, 96];
// 旧写法
const youngest = Math.min.apply(Math, ages);
const oldest = Math.max.apply(Math, ages);
const type = Object.prototype.toString.call(youngest);
// 新写法
const youngest = Reflect.apply(Math.min, Math, ages);
const oldest = Reflect.apply(Math.max, Math, ages);
const type = Reflect.apply(Object.prototype.toString, youngest, []);
实例:使用 Proxy 实现观察者模式
观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。
//初始化观察者队列
const uniqueObserveList = new Set();
//将监听回调加入队列
const observe = fn => uniqueObserveList.add(fn);
//设置Proxy代理,拦截赋值操作
const observable = obj => new Proxy(obj, {set});
//用来拦截属性的赋值操作
function set(target, key, value, receiver){
//内部调用对应的 Reflect 方法
const result = Reflect.set(target, key, value, receiver);
//额外执行观察者队列
uniqueObserveList.forEach( item => item() );
return result;
}
//观察目标
const person = observable({
name: '张三',
age: 20
});
//观察者
function print(){
console.log( `${person.name}, ${person.age}` );
}
//print作为监听触发的回调函数
observe(print);
person.name = '李四';
// 输出
// 李四, 20
Promise
Promise诞生的原因
假如在一个行为中,需要执行多个异步请求,每一个请求又需要依赖上一个请求的结果,按照回调 函数的处理方法,代码如下所示:
//第一个请求
$.ajax({
url: 'url1',
success: function () {
//第二个请求
$.ajax({
url: 'url2',
success: function () {
//第三个请求
$.ajax({
url: 'url3',
success: function () {
//第四个请求
$.ajax({
url: 'url4',
success: function () {
//成功的回调
}
})
}
})
}
})
}
})
导致代码的嵌套太深,引发"回调地 狱"。
回调地狱存在以下几个问题:
- 代码臃肿,可读性差。
- 代码耦合度高,可维护性差,难以复用。
- 回调函数都是匿名函数,不方便调试。
Promise的生命周期
3种状态:
- pending(进行中)
- fulfilled(已成功)
- rejected(已失败)
状态的改变只有两种可能,
- 一种是在Promise执行成功时,有 pending状态改变为fulfilled状态,
- 另一种是Promise执行失败时,有pending状态改变为rejected状 态。
Promise的基本用法
Promise对象本身是一个构造函数,可以通过new操作符生成Promise的实例。
const promise = new Promise((resolve,reject)=>{
//异步处理请求
if(/异步请求标识/){
resolve();
}else{
reject();
}
})
promise执行的过程:
-
在接收的函数中处理异步请求,通过判断异步请求的结果,来调用对应的resolve()和reject()函数。
-
如果结果为“true”,则表示请求成功,调用resolve()函数,promise状态从pedding变成fulfillied;
如果结果为“false",则表示请求失败,调用reject()函数,promise的状态就从pedding变成rejected
-
resolve和reject函数可以传递参数,作为后续.then和.catch执行的数据源
-
promise一旦创建立即执行,同步代码执行完毕之后才会执行.then()函数
实现原生 get 类型的 Ajax 请求的代码如下所示:
//封装原生get类型Ajax请求
function ajaxGetPromise(url) {
const promise = new Promise(function (resolve, reject) {
const handler = function () {
if (this.readyState !== 4) {
return;
}
//当状态码为200时,表示请求成功,执行resolve()函数
if (this.status === 200) {
//将请求的响应体作为参数,传递给resolve()函数
resolve(this.response);
} else {
//当状态码不为200时,表示请求失败,reject()函数
reject(new Error(this.statusText));
}
}
//原生ajax操作
const client = new XMLHttpRequest();
client.open("GET", url);
client.onreadystatechange = handler;
client.responseType = "json";
client.setReqestHeader("Accept", "application/json");
client.send();
});
return promise;
}
then()函数
.then()函数表示在Promise实例状态改变时执行的回调函数。
Promise所传参数:第一个参数是调用resolve函数,所需要执行的回调函数(函数参数为resolve所传的参数)
第二个参数是调用reject函数,所需要执行的回调函数(函数参数为reject所传的参数)
用法:
const promise = new Promise((resolve, reject) => {
resolve(1);
});
//then()函数链式调用
promise.then((result) => {
console.log(result); //1
return 2;
}).then((result) => {
console.log(result); //2
return 3;
}).then((result) => {
console.log(result); //3
return 4;
}).then((result) => {
console.log(result); //4
})
catch()函数
catch()
函数 是Promise
执行失败之后的回调,它所接收的参数就是执行reject()
函数时传递的参数。
const promise = new Promise((resolve, reject) => {
try {
throw new Error('test');
} catch (err) {
reject(err);
}
});
promise.catch((err) => {
console.log(err);//在控制台打印错误原因
})
Promise函数
Promise.all()
用于将多个 Promise 实例,包装成一个新的 Promise 实例
const p = Promise.all([p1, p2, p3]);
参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。
p的状态 由参数p1,p2,p3的状态决定:
- 只有当p1,p2,p3,都是fulfilled,p的状态才会是fulfilled
- p1,p2,p3只要有一个rejected,p的状态就是rejected
// 生成一个Promise对象的数组
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
return getJSON('/post/' + id + ".json");
});
Promise.all(promises).then(function (posts) {
// ...
}).catch(function (reason) {
// ...
});
Promise.race()
将多个 Promise 实例,包装成一个新的 Promise 实例。
const p = Promise.race([p1, p2, p3]);
注意:只要p1,p2,p3中有一个实例率先改变状态,那个率先改变的promise实例的返回值就传递给p的回调函数。
即是在多个请求中返回获取速度最快的结果,无论其是成功还是失败。
let a = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('a成功')
}, 1000)
})
let b = new Promise((resolve, reject) => {
setTimeout(() => {
reject('失败')
}, 2000)
})
Promise.race([a, b]).then(res => {
console.log(res)
}, err => {
console.log(err)
})
Promise.reject()
Promise.reject(reason)
方法也会返回一个新的 Promise 实例,该实例的状态为 rejected 。
返回Promise
一个因给定原因被拒绝的对象。
const p = Promise.reject('出错了');
// 等同于
const p = new Promise((resolve, reject) => reject('出错了'))
p.then(null, function (s) {
console.log(s)
});
// 出错了
Promise.reject()
方法的参数,会原封不动地作为 reject 的理由,变成后续方法的参数。
Iterator与for…of循环
Iterator概述
JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)、对象(Object)、Map、Set
遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。
任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。
Iterator 对象的本质:
一个指针对象
Iterator 的作用:
- 为各种数据结构,提供一个统一的、简便的访问接口
- 使得数据 结构的成员能够按某种次序排列
- Iterator 接口主要供 for…of 使用
Iterator 的遍历过程:
- 创建一个指针对象,指向当前数据结构的起始位置。
- 第一次调用next(),可以将指针指向数据结构的第一个成员
- 第二次调用next(),可以将指针指向第二个成员
- 不断调用指针对象 的next方法,直到指向数据结构的结束位置
返回当前成员信息中包含:value:当前成员的值;done:布尔值,表示遍历是否结束,即是否还有必要再一 次调用 next 方法。(false:未结束;true:结束)
var it = makeIterator(['a', 'b']);
//每次调用 next方法,指针就会指向数组的下一个成员
it.next() // { value: "a", done: false }
it.next() // { value: "b", done: false }
it.next() // { value: undefined, done: true }
//遍历器生成函数
function makeIterator(array) {
var nextIndex = 0;
//返回该数组的遍历器对象(即指针对象) it
return {
next: function () {
return nextIndex < array.length ? {
value: array[nextIndex++],
done: false
} : {
value: undefined,
done: true
};
}
};
}
function makeIterator(array) {
var nextIndex = 0;
return {
next: function () {
return nextIndex < array.length ? {
value: array[nextIndex++]
} : {
done: true
};
}
};
}
默认 Iterator 接口
ES6 规定,默认的 Iterator 接口部署在数据结构的 Symbol.iterator 属性,Symbol.iterator 属性本身是一个函数,执行这个函数,就会返回一个遍历器。
是一个表达式,返回symbol对象的iterator属性,这是一个预定好的,类型为symbol的特殊值,所以要放在方括号内。
凡是部署了 Symbol.iterator 属性的数据结构,就称为部署了遍历器接口。
原生具备 Iterator 接口的数据结构如下。
- Array
- Map
- Set
- String
- 函数的 arguments 对象
- NodeList 对象
let arr = ['a', 'b', 'c'];
let iter = arr[Symbol.iterator]();
console.log(iter.next()); // { value: 'a', done: false }
console.log(iter.next());// { value: 'b', done: false }
console.log(iter.next()); // { value: 'c', done: false }
console.log(iter.next());// { value: undefined, done: true }
变量 arr 是一个数组,原生就具有遍历器接口,部署在 arr 的 Symbol.iterator 属 性上面。所以,调用这个属性,就得到遍历器对象。
for…of 循环
一个数据结构只要部署了 Symbol.iterator 属性,就被视为具有 iterator 接口,就可以用 for…of 循环遍历它的成员。也就是说, for…of 循环内部调用的是数据结构的 Symbol.iterator 方法。
for…of 循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如 arguments 对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。
数组
数组原生具备 iterator 接口(即默认部署了 Symbol.iterator 属性), for…of 循环本质上就是调用这个接口产生的遍历器
const arr = ['red', 'green', 'blue'];
for (let v of arr) {
console.log(v); // red green blue
}
//空对象obj部署了数组arr的Symbol.iterator属性
const obj = {};
obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);//把arr的值一个一个绑定上去
// console.log(obj[Symbol.iterator]);
for (let v of obj) {
console.log(v); // red green blue
}
Set 和 Map 结构
Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用 for…of 循环。
var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
for (var e of engines) {
console.log(e);
}
// Gecko
// Trident
// Webkit
var es6 = new Map();
es6.set("edition", 6);
es6.set("committee", "TC39");
es6.set("standard", "ECMA-262");
for (var [name, value] of es6) {
console.log(name + ": " + value);
}
// edition: 6
// committee: TC39
// standard: ECMA-262
注意:
- 遍历的顺序是按 照各个成员被添加进数据结构的顺序
- Set 结构遍历时,返回的是一个值,而 Map 结构遍历时, 返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。
let map = new Map().set('a', 1).set('b', 2);
for (let pair of map) {
console.log(pair);
}
// ['a', 1]
// ['b', 2]
for (let [key, value] of map) {
console.log(key + ' : ' + value);
}
// a : 1
// b : 2
类似数组的对象
是类似数组一样有length属性和索引属性的对象。
举例:arguments 对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串
// 字符串
let str = "hello";
for (let s of str) {
console.log(s); // h e l l o
}
// DOM NodeList对象
let paras = document.querySelectorAll("p");
for (let p of paras) {
p.classList.add("test");
}
// arguments对象
function printArgs() {
for (let x of arguments) {
console.log(x);
}
}
printArgs('a', 'b');
// 'a'
// 'b'
Generator 函数
基本概念
Generator 函数是 ES6 提供的一种异步编程解决方案。
Generator 函数是一个状态机, 封装了多个内部状态。
Generator 函数是一个遍历器对象生成函数(返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态)
Generator 函数是一个指向内部状态的 指针对象。
特征:
- function 关键字与函数名之 间有一个星号(*)
- 函数体内部使用 yield 表达式,定义不同的内部状态
- yield 表达式是暂停执行的标记,而 next 方法可以恢复执行。
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();//Generator 函数
console.log(hw.next());//{value: "hello", done: false}
yield 表达式
yield 表达式就是暂停标志
调用 next 方法才会遍历下一个内部状态,提供了一种可以暂停执行的函数
yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错
遍历器对象的next()方法的逻辑:
- 遇到yield表达式,就暂停执行后面的操作,并将紧跟yield后面的表达式的值作为返回的对象的value属性值
- 调用next方法时,就继续往下执行,直到遇到yield表达式,没有遇到就一直运行直到遇到return语句为止,如无return,则返回value值为undefined
“惰性求值”(Lazy Evaluation)
function* gen() {
yield 123 + 456;
}
var a = gen();
console.log(a.next());//{ value: 579, done: false }
异步操作的同步化表达
Generator 函数的一个重要实际意义 就是用来处理异步操作,改写回调函数。
异步操作的后续操作可以放在 yield 表达式下面,等到调用 next 方法时再执行。
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function (response) {
it.next(response);
});
}
var it = main();
console.log(it.next());
main 函数,就是通过 Ajax 操作获取数据
注意, makeAjaxCall 函数中的 next 方法,必须加上 response 参数,因 为 yield 表达式,本身是没有值的,总是等于 undefined
Class 的基本语法
类的由来
JavaScript 语言中,生成实例对象的传统方法是通过构造函数
function Point(x, y) {
this.x = x;
this.y = y;
}
Point.prototype.toString = function () {
return '(' + this.x + ', ' + this.y + ')';
};
var p = new Point(1, 2);
console.log(p.toString());//(1, 2)
js中生成实例的写法与传统的面向对象语言(c++;java)差异很大,容易让人困惑,而Class(类)的出现写法更接近传统语言,可以通过class关键字,可以定义类。让对象原型的写法更加清晰、更像面向对象编程的语法
class Point {
constructor(x, y) {
this.x = x;// this关键字则代表实例对象
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
constructor 方法
是类的默认方法,通过new命令生成对象实例时,自动调用该方法,这个方法如果没有显示定义的话,一个空的constructor()方法会被默认添加的。
class Point {
}
// 等同于
class Point {
constructor() {}
}
constructor() 方法默认返回实例对象(即 this ),完全可以指定返回另外一个对象。
class Foo {
constructor() {
return Object.create(null);
}
}
console.log(new Foo() instanceof Foo);//false
constructor() 函数返回一个全新的对象,结果导致实例对象不是 Foo 类的实例。 类必须使用 new 调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用 new 也可以执 行。
类的实例
与 ES5 一样,实例的属性除非显式定义在其本身(即定义在 this 对象上),否则都是定义在原型 上(即定义在 class 上)。
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}
var point = new Point(2, 3);
point.toString() // (2, 3)
point.hasOwnProperty('x') // true
point.hasOwnProperty('y') // true
point.hasOwnProperty('toString') // false
point.__proto__.hasOwnProperty('toString') // true
类的所有实例共享一个原型对象。意味着,可以通过实例的 __proto__
属性为“类”添加方法。
var p1 = new Point(2,3);//p1 和 p2 都是 Point 的实例
var p2 = new Point(3,2);
p1.__proto__ === p2.__proto__
//true
生产环境中,我们可以使用 Object.getPrototypeOf(Object.getPrototypeOf
方法可以用来从子类上获取父类) 方法来获取实例对象的原型,然后再来为原 型添加方法/属性。
// 通过实例的__proto__属性,或者获取实例属性的原型来为“类”添加方法
var t1 = new Point(2, 3); // 类Point省略
var t2 = new Point(3, 2);
t1.__proto__.printName = function () {
return 'Oops'
};
//建议使用以下一种替换,函数可以获取实例的原型
Object.getPrototypeOf(t1).printName = function () {
return 'Oops'
};
Object.assign(Object.getPrototypeOf(t1), {
printName() {
return 'Oops'
}
})
// 获取子类ColorPoint的父类,或者判断
Object.getPrototypeOf(ColorPoint) // 得到Point
Object.getPrototypeOf(ColorPoint) === Point // true
在 p1 的原型上添加了一个 printName() 方法,由于 p1 的原型就是 p2 的原型,因此 p2,p3 也可以调用这个方法。使用实例的__proto__
属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义
var p1 = new Point(2, 3);
var p2 = new Point(3, 2);
p1.__proto__.printName = function () {
return 'Oops'
};
p1.printName() // "Oops"
p2.printName() // "Oops"
var p3 = new Point(4, 2);
p3.printName() // "Oops"
Class 的继承
Class 可以通过extends
关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。
class Point {
}
// ColorPoint 继承自 Point
class ColorPoint extends Point {
}
**子类必须在constructor
方法中调用super
方法,否则新建实例时会报错。**这是因为子类自己的this
对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super
方法,子类就得不到this
对象。
只有调用super
之后,才可以使用this
关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super
方法才能调用父类实例。
注意:
- 作为函数时
super()
只能用在子类的构造函数之中,用在其他地方就会报错。 super
作为对象时- 在普通方法中(满足两个条件,第一不是构造方法,第二不是静态方法),指向父类的原型对象
- 普通方法中,表现形式有两种
- super.xxx(…),其中xxx表示父类上的某个原型方法,如果没有的话那就返回undefined。
- super.x,其中x表示父类上的某个原型属性,如果没有的话那就返回undefined。
- 普通方法中,表现形式有两种
- 在静态方法中(加入static关键字的方法),指向父类
- 在普通方法中(满足两个条件,第一不是构造方法,第二不是静态方法),指向父类的原型对象
- 使用
super
的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super
关键字
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
}
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y);
this.color = color;
}
toString() {
return this.color + ' ' + super.toString();
}
}
let ins = new ColorPoint(1, 2, 'red');
console.log(ins.toString());//red [object Object]
console.log(ins.x);//1
console.log(ins.y);//2
ES5与ES6继承的区别:
ES5 的继承,实质是先创造子类的实例对象this
,然后再将父类的方法添加到this
上面(Parent.apply(this)
)。
ES6 的继承,机制完全不同,实质是先将父类实例对象的属性和方法,加到this
上面(所以必须先调用super
方法),然后再用子类的构造函数修改this
。
静态方法
父类的静态方法,也会被子类继承
class A {
static geta() {
console.log('A');//A
}
}
class B extends A {
static getb() {
console.log('B');//B
}
}
console.log(B.geta());//A undefined
console.log(A.geta());//A undefined
console.log(B.getb());//B undefined
静态方法的规则:
- 静态方法只能访问类的静态成员,不能访问类的非静态成员。
- 非静态方法可以访问类的静态成员,也可以访问类的非静态成员。
- 静态方法既可以用实例来调用,也可以用类名来调用。
调用类中的另外的静态方法要使用this的关键字:
class StaticMethodCall {
static staticMethod() {
return 'Static method has been called';
}
static anotherStaticMethod() {
return this.staticMethod() + ' from another static method';
}
}
StaticMethodCall.staticMethod();
// 'Static method has been called'
StaticMethodCall.anotherStaticMethod();
// 'Static method has been called from another static method'