深入理解ES6笔记

目录

var

let

const

const和let的异同点

临时死区(Temporary Dead Zone)

循环中块级作用域的绑定

在全局作用域声明

最佳实践

二,字符串和正则表达式

更好的Unicode支持

UTF-16码位:

codePointAt():

String.fromCodePoiont():

normalize():

正则表达式

其他字符串变更

正则表达式的其他更新

模板字面量

三,函数

函数的默认参数

无命名参数

展开运算符(...)

块级函数

箭头函数(=>)

尾调用优化

四,扩展对象的功能性

对象类别

对象字面量语法拓展

ES6对象新增方法

重复的对象字面量属性

自有属性枚举顺序

增强对象原型

五,解构:使数据访问更便捷

解构的分类

对象解构

数组解构

混合解构

解构参数

六,symbol和symbol属性

原始数据类型

Symbol

七,set集合与map集合

Set

Weak Set集合

Map

Weak Map

八,迭代器(lterator)和生成器(generator)

迭代器(Iterator)

生成器(Generator)

异步任务执行器

九,JavaScript中的类

ES5中的近类结构

ES6 class类

十,改进的数组功能

创建数组

给数组添加新方法

十一,promise与异步编程

Promise

十二,代理(proxy)与反射(reflection)

反射 Reflect

代理 Proxy

十三,用模块封装代码

模块的定义

模块的导出

模块的引用

默认模块的使用

模块的使用限制

修改模块导入和导出名

无绑定导入

浏览器加载模块

数组常用方法

var

使用var声明的变量,无论是在代码的哪个地方声明的,都会提升到当前作用域的最顶部,这种行为叫做变量提升(Hoisting)

如果在函数内部声明的变量,都会被提升到该函数开头,而在全局声明的变量,就会提升到全局作用域的顶部。

在函数嵌套函数的场景下,变量只会提升到最近的一个函数顶部,而不会提升到外部函数。

let

let和const都能够声明块级作用域,用法和var是类似的,let的特点是不会变量提升,而是被锁在当前块中。

禁止重复声明

const

声明常量,一旦声明,不可更改,而且常量必须初始化赋值。

const不允许修改绑定,单允许修改值,所以声明对象Object,是可以修改对象内部的属性值

const text = {
      a: 1
    }
    text.a = 2 //没有直接修改text的值,而是修改text.a的属性值,这是允许的。

const和let的异同点

相同点:const和let都是在当前块内有效,执行到块外会被销毁,也不存在变量提升,不能重复声明。

不同点:const不能再赋值,let声明的变量可以重复赋值。

临时死区(Temporary Dead Zone)

临时死区的意思是在当前作用域的块内,在声明变量前的区域叫做临时死区。

if (true) {
      //这块区域是TDZ
      let a = 1
    }

循环中块级作用域的绑定

在for循环中使用var声明的循环变量,会跳出循环体污染当前的函数。

for(var i = 0; i < 5; i++) {
      setTimeout(() => {
        console.log(i) //5, 5, 5, 5, 5
      }, 0)
    }
    console.log(i) //5 i跳出循环体污染外部函数
    
    
for(let i = 0; i < 5; i++) {
      setTimeout(() => {
        console.log(i) // 0,1,2,3,4
      }, 0)
    }
    console.log(i)//i is not defined i无法污染外部函数

在全局作用域声明

如果在全局作用域使用let或者const声明,当声明的变量本身就是全局属性,比如closed。只会覆盖该全局变量,而不会替换它。

   
 window.closed = false
    let closed = true
    
    closed // true
    window.closed // false

最佳实践

在实际开发中,我们选择使用var、let还是const,取决于我们的变量是不是需要更新,通常我们希望变量保证不被恶意修改,而使用大量的const,在react中,props传递的对象是不可更改的,所以使用const声明,声明一个对象的时候,也推荐使用const,当你需要修改声明的变量值时,使用let,var能用的场景都可以使用let替代。

二,字符串和正则表达式

字符串(String)是JavaScript6大原始数据类型。其他几个分别是Boolean、Null、Undefined、Number、Symbol(es6新增)。

更好的Unicode支持

当Unicode引入扩展字符集之后,16位的字符已经不足以满足字符串的发展,所以才在ES6中更新了Unicode的支持。

UTF-16码位:

ES6强制使用UTF-16字符串编码

codePointAt():

该方法支持UTF-16,接受编码单元的位置而非字符串位置作为参数,返回与字符串中给定位置对应的码位,即一个整数值。

let a='吉a' console.log(a.charCodePointAt(0)) //134071 console.log(a.charCodePointAt(1)) //57271 console.log(a.charCodePointAt(2)) //97 ​

String.fromCodePoiont():

作用与codePointAt相反,可以根据指定的码位生成一个字符。

normalize()

提供Unicode的标准形式,接受一个可选的字符串参数,指明应用某种Unicode标准形式。

正则表达式

正则表达式u修饰符: 当给正则表达式添加u字符时,它就从编码单元操作模式切换为字符模式。

检测

function hasReg(){
 try {
  var pattern=new RegExp(".","u")
  return true;
 } catch(ex){
  return false
 }
}

其他字符串变更

字符串中的子串识别

以前我们使用indexOf()来检测字符串中是否包含另外一段字符串。

    let t = 'abcdefg'
    if(t.indexOf('cde') > -1) {
      console.log(2)
    }
    //输出2,因为t字符串中包含cde字符串。

在ES6中,新增了3个新方法。每个方法都接收2个参数,需要检测的子字符串,以及开始匹配的索引位置。

includes(str, index):如果在字符串中检测到指定文本,返回true,否则false。

    let t = 'abcdefg'
    if(t.includes('cde')) {
      console.log(2)
    }
    //true

startsWith(str, index):如果在字符串起始部分检测到指定文本,返回true,否则返回false。

    let t = 'abcdefg'
    if(t.startsWith('ab')) {
      console.log(2)
    }
    //true

endsWith(str, index):如果在字符串的结束部分检测到指定文本,返回true,否则返回false。

    let t = 'abcdefg'
    if(t.endsWith('fg')) {
      console.log(2)
    }
    //true

如果你只是需要匹配字符串中是否包含某子字符串,那么推荐使用新增的方法,如果需要找到匹配字符串的位置,使用indexOf()。

repeat(number)

接收一个Number类型的数据,返回一个重复N次的新字符串。即使这个字符串是空字符,也你能返回N个空字符的新字符串。

console.log('ab'.repeat(3)) //ababab

正则表达式的其他更新

正则表达式 y 修饰符:该属性会通知搜索正则表达式的 lastIndex 属性开始进行,如果在指定位置没有匹配成功,则停止继续匹配。

flags属性:该属性可以返回所有应用于当前正则表达式的修饰符字符串。

模板字面量

模板字面量反撇号``

let a = `123` 

在字符串中使用反撇号,只需要加上转义符。

let a = `12\`3` //字符串内插入反撇号的方式。 12`3

在多行字符串的使用价值:

不使用模板字面量,实现多行字符串,会使用换行符/n。

    let a = '123\n456'
    console.log(a) 
    // 123
    // 456

使用模板字面量

    let a = `123
    456
    `
    console.log(a)
    // 123
    // 456

字符串占位符 在模板字面量插入变量的方法。

使用${params}直接插入要添加到字符串的位置。

    let t = 'haha'
    let a = `hello,${t}`
    console.log(a) //hello,haha

标签模板 tag是一个方法,方法名你可以任意命名,每个标签都可以执行模板字面量上的转换并返回最终的字符串值。简单说,它就是一个包含了反撇号表达式的函数。这个函数以反撇号表达式作为参数,然后标签名就是这个函数的函数名

    function tag(literals, ...substitutions) {
        //literals是数组,第一个位置是"",第二个位置是占位符之间的字符串,在本例中是haha
        //substitutions是字符串中的模板字面量,可能多个 
        //函数最终返回字符串
    }
    let a = 4
    let t = tag`${a} haha`
    console.log(t) //4 haha

三,函数

函数的默认参数

在ES5中,在函数体内要对形参的值进行进一步的判断,如果不满足条件,则给其一个默认值。例如:

   function makeRequest(url, timeout, callback) 
{
    timeout = timeout || 2000;
    
    //other code          
}

而在ES6中,为了更加简化函数体的代码,给形参赋默认值的操作可以直接在写参数的时候加上,以此减少函数体内的代码量。例如:

   
function makeRequest(url, timeout = 2000, callback)
{
        //other code
}

使用ES6的默认值写法可以让函数体内部的代码更加简洁优雅

默认值对arguments对象的影响

arguments对象是是一个类数组对象,它存在函数内部,它将当前函数的所有参数组成了一个类数组对象。

修改参数默认值对arguments的影响。

1、在ES5的非严格模式下,一开始输入的参数是1,那么可以获取到arguments[0](表示第一个参数)全等于num,修改num = 2之后,arguments[0]也能更新到2。

    function a(num){
      console.log(num === arguments[0]) //true
      num = 2 //修改参数默认值
      console.log(num === arguments[0]) //true
    }
    a(1)

2、在ES5的严格模式下,arguments就不能在函数内修改默认值后跟随着跟新了。

    "use strict"; //严格模式   
    function a(num) {
      console.log(num === arguments[0]); // true
      num = 2;
      console.log(num === arguments[0]); // false
    }
    a(1);

在ES6环境下,默认值对arguments的影响和ES5严格模式是同样的标准。

默认参数表达式

参数不仅可以设置默认值为字符串,数字,数组或者对象,还可以是一个函数。

    function add() {
      return 10
    }
    function a(num = add()){
      console.log(num)
    }
    a() // 10

默认参数的临时死区

第一章提到了let和const的临时死区(TDZ),默认参数既然是参数,那么也同样有临时死区,函数的作用域是独立的,a函数不能共享b函数的作用域参数。

    //这是个默认参数临时死区的例子,当初始化a时,b还没有声明,所以第一个参数对b来说就是临时死区。
    function add(a = b, b){
      console.log(a + b)
    }
    add(undefined, 2) // b is not define

无命名参数

上面说的参数都是命名参数,而无命名参数也是函数传参时经常用到的。当传入的参数是一个对象,不是一个具体的参数名,则是无命名参数。

    function add(object){
      console.log(object.a + object.b)
    }
    let obj = {
      a: 1,
      b: 2
    }
    add(obj) // 3

不定参数:使用...(展开运算符)的参数就是不定参数,它表示一个数组。

    function add(...arr){
      console.log(a + b)
    }
    let a = 1,b = 2
    add(a, b) // 3

不定参数的使用限制:必须放在所有参数的末尾,不能用于对象字面量setter中。

    //错误的写法1
    function add(...arr, c){
      console.log(a + b)
    }
    let a = 1,b = 2,c = 3
    add(a, b, c)
    
    //错误的写法2
    let obj = {
      set add(...arr) {
      
      }
    }

增强的Function构造函数

在ES5中,可以像下面这样使用构造函数:

var add = new Function( "first", "second", "return first + second" );

console.log( add(1, 1) ); // 2

在ES6中,Function构造函数可以使用默认参数和不定参数。例如:

var add = new Function("first", "second=first", "return first + second");

console.log( add(1, 1) ); //2
console.log( add(1) ); //2

var pickFirst = new Function( "...args", "return args[0]" );

console.log( pickFirst(1, 2) ); // 1

展开运算符(...)

展开运算符的作用是解构数组,然后将每个数组元素作为函数参数。

有了展开运算符,我们操作数组的时候,就可以不再使用apply来指定上下文环境了。

    //ES5的写法
    let arr = [10, 20, 50, 40, 30]
    let a = Math.max.apply(null, arr)
    console.log(a) // 50
    
    //ES6的写法
    let arr = [10, 20, 50, 40, 30]
    let a = Math.max(...arr)
    console.log(a) // 50

name属性

   JavaScript中,有多种定义函数的方式。例如:正常定义的函数、函数表达式、匿名函数等。为了便于调试函数,于是在ES6中,添加了函数的 name 属性。该属性的值可以返回当前的函数名。

  当然也有一些特殊的情况,会在函数名的前面加上一些字符串前缀。例如:

    getter函数,函数名前面会有get

    setter函数,函数名前面会有set

    bind()函数,函数名前面会有bound

  注意,函数name属性不一定和原函数名完全相同,所以它只是一个调试的辅助信息,而不能用它来获取对函数的引用

明确函数的多种用途

  JavaScript函数有两个不同的内部方法:[[ Call ]] 、 [[ Construct ]]。

  当通过 new 关键字调用函数的时候,执行的是 [[ Construct ]] 函数。如果不是通过 new 关键字来调用函数,那么执行的就是 [[ Call ]] 函数。

  不是所有函数都有 [[ Construct ]]方法。

    例如ES6中的箭头函数就没有这个[[ Condtruct ]]方法。

   在ES6中,可以通过 new.target 这个属性来判断是否是通过 new 关键字调用的函数。

块级函数

严格模式下:在ES6中,你可以在块级作用域内声明函数,该函数的作用域只限于当前块,不能在块的外部访问。

    "use strict";
    if(true) {
      const a = function(){
      
      }
    } 

非严格模式:即使在ES6中,非严格模式下的块级函数,他的作用域会被提升到父级函数的顶部。

箭头函数(=>)

    const arr = [5, 10]
    const s = arr.reduce((sum, item) => sum + item)
    console.log(s) // 15

箭头函数和普通函数的区别是:

1、箭头函数没有this,函数内部的this来自于父级最近的非箭头函数,并且不能改变this的指向。

2、箭头函数没有super

3、箭头函数没有arguments

4、箭头函数没有new.target绑定。

5、不能使用new

6、没有原型

7、不支持重复的命名参数。

箭头函数的简单理解

1、箭头函数的左边表示输入的参数,右边表示输出的结果。

    const s = a => a
    console.log(s(2)) // 2

2、箭头函数中,没有this绑定。

3、箭头函数还可以输出对象,在react的action中就推荐这种写法。

    const action = (type, a) => ({
      type: "TYPE",
      a
    })

4、支持立即执行函数表达式写法

    const test = ((id) => {
      return {
        getId() {
          console.log(id)
        }
      }
    })(18)
    test.getId() // 18

5、箭头函数给数组排序

    const arr = [10, 50, 30, 40, 20]
    const s = arr.sort((a, b) => a - b)
    console.log(s) // [10,20,30,40,50]

尾调用优化

当一个函数作为另一个函数的最后一条语句被调用时,就叫做尾调用

ES6中,引擎会帮你做好尾调用的优化工作,但需要满足下面3个要求:

1、函数不是闭包

2、尾调用是函数最后一条语句

3、尾调用结果作为函数返回

一个满足以上要求的函数如下所示:

    "use strict";   
    function a() {
      return b();
    }

下面的都是不满足的写法:

    //没有return不优化
    "use strict";
    function a() {
      b();
    }
    
    //不是直接返回函数不优化
    "use strict";
    function a() {
      return 1 + b();
    }
    
    //尾调用是函数不是最后一条语句不优化
    "use strict";
    function a() {
      const s = b();
      return s
    }
    
    //闭包不优化
    "use strict";
    function a() {
      const num = 1
      function b() {
        return num
      }
      return b
    }

尾调用实际用途——递归函数优化

    //新型尾优化写法
    "use strict";  
    function a(n, p = 1) {
      if(n <= 1) {
        return 1 * p
      }
      let s = n * p
      return a(n - 1, s)
    }
    //求 1 x 2 x 3的阶乘
    let sum = a(3)
    console.log(sum) // 6

四,扩展对象的功能性

对象类别

在ES6中,对象分为下面几种叫法。

1、普通对象:具有JavaScript对象所有的内部默认行为

2、特异对象:具有某些与默认行为不符的内部行为

3、标准对象:es6规范定义的对象,array,data等,既可以是普通对象,也可以是特异对象

4、内建对象:脚本开始执行是存在于JavaScript执行环境中的对象,所以的标准对象都是内建对象

对象字面量语法拓展

ES6针对对象的语法扩展了一下功能

1、属性初始值简写

    //ES5
   function createPerson(name, age)
{
     return {
         name: name,
         age: age       
    }
}
    
    //ES6
   
 function createPerson(name, age)
 {
      return {
           name,
           age   
      }
}

2、对象方法简写

    // ES5
    var person = {
    name: 'foo',
    sayName: function() {
        console.log(this.name);
    }
}
    
    // ES6
   var person = {
    name: "foo",
    sayName() {
        console.log(this.name);
    }
}

3、属性名可计算

属性名可以传入变量或者常量,而不只是一个固定的字符串。

    const id = 5
    const obj = {
      [`my-${id}`]: id
    }
    console.log(obj['my-5']) // 5

ES6对象新增方法

在Object原始对象上新增方法,原则上来说不可取,但是为了解决全世界各地提交的issue,在ES6中的全局Object对象上新增了一些方法。

1、Object.is()

该方法接收两个参数,判断它们是否相等。与全等“===”类似。但是有两个特例:

1)参数:+0 和 -0

console.log(+0 === -0); //true
console.log(Object.is(+0, -0)); //false

2) 参数:NaN

console.log(NaN === NaN); //false
console.log(Object.is(NaN, NaN)); //true

2、Object.assign()

共2个参数,将第2个参数复制到第1个参数中。

使用Object.assign(),你就可以不是有继承就能获得另一个对象的所有属性,快捷好用。 Object.assign 方法只复制源对象中可枚举的属性和对象自身的属性。 看一个实现Component的例子。

const target = { a: 1, b: 2 };
const source = { b: 4, c: 5 };

const returnedTarget = Object.assign(target, source);

console.log(target);
// expected output: Object { a: 1, b: 4, c: 5 }

console.log(returnedTarget);
// expected output: Object { a: 1, b: 4, c: 5 }

重复的对象字面量属性

ES5的严格模式下,如果你的对象中出现了key相同的情况,那么就会抛出错误。而在ES6的严格模式下,不会报错,后面的key会覆盖掉前面相同的key。

    const state = {
      id: 1,
      id: 2
    }
    console.log(state.id) // 2

自有属性枚举顺序

在ES5中没有定义对象属性的枚举顺序,在ES6中,严格规定了对象的自有属性被枚举时的返回顺序。

自有属性枚举顺序的规则:

    1)所有数字键按升序排序;

    2)所有字符串键按照它们被加入对象的顺序排序;

    3)所有symbol键按它们被加入对象的顺序排序。

var obj = {
    a: 1,
    0: 1,
    c: 1,
    2: 1,
    b: 1,
    1: 1
}
obj.d = 1;
console.log( Object.getOwnPropertyNames(obj).join("") ); // "012adcd"  
 getOwnPropertyNames 返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括Symbol值作为名称的属性)组成的数组。

  注意:1)数值键在枚举时会被重新组合和排序;

    2)先枚举数字,再枚举字符串

增强对象原型

如果你想定义一个对象,你会想到很多方法。

    let a = {}
    
    let b = Object.create(a)
    
    function C() {}
    
    class D {}

为了让开发者获得更多对原型的控制力,于是ES6对原型进行了修改。

改变原型对象

    在ES6中,添加了Object.setPropertyOf()方法来改变原型的对象及替代第一个参数原型的对象。

   let a = {
      name() {
        return 'eryue'
      }
    }
    let b = Object.create(a)
    console.log(b.name()) // eryue
      
    //使用setPrototypeOf改变b的原型
    let c = {
      name() {
        return "aa"
      }
    }    
    Object.setPrototypeOf(b, c)    
    console.log(b.name()) //aa

方法的定义

即在对象的内部的函数叫方法,没有在对象内部的函数叫函数。例如:

    let a = {
      //方法
      name() {
        return 'eryue'
      }
    }
    //函数
    function name() {}

五,解构:使数据访问更便捷

解构是从对象中提取出更小元素的过程。赋值是对解构出来的元素进行重新赋值。

解构的分类

1、对象解构

2、数组解构

3、混合解构

4、解构参数

对象解构

对象解构的语法是在一个赋值操作符的左边,设置一个对象字面量

    let obj = {
      a: 1,
      b: [1, 2]
    }
    // 对象解构
    const { a, b } = obj
    console.log(a, b) //1  [1, 2]

默认值。

对解构赋值中不存在的属性可以随意定义一个默认值。举例:

 let node = {
     type: "Identifier",
     name: "foo"
 }
 
 let {type, name, value=true} = node;
 
 console.log(type); //"Identifier"
 console.log(name); //"foo"
 console.log(value); //true

为非同名局部变量赋值。

如果希望用不同命名的局部变量来存储对象属性的值,可以用这样一个扩展语法来实现。举例:

 let node = {
     type: "Identifier",
     name: "foo"
 }
 
 let {type:localType, name:localName} = node;
 
 console.log(localType); //"Identifier"
 console.log(localName); //"foo"

嵌套对象解构。

  解构嵌套对象与对象字面量的语法相似。可以拆解以获取想要的信息。举例:

 let node = {
     type: "Identifier",
     name: "foo",
     loc: {
          start: {
             line: 1,
              column: 1
          },
         end: {
             line: 1,
             column: 4
         }
    }
 };
 
 let { loc: {start} } = node;
 
 console.log(start.line); //1
 console.log(start.column); // 1

数组解构

数组解构比对象解构简单,因为数组只有数组字面量,不需要像对象一个使用key属性。

数组解构 你可以选择性的解构元素,不需要解构的元素就使用逗号代替。

    let arr = [1, 2, 3]
    
    //解构前2个元素
    const [a, b] = arr
    console.log(a,b) //1 2
    
    //解构中间的元素
    const [, b,] = arr
    console.log(b) // 2

解构赋值

    //初始化一个变量a
    let a = "haha";
    //定义一个数组
    let arr = [1, 2, 3];
    //解构赋值a,将arr数组的第一个元素解构赋值给a,
    [a] = arr;
    console.log(a); // 1

使用解构赋值,还可以调换2个变量的值。

    let a = 1, b = 2;
    [a, b] = [b, a];
    console.log(a, b); // 2 1 

嵌套数组解构

    let arr = [1, [2, 3], 4];
    let [a, [,b]] = arr;
    console.log(a, b) // 1 3
    
    //实际解构过程,左边的变量和右边的数组元素一一对应下标。
    var a = arr[0],
    _arr$ = arr[1],
    b = _arr$[1];

不定元素解构 三个点的解构赋值必须放在所有解构元素的最末尾,否则报错。

    let arr = [1, 2, 3, 4];
    let [...a] = arr;
    console.log(a) //[1,2,3,4] 这种做法就是克隆arr数组。

混合解构

混合解构指的是对象和数组混合起来

    let obj = {
      a: {
        id: 1
      },
      b: [2, 3]
    }
    
    const {
      a: {id},
      b:[...arr]
    } = obj;
    console.log(id, arr) //id = 1, arr = [2, 3]

解构参数

即将参数,尤其是对象数据类型的参数解构为更加易读的代码

  function setCookie(name, value, options) {
      options = options || {};
  
      let secure = options.secure,
          path = options.path,
          domain = options.domain,
          expires = options.expires
  
      //设置cookie代码
 }
 
 //第三个参数映射到options中
 setCookie("type", "js", {
     secure: true,
     expires: 60000
 });
 
 如果我们来解构参数的话,可以这么写:
  function setCookie(name, value, {secure, path, domain, expires}) {
     //设置cookie代码
 }
 setCookie("type", "js", {
     secure: true,
     expires: 60000
 });
 
 但是这种情况下,如果不传递参数会报错,所以我们可以将其优化为:
  function setCookie( name, value, {secure, path, domain, expires} = {} ) {
     //设置cookie代码
 }
 setCookie("type", "js", {
     secure: true,
     expires: 60000
 });

六,symbol和symbol属性

原始数据类型

null、undefined ,Number 数字类型,String 字符串,boolean 布尔型

Symbol

Symbol 指的是ES6中的私有属性

Symbol 是一种特殊的、不可变的数据类型,可以作为对象属性的标识符使用。Symbol 对象是一个 symbol primitive data type 的隐式对象包装器。

symbol 数据类型是一个原始数据类型。

创建一个Symbol:

 let firstName = Symbol();
 let person = {};
 
 person[firstName] = "zxx";
 console.log(person[firstName]); //"zxx"

Symbol不能使用new

    const name = new Symbol(); //不可以这样做。
    //Symbol is not a constructor

最大的用法是用来定义对象的唯一属性名。

使用Symbol:

使用Number的时候,我们可以这样写:

    const b = Number(10) // 10
    //简写
    const b = 10

同理,使用Symbol,我们可以这样:

const name1 = Symbol('sym1'); // Symbol(sym1)

在所有使用可计算属性名的地方,都能使用Symbol类型。比如在对象中的key。

const name = Symbol('name');
    const obj = {
      [name]: "haha"
    }
    console.log(obj[name]) // haha

你还可以使用Object.defineProperty()和Object.defineProperties()方法。这2个方法是对象的方法,会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。但是作为Symbol类型key,也不影响使用。

    // 设置对象属性只读。3 2
    Object.defineProperty(obj, name, {writable: false})

Symbol全局共享

有时候我们可能希望在不同的代码中共享同一个Symbol,在ES6中提供了一个可以随时访问的全局Symbol注册表,它是一个类似于全局作用域的共享环境。如果要创建一个可以共享的Symbol,使用Symbol.for()方法。它只接受要给参数,也就是即将创建的Symbol的字符串的标识符。举例

 let uid = Symbol.for("uid");
 let object = {};
 Object[uid] = "12345";
 
 console.log( Object[uid] ); //"12345"
 console.log( uid ); //  "Symbol(uid)"

Symbol与类型强制转换

JavaScript中的类型可以自动转换。比如Number转换成字符串。

    let a = 1;
    console.log(typeof a); // number
    console.log(a + ' haha') // '1haha'

但是没有与Symbol逻辑等价的值,因而Symbol使用起来不是很灵活,尤其是不能将Symbol强制转换为字符串和数字类型。

    let a = Symbol('a');
    console.log(typeof a);
    console.log(a + ' haha') // Cannot convert a Symbol value to a string

Symbol检索

在ES6中,添加了 Object.getOwnPropertySymbols() 方法,该方法返回一个包含所有Symbol自有属性的数组。

 let uid = Symbol.for("uid");
 let object = {
     [uid]: "12345"
 };
 let symbols = Object.getOwnPropertySymbols(object);
 
 console.log( symbols.length ); // 1
 console.log( symbols[0] ); // "Symbols(uid)"
 console.log( object[symbols[0]] ); // "12345"

七,set集合与map集合

Set

Set是有序列表,含有相互独立的非重复值。

创建Set new set()

add() :可以向集合中添加元素

访问集合的 size 属性可以获取集合中目前的元素的数量

has() 方法,可以判断Set集合中是否存在某个值

delete() 方法,可以移除Set集合中的某一个元素

clear() 方法,可以移除集合中的所有元素

         let set = new Set();
             set.add(5);
             set.add("5");
         console.log( set.size ); //2
 
 console.log( set.has(5) ); //true
 console.log( set.has(6) ); //false

      set.delete(5);
 console.log( set.has(5) ); //false
 
      set.clear();
 console.log( set.has("5") ); //false
 console.log( set.size ); //0

forEach() 方法来迭代 Set 集合。 forEach() 接收的三个参数为:value / key / ownerSet(原来的Set集合)。

set集合中下一次索引的位置 / 与第一个参数一样的值 / 被遍历的set本身

Set本身没有key,而forEach方法中的key被设置成了元素本身。

      let set = new Set( [1, 2] );
       set.forEach(function(value, key, ownerSet) {
            console.log( key + " " + value );
            console.log( ownerSet === Set );
       });
  
      //打印
      //1 1
      //true
     //2 2
     //true

Set和Array的转换

将数组转为Set集合:给Set构造函数传入数组即可; 将Set转为数组:使用展开运算符。

    //数组转换成Set
    const arr = [1, 2, 2, '3', '3']
    let set = new Set(arr);
    console.log(set) // Set(3) {1, 2, "3"}

    //Set转换成数组
     let set = new Set( [1,2,3,3,3,4,5] ),
      array = [...set];
  console.log( array ); //[1,2,3,4,5]
 // 数组去重
    let arr = [1, 1, 2, 3];
    let unique = [... new Set(arr)];
 
 
    let a = new Set([1, 2, 3]);
    let b = new Set([4, 3, 2]);
    
    // 并集
    let union = [...new Set([...a, ...b])];
    
    // 交集
    let intersect = [...new Set([...a].filter(x => b.has(x)))];
    
    // 差集

Weak Set集合

Set集合本身是强引用,只要new Set()实例化的引用存在,就不释放内存,比如你定义了一个DOM元素的Set集合,然后在某个js中引用了该实例,但是当页面关闭或者跳转时,你希望该引用应立即释放内存, Weak Set(弱引用Set集合)中的弱引用如果是对象的唯一引用,则会被回收并释放相应内存。

Weak Set创建、添加、判断、删除方法:

 let set = new WeakSet(),
      key = {};
             
      //向集合中添加对象
      set.add(key);
      console.log( set.has(key) ); // true
 
      set.delete(key);
      console.log(set.has(key)); //false

和Set的区别:

1、 在Weak Set实例中,向add()传非对象参数,会报错;向has()、delete()传非对象参数,则会返回false。

2、Weak Set集合不可以迭代,不能被用于for-of循环,不支持forEach()方法。 3、Weak Set集合不暴露任何迭代器(keys()、values())所以无法通过程序本身来检测其中的内容。 4、Weak Set集合不支持size属性。

Map

Map是存储许多键值对的有序列表,key和value支持所有数据类型。

Map类型是一种存储许多键值对的有序列表。可以通过set()方法向Map集合中添加新元素;通过get()方法从Map集合中获取信息。在对象中,无法用对象作为对象属性的键名,但在Map集合中可以这样做。

Map集合支持的方法。

set()

get()

has(key) //检测指定的键名是否存在 delete(key) //移除指定键名及对应值 clear() //移除Map所有键值对

map.size属性

Map集合的初始化方法

向Map构造函数传入数组来初始化一个Map集合。数组中的每一个元素都是一个子数组,子数组中包含一个键值对的键名与值两个元素

   let map = new Map( [["name", "dyx"], ["age", "25"]] );
 
 console.log( map.has("name") ); // true
 console.log( map.get("name") ); // "dyx"
 console.log( map.has("age") ); // true
 console.log( map.get("age") ); // 25
 console.log( map.size ); // 2

Map同样可以使用forEach遍历key、value

    let map = new Map();
    const key = {};
    map.set(key, 'a');
    map.set('name', 'dyx');
    map.set('id', 1);
    map.forEach((value, key) => {
      console.log(key, value)
    })
    
    //Object {} "a"
    //"name" "dyx"
    //"id" 1

Weak Map

Weak Map集合中的键名必须是一个对象,如果使用非对象键名会报错。 Weak Map是无序列表。 Weak Map不支持forEach()方法、size属性及clear()方法。

八,迭代器(lterator)和生成器(generator)

迭代器(Iterator)

循环语句的问题

在循环、多重循环中,通过变量来跟踪数组索引的行为容易导致程序出错。迭代器的出现旨在消除这种复杂性,并减少循环中的错误。

什么是迭代器?

迭代器是一种特殊对象。它有一些专门为迭代过程设计的专有接口。例如:next( )方法,该方法用于返回一个结果对象。结果对象有两个属性:value 和 done。value 表示下一个将要返回的值,done 是一个布尔值,当没有更多可返回的数据时,值为 true,否则为 false。如果没有更多数据返回时,value 的值为 undefined。

ES5实现迭代器的代码如下:

    //实现一个返回迭代器对象的函数,注意该函数不是迭代器,返回结果才叫做迭代器。
    function createIterator(items) {
      var i = 0;
      return {
        next() {
          var done = (i >= items.length); // 判断i是否小于遍历的对象长度。
          var value = !done ? items[i++] : undefined; //如果done为false,设置value为当前遍历的值。
          return {
            done,
            value
          }
        }
      }
    }
    const a = createIterator([1, 2, 3]);
    
    //该方法返回的最终是一个对象,包含value、done属性。
    console.log(a.next()); //{value: 1, done: false}
    console.log(a.next()); //{value: 2, done: false}
    console.log(a.next()); //{value: 3, done: false}
    console.log(a.next()); //{value: undefined, done: true}

生成器(Generator)

什么是生成器?

  1. 生成器是一种返回迭代器的函数,通过 function 关键字后的星号 “*” 来表示。生成器函数中也会用到新的关键字 yield。生成器也是ES6的一个重要特性。

  2. 每当执行完一条 yield 语句后,函数就会自动停止执行。即使是在for循环中使用yield关键字,也会暂停循环

  3. 使用 yield 关键字可以返回任何值或表达式,所以通过生成器函数批量地给迭代器添加元素。

    //生成器函数,ES6内部实现了迭代器功能,使用yield来迭代输出。
    function *createIterator() {
      yield 1;
      yield 2;
      yield 3;
    }
    const a = createIterator();
    console.log(a.next()); //{value: 1, done: false}
    console.log(a.next()); //{value: 2, done: false}
    console.log(a.next()); //{value: 3, done: false}
    console.log(a.next()); //{value: undefined, done: true}

   即使是在for循环中使用yield关键字,也会暂停循环
   function *createIterator(items) {
      for(let i = 0; i < items.length;  i++) {
        yield items[i]
      }
    }
    const a = createIterator([1, 2, 3]);
    console.log(a.next()); //{value: 1, done: false}

yield使用限制

yield只可以在生成器函数内部使用,如果在非生成器函数内部使用,则会报错。

    function *createIterator(items) {
        //你应该在这里使用yield
      items.map((value, key) => {
        yield value //语法错误,在map的回调函数里面使用了yield
      })
    }
    const a = createIterator([1, 2, 3]);
    console.log(a.next()); //无输出

生成器函数表达式

    const createIterator = function *() {
        yield 1;
        yield 2;
    }
    const a = createIterator();
    console.log(a.next());

在对象中添加生成器函数

我们可以在obj中添加一个生成器,也就是添加一个带星号的方法:

    const obj = {
      a: 1,
      *createIterator() {
        yield this.a
      }
    }
    const a = obj.createIterator();
    console.log(a.next());  //{value: 1, done: false}

可迭代对象和for of循环

迭代器是对象,生成器是返回迭代器的函数。

凡是通过生成器生成的迭代器,都是可以迭代的对象(可迭代对象具有Symbol.iterator属性),也就是可以通过for of将value遍历出来。

    function *createIterator() {
      yield 1;
      yield 2;
      yield 3;
    }
    const a = createIterator();
    for(let value of a) {
      console.log(value)
    }
    // 1 2 3

由于具有 Symbol,iterator 属性的对象都有默认的迭代器,因此可以用它来判断对象是否为可迭代对象。

  function isIterable(object)
  {
      return typeof object[Symbol.iterator] === "function";
  }
  console.log( isIterable([1,2,3]) ); //true
  console.log( isIterable(["Hello"]) ); //true
  console.log( isIterable( new Map() ) ); //true
  console.log( isIterable( new Set() ) ); //true
  console.log( isIterable( new WeakMap() ) ); //true
  console.log( isIterable( new WeakSet() ) ); //true

创建可迭代对象

默认情况下定义的对象(object)是不可迭代的,但是可以通过Symbol.iterator创建迭代器。

    const obj = {
      items: []
    }
    obj.items.push(1);//这样子虽然向数组添加了新元素,但是obj不可迭代
    for (let x of obj) {
      console.log(x) // _iterator[Symbol.iterator] is not a function
    }

    //接下来给obj添加一个生成器,使obj成为一个可以迭代的对象。
    const obj = {
      items: [],
      *[Symbol.iterator]() {
        for (let item of this.items) {
          yield item;
        }
      }
    }
    obj.items.push(1)
    //现在可以通过for of迭代obj了。
    for (let x of obj) {
      console.log(x)
    }
    //1

内建迭代器

在ES6中,有三种类型的集合对象:数组、Map集合、Set集合。这三类集合中都内建了一下三种迭代器:

 这三个迭代器,在这三类集合中都可以使用。

1、entries() 返回迭代器:返回键值对

    //数组
    const arr = ['a', 'b', 'c'];
    for(let v of arr.entries()) {
      console.log(v)
    }
    // [0, 'a'] [1, 'b'] [2, 'c']
    
    //Set
    const arr = new Set(['a', 'b', 'c']);
    for(let v of arr.entries()) {
      console.log(v)
    }
    // ['a', 'a'] ['b', 'b'] ['c', 'c']

    //Map
    const arr = new Map();
    arr.set('a', 'a');
    arr.set('b', 'b');
    for(let v of arr.entries()) {
      console.log(v)
    }
    // ['a', 'a'] ['b', 'b']

2、values() 返回迭代器:返回键值对的value

    //数组
    const arr = ['a', 'b', 'c'];
    for(let v of arr.values()) {
      console.log(v)
    }
    //'a' 'b' 'c'

    //Set
    const arr = new Set(['a', 'b', 'c']);
    for(let v of arr.values()) {
      console.log(v)
    }
    // 'a' 'b' 'c'

    //Map
    const arr = new Map();
    arr.set('a', 'a');
    arr.set('b', 'b');
    for(let v of arr.values()) {
      console.log(v)
    }
    // 'a' 'b'

3、keys() 返回迭代器:返回键值对的key

    //数组
    const arr = ['a', 'b', 'c'];
    for(let v of arr.keys()) {
      console.log(v)
    }
    // 0 1 2
    
    //Set
    const arr = new Set(['a', 'b', 'c']);
    for(let v of arr.keys()) {
      console.log(v)
    }
    // 'a' 'b' 'c'

    //Map
    const arr = new Map();
    arr.set('a', 'a');
    arr.set('b', 'b');
    for(let v of arr.keys()) {
      console.log(v)
    }
    // 'a' 'b'

不同集合的类型还有自己默认的迭代器,在for of中,数组和Set的默认迭代器是values(),Map的默认迭代器是entries()。

for of循环解构

对象本身不支持迭代,但是我们可以自己添加一个生成器,返回一个key,value的迭代器,然后使用for of循环解构key和value。

    const obj = {
      a: 1,
      b: 2,
      *[Symbol.iterator]() {
        for(let i in obj) {
          yield [i, obj[i]]
        }
      }
    }
    for(let [key, value] of obj) {
      console.log(key, value)
    }
    // 'a' 1, 'b' 2

字符串迭代器

ES5中规定可以通过方括号访问字符串中的字符,及 text[0]可以取得字符串 text 的第一个字符,并以此类推。由于方括号操作的是编码单元而非字符,因此无法正确地访问双字节字符。在ES6中,由于全面支持了 Unicode,并且可以通过改变字符串的默认迭代器来使其操作字符,而不是编码单元。此时,可以通过 for-of 循环输出正确的内容。

    const str = 'a 吉 a';
    for(let v of str) {
      console.log(v)
    }
    // a
    //空
    //吉
    //空
    //a

NodeList迭代器

自从ES6添加了默认迭代器后,DOM中定义的NodeList类型也拥有了默认迭代器,其行为与数组的默认迭代器完全一致。

    const divs = document.getElementByTagName('div');
    for(let d of divs) {
      console.log(d.id)
    }

展开运算符与非数组可迭代对象

如果想将可迭代对象转换为数组,使用展开运算符是最简单的方法

    const a = [1, 2, 3];
    const b = [4, 5, 6];
    const c = [0,...a, ...b]
    console.log(c) // [0,1, 2, 3, 4, 5, 6]

高级迭代器功能

1、传参

生成器里面有2个yield,当执行第一个next()的时候,返回value为1,然后给第二个next()传入参数10,传递的参数会替代掉上一个next()的yield返回值。在下面的例子中就是first。

    function *createIterator() {
      let first = yield 1;
      yield first + 2;
    }
    let i = createIterator();
    console.log(i.next()); // {value: 1, done: false}
    console.log(i.next(10)); // {value: 12, done: false}

2、在迭代器中抛出错误

    function *createIterator() {
      let first = yield 1;
      yield first + 2;
    }
    let i = createIterator();
    console.log(i.next()); // {value: 1, done: false}
    console.log(i.throw(new Error('error'))); // error
    console.log(i.next()); //不再执行

3、生成器返回语句

生成器中添加return表示退出操作。

    function *createIterator() {
      let first = yield 1;
      return;
      yield first + 2;
    }
    let i = createIterator();
    console.log(i.next()); // {value: 1, done: false}
    console.log(i.next()); // {value: undefined, done: true}

4、委托生成器

生成器嵌套生成器

    function *aIterator() {
      yield 1;
    }
    function *bIterator() {
      yield 2;
    }
    function *cIterator() {
      yield *aIterator()
      yield *bIterator()
    }
    
    let i = cIterator();
    console.log(i.next()); // {value: 1, done: false}
    console.log(i.next()); // {value: 2, done: false}

异步任务执行器

任务执行器是一个函数,用来循环执行生成器,因为我们知道生成器需要执行N次next()方法,才能运行完,所以我们需要一个自动任务执行器帮我们做这些事情,这就是任务执行器的作用。

给同步函数添加一个延迟方法,即可以将其变为异步函数

 function fetchData()
 {
     return function(callback)
     {
         setTimeout(function() {
             callback(null, "Hi");
         }, 50);
     }
 } 

九,JavaScript中的类

ES5中的近类结构

 ES5及早期版本中没有类的概念,因此用了一个相近的思路来创建一个自定义类型:首先创建一个构造函数,然后定义另一个方法并赋值给构造函数的原型。

  function PersonType(name)
  {
      this.name = name;
  }
  
  PersonType.prototype.sayName = function() {
      console.log(this.name);
  }
  
 var person = new PersonType("zxx");
 person.sayName(); // "zxx"
 
 console.log(person instanceof PersonType); //true
 console.log(person instanceof Object); //true

ES6 class类

类声明

  class PersonClass {
      //等价于PersonType构造函数
      constructor(name) {
          this.name = name;
      }
  
      //等价于PersonType.prototype.sayName
      sayName() {
          console.log(this.name);
     }
 }
 
 let person = new PersonClass("zxx");
 person.sayName(); //"zxx"
 
 console.log(person instanceof PersonClass); //true
 console.log( typeof PersonClass); //"function"
 console.log( typeof PersonClass.prototype.sayName); //"function"

私有属性:在class中实现私有属性,只需要在构造方法中定义this.xx = xx。自有属性是实例中的属性,不会出现在原型上,只能在类的构造函数或方法中创建,此例中的 name 就是一个自有属性。

类声明和函数声明的区别和特点

1.函数声明可以被提升,而类声明与 let 类似,不能被提升;

2.类声明中的所有代码将运行在严格模式下;

3.在类中,所有方法都是不可枚举的;

4.每个类中,都有一个名为 [[Construct]] 的内部方法,通过关键字new调用那些不含 [[Construct]] 的方法会导致程序抛出错误;

5.使用除 new 以外的方式调用类的构造函数会导致程序抛出错误;

6.在类中,修改类名会导致程序报错。

类表达式

类和函数都有两种存在形式:声明形式和表达形式。声明形式的函数和类都由相应关键字(分别为function,class)进行定义,随后紧跟一个标识符。表达形式的函数和类与之类似,只是不需要再关键字后加标识符

 let PersonClass = class {
     //等价于PersonType构造函数
     constructor(name) {
         this.name = name;
     }
 
     //等价于PersonType.prototype.sayName
     sayName() {
         console.log(this.name);
     }
 }
 
 let person = new PersonClass("zxx");
 person.sayName(); //"zxx"
 
 console.log(person instanceof PersonClass); //true
 console.log(person instanceof Object); //true
 console.log( typeof PersonClass); //"function"
 console.log( typeof PersonClass.prototype.sayName); //"function"

类是一等公民

 一等公民是指一个可以传入函数,可以从函数返回,并且可以赋值给变量的值。

1、可以将类作为参数传入函数。

    //新建一个类
    let A = class {
      sayName() {
        return 'a'
      }
    }
    //该函数返回一个类的实例
    function test(classA) {
      return new classA()
    }
    //给test函数传入A
    let t = test(A)
    console.log(t.sayName()) // a

访问器属性

尽管应该在类构造函数中创建自己的属性,但类也支持直接在原型上定义访问器属性。创建 getter 时,需要在关键字 get 后紧跟一个空格和相应的标识符;创建 setter 时,只需要把 getter 关键字 get 替换为set即可。

可计算成员名称

类方法和访问器属性也支持使用可计算名称。用方括号包裹一个表达式,即可使用可计算名称

 let methodName = "sayName";
 
 class PersonClass {
     constructor(name) {
         this.name = name;
     }
 
     [methodName]() {
         console.log(this.name);
     }
 };
 
 let me = new PersonClass("zxx");
   me.sayName(); // "zxx"

同样地,在访问器属性中也可以使用可计算名称。例如:
 let propertyName = "html";
 
 class CustomHTMLElement {
     constructor(element) {
         this.element = element;
     }
 
     get [propertyName]() {
         return this.element.innerHTML;
     }
 
     set [propertyName]() {
         this.element.innerHTML = value;
     }
 }

生成器方法

在类中,也可以通过 “*” 来定义生成器。如果类是用来表示值的集合的,那么为它定义一个默认迭代器会更有用。

    class A {
      *printId() {
        yield 1;
        yield 2;
        yield 3;
      }
    }
    let a = new A()
    let x = a.printId()
    
    x.next() // {done: false, value: 1}
    x.next() // {done: false, value: 2}
    x.next() // {done: false, value: 3}

静态成员

静态成员是指在方法名或属性名前面加上static关键字,和普通方法不一样的是,static修饰的方法不能在实例中访问,只能在类中直接访问。

   class PersonClass {
     //等价于PersonType的构造函数
     constructor(name) {
         this.name = name;
     }
 
     //等价于PerosonType.prototype.sayName
     sayName() {
         console.log(this.name);
     }
     
     //等价于PersonType.create()
     static create(name) {
         return new PersonClass(name);
     }
 }
 
 let person = PersonClass.create("zxx"); // PersonClass.create is not a function

继承与派生类

1)使用 extends 关键字可以指定类继承的函数。

2)通过 super() 方法即可访问基类的构造函数。

3)继承自其它类的类被称作派生类。

4)如果不使用构造函数,则当创建新的类实例时会自动调用 super() 并传入所有参数。

在派生类中,如果使用了构造方法,就必须使用super()。

    class Component {
      constructor([a, b] = props) {
        this.a = a
        this.b = b
      }
      add() {
        return this.a + this.b
      }
    }
    
    class T extends Component {
      constructor(props) {
        super(props)
      }
    }
    
    let t = new T([2, 3])
    console.log(t.add()) // 5

关于super使用的几点要求:

1、只可以在派生类中使用super。派生类是指继承自其它类的新类。

2、在构造函数中访问this之前要调用super(),负责初始化this。

    class T extends Component {
      constructor(props) {
        this.name = 1 // 错误,必须先写super()
        super(props)
      }
    }

3、如果不想调用super,可以让类的构造函数返回一个对象。

类方法遮蔽

派生类中的方法总会覆盖基类中的同名方法。如果仍然想使用基类中的方法,则可以借助 super 关键字来实现。

    class Component {
      constructor([a, b] = props) {
        this.a = a
        this.b = b
      }
      //父类的add方法,求和
      add() {
        return this.a + this.b
      }
    }
    
    class T extends Component {
      constructor(props) {
        super(props)
      }
      //重写add方法,求积
      add() {
        return this.a * this.b
      }
    }
    let t = new T([2, 3])
    console.log(t.add()) // 6

静态成员继承

派生类继承基类后,基类中的静态成员也可以直接在派生类中使用。

    class Component {
      constructor([a, b] = props) {
        this.a = a
        this.b = b
      }
      static printSum([a, b] = props) {
        return a + b
      }
    }
    class T extends Component {
      constructor(props) {
        super(props)
      }
    }
    console.log(T.printSum([2, 3])) // 5

派生自表达式的类

 只要表达式可以被解析为一个函数,并且具有[[Construct]]属性和原型,那么就可以用extends进行派生。

 extends强大的功能使得类可以继承自任意类型的表达式,而且可以是不止一个的表达式,这样可以动态地确定类的继承目标,创建不同的继承方法。举例:

 function Rectangle(length, width)
 {
     this.length = length;
     this.width = width;
 }
 
 Rectangle.prototype.getArea = function() {
     return this.length * this.width;
 }
 
 function getBase()
 {
     return Rectangle;
 }
 
 class Square extends getBase() {
     constructor(length) {
         super(length * length);
     }
 }
 
 var x = new Square(3);
 
 console.log(x.getArea()); //9
 console.log(x instanceof Rectangle); //true

内建对象的继承

在ES5中,无法通过继承的方式创建属于自己的特殊数组。而ES6类语法的一个目标是支持内建对象的继承。具体是:先由基类(Array)创建this的值,然后派生类的构造函数(MyArray)再修改这个值。举例:

 class MyArray extends Array {
     //空
 }
 
 var colors = new MyArray();
 colors[0] = "red";
 
 console.log(colors.length); //1
 
 colors.length = 0;
 console.log(colors[0]) // undefined

Symbol.species

内建对象继承的一个实用之处是,原本在内建对象中返回实例自身的方法将自动返回派生类的实例。

通过Symbol.species可以定义当派生类的方法返回实例时,应该返回的值的类型。

在构造函数中使用new.target

类的构造函数必须通过new关键字调用,所以总是在类的构造函数中定义new.target属性。但是其值有时会不同。每个构造函数都可以根据自身被调用的方式改变自己的行为。例如,可以用new.target创建一个抽象基类(不能被直接实例化的类)

 //抽象基类
 class Shape {
     constructor() {
         if (new.target === Shape) {
            throw new Error("这个类不能被直接实例化");
         }
     }
  }
  
 class Rectangle extends Shape {
     constructor(length, width) {
         super();
         this.length = length;
         this.width = width;
     }
 }
 
 var x = new Shape(); // 抛出错误
 
 var y = new Rectangle(3,4); //没有错误
 console.log(y instanceof Shape); // true

十,改进的数组功能

创建数组

 在ES6以前,创建数组的方式有两种:一种是调用Array构造函数,一种是使用数组自面量语法。由于在使用Array构造函数创建数组的时候,有时会有产生一定的怪异行为(如传入一个字符串数字的时候)。所以增加了 Array.of( ) 和 Array.from( ) 方法。

Array.of()

ES5中new一个人数组的时候,会存在一个令人困惑的情况。当new一个数字的时候,生成的是一个长度为该数字的数组,当new一个字符串的时候,生成的是该字符串为元素的数组。

    const a = new Array(2)
    const b = new Array("2")
    console.log(a, b) //[undefined, undefined] ["2"]

这样一来,导致new Array的行为是不可预测的,Array.of()出现为的就是解决这个情况。

    const c = Array.of(2)
    const d = Array.of("2")
    console.log(c, d) // [2] ["2"]

使用Array.of()创建的数组传入的参数都是作为数组的元素,而不在是数组长度,这样就避免了使用上的歧义。

Array.from()

Array.from( ) 常用来将类数组对象(如arguments)、可迭代对象转化为数组。它接受可迭代对象或者类数组对象作为第一个参数,最终返回一个数组。

下面的例子讲的是将arguments转换成数组。arguments是类数组对象,他表示的是当前函数的所有参数,如果函数没有参数,那么arguments就为空。

    function test(a, b) {
      let arr = Array.from(arguments)
      console.log(arr)
    }
    test(1, 2) //[1, 2]

映射转换:Array.from(arg1, arg2),我们可以给该方法提供2个参数,第二个参数作为第一个参数的转换。

    function test(a, b) {
      let arr = Array.from(arguments, value => value + 2)
      console.log(arr)
    }
    test(1, 2) //[3, 4]

Array.from还可以设置第三个参数,指定this。

Array.from()转换可迭代对象:这个用法只需要一个例子,数组去重。

    function test() {
      return Array.from(new Set(...arguments))
    }
    const s = test([1, "2", 3, 3, "2"])
    console.log(s) // [1,"2",3]

给数组添加新方法

ES6给数组添加了几个新方法:find()、findIndex()、fill()、copyWithin()。

1、find():传入一个指定的参数,找到数组中符合当前搜索规则的第一个元素,返回它,并且终止搜索。

    const arr = [1, "2", 3, 3, "2"]
    console.log(arr.find(n => typeof n === "number")) // 1

2、findIndex():传入一个指定的参数,找到数组中符合当前搜索规则的第一个元素,返回它的下标,终止搜索。

    const arr = [1, "2", 3, 3, "2"]
    console.log(arr.findIndex(n => typeof n === "number")) // 0

3、fill():fill( )方法可以用指定的值填充一至多个元素。当传入一个值时,fill( )方法会用这个值重写数组中的所有值。如果只想改变某一个或者某一部分的值,可以传入开始索引和不包含结束索引(不包含结束索引当前值)这两个参数。

   let numbers = [1,2,3,4];
  numbers.fill(1);
  
  console.log(numbers.toString()); // 1,1,1,1
  
  let numbers = [1,2,3,4];
  numbers.fill(1,2);
  
  console.log(numbers.toString()); //1,2,1,1
 
 numbers.fill(0,1,3);
 
 console.log(numbers.toString()); // 1,0,0,1

4、copyWithin(): copyWith( )方法与 fill( )方法类似,不过它是从数组中复制元素的值。调用该方法需要传入两个参数:一个是该方法开始填充值的位置,一个是该方法复制值的索引位置。如果给该方法中传入了第3个参数,则表示停止复制值的位置(但是该参数是不包含结束索引)

 let numbers = [1,2,3,4];
 numbers.copyWith(2,0);
 console.log(numbers.toString()); //1,2,1,2

定型数组

所谓定型数组,是指将任何数字转换为一个包含数字比特的数组,是一种用于处理数值类型数据的专用数组,可以为JavaScript提供快速的按位运算。

定型数组提供了适用面更广的API和更高的性能。设计定型数组的目的就是提高与WebGL等原生库交换二进制数据的效率。由于定型数组的二进制表示对操作系统而言是一种容易使用的格式,JavaScript引擎可以重度优化算术运算、按位运算和其他对定型数组的常见操作,因此使用它们速度极快。

定型数组支持存储、操作以下8种不同的数值类型:

 有符号的8位整数 (int8)

 无符号的8位整数 (uint8)

 有符号的16位整数 (int16)

 无符号的16位整数 (uint16)

 有符号的32位整数 (int32)

 无符号的32位整数 (uint32)

 32位浮点数    (float32)

 64位浮点数    (float64)

数组缓冲区:是一段可以包含特定数量字节的内存地址。可以通过ArrayBuffer构造函数来创建数组缓冲区。//let buffer = new ArrayBuffer(10); //分配10字节

定型数组与普通数组的相似之处

  1. 通用方法

    数组中大部分的方法都可以在定型数组中使用。

  1. 相同的迭代器

    展开运算符能够将可迭代对象转换为普通数组,也可以将定型数组转换为普通数组。

  1. of( )、from( )方法

    定型数组也可以像普通数组那样使用of( ) 和 from( ) 方法,只不过返回的也是定型数组。

定型数组和普通数组的差别

  1. 行为差异

  当操作普通数组的时候,可以改变数组的大小,但是定型数组始终保持相同的尺寸。

  1. 缺失的方法

 这些方法只在普通数组中有,而在定型数组中没有。想一想是为什么呢?因为这几个方法基本上都是以改变数组大小为目的的,而我们刚才说的第一条里面,定型数组始终是保持相同尺寸的,所以呢,定型数组中也就不包含这些方法了。列举一下这些方法:

    concat( )

    pop( )

    push( )

    shift( )

    unshift( )

    splice( )

上面的方法中,除了concat( )之外,其它几个都可以改变数组的尺寸大小。

十一,promise与异步编程

是异步编程的一种解决方案。Promise 是一个对象,从它可以获取异步操作的消息。,它既可以像事件和回调函数一样指定稍后执行的代码,也可以明确指示代码是否成功执行。

异步编程的背景知识

机制

JavaScript引擎是基于单线程(Single-threaded)事件循环的概念构建,即同一时刻只允许一个代码块在执行。这些代码块被放在一个任务队列(job queue)中,每当一段代码准备执行时,都会被添加到任务队列。每当JavaScript引擎中的一段代码结束执行,事件循环(event loop)会执行队列中下一个任务。事件循环是JavaScript引擎的一段代码,负责监控代码执行并管理任务队列。

事件模型

例如点击按钮或者按下键盘按键会触发的onclick事件,是JavaScript中最基础的异步编程形式。尽管事件模型适用于响应用户交互和完成类似的低频功能,但对更复杂的需求来说却不是很灵活。

回调模式

回调模式和事件模型类似,异步代码都会在未来的某个时间点执行,二者的区别是回调模式中被调用的函数是作为参数传入的。

 readFile("example.txt", function(err, contents) {
     if (err) {
         throw err;
     }
     console.log(contents);
 });
 console.log("Hi");

Promise

Promise相当于异步操作结果的占位符,它不去订阅事件,也不会传递一个回调函数给目标参数,而是让函数返回一个Promise

Promise的声明周期

var myFirstPromise = new Promise(function(resolve, reject){
    //当异步代码执行成功时,我们才会调用resolve(...), 当异步代码失败时就会调用reject(...)
    //在本例中,我们使用setTimeout(...)来模拟异步代码,实际编码时可能是XHR请求或是HTML5的一些API方法.
    setTimeout(function(){
        resolve("成功!"); //代码正常执行!
    }, 250);
});
 
myFirstPromise.then(function(successMessage){
    //successMessage的值是上面调用resolve(...)方法传入的值.
    //successMessage参数不一定非要是字符串类型,这里只是举个例子
    document.write("Yay! " + successMessage);
});

Promise先是处于进行中(pending)的状态,此时操作尚未完成;等异步操作执行结束后,Promise变为已处理(settled)状态。分为两种状态,已经完成(fulfilled),拒绝(rejected)

Promise的状态改变时,调用then()方法来采取特定的行动。

then()方法接受2个参数:第一个参数是变为fulfilled时要调用的函数,第2个是状态变为rejected时需要调用的函数。

Promise还有一个catch()方法,相当于只给其传入拒绝处理程序的then()方法

创建未完成的Promise

用Promise构造函数可以创建新的Promise,构造函数只接受一个参数:包含初始化Promise代码的执行器(executor)函数。执行器接受2个参数,分别是执行成功完成时调用的resolve()函数,执行失败时,调用reject()函数。执行器函数会立即执行,然后才执行后续流程中的代码(即resolve() / reject()会放到任务队列中再执行)。

创建已处理的Promise

使用Promise.resolve() / Promise.reject()来实现根据特定的值来创建已解决的Promise。

 let promise = Promise.resolve(42);
 promise.then(function(value) {
     console.log(value); //42
 });
 
 let promise = Promise.reject(42);
 promise.catch(function(value){
     console.log(value); // 42
 });

执行器错误

每个执行器中都隐含一个try-catch块,所以错误会被捕获并传入拒绝处理程序。

使用Promise构建函数创建新的Promise

Promise构造函数只有一个参数,该参数是一个函数,被称作执行器,执行器有2个参数,分别是resolve()和reject(),一个表示成功的回调,一个表示失败的回调。

    new Promise(function(resolve, reject) {
      setTimeout(() => resolve(5), 0)
    }).then(v => console.log(v)) // 5

记住,Promise实例只能通过resolve或者reject函数来返回,并且使用then()或者catch()获取,不能在new Promise里面直接return,这样是获取不到Promise返回值的。

1、我们也可以使用Promise直接resolve(value)。

    Promise.resolve(5).then(v => console.log(v)) // 5

2、也可以使用reject(value)

    Promise.reject(5).catch(v => console.log(v)) // 5

3、执行器错误通过catch捕捉。

    new Promise(function(resolve, reject) {
        if(true) {
           throw new Error('error!!')
        }
    }).catch(v => console.log(v.message)) // error!!

Promise的其他方法

在Promise的构造函数中,除了reject()和resolve()之外,还有2个方法,Promise.all()、Promise.race()。

Promise.all():

该方法只接受一个参数并返回一个Promise,该参数是一个含有多个受监视Promise的可迭代对象,只有当可迭代对象中所有Promise都被解决后,返回的Promise才会被解决,只有当可迭代对象中所有Promise都被完成后返回的Promise才会被完成。

语法很简单:参数只有一个,可迭代对象,可以是数组,或者Symbol类型等。

    Promise.all(iterable).then().catch()

当迭代对象中所有的Promise都被解决并返回后,最后的Promise里面存的值按照传入参数数组中的Promise的顺序储存

  let p1 = new Promise(function(reolve, reject) {
      resolve(42);
  });
  
  let p2 = new Promise(function(resolve, reject) {
      resolve(43);
  });
  
  let p3 = new Promise(function(resolve, reject) {
     resolve(44);
 });
 
 let p4 = Promise.all([p1, p2, p3]);
 
 p4.then(function(value) {
     console.log( Array.isArray(value) ); // true
     console.log( value[0] ); // 42
     console.log( value[1] ); // 43
     console.log( value[2] ); // 44
 });

2.当迭代对象中有被拒绝的Promise时,只要有一个被拒绝,那么返回的Promise没等所有Promise都完成就立即被拒绝,

 let p1 = new Promise(function(reolve, reject) {
     resolve(42);
 });
 
 let p2 = new Promise(function(resolve, reject) {
     reject(43);
 });
 
 let p3 = new Promise(function(resolve, reject) {
     resolve(44);
 });
 
 let p4 = Promise.all([p1, p2, p3]);
 
 p4.then(function(value) {
     console.log( Array.isArray(value) ); // true
     console.log( value ); // 43
 });

Promise.race(): Promise.race()与Promise.all()稍有不同。在可迭代对象中,只要有一个Promise被解决,返回的Promise就解决,无须等到所有Promise都被完成。

如果先解决的是已完成Promise,则返回已完成Promise;如果先解决的是已拒绝的Promise,则返回已拒绝Promise。

 let p1 = new Promise(function(resolve, reject) {
     setTimeout(function() {resolve(42);}, 0);
 });
 
 let p2 = Promise.reject(43);
 
 let p3 = new Promise(function(resolve,reject) {
     resolve(44)
 });
 
 let p4 = Promise.race([p1, p2, p3]);
 
 p4.catch(function(value) {
     console.log(value); // 43
 });

Promise和异步的联系

只要每个异步操作都返回Promise,以Promise作为通用接口用于所有异步代码可以简化任务执行器。

Promise本身不是异步的,只有他的then()或者catch()方法才是异步,也可以说Promise的返回值是异步的。通常Promise被使用在node,或者是前端的ajax请求、前端DOM渲染顺序等地方。

十二,代理(proxy)与反射(reflection)

代理和反射

  1. 调用new Proxy()可以创建代理。代理可以拦截 JavaScript 引擎内部目标的底层对象操作,这些底层操作被拦截后会触发响应特定操作的陷阱函数。

  2. 反射以 Reflect 对象的形式出现,每个代理陷阱对应一个命名和参数都相同的 Reflect 方法。

创建一个简单的代理

  用 Proxy 构造函数创建代理需要传入两个参数:目标(target)和处理程序(handler)。不使用任何陷阱的处理程序等价于简单的转发代理。

  let target = {};
  let proxy = new Proxy(target, {}); 
  proxy.name = "proxy";
  console.log(proxy.name); // "proxy"
  console.log(target.name); // "proxy" 
  target.name = "target";
  console.log(proxy.name); //"target"
 console.log(target.name); //"target"

反射 Reflect

反射的概念

Reflect 是一个内置的对象,它提供可拦截JavaScript操作的方法。方法与代理处理程序的方法相同。Reflect 不是一个函数对象,因此它是不可构造的。

反射的使用

Reflect提供了一些静态方法,静态方法是指只能通过对象自身访问的的方法

静态方法列表:这么多静态方法,你需要学会的是如何使用它们

1、Reflect.apply() 2、Reflect.construct() 3、Reflect.defineProperty() 4、Reflect.deleteProperty() 5、Reflect.enumerate() 6、Reflect.get() 7、Reflect.getOwnPropertyDescriptor() 8、Reflect.getPrototypeOf() 9、Reflect.has() 10、Reflect.isExtensible() 11、Reflect.ownKeys() 12、Reflect.preventExtensions() 13、Reflect.set() 14、Reflect.setPrototypeOf()

静态方法的使用:

demo1:使用Reflect.get()获取目标对象指定key的value。

    let obj = {
        a: 1
    };
    
    let s1 = Reflect.get(obj, "a")
    console.log(s1) // 1

demo2:使用Reflect.apply给目标函数floor传入指定的参数。

    const s2 = Reflect.apply(Math.floor, undefined, [1.75]); 
    console.log(s2) // 1

进一步理解Reflect

Reflect可以拦截JavaScript代码,包括拦截对象,拦截函数等,然后对拦截到的对象或者函数进行读写等操作。

比如demo1的get()方法,拦截obj对象,然后读取key为a的值。当然,不用反射也可以读取a的值。

再看demo2的apply()方法,这个方法你应该比较了解了,和数组中使用apply不同的是,Reflect.apply()提供了3个参数,第一个参数是反射的函数,后面2个参数才是和数组的apply一致。demo2的例子我们可以理解成是拦截了Math.floor方法,然后传入参数,将返回值赋值给s2,这样我们就能在需要读取这个返回值的时候调用s2。

    //数组使用apply
    const arr = [1, 2, 3]
    function a() {
      return Array.concat.apply(null, arguments)
    }
    const s = a(arr)
    console.log(s) // [1, 2 ,3]

代理 Proxy

    let p = new Proxy(target, handler);

target:一个目标对象(可以是任何类型的对象,包括本机数组,函数,甚至另一个代理)用Proxy来包装。 handler:一个对象,其属性是当执行一个操作时定义代理的行为的函数。

代理的使用

主要看new Proxy({}, handler)的操作,指定目标obj对象,然后handler对象执行get()操作,get()返回值的判断是,如果name是target目标对象的属性,则返回target[name]的值,否则返回37,最后测试的时候,p.a是对象p的key,所以返回a的value,而p.b不存在,返回37。

    const obj = {
      a: 10
    }
    let handler = {
        get: function(target, name){
            console.log('test: ', target, name)
            // test:  {"a":10} a
            // test:  {"a":10} b
            return name in target ? target[name] : 37
        }
    }
    let p = new Proxy(obj, handler)
    console.log(p.a, p.b) // 10 37

这个例子的作用是拦截目标对象obj,当执行obj的读写操作时,进入handler函数进行判断,如果读取的key不存在,则返回默认值。

解决跨域访问

    module.exports = {
        devServer: {
           proxy: [
               {
                    context: "/api/*", //代理API
                    target: 'https://www.hyy.com', //目标URL
                    secure: false
              }
           ]
        }
    }

十三,用模块封装代码

模块的定义

模块是自动运行在严格模式下,并且没有办法退出运行的JavaScript代码。

模块可以是函数、数据、类,需要指定导出的模块名,才能被其他模块访问。

    //数据模块
    const obj = {a: 1}
    //函数模块
    const sum = (a, b) => {
      return a + b
    }
    //类模块
    class My extends React.Components {
    
    }

模块的特性

a) 在模块顶部创建的变量,仅在模块的顶级作用域中存在,不会自动被添加到全局共享作用域。

b) 模块必须导出一些外部可以访问的元素,如变量、函数。

c) 在模块的顶部,this 值是 undefined。

d) 模块不支持 HTML 风格的代码注释。

模块的导出

给数据、函数、类添加一个export,就能导出模块。一个配置型的JavaScript文件中,你可能会封装多种函数,然后给每个函数加上一个export关键字,就能在其他文件访问到。

    //数据模块
    export const obj = {a: 1}
    //函数模块
    export const sum = (a, b) => {
      return a + b
    }

模块的引用

在另外的js文件中,我们可以引用上面定义的模块。使用import关键字,导入分2种情况,一种是导入指定的模块,另外一种是导入全部模块。

1、导入指定的模块。

    //导入obj数据,My类
    import {obj, My} from './xx.js'
    
    //使用
    console.log(obj, My)

2、导入全部模块

    //导入全部模块
    import * as all from './xx.js'
    
    //使用
    console.log(all.obj, all.sun(1, 2), all.My)

默认模块的使用

如果给我们的模块加上default关键字,那么该js文件默认只导出该模块,你还需要把大括号去掉。

    //默认模块的定义
    function sum(a, b) {
        return a + b
    }
    export default sum
    
    //导入默认模块
    import sum from './xx.js'

模块的使用限制

不能在语句和函数之内使用export关键字,只能在模块顶部使用,

在react中,模块顶部导入其他模块。

    import react from 'react'

在vue中,模块顶部导入其他模块。

    <script>
        import sum from './xx.js'
    </script>

修改模块导入和导出名

有2种修改方式,一种是模块导出时修改,一种是导入模块时修改。

1、导出时修改:

    function sum(a, b) {
        return a + b
    }
    export {sum as add}

    import { add } from './xx.js'
    add(1, 2)

2、导入时修改:

    function sum(a, b) {
        return a + b
    }
    export sum

    import { sum as add } from './xx.js'
    add(1, 2)

无绑定导入

当你的模块没有可导出模块,全都是定义的全局变量的时候,你可以使用无绑定导入。

模块:

    let a = 1
    const PI = 3.1314

无绑定导入:

    import './xx.js'
    console.log(a, PI)

浏览器加载模块

Web浏览器中的模块加载顺序:

   其中,模块按照它们出现在HTML文件中的顺序执行。

 <!-- 先执行这个标签 -->
 <script type="module" src="module1.js"></script>
     
 <!-- 再执行这个标签 -->
 <script type="module" src="module2.js">
 import {sum} from "./example.js"
 
 let result = sum(1,2);
 </script>
 
 <!-- 最后执行这个标签 -->
 <script type="module" src="module2.js"></script>

浏览器模块说明符解析

模块说明符(module specifier)在前面的例子都是相对路径(如:字符串“./example.js”)。浏览器要求模块说明符具有以下几种格式之一:

以 / 开头的解析为从根目录开始。

以 ./ 开头的解析为从当前目录开始。

以 ../ 开头的解析为从父目录开始。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值