web07.ES6

ECMAScript 6 简介

ECMAScript 和 JavaScript 的关系

前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 JScript 和 ActionScript)。日常场合,这两个词是可以互换的。

ES6 与 ECMAScript 2015 的关系

  1. ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。
  2. ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简 称 ES2015)。
  3. ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵 盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标 准。

ECMAScript 的历史

ES6 从开始制定到最后发布,整整用了 15 年。

  1. ECMAScript 1.0 (1997)
  2. ECMAScript 2.0(1998年 6月)
  3. ECMAScript 3.0(1999年12月)
  4. 2000 年,ECMAScript 4.0 开始酝酿,这个版本最后没有通过,2007 年 10 月,ECMAScript 4.0 版草案发布
  5. 2009 年 12 月,ECMAScript 5.0 版正式发布
  6. 2011 年 6 月,ECMAScript 5.1 版发布
  7. 2013 年 3 月,ECMAScript 6 草案冻结
  8. 2013 年 12 月,ECMAScript 6 草案发布
  9. 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

不存在变量提升

  1. var 命令会发生“变量提升”现象,在声明之前使用,值为 undefined
  2. 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;
}
  1. 如果区块中存在 let 和 const 命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

  2. 在代码块内,使用 let 命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性 死区”(temporal dead zone,简称 TDZ)。

  3. 在 let 命令声明变量 tmp 之前,都属于变量 tmp 的“死区”

typeof x; // ReferenceError
let x;
  1. 变量 x 使用 let 命令声明,所以在声明之前,都属于 x 的“死区”,只要用到该变量就会报错。
  2. 如果一个变量根本没有被声明,使用 typeof 反而不会报错。
typeof undeclared_variable // "undefined"
  1. 有些“死区”比较隐蔽,不太容易发现
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
  1. 在变量 x 的声明语句还没有执行完成前,就去取 x 的值,导致报 错”x 未定义“。

不允许重复声明

  1. let 不允许在相同作用域内,重复声明同一个变量。
// 报错
function func() {
    let a = 10;
    var a = 1;
}
  1. 不能在函数内部重新声明参数。
function func(arg) {
    let arg;
}
func() // 报错
function func(arg) {
    {
        let arg;
    }
}
func() // 不报错

const命令

  1. const 声明一个只读的常量。一旦声明,常量的值就不能改变
const PI = 3.1415;
PI // 3.1415
PI = 3;
// TypeError: Assignment to constant variable.
  1. const 声明的变量不得改变值, const 一旦声明变量,就必须立即初始化,不能留到以后赋值。
const foo;// SyntaxError: Missing initializer in const declaration
  1. const 的作用域与 let 命令相同:只在声明所在的块级作用域内有效。
  2. const 命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用,不可重复声明

本质

const 实际上保证的是变量指向的那个内存地址所保存的数据不得改动。

  • 对于简单类型的数据,值就保存在变量指向的那个内存地址

  • 对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向 实际数据的指针,保证这个指针是固定的

const foo = {};
// 为 foo 添加一个属性,可以成功
foo.prop = 123;
foo.prop // 123
// 将 foo 指向另一个对象,就会报错
foo = {}; // TypeError: "foo" is read-only

ES6 声明变量的六种方法

ES5 只有两种声明变量的方法:var 命令和 function 命令

ES6 除了添加letconst命令,还有import 命令和class 命令

顶层对象的属性

  1. 浏览器环境:指的是 window 对象
  2. Node: global 对象
  3. ES5:顶层对象的属性与全局变量是等价的
  • var 命令和 function 命令声明的全局变 量,依旧是顶层对象的属性
  • let命令、 const命令、 class 命令声明的全局变量,不 属于顶层对象的属性

变量的解构赋值

数组的解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

  1. 只要等号两边的模式相同,左边的变量就会被赋予对应的值
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]

  1. 不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组
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
  1. 等号的右边不是数组(不 是可遍历的结构),报错
// 报错,因为等号右边的值,要么转为对象以后不具备 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

对象的解构赋值

解构不仅可以用于数组,还可以用于对象。

对象解构与数组的不同

  1. 数组的元素是按次序排列的,变量的取值由它的位置决定
  2. 对象的属性没有次序,变量必须与属性同名,才能取到正确的值
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

圆括号问题

  1. 一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须**解析到(或解析不到等号)**才能知道

  2. ES6 的规则是,只要有可能导致解构的歧 义,就不得使用圆括号。

  3. 建议只要有可能,就不要在模式中放置圆括号。

不能使用圆括号的情况

  1. 变量声明语句

    // 全部报错,都是·变量声明·语句,模式不能使用圆括号。
    let [(a)] = [1];
    let {x: (c)} = {};
    let ({x: c}) = {};
    let {(x: c)} = {};
    let {(x): c} = {};
    
  2. 函数参数:也属于变量声明语句,因此不能带有圆括号

    //整个模式放在圆括号之中,报错
    // 报错
    function f([(z)]) { return z; }
    // 报错
    function f([z,(x)]) { return x; }
    //一部分模式放在圆括号之中,报错
    [({ p: a }), { x: c }] = [{}, {}];
    
  3. 赋值语句的模式

    // 全部报错
    ({ p: a }) = { p: 42 };
    ([a]) = [5];
    

可以使用圆括号的情况

可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

//是赋值语句,不是声明语句,圆括号都不属于模式的一部分
[(b)] = [3];//正确,模式是取数组的第一个成员,跟圆括号无关
({ p: (d) } = {}); // 正确,模式是p,而不是d
[(parseInt.prop)] = [3]; // 正确

常见用法

  1. 交换变量的值

    let x = 1;
    let y = 2;
    [x, y] = [y, x];
    
  2. 从函数返回多个值

    // 返回一个数组
    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();
    
  3. 函数参数的定义

    方便地将一组参数与变量名对应起来

    // 参数是一组有次序的值
    function f([x, y, z]) { ... }
    f([1, 2, 3]);
    
    // 参数是一组无次序的值
    function f({x, y, z}) { ... }
    f({z: 3, y: 2, x: 1});
    
  4. 提取 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 ]
    
  5. 函数参数的默认值

    jQuery.ajax = function (url, {
        async = true,
        beforeSend = function () {},
        cache = true,
        complete = function () {},
        crossDomain = false,
        global = true,
        // ... more config
    } = {}) {
        // ... do stuff
    };
    
  6. 输入模块的指定方法

const { SourceMapConsumer, SourceNode } = require("source-map");

扩展运算符和rest运算符

​ 解决函数参数和数组元素长度未知情况下的编码问题

扩展运算符

  1. 用3个点表示(…)
  2. 用于将一个数组或类数组对象转换为用逗号分隔的值序列(单独的值的序列)
  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运算符的区别

  1. 两者是互为逆运算的。扩展运算符是将数组分割成独立的序列,而rest运算符是将独立的序列合并成一个数组
  2. 当三个点出现在函数的形参上或者出现在赋值等号的左侧,则表示它为rest运算符
  3. 当三个点出现在函数的实参上或者出现在赋值等号的右侧,则表示它为扩展运算符

字符串的新增方法

模板字符串

​ 模板字符串(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>
  1. 模板字符串中需要使用反引号,则前面要用反斜杠转义。

    let greeting = `\`Yo\` World!`;
    
  2. 模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

    $('#result').html(`
    <ul>
        <li>first</li>
        <li>second</li>
    </ul>
    `);
    
  3. 可以放入任意的 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"
    
  4. 模板字符串之中还能调用函数。

    function fn() {
    	return "Hello World";
    }
    console.log(`foo ${fn()} bar`);
    // foo Hello World bar
    
  5. 如果大括号内部是一个字符串,将会原样输出。

    `Hello ${'World'}`
    // "Hello World"
    

includes(), startsWith(), endsWith()

JavaScript 只有 indexOf 方法,可以用来确定一个字符串是否包含在另一个字符串中。

  1. includes():返回布尔值,表示是否找到了参数字符串。
  2. startsWith():返回布尔值,表示参数字符串是否在原字符串的头部。
  3. 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() 用于尾部补全。

语法

  1. 第一个参数是字符串补全生效的最大长度
  2. 第二个参数是用来补全的字符串
  3. 如果原字符串的长度,等于或大于最大长度,则字符串补全不生效
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 '

用途

  1. 为数值补全指定位数。

    console.log('1'.padStart(10, '0')); // "0000000001"
    console.log('12'.padStart(10, '0')); // "0000000012"
    console.log('123456'.padStart(10, '0')); // "0000123456"
    
  2. 提示字符串格式。

    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

箭头函数

  1. ES6 允许使用“箭头”( => )定义函数。

  2. 如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

    var f = () => 5;
    // 等同于
    var f = function () { return 5 };
    var sum = (num1, num2) => num1 + num2;
    // 等同于
    var sum = function(num1, num2) {
    	return num1 + num2;
    };
    
  3. 如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用 return 语句返 回。

    // 报错
    let getTempItem = id => { id: id, name: "Temp" };
    // 不报错
    let getTempItem = id => ({ id: id, name: "Temp" });
    
  4. 箭头函数可以与变量解构结合使用

    const full = ({ first, last }) => first + ' ' + last;
    // 等同于
    function full(person) {
    	return person.first + ' ' + person.last;
    }
    

使用注意点

  1. 箭头函数没有自己的 this 对象(详见下文)。(内部的 this 就是定义时上层作用域中的 this 。 也就是说,箭头函数内部的 this 指向是固定的)
  2. 不可以当作构造函数,也就是说,不可以对箭头函数使用 new 命令,否则会抛出一个错误。
  3. 不可以使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
  4. 不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

不适用场合

  1. 第一个场合是定义对象的方法,且该方法内部包括 this 。

    const cat = {
        lives: 9,
        jumps: () => {
            this.lives--;//this 指向全局对象
        }
    }
    
  2. 第二个场合是需 要动态 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 都能将其转为数组。

用于将两类对象转为真正的数组:

  1. 类似数组的对象(array-like object)
  2. 可遍历 (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具有的属性如下所示:

  1. 实例属性:age,weight,Symbol('one'),color

  2. 继承属性:name,type

  3. 可枚举属性:age,weigth,color

  4. 不可枚举属性:height Symbol

  5. 属性:Symbol('one')

  6. for...in遍历对象自身继承的可枚举属性(不包含Symbol属性)

    for(let key in cat){
    	console.log(key);
    }
    //age weight color name type constructor
    
  7. Object.keys(obj) 返回一个数组,包含可枚举属性,不包含继承属性和Symbol属 性。

    console.log(Object.keys(cat));
    //[ 'age', 'weight', 'color' ]
    
  8. Object.getOwnPropertyNames(obj) 返回一个数组,包含可枚举属性和不可枚举 属性不包含继承属性和Symbol属性。

console.log(Object.getOwnPropertyNames(cat));
//[ 'age', 'weight', 'color', 'height' ]
  1. Object.getOwnPropertySymbols(obj)返回一个数组,包含对象自身所有Symbol属性,不包 含其他属性。
console.log(Object.getOwnPropertySymbols(cat));
//[ Symbol(one) ]
  1. 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" }

只有字符串的包装对象,会产生可枚举属性。

注意点

  1. 浅拷贝

    Object.assign()方法实行的是浅拷贝,而不是深拷贝。

  2. 同名属性的替换

    一旦遇到同名属性, Object.assign() 的处理方法是替换,而不是添加。

    const target = { a: { b: 'c', d: 'e' } }
    const source = { a: { b: 'hello' } }
    Object.assign(target, source)
    // { a: { b: 'hello' } }
    
  3. 数组的处理

    可以用来处理数组,但是会把数组视为对象

    Object.assign([1, 2, 3], [4, 5])
    // [4, 5, 3]
    
  4. 取值函数的处理

    Object.assign()只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

    const source = {
        get foo() {
            return 1
        }
    };
    const target = {};
    Object.assign(target, source)
    // { foo: 1 }
    

常见用途

  1. 为对象添加属性

    class Point {
        constructor(x, y) {
            Object.assign(this, {x,y});
        }
    }
    

    通过 Object.assign() 方法,将 x 属性和 y 属性添加到 Point 类的对象实例。

  2. 为对象添加方法

    Object.assign(SomeClass.prototype, {
        someMethod(arg1, arg2) {
            ···
        },
        anotherMethod() {
            ···
        }
    });
    // 等同于下面的写法
    SomeClass.prototype.someMethod = function (arg1, arg2) {
        ···
    };
    SomeClass.prototype.anotherMethod = function () {
        ···
    };
    

    直接将两个函数放在大括号中,再使用 assign() 方法添 加到 SomeClass.prototype 之中。

  3. 克隆对象

    function clone(origin) {
    	return Object.assign({}, origin);
    }
    

    代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承 链,可以采用下面的代码。

    function clone(origin) {
    	let originProto = Object.getPrototypeOf(origin);
    	return Object.assign(Object.create(originProto), origin);
    }
    
  4. 合并多个对象

    将多个对象合并到某个对象。

    const merge =(target, ...sources) => Object.assign(target, ...sources);
    

    合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

    const merge =(...sources) => Object.assign({}, ...sources);
    
  5. 为属性指定默认值

    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 结构的实例有以下属性

  1. Set.prototype.constructor:构造函数,默认就是 Set 函数。
  2. Set.prototype.size:返回 Set 实例的成员总数

Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。

  1. Set.prototype.add(value)添加某个值,返回 Set 结构本身。
  2. Set.prototype.delete(value)删除某个值,返回一个布尔值,表示删除是否成功。
  3. Set.prototype.has(value) :返回一个布尔值,表示该值是否为 Set 的成员。
  4. 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常见用法

  1. 单一数组的去重

    由于set成员值具有唯一性,因此可以使用Set来进行数组的去重。

    let arr = [1,2,2,3,3,3,4,4,5,5,6,6];
    console.log(new Set(arr));
    
  2. 多个数组的合并去重

    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);
    
  3. 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()函数实际返回的是相同的值。

  1. keys():返回键名的遍历器。
  2. values():返回键值的遍历器。
  3. 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

实例的属性和操作方法

  1. size 属性

    返回 Map 结构的成员总数

    const map = new Map();
    map.set('foo', true);
    map.set('bar', false);
    console.log(map.size); // 2
    
  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');
    
  3. 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!
    
  4. 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
    
  5. 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
    
  6. 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 结构原生提供三个遍历器生成函数一个遍历方法

  1. Map.prototype.keys() :返回键名的遍历器。
  2. Map.prototype.values() :返回键值的遍历器
  3. Map.prototype.entries() :返回所有成员的遍历器。
  4. Map.prototype.forEach() :遍历 Map 的所有成员

Map 结构转为数组结构,比较快速的方法是使用扩展运算符( … )。

结合数组的 map 方法、 filter 方法,可以实现 Map 的遍历和过滤(Map 本身没有 map 和 filter 方 法)。

与其他数据结构的互相转换

  1. Map 转为数组

    ​ 使用扩展运算符( … )。

  2. 数组 转为 Map

    ​ 将数组传入 Map 构造函数,就可以转为 Map。

  3. Map 转为对象

    ​ 将数组传入 Map 构造函数,就可以转为 Map。

  4. 对象转为 Map

    ​ 对象转为 Map 可以通过 Object.entries() 。

  5. 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"]]]'
    
  6. 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时,应注意:

  1. 必须通过代理实例访问

    如果直接通过目标对象 person 访问name 属性,则不会触发栏截行为。

  2. 配置对象不能为空对象

    如果为空对象,则代表没有设置任何拦截,实际是对目标对象的访问。另外配置对象不能为null,否则会抛出异常。

Proxy实例函数及其基本使用

通过访问代理对象的属性来触发自定义配置对象的get()函数而get()函数只是 Proxy 实例支持的总共 13种函数中的一种,这13种函数汇总如下。

  1. get(target,propKey,receiver)。

    拦截对象属性的读取操作,例如调用proxy.name或者proxy[name],其中target表示是目标对象,propKey表示的是读取的属性值,receiver表示的是配置对象。

  2. set(target,propKey,value,receiver)。

拦截对象属性的写操作,即设置属性值,例如proxy.name='kingx’或者proxy[name] = ‘kingx’,其 中target表示目标对象,propKey表示的是将要设置的属性,value表示将要的属性的值,receiver 表示的是配置对象。

  1. has(target.propKey)。

    拦截hasProperty的操作,返回一个布尔值,最典型的表现形式是执行propKey in target,其中 target 表示目标对象,propKey表示判断的属性。

  2. deleteProperty(target,propKey)。

    拦截delete proxy[popKey]的操作,返回一个布尔值,表示是否执行成功,其中target表示目标对 象,propKey表示将要删除的属性。

  3. ownKeys(target)

    拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、 Object.keys(proxy)、for…in循环等操作,其中target表示的是获取对象自身所有的属性名。

  4. getownPropertyDescriptor(target,propKey)

  5. definePropertyitarget,propKey,propDesc)

  6. preventExtensions(target)

  7. getPrototypeOf(target)

  8. isExtensible(target)

  9. setPrototypeOf(target, proto)

  10. apply(target, object,args)。

  11. 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处 理下划线写法来实现真正的私有。

真正的私有要达到的目标有以下几个。

  1. 不能访问到私有属性,如果访问到私有属性则返回"undefined"。
  2. 不能直接修改私有属性,即是设置了也无效。
  3. 不能遍历出私有属性,遍历出来的属性中不会包含私有属性。
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对象的原因

  1. 更合理地规划与Object对象相关的API。
  2. 用一个单一的全局对象去存储这些函数,能够保持其他的JavaScript代码的整洁、干净
  3. 为了让代码更好维护,更 容易向下兼容
  4. 修改Object对象的某些函数的返回结果,可以让其变得更合理、使得代码更好维护。

Reflect静态函数

  1. Reflect.apply(target, thisArgument, argumentsList)

    对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。

  2. Reflect.construct(target, argumentsList[, newTarget])

    对构造函数进行 new 操作,相当于执行 new target(…args)。

  3. Reflect.defineProperty(target, propertyKey, attributes)

    Object.defineProperty() 类似。如果设置成功就会返回 true

  4. Reflect.deleteProperty(target, propertyKey)

    作为函数的delete操作符,相当于执行 delete target[name]。

  5. Reflect.get(target, propertyKey[, receiver])

    获取对象身上某个属性的值,类似于 target[name]。

  6. Reflect.getOwnPropertyDescriptor(target, propertyKey)

    类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined.

  7. Reflect.getPrototypeOf(target)

    类似于 Object.getPrototypeOf()。

  8. Reflect.has(target, propertyKey)

    判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。

  9. Reflect.isExtensible(target)

    类似于 Object.isExtensible().

  10. Reflect.ownKeys(target)

    返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).

  11. Reflect.preventExtensions(target)

    类似于 Object.preventExtensions()。返回一个Boolean。

  12. Reflect.set(target, propertyKey, value[, receiver])

    将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。

  13. 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 () {
                                //成功的回调
                            }
                        })
                    }
                })
            }
        })
    }
})

导致代码的嵌套太深,引发"回调地 狱"。

回调地狱存在以下几个问题:

  1. 代码臃肿,可读性差。
  2. 代码耦合度高,可维护性差,难以复用。
  3. 回调函数都是匿名函数,不方便调试。

Promise的生命周期

3种状态:

  1. pending(进行中)
  2. fulfilled(已成功)
  3. rejected(已失败)

状态的改变只有两种可能

  1. 一种是在Promise执行成功时,有 pending状态改变为fulfilled状态,
  2. 另一种是Promise执行失败时,有pending状态改变为rejected状 态。

Promise的基本用法

Promise对象本身是一个构造函数,可以通过new操作符生成Promise的实例

const promise = new Promise((resolve,reject)=>{
    //异步处理请求
    if(/异步请求标识/){
        resolve();
    }else{
        reject();
    }
})

promise执行的过程:

  1. 在接收的函数中处理异步请求,通过判断异步请求的结果,来调用对应的resolve()和reject()函数。

  2. 如果结果为“true”,则表示请求成功,调用resolve()函数,promise状态从pedding变成fulfillied;

    如果结果为“false",则表示请求失败,调用reject()函数,promise的状态就从pedding变成rejected

  3. resolve和reject函数可以传递参数,作为后续.then和.catch执行的数据源

  4. 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的状态决定:

  1. 只有当p1,p2,p3,都是fulfilled,p的状态才会是fulfilled
  2. 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 的作用:

  1. 为各种数据结构,提供一个统一的、简便的访问接口
  2. 使得数据 结构的成员能够按某种次序排列
  3. Iterator 接口主要供 for…of 使用

Iterator 的遍历过程:

  1. 创建一个指针对象,指向当前数据结构的起始位置。
  2. 第一次调用next(),可以将指针指向数据结构的第一个成员
  3. 第二次调用next(),可以将指针指向第二个成员
  4. 不断调用指针对象 的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 接口的数据结构如下。

  1. Array
  2. Map
  3. Set
  4. String
  5. 函数的 arguments 对象
  6. 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 循环可以使用的范围包括数组SetMap 结构、某些类似数组的对象(比如 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

注意

  1. 遍历的顺序是按 照各个成员被添加进数据结构的顺序
  2. 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 函数是一个指向内部状态的 指针对象。

特征:

  1. function 关键字与函数名之 间有一个星号(*)
  2. 函数体内部使用 yield 表达式定义不同的内部状态
  3. 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()方法的逻辑

  1. 遇到yield表达式,就暂停执行后面的操作,并将紧跟yield后面的表达式的值作为返回的对象的value属性值
  2. 调用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方法才能调用父类实例。

注意:

  1. 作为函数时super()只能用在子类的构造函数之中,用在其他地方就会报错。
  2. super作为对象时
    • 在普通方法中(满足两个条件,第一不是构造方法,第二不是静态方法),指向父类的原型对象
      • 普通方法中,表现形式有两种
        • super.xxx(…),其中xxx表示父类上的某个原型方法,如果没有的话那就返回undefined。
        • super.x,其中x表示父类上的某个原型属性,如果没有的话那就返回undefined。
    • 在静态方法中(加入static关键字的方法),指向父类
  3. 使用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

静态方法的规则:

  1. 静态方法只能访问类的静态成员,不能访问类的非静态成员。
  2. 非静态方法可以访问类的静态成员,也可以访问类的非静态成员。
  3. 静态方法既可以用实例来调用,也可以用类名来调用。

调用类中的另外的静态方法要使用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'
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值