浅谈ECMAScript新特性

1.新的标准规范

ECMAScript2015 是 js 的一种的新的标准规范,就是对 js 的写法上提出了新的语法要求和写法格式。

2.ECMAScript 和 javaScript 关系

ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。javascript 是 netscape 创 造的并交给了国际标准化组织 ECMA,之所以不叫做 JavaScript 由于商标的问题,java 是 sun 公司的商标,根据 授权协议只有 Netscape 公司可以合法使用 JavaScript 这个名字,另外就是为了体现 JavaScript 的标准的制定者 不是 ECMA 所以取名为 ECMAScript。


web.png
node.png

3.ES6 与 ECMAScript 2015 的关系

ES6 是 ECMA 为 JavaScript 制定的第 6 个版本的标准,标准委员会最终决定,标准在每年的 6 月份正式发布一 次,作为当年的正式版本。ECMAscript 2015 是在 2015 年 6 月份发布的 ES6 的第一个版本。依次类推 ECMAscript 2016 是 ES7, ECMAscript 2017 是 ES8, 后续版本都以年份为命名

块级作用域

1.块级作用域的种类

ECMAScript2015 为 js 提出的第三个作用域,凡是带{}的都是一个块级作用域。

if 语句的{},for 循环中的{},while 中的{},或者是我们单独写的{} try{}catch(error){}这些都提供了块级作用域。

块级作用域

为什么需要块级作用域
  • 内层变量会覆盖外层变量

    var a = 'glh';
    function fn() {
      console.log(a); //undefined
      if (false) {
        var a = 'hello';
      }
    }
    fn();
    

这是因为 fn 函数体内以及有 var 声明的变量 a,只是还没有赋值,默认为 undefined。

  • 用来计数的循环变量泄露为全局变量

    for (var i = 0; i < 10; i++) {}
    // 10
    console.log(i);
    

let const

1.let

基本用法

ECMAScript2015 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

let 主要声明块级作用域下的成员,这个成员变量的作用域范围只能在当前块级作用域以及子作用域链中。

2.const

const 声明变量的同时必须要赋值。

const 声明之后,不允许去修改它的值,这里面的值说的是不允许修改它,是声明之后不允许重新指向一个新

的内存地址,可以去修改内存地址中的属性成员。

数组

1.数组解构

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

  • 完全解构 将数组中的每一个值都对应上相应的变量。

    var arr = ['a', 'b', 'c'];
    let [com, ind, work] = arr;
    console.log(work);  // c
    
  • 不完全解构 数组中的部分值对应上了相应的变量。

    var arr = ['a', 'b', 'c'];
    let [, , work] = arr;
    console.log(work);  // c
    

    注意:模式没有匹配上的可以不填,但是必须要加逗号隔开。

  • 解构不成功

    右边的变量的个数超过了等号左边中数组的元素

    let [a, b, c] = [12];
    console.log(b); //undefined
    

    如果解构没有成功,则变量的值是 undefined,如果是展开运算的变量则是空数组。

    let [a, b, ...c] = [12];
    console.log(c); // []
    

2.数组的扩展

  • 展开运算符说明

    • 三个点(…)是一个展开运算符,其功能为对三个点后面的变量进行展开操作

    • 三个点(…)展开运算符:只能对具有 Iterator 接口的对象进行展开操作

  • 替代 apply()的使用技巧

    我们之前在求一个数组中的最大值得时候采用得方式是 Math.max.apply(null,[12,34,56,43]) ==56

    var max = Math.max.apply(null, [12, 34, 56, 43]);
    console.log(max); //56
    var max2 = Math.max(...[12, 34, 56, 43]);
    console.log(max2); //56
    
  • Array 类的扩展方法

    • Array.from()

    Array.from 方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)

    var arraylike = {
      0: 'a',
      1: 'b',
      2: 'c',
      length: 3,
    };
    var arr = Array.from(arraylike);
    ['a', 'b', 'c'];
    

    Array.from 还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组

    var arr = Array.from([1, 2, 3], function (x) {
      return x * x;
    });
    console.log(arr); //[1,4,9]
    
    • Array.of 方法用于将一组值,转换为数组,这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

      var arr = Array(3); //[emptyx3]
      

      这里面 3 表示数组中元素的长度。

      var arr = Array(2, 3, 4); //[2,3,4]
      

      Array()里的参数个数大于 1 的时候,表示的是数组元素。

      Array.of()方法不管里面参数的个数多少,都将其转为数组的元素。

      var arr = Array.of(3);
      console.log(arr); //[3]
      

对象

1.对象解构赋值

解构不仅可以用于数组,还可以用于对象。对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

let { name, work } = { name: 'glh', work: 'code' };
console.log(name, work); // glh code

2.对象的扩展

  • 对象的简写

    • 当变量名和属性名同名式,省略同名的属性值

      const foo = 'bar';
      const baz = { foo };
      // 等同于
      const baz = { foo: foo };
      
    • 省略方法中的 function

      const obj = {
        method() {
          return 'hello';
        },
      };
      // 等同于
      const obj = {
        method: function () {
          return 'hello';
        },
      };
      
    • 属性的赋值器(setter)和取值器(getter)

      const obj = {
        name: 'glh',
        get com() {
          return this.name;
        },
        set work(value) {
          this.name = this.name + value;
        },
      };
      console.log(obj.com); // glh
      obj.work = 'hello';
      console.log(obj.name); //glhhello
      
    • 属性名表达式

      es5 中定义对象的属性有两种方法,一种是用标识符(本质为字符串)做属性,一种是用表达式做属性

      方法一;
      obj.name = 'a';
      // 方法二
      obj['name'] = 'a';
      
      var obj = {
        name: 'a',
      };
      

      如果使用大括号定义对象,那么在 es5 中只能使用标识符定义属性。

      var obj = {
        name: 'a',
      };
      

      但是 ECMAScript2015 在使用大括号定义对象的时候,允许使用表达式定义属性,把表达式放在方括号中。

      let name = 'a';
      const obj = {
        [name]: 'glh',
      };
      console.log(obj); //{a: "glh"}
      

3.扩展运算符

  • 用于对象的解构

    • 对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

      let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
      console.log(z); //{a: 3, b: 4}
      

      上面代码中,变量z是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(ab),将它们连同值一起拷贝过来。

      注意: 1.解构赋值必须是最后一个参数。2.解构赋值的拷贝是浅拷贝。

  • 用于扩展运算

    • 对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

      let z = { name: 'a', work: 'b' };
      let n = { ...z };
      n; // { name: "a", work: "b" }
      

字符串

1.字符串模板

  • 传统的字符串里不能使用换行符,必须使用转义符\n 替代,字符串模板里可以使用。模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

  • 模板字符串中嵌入变量,需要将变量名写在${}之中。大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性

    var name = 'glh';
    var newName = `欢迎来到${name}`;
    console.log(newName);
    

2.标签模板

  • 模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能。

    console.log`hello`;
    等同于;
    console.log(['hello']);
    

    标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

    注意:如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

const name = 'Tony';
const age = 18;

// const string = `My name is ${name}, Age is ${age}`;
// console.log(string);

// 标签函数
// const str = console.log`My name is ${name}, Age is ${age}`;

// 标签函数作用是对字符串加工 返回一个新值。利用这个特性可以做语言切换(国际化),模板引擎插件
const Tag = (string, ...rest) => {
  console.log(string);
  console.log(rest);
  return string.join();
};
const result = Tag`My name is ${name}, Age is ${age}`;
console.log(result);
  

3.扩展的方法

  • 字符串实例的方法

    • includes()

      返回布尔值,表示是否找到了参数字符串

      const name = 'glh';
      const a = `Error: foo is not defined: ${name}`;
      var b = a.includes('Err');
      console.log(b); // true
      
    • startsWith()

      返回布尔值,表示参数字符串是否在原字符串的头部

      const name = 'glh';
      const a = `Error: foo is not defined: ${name}`;
      const b = a.startsWith('Err');
      console.log(b); / /true
      
    • endsWith()

      返回布尔值,表示参数字符串是否在原字符串的尾部

      const name = 'glh';
      const a = `Error: foo is not defined: ${name}`;
      const b = a.endsWith('glh');
      console.log(b); // true
      

函数

参数默认值

  • ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

    function fn(a, b = 'glh ') {
      console.log(a + b);
    }
    fn('hello'); //hello glh
    
  • 注意:

    • 参数变量是默认声明的,所以不能用letconst再次声明.
    • 使用参数默认值时,函数不能有同名参数
  • 参数默认值的位置

    • 通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

      function f(x = 1, y) {
        return [x, y];
      }
      
      f() // [1, undefined]
      f(2) // [2, undefined]
      f(, 1) // 报错
      

rest 参数

  • ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

    function add(...values) {
      console.log(values);
    }
    add(2, 5, 3); // [2, 5, 3]
    
  • rest 参数和函数中的参数解构有什么区别

    • rest 参数是发生在函数的定义阶段,函数的额参数解构是发生在函数的调用阶段
    • 二者互为逆运算
function add(...values) {
  //这是rest参数
  console.log(values);
}
add(2, 5, 3); // [2, 5, 3]

var arr = [1, 2, 3];
function fn(a, b, c) {
  console.log(a + b + c);
}
fn(...arr); //6  这是参数的解构

箭头函数

  • 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 getItem = id => { id: id, name: "Temp" }; //报错

let getItem = id => ({ id: id, name: "Temp" });//不报错
  • 箭头函数有几个使用注意点
    • 函数体内的this对象,就是定义时所在的对象,而不是调用时所在的对象。
    • 不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。
    • 不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。
var name = "web"
var obj={
    name: "a",
    fn(){
        var t = setTimeout(function() {
            console.log(this.name)// web  this是window(浏览器环境)
        },1000)
    }
}
obj.fn()
-----------------------------------
var name = "web"
var obj={
    name: "a",
    fn(){
        var t = setTimeout(() => {
            console.log(this.name)// a this是obj
        },1000)
    }
}
obj.fn()

Object

Object.assign()

  • Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

    const target = {
      a: 123,
      b: 123,
    };
    const sourcel = {
      a: 456,
      c: 456,
    };
    const result = Object.assign(target, sourcel);
    console.log(target); //{a: 456, b: 123, c: 456}
    console.log(target === result); //true
    

    如果目标对象与源对象有同名属性,则后面的属性会覆盖前面的属性。且assign()的返回值就是第一个对象。

    如果有多个源对象有同名属性,依然是后面的会覆盖前面的属性

    const target = { a: 1, b: 1 };
    const source1 = { b: 2, c: 2 };
    const source2 = { c: 3 };
    Object.assign(target, source1, source2);
    target; // {a:1, b:2, c:3}
    
  • 利用Object.assign()复制一个对象,且其中一个对象的修改不会影响到另一个对象

    const source = {
      a: 123,
    };
    var obj = Object.assign({}, source);
    obj.a = 456;
    console.log(obj); //{a: 456}
    console.log(source); //{a: 123}
    

Object.is()

  • Object.is就是用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

    ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

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

Proxy

概述

Proxy 可以理解成一个快递员,我们发快递还是接受快递,都需要这个快递员充当一个代理的作用。ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例,这个实例就是一个代理对象(快递员)。

 var proxy = new Proxy(target, handler);

目标对象

这个代理对象有两个参数,一个是代理的目标对象,第二个也是一个对象,它是配置对象,用来定制代理的拦截行为。

const person = {
  name: 'glh',
  age: 20,
};
const personProxy = new Proxy(person, {
  get(target, property) {
    console.log(target, property); //person{name:"glh",age:20}
    return 100;
  },
  set() {},
});

配置对象

  • 配置对象中一般有两个方法getset,get是用来拦截对目标对象属性的访问请求。get方法中有两个参数,第一个参数是目标对象,第二个参数是访问的那个属性。

    const person = {
      name: 'glh',
      age: 20,
    };
    const personProxy = new Proxy(person, {
      get(target, property) {
        console.log(target, property);
        return 100;
      },
      set() {},
    });
    
    console.log(personProxy.name); //100
    

注意,这个get方法的返回值就是我们获取的这个属性的返回值。

  • 这个get方法中有三个参数,一个是代理的目标对象,一个是代理的处理对象,第三个参数是 proxy 实例本身,且第三个参数是可选参数。

    const person = {
      name: 'glh',
      age: 20,
    };
    const personProxy = new Proxy(person, {
      get(target, property, o) {
        console.log(o); //proxy {name:"glh",age:20}
        return property in target ? target[property] : undefined;
      },
      set() {},
    });
    
    console.log(personProxy.age); //20
    
  • 这个set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选

    const person = {
      name: 'glh',
      age: 20,
    };
    const personProxy = new Proxy(person, {
      get(target, property, o) {
        return property in target ? target[property] : undefined;
      },
      set(obj, pro, value, o) {
        console.log(obj, pro, value, o);
      },
    });
    
    console.log((personProxy.name = 'x'));
    //{name: "glh", age: 20} "name" "x" Proxy {name: "zce", age: 20}
    

    可以去设置一些属性或修改

    const person = {
      name: 'glh',
      age: 20,
    };
    const personProxy = new Proxy(person, {
      get(target, property, o) {
        return property in target ? target[property] : undefined;
      },
      set(target, pro, value, o) {
        //可以做一些内部校验
        target[pro] = value;
      },
    });
    console.log((personProxy.name = 'hello'));
    person; // {name:"glh",age:20}
    

Reflect

概述

Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

  • Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法

  • 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

    // 老写法
    try {
      Object.defineProperty(target, property, attributes);
      // success
    } catch (e) {
      // failure
    }
    
    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
      // success
    } else {
      // failure
    }
    
  • Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

    // 老写法
    'assign' in Object; // true
    
    // 新写法
    Reflect.has(Object, 'assign'); // true
    

静态方法

  • Reflect.get

    • Reflect.get(target, name, receiver),Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined

      var myObject = {
        foo: 1,
        bar: 2,
        get baz() {
          return this.foo + this.bar;
        },
      };
      
      Reflect.get(myObject, 'foo'); // 1
      Reflect.get(myObject, 'bar'); // 2
      Reflect.get(myObject, 'baz'); // 3
      
    • 如果name属性部署了读取函数(getter),则读取函数的this绑定receiver

      var myObject = {
        foo: 1,
        bar: 2,
        get baz() {
          return this.foo + this.bar;
        },
      };
      
      var myReceiverObject = {
        foo: 4,
        bar: 4,
      };
      
      Reflect.get(myObject, 'baz', myReceiverObject); // 8
      
    • 如果第一个参数不是对象,Reflect.get方法会报错。

      Reflect.get(1, 'foo'); // 报错
      Reflect.get(false, 'foo'); // 报错
      
  • Reflect.set

    • Reflect.set(target, name, value, receiver),Reflect.set方法设置target对象的name属性等于value

      var myObject = {
        foo: 1,
      };
      
      myObject.foo; // 1
      
      Reflect.set(myObject, 'foo', 2);
      myObject.foo; // 2
      
    • 如果name属性设置了赋值函数,则赋值函数的this绑定receiver

      var myObject = {
        foo: 4,
        set bar(value) {
          return (this.foo = value);
        },
      };
      
      var myReceiverObject = {
        foo: 0,
      };
      
      Reflect.set(myObject, 'bar', 1, myReceiverObject);
      myObject.foo; // 4
      myReceiverObject.foo; // 1
      
  • Reflect.has

    • Reflect.has(obj, name),Reflect.has方法对应name in obj里面的in运算符

      var myObject = {
        foo: 1,
      };
      
      // 旧写法
      'foo' in myObject; // true
      
      // 新写法
      Reflect.has(myObject, 'foo'); // true
      

      如果Reflect.has()方法的第一个参数不是对象,会报错。

  • Reflect.deleteProperty

    • Reflect.deleteProperty(obj, name),Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性

      const myObj = { foo: 'bar' };
      
      // 旧写法
      delete myObj.foo;
      
      // 新写法
      Reflect.deleteProperty(myObj, 'foo');
      

      该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false。如果Reflect.deleteProperty()方法的第一个参数不是对象,会报错.

Promise

概述

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

promise 特点

Promise对象有以下两个特点。

(1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

(2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

promise 使用方法

  • ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

  • Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

    var p = new Promise(function (resolve, reject) {
      if (true) {
        resolve(data);
      } else {
        reject(data);
      }
    });
    

    resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

  • Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

    p.then(
      function (value) {
        // success业务处理
      },
      function (error) {
        // failure
      }
    );
    

    then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

    • 看一个简单的例子

      function time(ms) {
        return new Promise((resolve, reject) => {
          setTimeout(resolve, ms);
        });
      }
      time(1000).then(value => {
        console.log(value);
      });
      
  • Promise 新建后就会立即执行。

    let promise = new Promise(function (resolve, reject) {
      console.log('Promise');
      resolve();
    });
    
    promise.then(function () {
      console.log('resolved.');
    });
    
    console.log('Hi!');
    //Promise
    //Hi
    //resolved
    

class

概述

ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

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

  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}

基本介绍

  • constructor()

    • constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。
  • 类的实例

    • 生成类的实例的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。

      class Point {
        // ...
      }
      // 报错
      var point = Point(2, 3);
      // 正确
      var point = new Point(2, 3);
      
    • 与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

      class Point {
        constructor(x, y) {
          this.x = x;
          this.y = y;
        }
      
        toString() {
          return '(' + this.x + ', ' + this.y + ')';
        }
      }
      
      var point = new Point(2, 3);
      
      point.toString(); // (2, 3)
      
      point.hasOwnProperty('x'); // true
      point.hasOwnProperty('y'); // true
      point.hasOwnProperty('toString'); // false
      point.__proto__.hasOwnProperty('toString'); // true
      
    • 与 ES5 一样,类的所有实例共享一个原型对象

      var p1 = new Point(2, 3);
      var p2 = new Point(3, 2);
      
      p1.__proto__ === p2.__proto__;
      //true
      
  • getter 和 setter

    • 与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

      class MyClass {
        constructor() {
          // ...
        }
        get prop() {
          return 'getter';
        }
        set prop(value) {
          console.log('setter: ' + value);
        }
      }
      
      let inst = new MyClass();
      
      inst.prop = 123;
      // setter: 123
      
      inst.prop;
      // 'getter'
      
  • 属性表达式

    • 类的属性名,可以采用表达式

      let methodName = 'getArea';
      
      class Square {
        constructor(length) {
          // ...
        }
      
        [methodName]() {
          // ...
        }
      }
      

      上面代码中,Square类的方法名getArea,是从表达式得到的

static

  • 类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    Foo.classMethod(); // 'hello'
    
    var foo = new Foo();
    foo.classMethod();
    // TypeError: foo.classMethod is not a function
    

    解说:上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

  • 注意,如果静态方法包含this关键字,这个this指的是类,而不是实例

    class Foo {
      static bar() {
        this.baz();
      }
      static baz() {
        console.log('hello');
      }
      baz() {
        console.log('world');
      }
    }
    
    Foo.bar(); // hello
    

    解说:上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

  • 父类的静态方法,可以被子类继承

    class Foo {
      static classMethod() {
        return 'hello';
      }
    }
    
    class Bar extends Foo {}
    
    Bar.classMethod(); // 'hello'
    
  • 静态属性

    静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性

    ES6 明确规定,Class 内部只有静态方法,没有静态属性。现在有一个提案提供了类的静态属性,写法是在实例属性的前面,加上static关键字。

    class MyClass {
      static myStaticProp = 42;
    
      constructor() {
        console.log(MyClass.myStaticProp); // 42
      }
    }
    

继承

  • 简介

    • Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

      class Point {}
      
      class ColorPoint extends Point {}
      

      解说:上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point

    • 子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

      class Point {
        /* ... */
      }
      
      class ColorPoint extends Point {
        constructor() {}
      }
      
      let cp = new ColorPoint(); // ReferenceError
      
    • 在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

      class Point {
        constructor(x, y) {
          this.x = x;
          this.y = y;
        }
      }
      
      class ColorPoint extends Point {
        constructor(x, y, color) {
          this.color = color; // ReferenceError
          super(x, y);
          this.color = color; // 正确
        }
      }
      
  • super

    • super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同

      • 第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

        class A {}
        
        class B extends A {
          constructor() {
            super();
          }
        }
        

        解说:上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B的实例,因此super()在这里相当于A.prototype.constructor.call(this)`

      • 第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类

        class A {
          p() {
            return 2;
          }
        }
        
        class B extends A {
          constructor() {
            super();
            console.log(super.p()); // 2
          }
        }
        
        let b = new B();
        
      • 由于super指向父类的原型对象(prototype),所以定义在父类实例上的方法或属性,是无法通过super调用的

        class A {
          constructor() {
            this.p = 2;
          }
        }
        
        class B extends A {
          get m() {
            return super.p;
          }
        }
        
        let b = new B();
        b.m; // undefined
        

Set

基本用法

  • ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

  • Set本身是一个构造函数,用来生成 Set 数据结构。

  • Set函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

    const set = new Set([1, 2, 3, 4, 4]);
    console.log(set); //[1,2,3,4]
    
    • 数组去重

      [...new Set([1, 2, 3, 2, 4, 5])]; //[1,2,3,4,5]
      
    • 字符串去重

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

属性和方法

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

    • Set.prototype.constructor:构造函数,默认就是Set函数。
    • Set.prototype.size:返回Set实例的成员总数。
  • Set 实例的方法分为两大类

    • 操作方法(用于操作数据)

      • Set.prototype.add(value):添加某个值,返回 Set 结构本身

        const items = new Set([]);
        items.add(1).add(2).add(3);
        console.dir(items); //[1,2,3]
        
      • Set.prototype.delete(value):删除某个值,返回一个布尔值,表示删除是否成功

        const items = new Set([12, 23, 34]);
        var b = items.delete(12);
        console.log(b); //true
        console.log(items); //Set(2) {23, 34}
        
      • Set.prototype.has(value):返回一个布尔值,表示该值是否为Set的成员。

      • Set.prototype.clear():清除所有成员,没有返回值

        const items = new Set([12, 23, 34]);
        var b = items.clear(12);
        console.log(b); //undefined
        console.log(items); //Set(0) {}
        
    • 遍历方法(用于遍历成员)

      • Set.prototype.keys():返回键名的遍历器

      • Set.prototype.values():返回键值的遍历器

      • Set.prototype.entries()

        let set = new Set(['red', 'green', 'blue']);
        for (let item of set.keys()) {
          console.log(item);
        }
        // red
        // green
        // blue
        for (let item of set.values()) {
          console.log(item);
        }
        // red
        // green
        // blue
        for (let item of set.entries()) {
          console.log(item);
        }
        // ["red", "red"]
        // ["green", "green"]
        // ["blue", "blue"]
        
      • Set.prototype.forEach()

        let set = new Set([1, 4, 9]);
        set.forEach((value, key) => console.log(key + ' : ' + value));
        // 1 : 1
        // 4 : 4
        // 9 : 9
        

Map

概述

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

基本用法

  • 作为构造函数,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"
    

属性和方法

  • size 属性,size属性返回 Map 结构的成员总数。

    const map = new Map();
    map.set('foo', true);
    map.set('bar', false);
    
    map.size; // 2
    
  • Map.prototype.set(key, value) set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键, set()方法返回的是 set 对象可以采用链式写法

    const m = new Map();
    
    m.set('edition', 6); // 键是字符串
    m.set(262, 'standard'); // 键是数值
    m.set(undefined, 'nah'); // 键是 undefined
    
  • 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!
    
  • 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
    
  • Map.prototype.delete(key)delete方法删除某个键,返回true。如果删除失败,返回false

    const m = new Map();
    m.set(undefined, 'nah');
    m.has(undefined); // true
    
    m.delete(undefined);
    m.has(undefined); // false
    
  • Map.prototype.clear()clear方法清除所有成员,没有返回值

    let map = new Map();
    map.set('foo', true);
    map.set('bar', false);
    
    map.size; // 2
    map.clear();
    map.size; // 0
    

遍历

  • Map.prototype.keys():返回键名的遍历器。

  • Map.prototype.values():返回键值的遍历器

  • Map.prototype.entries():返回所有成员的遍历器

  • Map.prototype.forEach():遍历 Map 的所有成员

    const map = new Map([
      ['F', 'no'],
      ['T', 'yes'],
    ]);
    
    for (let key of map.keys()) {
      console.log(key);
    }
    // "F"
    // "T"
    
    for (let value of map.values()) {
      console.log(value);
    }
    // "no"
    // "yes"
    
    for (let item of map.entries()) {
      console.log(item[0], item[1]);
    }
    // "F" "no"
    // "T" "yes"
    
    // 或者
    for (let [key, value] of map.entries()) {
      console.log(key, value);
    }
    // "F" "no"
    // "T" "yes"
    map.forEach(function (value, key, map) {
      console.log('Key: %s, Value: %s', key, value);
    });
    

    forEach方法还可以接受第二个参数,用来绑定this

Symbol

概述

ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

var obj = {
  say: 'a',
};
var say = Symbol(); //say 是symbol类型
obj[say] = 'web';
console.log(obj); //{say: "a", Symbol(): "web"}

语法

  • Symbol函数前不能使用new命令,否则会报错

  • Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分

  • 每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性

    var a = Symbol();
    var b = Symbol();
    console.log(a === b); //false
    var a = Symbol('a');
    var b = Symbol('b');
    console.log(a === b); //false
    

    注意:Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的

    var a = Symbol('a');
    var b = Symbol('a');
    console.log(a === b); //false
    
  • Symbol 值不能与其他类型的值进行运算,会报错

    let sym = Symbol('My symbol');
    
    'your symbol is ' +
      sym // TypeError: can't convert symbol to string
      `your symbol is ${sym}`;
    // TypeError: can't convert symbol to string
    
  • Symbol 值作为对象属性名时,不能用点运算符

    const mySymbol = Symbol();
    const a = {};
    a.mySymbol = 'Hello!';
    console.log(a[mySymbol]); //undefined
    console.log(a['mySymbol']); //hello
    
  • Symbol 作为属性名,遍历对象的时候,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols()方法,可以获取指定对象的所有 Symbol 属性名。该方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

    const obj = {};
    let a = Symbol('a');
    let b = Symbol('b');
    obj[a] = 'Hello';
    obj[b] = 'World';
    const objectSymbols = Object.getOwnPropertySymbols(obj);
    console.log(objectSymbols); //[Symbol(a), Symbol(b)]
    
  • 有时,我们希望重新使用同一个 Symbol 值,Symbol.for()方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建一个以该字符串为名称的 Symbol 值,并将其注册到全局。

    let s1 = Symbol.for('foo');
    let s2 = Symbol.for('foo');
    
    s1 === s2; // true
    

    Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值

    Symbol.for('bar') === Symbol.for('bar');
    // true
    
    Symbol('bar') === Symbol('bar');
    

    由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。Symbol.for()为 Symbol 值登记的名字,是全局环境的,不管有没有在全局环境运行

    function foo() {
      return Symbol.for('bar');
    }
    
    const x = foo();
    const y = Symbol.for('bar');
    console.log(x === y); // true
    

可迭代接口

Iterater 的概念

  • 简单介绍

    JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

    遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

    Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

  • Iterator 的遍历过程

    • 创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

    • 第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员

    • 第二次调用指针对象的next方法,指针就指向数据结构的第二个成员

    • 不断调用指针对象的next方法,直到它指向数据结构的结束位置

      每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

      简单的Iterator遍历器的实现;
      var it = easyIterator(['a', 'b']);
      
      it.next(); // { value: "a", done: false }
      it.next(); // { value: "b", done: false }
      it.next(); // { value: undefined, done: true }
      
      function easyIterator(array) {
        var nextIndex = 0;
        return {
          next: function () {
            return nextIndex < array.length
              ? { value: array[nextIndex++], done: false }
              : { value: undefined, done: true };
          },
        };
      }
      

Iterater 接口

  • 字符串 数组 set map arguments 都有 iterater 接口,nodelist 集合,都可以用 for of 遍历

    var st = 'glh';
    for (i of st) {
      console.log(i); // g l h
    }
    var arr = [1, 2];
    for (v of arr) {
      console.log(v); //1 2
    }
    function fn(a, b, c) {
      for (i of arguments) {
        console.log(i); //1 2 3
      }
    }
    fn(1, 2, 3);
    

Modules

概述

  • JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

语法

  • export

    • export命令用于规定模块的对外接口

    • 一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量

      //demo.js
      export var firstName = 'Michael';
      export var lastName = 'Jackson';
      export var year = 1958;
      //或者
      var firstName = 'Michael';
      var lastName = 'Jackson';
      var year = 1958;
      
      export { firstName, lastName, year };
      
  • import

    • import命令用于输入其他模块提供的功能

    • import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同

      // main.js
      import { firstName, lastName, year } from './profile.js';
      
      function setName(element) {
        element.textContent = firstName + ' ' + lastName;
      }
      
  • export default

    • 为了给用户提供方便,就要用到export default命令,为模块指定默认输出

    • 本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字

    • import命令后面,不再使用大括号

    • export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次

      // export-default.js
      export default function () {
        console.log('foo');
      }
      
      // import-default.js
      import customName from './export-default';
      customName(); // 'foo'
      

浏览器端加载实现

  • 浏览器加载 ES6 模块,也使用标签,但是要加入type="module"属性

    // 01.js
    export var a = 123;
    
    //demo.html
    
    
  • 脚本异步加载

    
    
    

    解说:上面代码中,标签打开deferasync属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。defer 与 async 的区别是:defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer 是“渲染完再执行”,async 是“下载完就执行”。另外,如果有多个 defer 脚本,会按照它们在页面出现的顺序加载,而多个 async 脚本是不能保证加载顺序的。

你可能感兴趣的:(浅谈ECMAScript新特性)