ECMAScript6

一、let、const两个关键字的特点、作用和区别

作用:let和const这两个关键字的作用就是用来声明变量的,它们同ES5中的var作用一样。
与var的不同之处:用var声明的变量在全局内都有效,如下例:

    var a = [];
    for (var i = 0; i < 10; i++) {
      a[i] = function () {
        console.log(i);
      };
    }
    a[6](); // 10
    a[5](); // 10
    a[4](); // 10
    a[3](); // 10
    a[2](); // 10
    a[1](); // 10
    a[0](); // 10

如上例我们如果用var在for循环条件里面声明变量 i,那这个 i 就是全局变量。当循环结束后 i 就变成了10,无论调用a数组中的哪一个方法输出的都是10。但是我们用let声明变量 i 就不是另一种情况了:

var a = []
    for(let i = 0; i < 10; i++) {
      a[i] = function(){
        console.log(i);
      }
    }
    a[6]();  //6
    a[5]();  //5
    a[4]();  //4
    a[3]();  //3
    a[2]();  //2
    a[1]();  //1
    a[0]();  //0

用let的感觉就好比他每次循环都给你重新创建了一个i并把当前的值赋值进去;而var声明的i就是所有都指向它一个,都取它内部的值。另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域,加个例子:

    for (let i = 0; i < 3; i++) {
      let i = 'abc';
      console.log(i);
    }
    // abc
    // abc
    // abc

循环变量部分中的i似乎和循环体内部的i没一丁点关系,完全没任何联系。let声明的变量只在let所在代码块内有效

let特点不存在变量提升;在ES5中我们用var声明的变量,既是我们在声明一个变量之前输出这个变量,系统不会给我们报错,指挥在控制台输出“undefined”,这是因为,用var声明的变量在预解析的时候变量被提升了,并默认为其赋值 为“undefined”。但是用let关键字声明变量效果刚好相反,被声明变量不会存在变量提升的情况,输出变量必须在声明变量的语句之后,不然系统就给你报错! 还有 不允许重复声明

    // var 的情况
    console.log(one); // 输出undefined
    var one = 2;

    // let 的情况
    console.log(two); // 报错ReferenceError
    let two = 2;

暂时性死区(let):由上述论点,在一个代码块儿中,若用let声明一个变量,并把输出这个变量的语句放在声明之前,那么系统就会报错,那我们就可以将 以let声明变量的语句为界限,这个界限以上的区域就是 “暂时性死区(temporal dead zone,简称 TDZ)。”,在这个区域内访问变量都会报错!补充:ES6 明确规定,如果区块中存在let和const命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

    if (true) {
      // TDZ开始
      tmp = 'abc'; // ReferenceError
      console.log(tmp); // ReferenceError

      let tmp; // TDZ结束
      console.log(tmp); // undefined

      tmp = 123;
      console.log(tmp); // 123
    }

const的特点

  1. const声明一个只读的常量。一旦声明,就必须初始化,不能留到以后赋值,并且常量的值不能改变。修改就报错!
  2. const和let特点很相似,不存在变量提升只在自己所在代码块内有效不可重复声明、也存在 暂时性死区

const本质重点
const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

二、变量的解构赋值

什么是解构? ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。

1、数组的解构赋值

平时的赋值

    let a = 1;
    let b = 2;
    let c = 3;
    
    // 上面按个声明变量的语句 用解构就可以这样写
    let [a, b, c] = [1, 2, 3] // 数组中提取值,按照对应位置依次赋值。

本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

    let [foo, [[bar], baz]] = [1, [[2], 3]];
    foo // 1
    bar // 2
    baz // 3

    let [ , , third] = ["foo", "bar", "baz"]; // 不完全赋值,但是也可以正常进行
    third // "baz"

    let [x, , y] = [1, 2, 3];  //同上,不完全赋值,一一对应进行赋值
    x // 1
    y // 3

    let [head, ...tail] = [1, 2, 3, 4];
    head // 1
    tail // [2, 3, 4]

    let [x, y, ...z] = ['a'];
    x // "a"
    y // undefined    //如果解构不成功,变量的值就等于undefined。
    z // []  

默认值:解构赋值允许指定默认值的。

    let [foo = true] = [];
    foo // true

    let [x, y = 'b'] = ['a']; // x='a', y='b'
    let [x, y = 'b'] = ['a', undefined]; // x='a', y='b' 
    

注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

    function f() {
      console.log('aaa');
    }

    let [x = f()] = [1]; // 此处x等于1,因为在有值的情况下不会调用函数去求值了,只有在用到的时候才会去求值!

也可以这么写:

    function f() {
        console.log('aaa');
    }
    
    let x
    if ([1][0] === undefined) {
       x = f();
    } else {
      x = [1][0]
    }

默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

    let [x = 1, y = x] = [];     // x=1; y=1
    let [x = 1, y = x] = [2];    // x=2; y=2
    let [x = 1, y = x] = [1, 2]; // x=1; y=2
    let [x = y, y = 1] = [];     // ReferenceError: y is not defined

上面最后一个表达式之所以会报错,是因为x用y做默认值时,y还没有声明。

2、对象的解构赋值

对象解构的特点:对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。如下例子:

    let { bar, foo } = { foo: 'aaa', bar: 'bbb' };
    foo // "aaa"
    bar // "bbb"

    let { baz } = { foo: 'aaa', bar: 'bbb' };
    baz // undefined

对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

    let { foo: foo, bar: bar } = { foo: 'aaa', bar: 'bbb' };
    let { foo: baz } = { foo: 'aaa', bar: 'bbb' };
    baz // "aaa"
    foo // error: foo is not defined
    //上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo。

下面代码有三次解构赋值,分别是对loc、start、line三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,loc和start都是模式,不是变量。

const node = {
  loc: {
    start: {
      line: 1,
      column: 5
    }
  }
};

let { loc, loc: { start }, loc: { start: { line }} } = node;
line // 1
loc  // Object {start: Object}
start // Object {line: 1, column: 5}

对象中的默认值情况

var {x = 3} = {};
x // 3

var {x, y = 5} = {x: 1};
x // 1
y // 5

var {x: y = 3} = {};
y // 3

var {x: y = 3} = {x: 5};
y // 5

var { message: msg = 'Something went wrong' } = {};
msg // "Something went wrong"

注意

  1. 如果要将一个已经声明的变量用于解构赋值,必须非常小心
// 错误的写法
let x;
{x} = {x: 1};
// SyntaxError: syntax error

上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

// 正确的写法
let x;
({x} = {x: 1});

上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。

  1. 解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。
({} = [true, false]);
({} = 'abc');
({} = []);

上面的表达式虽然毫无意义,但是语法是合法的,可以执行。

  1. 由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。
let arr = [1, 2, 3];
let {0 : first, [arr.length - 1] : last} = arr;
first // 1
last // 3

上面代码对数组进行对象解构。数组arr的0键对应的值是1,[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于“属性名表达式”

3、字符串的解构赋值

字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

const [a, b, c, d, e] = 'hello';
a // "h"
b // "e"
c // "l"
d // "l"
e // "o"
//  类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。
let {length : len} = 'hello';
len // 5
4、数值和布尔值的解构赋值

解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

let {toString: s} = 123;
s === Number.prototype.toString // true

let {toString: s} = true;
s === Boolean.prototype.toString // true

上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象 由于undefined和null无法转为对象,所以对它们进行解构赋值,都会报错。

let { prop: x } = undefined; // TypeError
let { prop: y } = null; // TypeError
5、圆括号问题

解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

三、模板字符串、字符串新增方法

模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

    $('#list').html(`
    <ul>
      <li>first</li>
      <li>second</li>
    </ul>
    `);

模板字符串中嵌入变量,需要将变量名写在 ${} 之中,并且还可以进行运算。例如

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}`

模板字符串之中还能调用函数。

function fn() {
  return "Hello World";
}

`foo ${fn()} bar`
// foo Hello World bar

字符串新增方法

  1. String.fromCodePoint():ES5 提供String.fromCharCode()方法,用于从 Unicode 码点返回对应字符,但是这个方法不能识别码点大于0xFFFF的字符。
  2. String.raw():ES6 还为原生的 String 对象,提供了一个raw()方法。该方法返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,往往用于模板字符串的处理方法。如果原字符串的斜杠已经转义,那么String.raw()会进行再次转义。
  3. 实例方法:codePointAt():ES6 提供了codePointAt()方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。总之,codePointAt()方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt()方法相同。codePointAt()方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString()方法转换一下。
  4. 实例方法:repeat():repeat方法返回一个新字符串,表示将原字符串重复n次;如果repeat的参数是负数或者Infinity,会报错;如果repeat的参数是字符串,则会先转换成数字。
  5. 实例方法:padStart(),padEnd():ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。
  6. 实例方法:trimStart(),trimEnd() :ES2019 对字符串实例新增了trimStart()和trimEnd()这两个方法。它们的行为与trim()一致,trimStart()消除字符串头部的空格,trimEnd()消除尾部的空格。它们返回的都是新字符串,不会修改原始字符串。
四、正则表达式的扩展
  1. RegExp 构造函数

在 ES5 中,RegExp构造函数的参数有两种情况
第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

var regex = new RegExp('xyz', 'i');
// 等价于
var regex = /xyz/i;

第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

var regex = new RegExp(/xyz/i);
// 等价于
var regex = /xyz/i;

但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。

var regex = new RegExp(/xyz/, 'i');
// Uncaught TypeError: Cannot supply flags when constructing one RegExp from another

ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

new RegExp(/abc/ig, 'i').flags
// "i"

上面代码中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。

  1. 字符串的正则方法

字符串对象共有 4 个方法,可以使用正则表达式:match()、replace()、search()、split()

ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

String.prototype.match 调用 RegExp.prototype[Symbol.match]
String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
String.prototype.search 调用 RegExp.prototype[Symbol.search]
String.prototype.split 调用 RegExp.prototype[Symbol.split]

五、箭头函数

基本用法
ES6 允许使用“箭头”(=>)定义函数。

var f = v => v;

// 等同于
var f = function (v) {
  return v;
};

如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。注意如果无参数必须把小括号带上。

var f = () => 5;
// 等同于
var f = function () { return 5 };

var sum = (num1, num2) => num1 + num2;
// 等同于
var sum = function(num1, num2) {
  return num1 + num2;
};

如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

var sum = (num1,num2) => { return num1 + num2 }

由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

// 报错
let getTempItem = id => { id: id, name: "Temp" };

// 不报错
let getTempItem = id => ({ id: id, name: "Temp" });

下面是一种特殊情况,虽然可以运行,但会得到错误的结果。

let foo = () => { a: 1 };
foo() // undefined

上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。

如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

let fn = () => void doesNotReturn();

箭头函数的一个用处是简化回调函数。

// 正常函数写法
[1,2,3].map(function (x) {
  return x * x;
});

// 箭头函数写法
[1,2,3].map(x => x * x);

另一个例子:

// 正常函数写法
var result = values.sort(function (a, b) {
  return a - b;
});

// 箭头函数写法
var result = values.sort((a, b) => a - b);

下面是 rest 参数与箭头函数结合的例子。

const numbers = (...nums) => nums;

numbers(1, 2, 3, 4, 5)
// [1,2,3,4,5]

const headAndTail = (head, ...tail) => [head, tail];

headAndTail(1, 2, 3, 4, 5)
// [1,[2,3,4,5]]
六、数组的扩展
  1. 扩展运算符
    含义:扩展运算符(spread)是三个点(…)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。
console.log(...[1, 2, 3])
// 1 2 3

console.log(1, ...[2, 3, 4], 5)
// 1 2 3 4 5

[...document.querySelectorAll('div')]
// [<div>, <div>, <div>]

该运算符主要用于函数调用。

function push(array, ...items) {
  array.push(...items);
}

function add(x, y) {
  return x + y;
}

const numbers = [4, 38];
add(...numbers) // 42

上面代码中,array.push(…items)和add(…numbers)这两行,都是函数的调用,它们都使用了扩展运算符。该运算符将一个数组,变为参数序列。

扩展运算符与正常的函数参数可以结合使用,非常灵活。

function f(v, w, x, y, z) { }
const args = [0, 1];
f(-1, ...args, 2, ...[3]);

扩展运算符后面还可以放置表达式。

const arr = [
  ...(x > 0 ? ['a'] : []),
  'b',
];

如果扩展运算符后面是一个空数组,则不产生任何效果。

[...[], 1]
// [1]

注意,只有函数调用时,扩展运算符才可以放在圆括号中,否则会报错。

(...[1, 2])
// Uncaught SyntaxError: Unexpected number

console.log((...[1, 2]))
// Uncaught SyntaxError: Unexpected number

console.log(...[1, 2])
// 1 2

上面三种情况,扩展运算符都放在圆括号里面,但是前两种情况会报错,因为扩展运算符所在的括号不是函数调用。

替代函数的 apply 方法:
由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。

// ES5 的写法
function f(x, y, z) {
  // ...
}
var args = [0, 1, 2];
f.apply(null, args);

// ES6的写法
function f(x, y, z) {
  // ...
}
let args = [0, 1, 2];
f(...args);

下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。

// ES5 的写法
Math.max.apply(null, [14, 3, 77])

// ES6 的写法
Math.max(...[14, 3, 77])

// 等同于
Math.max(14, 3, 77);

上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。

另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。

// ES5的 写法
var arr1 = [0, 1, 2];
var arr2 = [3, 4, 5];
Array.prototype.push.apply(arr1, arr2);

// ES6 的写法
let arr1 = [0, 1, 2];
let arr2 = [3, 4, 5];
arr1.push(...arr2);

上面代码的 ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。

下面是另外一个例子。

// ES5
new (Date.bind.apply(Date, [null, 2015, 1, 1]))
// ES6
new Date(...[2015, 1, 1]);
七、Set 和 Map 数据结构⭐
1、Set

基本用法:ES6 提供了新的数据结构 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

上面代码通过 add() 方法向 Set 结构 加入成员,结果表明 Set 结构 不会添加重复的值

Set函数可以接受一个数组作为参数,用来初始化。如下例:

// 例一
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

上面代码中,例一和例二都是Set函数接受数组作为参数,例三是接受类似数组的对象作为参数。也展示了一种去除数组重复成员的方法。

// 去除数组的重复成员
[...new Set(array)]  //  返回一个(没有重复项)数组

上面的方法也可以用于,去除字符串里面的重复字符。

[...new Set('ababbc')].join('')
// "abc"

向 Set 加入值的时候,不会发生类型转换,所以5和"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是向 Set 加入值时认为NaN等于自身,而精确相等运算符认为NaN不等于自身。

let set = new Set();
let a = NaN;
let b = NaN;
set.add(a);
set.add(b);
set // Set {NaN}

上面代码向 Set 实例添加了两次NaN,但是只会加入一个。这表明,在 Set 内部,两个NaN是相等的。但是两个对象是不想等的。

const sets = new Set()
sets.add({})
set.size // 1

sets.add({})
set.size // 2

上面代码表示,由于两个空对象不相等,所以它们被视为两个值。

2、Set 实例的属性和方法

Set 结构的实例有以下属性。

  • Set.prototype.constructor:构造函数,默认就是Set函数。
  • Set.prototype.size:返回Set实例的成员总数。
  • Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。

下面先介绍四个操作方法。

  • Set.prototype.add(value):添加某个值,返回 Set 结构本身。
  • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
  • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。
  • Set.prototype.clear():清除所有成员,没有返回值。
s.add(1).add(2).add(2);
// 注意2被加入了两次

s.size // 2

s.has(1) // true
s.has(2) // true
s.has(3) // false

s.delete(2);
s.has(2) // false

Array.from方法可以将 Set 结构转为数组。

const items = new Set([1, 2, 3, 4, 5]);
const array = Array.from(items);

这就提供了去除数组重复成员的另一种方法。

function dedupe(array) {
  return Array.from(new Set(array));
}

dedupe([1, 1, 2, 3]) // [1, 2, 3]

遍历操作
Set 结构的实例有四个遍历方法,可以用于遍历成员。

  • Set.prototype.keys():返回键名的遍历器
  • Set.prototype.values():返回键值的遍历器
  • Set.prototype.entries():返回键值对的遍历器
  • Set.prototype.forEach():使用回调函数遍历每个成员

需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

3、Map

含义和基本用法:JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

const data = {};
const element = document.getElementById('myDiv');

data[element] = 'metadata';
data['[object HTMLDivElement]'] // "metadata"

上面代码原意是将一个 DOM 节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]。

为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

const m = new Map();
const o = {p: 'Hello World'};

m.set(o, 'content')
m.get(o) // "content"

m.has(o) // true
m.delete(o) // true
m.has(o) // false

上面代码使用 Map 结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。

上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

const map = new Map([
  ['name', '张三'],
  ['title', 'Author']
]);

map.size // 2
map.has('name') // true
map.get('name') // "张三"
map.has('title') // true
map.get('title') // "Author"

上面代码在新建 Map 实例时,就指定了两个键name和title。

Map构造函数接受数组作为参数,实际上执行的是下面的算法。

const items = [
  ['name', '张三'],
  ['title', 'Author']
];

const map = new Map();

items.forEach(
  ([key, value]) => map.set(key, value)
);

事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,(Set和Map都可以用来生成新的 Map。)

const set = new Set([
  ['foo', 1],
  ['bar', 2]
]);
const m1 = new Map(set);
m1.get('foo') // 1

const m2 = new Map([['baz', 3]]);
const m3 = new Map(m2);
m3.get('baz') // 3

如果多次给一个键赋值,那么后面的值会覆盖前面的值,若是读取一个未知的键那就报undefined

const map = new Map();

map
.set(1, 'aaa')
.set(1, 'bbb');

map.get(1) // "bbb"

// 若是读取一个未知的键那就报undefined
new Map().get('asfddfsasadf')
// undefined

注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

const map = new Map();

map.set(['a'], 555);
map.get(['a']) // undefined

上面代码的set和get方法,表面是针对同一个键,但实际上这是两个不同的数组实例,内存地址是不一样的,因此get方法无法读取该键,返回undefined。

同理,同样的值的两个实例,在 Map 结构中被视为两个键。

const map = new Map();

const k1 = ['a'];
const k2 = ['a'];

map
.set(k1, 111)
.set(k2, 222);

map.get(k1) // 111
map.get(k2) // 222

实例的属性和操作方法:Map 结构的实例有以下属性和操作方法。

  1. size属性
    size属性返回 Map 结构的成员总数。
const map = new Map();
map.set('foo', true);
map.set('bar', false);

map.size // 2
  1. Map.prototype.set(key, value)
    set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。
const m = new Map();

m.set('edition', 6)        // 键是字符串
m.set(262, 'standard')     // 键是数值
m.set(undefined, 'nah')    // 键是 undefined

set方法返回的是当前的Map对象,因此可以采用链式写法。

const m = new Map();
   .set('edition', 6) 
   .set(262, 'standard')
   .set(undefined, 'nah')
  1. Map.prototype.get(key)
    get方法读取key对应的键值,如果找不到key,返回undefined。
const m = new Map();

const hello = function() {console.log('hello');};
m.set(hello, 'Hello ES6!') // 键是函数

m.get(hello)  // Hello ES6!
  1. 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
  1. Map.prototype.delete(key)
    delete方法删除某个键,删除成功就返回true,失败就返回false
const m = new Map()
m.set('edition', 6);
m.set(262, 'standard');
m.delete('edition') // 通过键值 删除对应项
  1. Map.prototype.clear()
    clear方法,清空Map实例对象中的所有成员,没有返回值

遍历方法

Map 结构原生提供三个遍历器生成函数和一个遍历方法。

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

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

  • Map转为数组
    前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(…)。
const myMap = new Map()
  .set(true, 7)
  .set({foo: 3}, ['abc']);
[...myMap]
// [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
  • 数组转Map
    将数组传入 Map 构造函数,就可以转为 Map。
new Map([
  [true, 7],
  [{foo: 3}, ['abc']]
])
// Map {
//   true => 7,
//   Object {foo: 3} => ['abc']
// }
  • Map 转为对象
    如果所有 Map 的键都是字符串,它可以无损地转为对象。
function strMapToObj(strMap) {
  let obj = Object.create(null);
  for (let [k,v] of strMap) {
    obj[k] = v;
  }
  return obj;
}

const myMap = new Map()
  .set('yes', true)
  .set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }

如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

  • 对象转为 Map
    对象转为 Map 可以通过Object.entries()。
let obj = {"a":1, "b":2};
let map = new Map(Object.entries(obj));

此外,也可以自己实现一个转换函数。

function objToStrMap(obj) {
  let strMap = new Map();
  for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
  }
  return strMap;
}

objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
  • Map 转为 JSON
    Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。
function strMapToJson(strMap) {
  return JSON.stringify(strMapToObj(strMap));
}

let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'

另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

function mapToArrayJson(map) {
  return JSON.stringify([...map]);
}

let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
  • JSON 转为 Map
    JSON 转为 Map,正常情况下,所有键名都是字符串。
function jsonToStrMap(jsonStr) {
  return objToStrMap(JSON.parse(jsonStr));
}

jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}

但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

function jsonToMap(jsonStr) {
  return new Map(JSON.parse(jsonStr));
}

jsonToMap('[[true,7],[{"foo":3},["abc"]]]')
// Map {true => 7, Object {foo: 3} => ['abc']}
八、Class(类)

JS语言中没有类的概念,当我们实例化一个对象的时候,用的是构造函数的通过关键字NEW来实现的。继承也是通过原型链来处理的,如下列:

function Person(name, age, sex) (
  this.name = name;
  this.age = age;
  this.sex = sex;
)
Person.prototype.sayHi = funciton() {
  return 我的名字叫" + this.name+"今年"+this.age+"岁了";
}
// 实例化一个对象
var obj=new Person("laotie",88);//通过构造函数创建对象,必须使用new 运算符

ES6引入了Class(类)这个概念,通过class关键字可以定义类。该关键字的出现使得其在对象写法上更加清晰,更像是一种面向对象的语言。如果将之前的代码改为ES6的写法就会是这个样子:

class Person{//定义了一个名字为Person的类
    constructor(name,age){//constructor是一个构造方法,用来接收参数
        this.name = name;//this代表的是实例对象
        this.age=age;
    }
    say(){//这是一个类的方法,注意千万不要加上function
        return "我的名字叫" + this.name+"今年"+this.age+"岁了";
    }
}

注意:   1.在类中声明方法的时候,千万不要给该方法加上function关键字
        2.方法之间不要用逗号分隔,否则会报错

Class继承
一个例子:


class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  say() {
    return name+age;
  }
}
// 在class类中,可以使用extends关键字,实现 子类继承父类
// 语法 : class 子类 extends 父类 {}
class American extends Person {
  constructor() {
    // 问题1: 为什么一定要在constructor 中调用super
    // 答:因为如果一个子类,通过extends关键字继承了父类,那么在子类的constructor构造函数中,必须优先调用一下super()
    // 问题2: super是个什么东西?
    // 答:super是一个函数,而且它是父类的构造器;子类中的super,其实就是父类中,constructor构造器的一个引用
    // 问题3: 为什么调用了super()之后,a1实例的name和age都变成undefined了?
    // 答:调用super(),相当于调用父类constructor构造器,因为没传值,所以都为undefined
    super()
  }
  // 正确写法
  constructor(name, age) {
    super(name, age)
  }
}
const a1 = new American('jack', 20);
 
class Chinese extends Person {
  // 正确写法
  constructor(name, age, personNumber) {
    super(name, age)
    // 语法规范:在子类中,this只能放在super之后调用
    this.personNumber = personNumber
 
  }
}
const a2 = new Chinese('张三', 30, 360202198905108888);
九、模块化

在之前的javascript中是没有模块化概念的。如果要进行模块化操作,需要引入第三方的类库。随着技术的发展,前后端分离,前端的业务变的越来越复杂化。直至ES6带来了模块化,才让javascript第一次支持了module。ES6的模块化分为导出(export)与导入(import)两个模块。

  1. export的用法
    在ES6中每一个模块即是一个文件,在文件中定义的变量,函数,对象在外部是无法获取的。如果你希望外部可以读取模块当中的内容,就必须使用export来对其进行暴露(输出)。先来看个例子,来对一个变量进行模块化。我们先来创建一个test.js文件,来对这一个变量进行输出:
export let myName="hanfei";

然后可以创建一个index.js文件,以import的形式将这个变量进行引入:

import {myName} from "./test.js";
console.log(myName);//hanfei

如果要输出多个变量可以将这些变量包装成对象进行模块化输出:

let myName="hanfei";
let myAge=10;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
    myName,
    myAge,
    myfn
}

/******************************接收的代码调整为**********************/

import {myfn,myAge,myName} from "./test.js";
console.log(myfn());//我是hanfei!今年10岁了
console.log(myAge);//10
console.log(myName);//hanfei

如果你不想暴露模块当中的变量名字,可以通过as来进行操作:

let myName="hanfei";
let myAge=10;
let myfn=function(){
    return "我是"+myName+"!今年"+myAge+"岁了"
}
export {
// 用as将模块中的变量名字修改掉
    myName as name,
    myAge as age,
    myfn as fn
}
/******************************接收的代码调整为**********************/
import {fn,age,name} from "./test.js";
console.log(fn());//我是hanfei!今年10岁了
console.log(age);//10
console.log(name);//hanfei

也可以直接导入整个模块,将上面的接收代码修改为:

import * as info from "./test.js";//通过*来批量接收,as 来指定接收的名字
console.log(info.fn());//我是hanfei!今年10岁了
console.log(info.age);//10
console.log(info.name);//hanfei
  1. 默认导出(default export)

一个模块只能有一个默认导出,对于默认导出,导入的名称可以和导出的名称不一致。

/******************************导出**********************/
export default function(){
    return "默认导出一个方法"
}
/******************************引入**********************/
import myFn from "./test.js";//注意这里默认导出不需要用{}。
console.log(myFn());//默认导出一个方法

可以将所有需要导出的变量放入一个对象中,然后通过default export进行导出:

/******************************导出**********************/
export default {
    myFn(){
        return "默认导出一个方法"
    },
    myName:"hanfei"
}
/******************************引入**********************/
import myObj from "./test.js";
console.log(myObj.myFn(),myObj.myName);//默认导出一个方法 hanfei

同样也支持混合导出

/******************************导出**********************/
export default function(){
    return "默认导出一个方法"
}
export var myName="hanfei";
/******************************引入**********************/
import myFn,{myName} from "./test.js";
console.log(myFn(),myName);//默认导出一个方法 hanfei
  1. 重命名export和import

如果导入的多个文件中,变量名字相同,即会产生命名冲突的问题,为了解决该问题,ES6为提供了重命名的方法,当你在导入名称时可以这样做:

/******************************test1.js**********************/
export let myName="我来自test1.js";
/******************************test2.js**********************/
export let myName="我来自test2.js";
/******************************index.js**********************/
import {myName as name1} from "./test1.js";
import {myName as name2} from "./test2.js";
console.log(name1);//我来自test1.js
console.log(name2);//我来自test1.js

conosle.log(first)  // undefined
var first = 123
console.log(first)  // 123

 var num = 123;
    function foo(){
        console.log(num);  // undefined
        var num = 46;
        console.log(num) // 46
    }
    foo()


 let num = 789;
    function foo(){
        let num = 46;
    console.log(num) // 46
    }
    foo()
    console.log(num) // 789


const normalObject={}; //定义一个常量
normalObject.propertyOne='this is property of One'; 
normalObject.propertyOne='this is a new property'; //重新给对象的属性赋值
console.log(normalObject.propertyOne); //'this is a new property';

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值