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 [abcde] = 'hello';
console.log(abcde); // "h"

const [a, b, c, d, e] = 'hello';
console.log(a); // "h"
console.log(b); // "e"
console.log(c); // "l"
console.log(d); // "l"
console.log(e); // "o"

类似数组的对象都有一个 length 属性,因此还可以对这个属性解构赋值。

let {length : len} = 'hello';
console.log(len); // 5

函数参数的解构赋值

函数的参数也可以使用解构赋值

//数组参数就被解构成变量x和y
function add([x, y]){
	return x + y;
}
add([1, 2]); // 3

圆括号问题

  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
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值