zoukankan      html  css  js  c++  java
  • ES2015简介和基本语法

    ECMAScript 6(以下简称ES6)是JavaScript语言的下一代标准。因为当前版本的ES6是在2015年发布的,所以又称ECMAScript 2015。也就是说,ES6就是ES2015。

    说明:此文章根据《实战ES2015:深入现代JavaScript+应用开发》这本书做的笔记,更多详细内容请查看书籍。电子版在文章底部。

    一、ECMAScript的发展历程

     
    image.png

    二、ES2015能为实际开发带来什么

    ECMAScript的发展速度在不断加快,影响范围越来越大,除了Web前端开发以外,借助着Node.js的力量在服务器、桌面端甚至硬件设备等领域中也发光发热着。

    ES2015概述:

    ES2015标注提供了许多新的语法和编程特性以提高ECMAScript的开发效率并优化ECMAScript的开发体验。
    ES2015的别名:Harmony(和谐)

    语法糖:

    ECMAScript带来了可用性非常高的语法糖,这些语法糖的开发初衷是方便开发者使用,使用语法糖能够增加程序的可读性,从而减少程序代码出错的几率。
    如ES2015中非常重要的箭头函数,大大地增强了ECMAScript在复杂业务逻辑中的处理能力。

    使用ES2015前:

    el.on('click',function(evt) {
        var self = this;
        fecch('/api').then(function (res) {
            return res.json();
        }).then(function (result) {
            self.something(result);
            //...
        })
    })
    

    使用ES2015后:

    el.on('click',evt=>{
        fetch('/api').then(res=>res.json()).then(result=>this.something(result))
    })
    

    模块化和组件化:

    在程序代码可以通过模块化进行解耦后,组件化开发便能借此进一步推进项目程序的工程化进度。组件化开发是模块化开发的高级体现,组件化更能表现出模块化开发的意义和重要性。
    组件化开发所重视的是组件之间的非耦合关系和组件的可重用性,组件之间也可以存在依赖性,可以利用模块化来实现组件化开发。同一类内容块可以抽象化为一个组件,并在生产中重复使用。

    const和let:

    const为ECMAScript带来了定义常量的能力,let为ECMAScript修复了从前var因为代码习惯不佳而导致的代码作用域混乱等问题,同时实现了块状作用域。
    const可以实现变量名与内存地址的强绑定,让变量不会因为除了定义语句和删除语句以外的代码而丢失内存地址的绑定,从而保证了变量与内存之间的安全性。

    总结:语法糖、模块化、组件化等工程优势,可以在实际开发中提升开发效率和代码质量。

    三、ES2015新语法

    新语法:

    • let、const和块级作用域
    • 箭头函数(Arrow Function)
    • 模板字符串(Template String)
    • 对象字面量扩展语法(Enhanced Object Literals)
    • 表达式结构(Destructuring)
    • 函数参数表达、传参
    • 新的数据结构
    • 类语法(Classes)
    • 生成器(Generator)
    • Promise
    • 代码模块化
    • Symbol
    • Proxy

    let、const和作用域

    let和const是继var之后新的变量定义方法,与let相比,const更容易被理解。const就是constant的缩写,用于定义变量,即不可变量。const定义常量的原理是阻隔变量名所对应的内存地址被改变。
    变量与内存之间的关系由三个部分组成:变量名、内存绑定和内存(内存地址)。


     
    image.png

    ECMAScript在对变量的引用进行读取时,会从该变量对应的内存地址所指向的内存空间中读取内容。当用户改变变量的值时,引擎会重新从内存中分配一个新的内存空间以存储新的值,并将新的内存地址与变量进行绑定。const的原理便是在变量名与内存地址之间建立不可变得绑定,当后面的程序尝试申请新的内存空间时,引擎便会抛出错误。

    在ES2015中,let可以说是var的进化版本,var大部分情况下可以被let替代,let和var的异同点如下表:

     
    image.png

    变量的生命周期:

    在ECMAScript中,一个变量(或常量)的生命周期(Life Cycle)模式是固定的,由两种因素决定,分别是作用域和对其的引用。

    从工程化的角度,应该在ES2015中遵从以下三条原则:

    (1)一般情况下,使用const来定义值的存储容器(常量);
    (2)只有在值容器明确地被确定将会被改变时才使用let来定义(变量);
    (3)不再使用var。

    循环语句:

    ECMAScript引入了一种新的循环语句for...of,主要的用途是代替for...in循环语句;为Array对象引入了Array.forEach方法以代替for循环,Array.forEach方法的特点是自带闭包,以解决因为缺乏块级作用域导致需要使用取巧的方法来解决var的作用域问题。
    因为块级作用域的存在,使得for循环中的每一个当前值可以仅保留在所对应的循环体中,配合for-of循环语句更是免去了回调函数的使用。

    const arr=[1,2,3];
    for(const item of arr){
         console.log(item);
    }
    

    配合ES2015中的解构(Destructuring)特性,在处理JSON数据时,更加得心应手。

    const Zootopia=[
        {name:'Nick',gender:1,species:'Fox'},
        {name:'Judy',gender:0,species:'Bunny'}
    ];
    for(const {name,species} of Zootopia){
        console.log(`hi,I am ${name},and I am a ${species}`);
    }
    

    forEach方法需要传入一个回调函数来接收循环的每一个循环元素并作为循环体以执行。同时,这个回调函数在标准定义中会被传入三个参数,分别为:当前值,当前值的下标和循环数组自身。在ES2015标准中,数组类型再次被赋予了一个名为entries的方法,它可以返回对应的数组中每一个元素与其下标配对的一个新数组。

    这个新特性可以与解构和for-of循环配合使用。

    const Zootopia=[
        {name:'Nick',gender:1,species:'Fox'},
        {name:'Judy',gender:0,species:'Bunny'}
    ];
    for(const [index,{name,species}] of Zootopia.entries){
        console.log(`${index}.Hi,I am ${name},and I am a ${species}`);
    }
    //0.Hi,I am Nick,and I am a Fox
    //1.Hi,I am Judy,and I am a Bunny
    

    箭头函数

    箭头函数,顾名思义便是使用箭头(=>)进行定义的函数,属于匿名函数(Anonymous Function)一类。
    相对于传统的function语句,箭头函数在简单函数使用中更为简洁直观。

    const arr=[1,2,3];
    //箭头函数
    const squares=arr.map(x=>x*x);
    //传统语法
    const squares=arr.map(function(x){return x*x});
    

    箭头函数有四种使用语法
    (1)单一参数的单行箭头函数

    //Syntax:arg=>statement
    const fn=foo=>`${foo} world`  //means return `foo +' world'` 
    

    这是箭头函数最简洁的形式,常见于用作简单的处理函数,如过滤。

    let array=['a','bc','def','ghij'];
    array=array.filter(item=>item.length>=2);  //bc,def,ghij
    

    (2)多参数的单行箭头函数

    //Syntax:(arg1,arg2)=>statement
    const fn=(foo,bar)=>foo+bar
    

    多参数的语法跟普通函数一样,以括号来包裹参数列,这种形式常见于数组的处理,如排序。

    let array=['a','bc','def','ghij'];
    array=array.sort((a,b)=>a.length<b.length);  //ghij,def,bc,a
    

    (3)多行箭头函数

    //Syntax:arg=>{...}
    //单一参数 
    foo=>{return `${foo} world`}
    
    //Syntax:(arg1,arg2)=>{...}
    //多参数
    (foo+bar)=>{return foo+bar}
    

    (4)无参数箭头函数
    如果一个箭头函数无参数传入,需要用一对空的括号来表示空的参数列表。

    //Syntax:()=>statement
    const greet=()=>'hello world'
    

    模板字符串

    当我们使用普通的字符串时,会使用单引号或双引号来包裹字符串的内容,在ES2015的模板字符串中使用反勾号`。

    //Syntax:`string...`
    const str=`something`
    

    (1)支持元素注入:
    可以将一些元素注入到ES2015的模板字符串中。

    //Syntax:`before-${injectVariable}-after`
    const str="hello world"
    const num=1
    const bool=true
    const obj={foo:'bar'}
    const arr=[1,2,3]
    
    const str1=`String:${str}`         //=>String:hello world
    const str2=`Number:${num}`         //=>Number:1
    const str3=`Boolean:${bool}`      //=>Boolean:true
    const str4=`Object:${obj}`        //=>Object:[object Object]
    const str5=`Array:${arr}`        //=>Array:1,2,3
    

    (2)支持换行:

    /**
    *Syntax:`
    *content
    *`
    */
    const sql=`
    select * from Users 
    where FirstName='mike' 
    limit 5;
    `
    

    多行字符串无法像普通字符串使用双引号嵌套单引号来表达字符串中的字符串,可以使用反斜杠将需要显示的反勾号转义为普通的字符。添加了\`用于打印`。

    const str1="Here is the outer string.'This is a string in another string'"
    const str2=`Here is the outer string.\`This is a string in another string\``
    

    对象字面量扩展语法

    在ES2015之前的ECMAScript的标准中,对象字面量只是一种用于表达对象的语法,只具有表达的功能,并不起到更大的作用。在ES2015中,为ECMASCript开发者开放了更多关于对象的操作权限,其中便有更多的对象字面量语法。

    (1)函数类属性的省略语法:
    ES2015中引入了类机制(Class),普通的对象字面量也吸收了一些语法糖,可以让方法属性省略function,以一种直观的语法来表达。

    //Syntax:{method(){...}}
    const obj={
        //before
       foo:function(){
         return 'foo'
       },
       //after
      bar(){
        return 'bar'
       }
    }
    

    有了这个语法糖,对象字面量中的方法类属性更像是一个方法,而不只是一个以函数为值得属性。

    (2)支持_proto_注入:
    在ES2015中开放了向对象字面量注入_proto_的功能,这样做的意义在于开发者可以得到更高的操作权限,从而更加灵活地创建和操作对象。
    在ES2015标准中,开发者允许直接向一个对象字面量注入_proto_,使其直接成为指定类的一个实例,无须另外创建一个类来实现继承。

    //Syntax:{_proto_:...}
    import {EventEmitter} from 'events'
    
    const machine={
        _proto_:new EventEmitter(),
        method(){...}
    }
    
    console.log(machine)   //=>EventEmitter{}
    console.log(machine instanceof EventEmitter)  //=>true
    

    (3)可动态计算的属性名:
    在ES2015标准对于对象字面量的处理中,引入了一个新语法,这个语法允许我们直接使用一个表达式来表达一个属性名。

    //Syntax:{[statement]:value}
    const prefix='es2015'
    const obj={
         [prefix+'enhancedObject']:'foobar'
    }
    

    (4)将属性名定义省略:
    在某些场景中,需要将一些已经别定义的变量(或常量)作为其它对象字面量的属性值进行返回或传入操作。

    //Syntax:{injectVariable}
    const foo=123
    const bar =()=>foo
    
    const obj={
          foo,
          bar
    }
    console.log(obj)  //=>{foo:123,bar:[Function:bar]}
    

    表达式结构

    在ES2015之前工程师们一般使用对象字面量和数组来模拟函数多返回值,在ES2015中同样可以使用类似的语法来实现函数多返回值,且语法上更加简洁。

    (1)使用对象作为返回载体(带有标签的多返回值)

    //Syntax:{arg1,arg2}={arg1:value1,arg2:value2}
    
    function getState(){
       return {
          error:null,
          logined:true,
          user:{},
      }
    }
    
    const {error,logined,user}=getState()
    

    (2)使用数组作为返回载体
    使用数组作为返回载体与使用对象作为返回载体的区别是:数组需要让被赋予的变量(或常量)名按照数组的顺序获得值。

    //Syntax:[arg1,arg2]=[value1,value2]
    const[foo,bar]=[1,2]
    console.log(foo,bar)  //=>1  2
    

    跳过数组中某些元素,通过空开一个元素的方式来实现。

    //Syntax:[arg1, ,bar]=[1,2,3]
    console.log(foo,bar)   //=>1  3
    

    不定项的获取后续元素,用...语句实现。

    //Syntax:[arg1,arg2,...restArgs]=[value1,value2,value3,value4]
    const [a,b,...rest]=[1,2,3,4,5]
    console.log(a,b)     //=>1   2
    console.log(rest)   //=>[3,4,5]
    

    (3)使用场景

    • Promise与模式匹配
      注意:如果在Promise.then方法中传入的是一个带有解构参数的箭头函数时,解构参数外必须要有一个括号包裹,否则会抛出语法错误。
    function fetchData(){
        return new Promise((resolve,reject)=>{
              resolve(['foo','bar'])
        })
    }
    
    fetchData().then(([value1,value2])=>{
            console.log(value1,value2)   //=>foo  bar
    })
    
    fetchData().then([value1,value2]=>{   //=>SyntaxError
            //...
    })
    

    如果参数过多但在某些场景下并不需要全部参数,或者文档约定不完善的情况下,使用对象作为传递载体更佳。

    function fetchData(){
        return new Promise((resolve,reject)=>{
              resolve({
                 code:200,
                 message:ok,
                 data:['foo','bar']
               })
        })
    }
    
    fetchData().then(({data})=>{
            console.log(data)   //=>foo  bar ...
    })
    
    • Swap(变量值交换)
      Swap表示定义一个函数或一种语法来交换两个变量的值。在ES2015中,可以使用模式匹配来实现Swap。
     function swap(a,b){
        var tmp=a
         a=b
         b=tmp
     }
     let foo=1
     let bar=2
    
    //Before Swap
    console.log(foo,bar)  //=>1   2
    //Swap
    [foo,bar]=[bar,foo]
    //After Swap
    console.log(foo,bar)  //=>2  1
    

    (4)高级用法

    • 解构别名
      如果不想使用其中的属性名作为新的变量名(或常量名),可以使用别名获得相应的返回值,只要在原来的返回值后面加上“:x”,其中x就是希望使用的变量名。
    function fetchData(){
        return{
           response:['foo','bar']
        }
    }
    
    const{response:data}=fetchData()
    console.log(data)   //=>foo bar
    
    • 无法匹配的缺省值
      如果在模式匹配中,存在无法匹配的缺省值(载体对象不存在相应的值或目标参数所对应下标超出了载体数组的下标范围),默认情况下会返回undefined。
    //Object
    const {foo,bar}={foo:1}
    console.log(foo,bar)   //=>1  undefined
    //Array
    const [a,b,c]=[1,2]
    console.log(a,b,c)   //=>1   2   undefined
    

    如果不希望得到undefined,可以为参数赋予一个默认值,当无法匹配到相应的值时,会使用该默认值。

    const {foo=1}={bar:1}
    console.log(foo)   //=>1
    
    const [a,b=2]=[1]
    console.log(a,b)   //=>1   2
    
    • 深层匹配
      通过嵌套解构表达式来获取深层的内容,可以在对象中嵌套数组来获取对象中数组的某元素,反之亦然。
     //Object in Object
    const  {a,b:{c}}={a:1,b:{c:2}}
    console.log(a,c)   //=>1  2
    
    //Array in Object
    const  {d,e:[f]}={d:1,e:[2,3]}
    console.log(d,f)  //=>1  2
    
    //Object in Array
    consot [g,{h}]=[1,{h:2}]
    console.log(g,h)  //=>1  2
    
    //Array in Array
    const [i,[j]]=[1,[2,3]]
    console.log(i,j)  //=>1   2
    

    函数参数表达、传参

    (1)默认参数值
    使用语法:
    ES2015中使用语法直接实现默认参数值语法显得更加简洁而直观。

    //Syntax:function name(arg=defaultValue){...}
    function fn(arg='foo'){
      console.log(arg)
    }
    fn()  //=>foo
    fn('bar')   //=>bar
    

    使用场景:
    同时提供回调函数和Promise返回方式的接口

    const noop=()=>{}
    
    function api(callback=noop){
       return new Promise((resolve,reject)=>{
           const value='footbar'
               resolve(value)
                   callback(null,value)
           })
    }
    //Callback
    api((err,data)=>{
       if(err) return console.error(err)
    })
    //Promise
    api().then(value=>{
       //...
    })
    .catch(err=>console.error(err))
    

    函数的默认参数特性用在某一个对象的方法中,所指定的默认参数还可以被定为该对象的某一个属性

    const obj={
        msg:'World',
        greet(message=this.msg){
            console.log(`Hello ${message}`)
        }
    }
    
    obj.greet()   //=>Hello World
    obj.greet('ES2015')   //=>Hello ES2015
    

    (2)剩余参数
    使用语法
    ES2015中对剩余参数有了更为优雅和标准的语法,直接将需要获取的参数列表转换为一个正常数组,以便使用。

    //Syntax:function fn([arg,]...restArgs){}
    function fn(foo, ...rest){
         console.log(`foo:${foo}`)
         console.log(`Rest Arguments:${rest.join(',')}`)
    }
    fn(1,2,3,4,5)
    //=>foo:1
    //Rest Arguments:2,3,4,5
    

    使用场景
    十分常用的merge和mixin函数(合并对象)就会需要使用到剩余函数这个特性来实线。

    function merge(target={},...objs){
        for(const obj of objs){
            const keys=Object.keys(obj)
            for(const key of keys){
                target[key]=obj(key)
            }
        }
        return target
    }
    console.log(merge({a:1},{b:2},{c:3}))   //=>{a:1,b:2,c:3}
    

    注意事项
    注意:一旦一个函数的参数列表中使用了剩余参数的语法糖,便不可以再添加任何参数,否则会抛出错误。

    function fn1(...rest){}    //Correct
    function fn1(...rest,foo){}  //Syntax Error
    

    arguments与剩余函数
    虽然从语言角度看,arguments和...args是可以同时使用的,但有一种情况除外,arguments在箭头函数中,会跟随上下文绑定到上层,所以在不确定上下文绑定结果的情况下,尽可能不要在箭头函数中使用arguments,而要使用..args。
    (3)解构传参
    ES2015中的解构传参是使用数组作为传入参数以控制函数的调用情况,不同的是解构传参不会替换函数调用中的上下文。
    与剩余参数一样,解构传参使用...作为语法糖标识符。

    //Syntax:fn(...[arg1,arg2])
    function sum(...numbers){
    return numbers.reduce((a,b)=>a+b)
    }
    sum(...[1,2,3])  //=>6
    

    新的数据解构

    在ECMAScript中定义了以下几种基本的数据结构,分为值类型(Primitive Types)和引用类型(Reference
    Types)。

    值类型数据结构:

    • String 字符串
    • Number 数值
    • Boolean 布尔型(true与false)
    • Null 空值
    • Undefined 未定义值

    引用类型数据结构:

    • Object 对象
    • Array 数组
    • RegExp(Regular Expression with pattern)正则表达式
    • Date 日期
    • Error 错误

    (1)Set有序集合
    ECMAScript中,Array表示一系列元素的有序集合,其中每一个元素都会带有自身处在这个集合内的位置并以自然数作为标记,即带有下标。无序集合可以把它当成没有排序概念的数组,并且元素不可重复。

    使用语法
    在ES2015中,集合与数组不一样的是,集合无法像数组那样使用[]语法来直接生成,而需要用新建对象的方法来创建一个新的集合对象。

    //Syntax:new Set([iterable]):Set
    const set=new Set()
    

    可以使用一个现成的数组作为集合对象的初始元素

    const set=new Set([1,2,3])
    

    集合对象的操作方法


     
    image.png

    增删元素
    可以通过add、delete和clear方法来添加,删除,清空集合内的元素。

    const set =new Set()
    //添加元素
    set.add(1)
    .add(2)
    .add(3)
    .add(3)  //这一句不会起到任何作用,因为元素3已存在于集合内
    console.log(set)     //Set{1,2,3}
    
    //删除元素
    set.delete(2)
    console.log(set)   //Set{1,3}
    
    //清空集合
    set.clear()
    console.log(set)    //set{}
    

    检查元素

    const set=new Set([1,2,3])
    //检查元素
    set.has(2)   //=>true
    set.has(4)  //=>false
    

    遍历元素
    集合对象自身定义了forEach方法,跟数组类型中的forEach一样,传入一个回调函数以接受集合内的元素,并且可以为这个回调函数指定一个上下文。

    const set=new Set([1,2,3,4])
    set.forEach(item=>{
        console.log(item)
    })
    //=>1    2    3    4
    
    set.forEach(item=>{
        console.log(item*this.foo)
    },{foo:2})
    //=>2    4    6    8
    

    在ES2015中,由于Symbol的引入,数组等类型有了一个新属性Symbol.iterator(迭代子),这些类型的新名称--可迭代对象(Iterable Object),其中包括数组类型、字符串类型、集合类型、字典类型(Map)、生成器类型(Generator),for-of循环语句可以对可迭代对象进行迭代,配合const或let使用,从而解决forEach方法不可中断的问题。

    const set=new Set([1,2,3,4])
    for(const val of set){
       console.log(val)
    }
    //=>1   2    3   4
    

    (2)WeakSet
    WeakSet最大的应用意义在于,可以直接对引擎中垃圾收集器的运行情况有程序化的探知方式,开发者可以利用WeakSet的特性以更高的定制化方案来优化程序的内存使用方案。

    WeakSet与Set的区别:
    a.WeakSet不能包含值类型元素,否则会抛出一个TypeError;
    b.WeakSet不能包含无引用的对象,否则会自动清除出集合;
    c.WeakSet无法被探知其大小,也无法被探知其中所包含的元素。

    (3)Map映射类型
    映射类型在计算机科学中的定义属于关联数组(Associative Array),关联数组的定义为若干个键值对(Key/Value Pair)组成的集合,其中每一个键都只能出现一次。

    使用语法
    映射类型需要创建一个相应的实例来使用。

    //Syntax:new Map([iterable]):Map
    const map=new Map()
    

    在创建映射对象时,可以将一个以二元数组(键值对)作为元素的数组传入到构建函数中,其中每一个键值对都会加入到该映射对象中。该数组内的元素会以数组顺序进行处理,如果存在相同的键,则会按照FIFO(First In First Out,先进先出)原则,以该键最后一个处理的对应值为最终值。

    const map = new Map([['foo', 1 ], [ 'foo', 2 ]])
    console.log(map.get('foo'))   //=> 2
    

    与对象字面量一样,映射对象可以对其中的键值对进行添加、检查、获取、删除等操作。
    当然,作为新特性的映射对象也拥有一些Object没有的方法。


     
    image.png

    增删键值对
    与集合对象类似,可以通过set、delete和clear方法对映射对象内的键值对进行操作。

    const map=new Map()
    // 添加键值对
    map.set('foo','hello')
    map.set('bar','es2015')
    map.set('bar','world')   //=>将覆盖之前加入的值
    
    //删除指定的键值对
    map.delete('foo')
    //清空映射对象
    map.clear()
    

    获取键值对
    映射对象由键值对组成,所以可以利用键来获取相应的值。

    const map=new Map()
    map.set('foo','bar')
    console.log(map.get('foo'))    //=> bar
    

    检查键值对
    映射对象可以通过has (key)方法来检査其中是否包含某一个键值对

    const map=new Map([ 'foo', 1 ])
    console.log(map.has('foo')) //=> true
    console.log(map.has('bar')) //=>false
    

    遍历键值对
    映射对象是关联数组的一种实现,所以映射对象在设计上同样是一种可迭代对象,可以通过for-of循环语句对其中的键值对进行历遍。也可以使用己实现在映射对象中的forEach方法来进行历遍。

    映射对象带有entries ()方法,这个与集合对象中的entries()类似,用于返回一个包
    含所有键值对的可迭代对象,而for-of循环语句和forEach便是先利用entries ()方法先
    将映射对象转换为一个类数组对象,然后再进行迭代。

    const map=new Map([['foo',1],['bar',2]])
    console.log(Array.from(map.entries()))     //=>[['bar',1],['bar',2]]
    
    for(const [key,value] of map){
        console.log(`${key}:${value}`)
    }
    //=>foo:1     bar:2
    
    map.forEach((value,key,map)=>{
      console.log(`${key}:${value}`)
    })
    

    (4)WeakMap
    WeakMap的键会检查变量引用,只要其中任意一个引用被解除,该值对就会被删除。

    //Syntax:new WeakMap([iterable]):WeakMap
    const weakm=new WeakMap()
    let keyObject={id:1}
    const valObject={score:100}
    
    weakm.set(keyObject,valObject)
    weakm.get(keyObject)    //=>{score:100}
    keyObject=null
    console.log(weakm.has(keyObject))   //=>false
    

    类语法

    ES2015中的类语法与其他C语言家族成员的类语法有许多相同之处,如果开发者有在
    JavaScript中使用过基于原型的类机制,那么也可以很容易接受ES2015的语法。

    基本定义语法

    // Syntax: class name { ... }
    class Animal {
        constructor(family, specie, hue) {
            this.family =family
            this.specie = specie
            this.hue = hue
    
        yell() {
          console.log(this.hue)
       }
    }
    
    const doge = new Animal('Canidae', 'Canis lupus’, 'Woug')
    doge.yell()   //=> Woug
    

    这里需要注意的是,在类中定义的方法,都是带有作用域的普通函数,而不是箭头函数,方法内第一层所引用的this都指向当前实例,如果实例方法内包含箭头函数,则引擎就会根据包含层级把箭头函数内引用的this所指向的实际对象一直向上层搜索,直到到达一个函数作用域或块级作用域为止。如果一直搜索到达了运行环境的最上层,就会被指向undefined。

    class Point{
        constructor(x,y){
            this.x=x
            this.y=y
        }
        moveRight(step){
            return new Promise(resolve=>resolve({
                x:this.x+step,
                y:this.y
                }))
        }
    }
    const p=new Point(2,5)
    p.moveRight(3)
        .thien(({x,y})=>console.log(`(${x},${y}`))   //=>(5,5)
    

    继承语法

    //Syntax:class SubClass extends  SuperClass{}
    class Point2D{
        constructor(x,y){
            this.x=x
            this.y=y
        }
        toString(){
            return `(${this.x},${this.y})`
        }
    }
    class Point3D extends Point2D{
        constructor(x,y,z){
            super(x,y)
            this.x=x
        }
        toString(){
            return `(${this.x},${this.y},${this.z})`
        }
    }
    

    ES2015的继承语法可以将以前使用构建函数模拟的类作为父类来继承,并非只由class语法定义的类才可以使用。

    function Cat() {}
    Cat.prototype.climb = function () {
        return "I can climb"
    }
    Cat.prototype.yell = function () {
        return "Meow"
    }
    class Tiger extends Cat{
        yell(){
            return "Aoh"
        }
    }
    
    const tiger=new Tiger()
    console.log(tiger.yell())    //=>Aoh
    console.log(tiger.climb())  //=>I can climb
    

    需要注意的是,如果一个子类继承了父类,那么在子类的constructor构造函数中必须使用super函数调用父类的构造函数后才能在子类的constructor构造函数中使用this,否则会报出this is defined的错误。

    class Foo{}
    class Bar extends Foo{
       constructor(){
          this.property=1
       }
    }
    new Bar()  //=>RerenceError:this is defined
    

    这个问題在除constructor构造函数以外的方法中并不会出现,即便在子类的构造
    函数中并没有调用super函数,在其他方法中依然可以调用this来指向当前实例。

    Getter/Setter
    Getter/Setter是一种元编程(Meta-programming)的概念,元编程的特点在于,允许程序可以对运行时(Runtime)的对象进行读取和操作,从而使程序可以脱离代码从字面上为程序定义的一些限制,有了对对象的更高操作权限。

     const List={
        _array:[],
         set new(value){
            this._array.push(value)
         },
         get last(){
             return this._array[0]
         },
         get value(){
             return this._array
         }
     }
    
    List.new=1
    List.new=2
    List.new=3
    console.log(List.last)     //=>1
    console.log(List.value)    //=>[1,2,3]
    

    ES2015的类机制同样支持Getter/Setter在类中的使用,配合元编程的概念,类的能力会变得更加强大。

    class Point{
        constructor(x,y){
            this.x=x
            this.y=y
        }
        get d(){
            return Math.sqrt(Math.pow(this.x,2)+Math.pow(this.y,2))
        }
    }
    const p=new Point(3,4)
    console.log(p.d)   //=>5
    

    静态方法
    可以通过实现一个静态方法来扩展类

    // Syntax: class Name { static fn() { ... } }
    class Animal {
        constructor(family, specie, hue) {
            this.family = family
            this.specie = specie
            this.hue = hue
        }
        yell() {
            console.log(this.hue)
        }
        static extend(constructor, ..._args) {
          return class extends Animal {
            constructor{...args) {
            super(..._args)
            constructor.call(this, ...args)
              }
           }
        }
    }
    
    const Dog = Animal.extend(function(name) {
        this.name = name
    }, 'Canidae', 'Canis lupus', 'Woug')
    
     const doge=new Dog('Doge')
     doge.yell(> //=> Woug
     console.log(doge.name) //=> Doge
    

    高级技巧
    在Object类及其所有子类(在ECMAScript中,除了null、undefined以外,一切类型和类都可以看做是Object的子类)的实例中,有一个利用Symbol.toStringTag作为键的属性,定义着当这个对象的toString()方法被调用时,所返回的Tag的内容是什么。这就意味着可以进行一些自定义操作,通过[]语法和Getter特性为一个类自定义toString标签。

    class Foo{
         get [Symbol.toStringTag](){
             return 'Bar'
        }
    }
    const obj=new Foo()
    console.log(obj.toString())   //=>[object  Bar]
    

    注意事项
    类的继承必须是单项的,不可能出现A类继承于B类的同时B类也继承A类的现象,这就意味着,父类必须在子类定义之前被定义。

    生成器(Generator)

    生成器的主要功能是:通过一段程序,持续迭代或枚举出符合某个公式或算法的有序数列中的元素,这个程序便是用于实现这个公式或算法的,而不需要将目标数列完整写出。
    生成器是ES2015中同时包含语法和底层支持的一个新特性。

    (1)基本概念
    生成器函数
    生成器函数是ES2015中生成器的最主要表现方式,它与普通函数的语法差别在于,在function语句之后和函数名之前,有一个“*”作为生成器函数的标示符。

    function* fibo(){
    //...
    }
    

    生成器函数并不是强制性使用声明式进行定义的,与普通函数—样也可以使用表达式进行定义。

    const fnName = function*() {/*...*/}
    

    生成器函数的函数体内容将会是所生成的生成器的执行内容,在这些内容之中,yield语句的引入使得生成器函数与普通函数有了区别。yield语句的作用与return语句冇些相似,但并非退出函数体,而是切出当前函数的运行时(此处为一个类协程,Semi-coroutine),与此同时可以将一个值(可以是任何类型)带到主线程中。

    我们以一个比较形象的例子来做比喻,你可以把整个生成器运行时看成一条长长的瑞士卷,while (true)是无限长的,ECMAScript引擎每一次遇到yield语句时,就好比在瑞士卷上切一刀,而切面所呈现的“纹路”则是yield语句所得的值。

    生成器
    从计算机科学角度上看,生成器是—种类协程或半协程(Semi-coroutine),它提供了一种可以通过特定语句或方法使其执行对象(Execution)暂停的功能,而这语句一般都是yield语句。上面的斐波那契数列生成器便是通过yield语句将每一次的公式计算结果切出执行对象,并带到主线程上来的。

    在ES2015中,yield语句可以将一个值带出协程,向主线程也可以通过生成器对象的方法将一个值带回生成器的执行对象中去。

    const inputValue =yield outputValue
    

    生成器切出执行对象并带出outputValue,主线程经过同步或异步处理后,通过.next (val)方法将inputValue带回生成器的执行对象中。

    (2)使用方法
    构建生成器函数
    使用生成器的第一步自然是要构建一个生成器函数,以生成相对应的生成器对象。

    启动生成器
    生成器函数不能直接作为普通的函数来使用,因为在调用时无法直接执行其中的逻辑代码。执行生成器函数会返回一个生成器对象,用于运行生成器内容和接受其中的值。

    运行生成器内容
    因为生成器对象自身也是一种可迭代对象,所以直接使用for-of循环将其中输出的值打印出来。

    Promise

    Promise意在让异步代码变得干净和直观,让异步代码变得井然有序。
    Promise在设计上具有原子性,即只有三种状态:等待(Pending)、成功(Fulfilled)、失败(Rejected)。在调用支持Promise的异步方法时,逻辑变得非常简单,在大规模的软件工程开发中具有良好的健壮性。

    (1)基本语法
    创建Promise对象:
    要想给一个函数赋予Promise能力,就要先创建一个Promise对象,并将其作为函数值返回。Promise对象要求传入一个函数,并带有resolve和reject参数。这是两个用于结束Promise等待的函数,对应的状态分别是成功和失败。

    //Syntax:
    //new Promise(executor):Promise
    //new Promise((resolve,reject)=>statements):Promise
    
    function asyncMethod(...args){
        return new Promise((resolve,reject)=>{
            //...
        })
    }
    

    将新创建的Promise对象作为异步方法的返回值,所有的状态就可以使用它所提供的方法进行控制了。

    进行异步操作:
    创建了 Promise对象后,就可以进行异步操作,并通过resolve (value)和
    reject (reason)方法来控制Promise的原子状态。

    1. resolve(value)方法控制的是当前Promise对象是否进入成功状态,一旦执行该方法并传入有且只有一个返回值,Promise便会从等待状态(Pending)进入成功状态(Fulfilled),Promise也不会再接收任何状态的变。
    2. reject (reason)方法控制的是当前Promise对象是否进入失败阶段,与resolve方法相冋,一旦进入失败阶段就无法再改变。
    //Syntax:
    //resolve(value)
    //reject(reason)
    
    new Promise((resolve,reject)=>{
        api.call('fetch-data',(err,data)=>{
            if(err) return reject(err)
            resolve(data)
        })
    })
    

    其中在Promise的首层函数作用域中一旦出现throw语句,Promise对象便会直接进入失败状态,并以throw语句的抛出值作为错误值进行错误处理。

    (new Promise(function() {
        throw new Error ('test')
    )))
    .catch(err =>console.error(err))
    

    但是相对的return语句并不会使Promise对象进入成功状态,而会使Promise停留在等待状态。所以在Promise对象的执行器(executor)内需要谨慎使用return语句来控制代码流程。

    处理Promise的状态
    与resolve(value)和reject(reason)方法对应的是,Promise对象有两个用于处理Promise对象状态变化的方法。


     
    image.png

    这两个方法都会返回一个Promise对象,Promise对象的组合便会成为一个Promise对象链,呈流水线的模式作业。

    //Syntax:promise.then(onFulfilled).catch(onRejected):Promise
     
     asyncMethod()
     .then((...args)=>args  /*...*/)
     .catch(err=>console.error(err))
    

    Promise链式处理默认被实现,即.then(onFulfilled)或.catch(onRejected)会处理在onFulfilled和onRejected中所返回或抛出的值。

    1. 如果onFulfilled或onRejected中所返回的值是一个Promise对象,则该Promise对象会被加入到Promise的处理链中。

    2. 如果onFulfilled或onRejected中返回的值并不是一个Promise对象,则会返回一个己经进入成功状态的Promise对象。

    3. 如果onFulfilled或onRejected中因为throw语句而抛出一个错误err,则会返回一个已经进入失败状态的Promise对象。

    之所以说Promise对象链呈流水线的模式进行作业,是因为在Promise对象对自身的onFulfilled和onRejected响应器的处理中,会对其中返回的Promise对象进行处理。其内部会将这个新的Promise对象加入到Promise对象链中,并将其暴露出来,使其继续接受新的Promise对象的加入。只有当Promise对象链中的上一个Promise对象进入成功或失畋阶段,下一个Promise对象才会被激活,这就形成了流水线的作业模式。

    Promise对象链还有一个十分实用的特性--Promise对象的状态是具有传递性的。

    如果Promise对象链中的某一环出现错误,Premise对象链便会从出错的环节开始,不断向下传递,直到出现任何一环的Promise对象对错误进行响应为止。

    (2)高级使用方法
    Promise.all(iterable)
    该方法可以传入一个可迭代对象(如数组),并返回一个Promise对象,该Promise对象会
    在当可迭代对象中的所冇Promise对象都进入完成状态(包括成功和失畋)后被激活。

    1.如果可迭代对象中的所有Promise对象都进入了成功状态,那么该方法返回的Promise
    对象也会进入成功状态,并以一个可迭代对象来承载其中的所有返回值。

    2.如果可迭代对象中Promise对象的其中一个进入了失败状态,那么该方法返回的Promise
    对象也会进入失败状态,并以那个进入失败状态的错误信息作为自己的错误信息。

    //Syntax:Promise.all(iterable):Promise
    const promises=[async(1),async(2),async(3),async(4)]
    
    Promise.all(promises)
    .then(values=>{
        //...
    })
    .catch(err=>console.error(err))
    

    Promise.race(iterable)
    Promise .race (iterable)方法同样也接受一个包含若干个Promise对象的可迭代对象,但不同的是这个方法会监听所有的Promise对象,并等待其中的第一个进入完成状态的Promise对象,一旦有第一个Promise对象进入了完成状态,该方法返回的Promise对象便会根据这第一个完成的Promise对象的状态而改变。

    //Syntax:Promise.race(iterable):Promise
    const promises=[async(1),async(2),async(3),async(4)]
    
    Promise.race(promises)
    .then(values=>{
        //...
    })
    .catch(err=>console.error(err))
    

    代码模块化

    ECMAScript包含了以往模块加载库的主要功能,还添加了一些非常使用的设计,以提高ECMAScript的模块化管理功能。

    (1)引入模块
    ES Module中有很多种引入模块的方法,最基本的便是import语句。

    import name form 'module-name'
    import * as name from 'module-name'
    import {member} from 'module-name'
    import {meber as alias} from 'module-name'
    import 'module-name'
    

    引入默认模块

    //Syntax:import namespace from 'module-name'
    
    import http from 'http'
    import url from 'url'
    import fs from 'fs'
    

    引入模块部分接口
    ES2015中的 模块化机制支持引入一个模块的部分接口

    //Syntax:import {meber1,meber2} from 'module-name'
    
    import {isEmpty} from 'lodash'
    import {EventEmitter} from 'events'
    
    console.log(isEmpty({}))  //=>true
    

    从模块中局部引用的接口定义一个别名,以避免指代不明或接口重名的情况出现。

    //Syntax:import {meber as alias} from 'module-name'
    import {createServer as createHTTPServer} from 'http'
    import {createServer as createHTTPSServer} from 'https'
    

    引入全部局部接口到指定命名空间
    有的模块不会定义默认接口,只是定义了若干个命名接口,将其中的所有接口定义到一个命名空间中,使用以下语法。

    //Syntax:import * as namespace from 'module-name'
    import * as lib from 'module'
    lib.method1()
    lib.method2()
    

    混入引入默认接口和命名接口
    同时引入默认接口和其它命名接口,可以通过混合语句来实现。

    //Syntax:import {default as  <default name>,method1} from 'module-name'
    import {default as Client,utils} from 'module'
    

    注意:引入的默认接口必须要使用as语句被赋予一个别名,因为在除模块引入语句以外的地方default是一个保留关键字,所以无法使用。

    import {default ,utils} from 'module'  //Wrong
    

    简洁的语法

    //Syntax:import  <default name>,{<named modules>} from 'module-name'
    import Client,{utils} from 'module'
    import Client,* as lib from 'module'
    

    不引入接口,仅运行模块代码
    在某些场景下,一些模块并不需要向外暴露任何接口,只需要执行内容的代码(如系统初始化)。

    //Syntax:import 'module-name'
    import 'system-apply'
    

    (2)定义模块
    ES Module中以文件名及其相对或绝对路径作为该模块被引用时的标识。

    (3)暴露模块
    暴露单一接口
    如果需要定义一个项目内的工具集模块,需要将其中定义的函数或者对象暴露到该文件所定义的模块上。

    //Syntax:export <statement>
    
    //module.js
    export const apiRoot='http://example.com/api'
    export function method(){
        //...
    }
    export class foo{
        //...
    }
    
    //app.js
    import {method,foo} from 'module.js'
    

    export 语句后所跟着的语句需要具有生命部分和赋值部分
    1.声明部分(Statement)为export语句提供了所暴露接口的标识;
    2.赋值部分(Assignment)为export语句提供了接口的值。

    那些不符合这两个条件的语句无法被暴露在当前文件所定义的模块上,以下代码被视为非法代码。

    //1
    export 'foo'
    //2
    const foo='bar'
    export foo
    //3
    export function(){}
    

    暴露模块默认接口
    在某些时候,一个模块只需要暴露一个接口,比如需要使用模块机制定义一个只含有一个单一工具类的模块时,就没有必要让这个工具类成为该模块的一部分,而是让这个类成为这个模块。

    //Syntax:export default <value>
    //client.js
    export default class Client{
       //...
    }
    //app.js
    import Client from 'client.js'
    

    混合使用暴露接口语句
    开发者可以为一个模块同时定义默认接口和其它命名接口。

    //module.js
    export default class  Client{
        //...
    }
    export const foo='bar'
    
    //app.js
    import Client,{foo} from 'module'
    

    暴露一个模块的所有接口
    在第三方类库的开发中,不免需要将各种不同的功能块分成若干个模块来进行开发,以便管理。ES Module可以将import语句和export组合,直接将一个模块的接口暴露到另外一个模块上。

    //Syntax:export * from  'other-module'
    //module-1.js
    export function foo(){/*....*/}
    //module.js
    export * from 'module-1'
    //app.js
    import {foo} from 'module'
    

    暴露一个模块的部分接口

    //Syntax:export {member} from 'module-name'
    export {member} from 'module'
    export {default as ModuleDefault} from 'module'
    

    暴露一个模块的默认接口
    可以将一个模块的默认接口作为另一个模块的默认接口。

    export {default} from  'module'
    

    Symbol

    Symbol的值具有互不等价的特性,开发者同时可以为Symbol值添加一个描述。
    (1)基本语法

    • 生成唯一的Symbol值
      执行Symbol({description})函数可以生成一个与其它Symbol值互不等价的Symbol值,其中Symbol()函数可以接受一个除Symbol值以外的值作为该Symbol值的描述,以便通过开发者的辨认判断其为可选的。
    //Syntax:Symbol([description]):Symbol
    
    const symbol=Symbol()    //=>Symbol()
    const symbolForSomething=Symbol('something')   //=>Symbol(something)
    const symbolWithNumber=Symbol(3.14)    //=>Symbol(3.14)
    const symbolWidthObject=Symbol({'foo':'bar'})  //=>Symbol([object Object])
    
    //Don't use a symbol to be another symbol's description
    const anotherSymbol=Symbol(symbol)  //=>TypeError:Cannot convert a Symbol value to a string
    

    描述值仅仅是起到描述的作用,不会对Symbol值本身起到任何改变的作用。即便是两个具有相同描述值的Symbol值也不具有等价性。

    const symbol1=Symbol('footer')
    const symbol2=Symbol('footer')
    symbol1==symbol2   //=>false
    

    注意:Symbol函数并不是一个构造函数,不能使用new语句来生成Symbol“对象”,否则会抛出TypeError错误。

    new Symbol()   //=>TypeError:Symbol is not  a constructor
    

    由此可知,Symbol是一种值类型而非引用类型。这就意味着如果将Symbol值作为函数形参进行传递,将会进行复制值传递而非引用传递,这跟其它值类型(字符串,数字等)的行为是一致的。

    const symbol=Symbol('hello')
    function fn1(_symbol){
       return _symbol==symbol
    }
    console.log(fn1(symbol))   //=>true
    function fn2(_symbol){
       _symbol=null
      console.log(_symbol)
    }
    fn2(symbol)  //=>null 
    

    如果希望得到一个Symbol“对象”,可以使用Object()函数实现。

    const symbol=Symbol('foo')
    typeof symbol   //=>symbol
    const symbolObj=Object(symbol)
    typeof symbolObj   //=>object
    
    • 注册全局可重用Symbol
      ES2015标准除了提供具有唯一性的Symbol值以外,同样还允许开发者在当前运行时中定义一些全局有效性的Symbol。开发者可以通过一个key向当前运行时注册一个需要在其他程序中使用的Symbol。
    //Syntax:Symbol.for([key]):Symbol
    const symbol=Symbol.for('footer')
    

    Symbol. for ()与Symbol ()的区別是,Symbol . for ()会根据传入的key在全局作用域中注册一个Symbol值,如果某一个key从未被注册到全局作用域中,便会创建一个Symbol值并根据key注册到全局环境中。如果该key己被注册,就会返冋一个与第一次使用所创建的Symbol值等价的Symbol值。

    const symbol = Symbol.for('foo')
    const obj ={}
    obj[symbol] = 'bar'
    
    const anotherSymbol = Symbol.for('foo')
    
    console.log(symbol === anotherSymbol) //=> true
    console.log (obj [anotherSymbol])    //=> jbar
    

    这在大型系统的开发中可以用于一些全局的配罝数据中或者用于需要多处使用的数据中。

    • 获取全局Symbol的key
      既然可以通过字符串的key在全局环境中注册一个全局Symbol,那么同样也可以根据这些全局的Symbol获取到它们所对应的key。
    //Syntax:Symbol kefFor(<global symbol>):String
    const  symbol=Symbol.for('foobar')
    console.log(Symbol.keyFor(symbol))   //=>foobar
    

    (2)常用Symbol值
    ES2015标准定义了一些内置的常用Symbol值,这些Symbol值的应用深入到了 ECMAScript引擎运行中的各个角落。开发者可以运用这些常用Symbol值对代码的内部运行逻辑进行修改或拓展,以实现更高级的需求。


     
    image.png

    (3)Symbol.iterator
    在ES2015标准中定义了可迭代对象(Iterable Object)和新的for-of循环语句,其中可迭代对象并不是一种类型,而是带有@@iterator属性和可以被for-of循环语句所遍历的对象的统称。

    for-of循环语句与可迭代对象
    for-of循环语句是ES2015中新增的循环语句,它可以对所有可迭代对象进行遍历,而不仅仅是数组。在ES2015中,默认的可迭代对象有:数组(Array)、字符串(String)、类型数组(TypedArray)、映射对象(Map)、集合对象(Set)和生成器实例(Generator)。

    // Array
    for (const el of [ 1, 2, 3 ]) console.log(el)
    // String
    for (const word of 'Hello World') console.log(word)
    // TypedArray
    for (const value of new Uint8Array([ 0x00, Oxff J)) console.log(value)
    //Map
    for (const entry of new Map ([ [' a', 1],   [ 'b', 2]])   console.log (entry)
    //Set
    for (const el of new Set([ 1, 2, 3, 3, 3 ]))    console.log (el)
    // Generator
    function* fn() { yield 1 }
    for (const value of fn ()) console.log(value)
    

    (4)Symbol.hasInstance
    Symbol.haslnstance为开发者提供了可以用于扩展instanceof语句内部逻辑的权限,开发者可以将其作为属性
    键,用于为一个类定义静态方法,该方法的第一个形参便是被检测的对象,而该方法的返回值便是决定了当次instanceof语句的返回结果。

    class Foo (
       static [Symbol.haslnstance](obj) {
       console.log(obj)      //=>{}
       return true
      }
    }
    console.log({} instanceof Foo)    //=>true
    

    (5)Symbol.match
    Symbol.match是正则表达式(或者对象)在作为字符串使用match ()方法时,内部运行逻辑的自定义逻辑入口。开发者可以通过Symbol.match来自行实现match ()方法的运行逻辑,比如利用strcmp (在ECMAScript中为String.prototype.localeCompare())来实现。

    const  re = /foo/
    re[Symbol.match]=function(str){
          const regexp=this
          console.log(str)   //=>bar
         //...
         return true
    }
    'bar'.match(re)   //=>true 
    

    (6)Symbol.toPrimitive
    Symbol.toPrimitive为开发者提供了更高级的控制权力,使得引用类型的对象在转换为值类型时可以进行自定义处理,无论是转换为字符串还是数字。

    开发者可以使用Symbol.toPrimitive作为属性键为对象定义一个方法,这个方法接受一个参数,这个参数用于判断当前隐式转换的目标类型。


     
    image.png

    需要注意的是,这里的default并不是因为目标类型无法被转换,而是因为语法上容易造成混乱。

    (7)Symbol.toStringTag
    常用Symbol的值在前面己经提到过,它的作用是可以决定这个类的实例在调用toString()方法时的标签内容。

    在Object类及其所有的子类的实例中,有一个利用Symbol .toStringTag作为键的属性,该属性定义着当这个对象的toString()方法被调用时,所返回的Tag的内容是什么。

    比如在开发者定义的类中,就可以通过Symbol. toStringTag来修改toString()屮的标签内容,利用它作为属性键为类型定义一个Getter。

    class Bar {}
    class Foo{
        get  [Symbol.toStringTagl() { return  'Bar'}
    }
    
    const obj =new  Foo()
    console.log(obj .toString() )   //=> [object Bar]
    


    电子书链接: 《实战ES2015:深入现代JavaScript+应用开发》 密码: uetw


    作者:ywyan
    链接:https://www.jianshu.com/p/220a54f7adce
    来源:简书
    简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。
  • 相关阅读:
    eclipse里打包时排除.svn目录
    UltraEdit搭建python IDE环境+设置快捷键
    Window下 VC2008 Express 配置 OpenGL
    N73 getRGB createRGBImage 透明色的问题
    Iphone UIApplication openURL能帮助你运行Maps,SMS,Browser, Calling甚至其他的应用程序
    Eclipse配置KEmulator
    洛谷P1439 【模板】最长公共子序列 (DP,离散化)
    Codeforces Round #642 (Div. 3) D. Constructing the Array (优先队列)
    Codeforces #6241 div2 C. Orac and LCM (数学)
    洛谷P1469 找筷子 (位运算)
  • 原文地址:https://www.cnblogs.com/panchanggui/p/10856277.html
Copyright © 2011-2022 走看看