ES6(简介及常用)-上

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ES6(简介及常用)-上相关的知识,希望对你有一定的参考价值。

、类的支持

1、简介

ES6中添加了对类的支持,引入了class关键字。JS本身就是面向对象的,ES6中提供的类实际上只是JS原型模式的包装。现在提供原生的class支持后,对象的创建,继承更加直观了,并且父类方法的调用,实例化,静态方法和构造函数等概念都更加形象化。javascript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

function Point(x, y) {

  this.x = x;

  this.y = y;

}

Point.prototype.toString = function () {

  return ‘(‘ + this.x + ‘, ‘ + this.y + ‘)‘;

};

var p = new Point(1, 2);

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方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。ES6 的类,完全可以看作构造函数的另一种写法。使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

class Bar {

  doStuff() {

    console.log(‘stuff‘);

  }

var b = new Bar();

b.doStuff() // "stuff"

prototype 属性使您有能力向对象添加属性和方法。(object.prototype.name=value)构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

class Point {

  constructor() {

    // ...

  }

  toString() {

    // ...

  }

  toValue() {

    // ...

  }

}

// 等同于

Point.prototype = {

  constructor() {},

  toString() {},

  toValue() {},

};

由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

class Point {

  constructor(){

    // ...

  }

}

Object.assign(Point.prototype, {

  toString(){},

  toValue(){}

});

prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的。

Point.prototype.constructor === Point // true

2、类的实例对象

生成类的实例对象的写法,与 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

上面代码中,x和y都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与 ES5 的行为保持一致。与 ES5 一样,类的所有实例共享一个原型对象。

var p1 = new Point(2,3);

var p2 = new Point(3,2);

p1.__proto__ === p2.__proto__

//true

上面代码中,p1和p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。

3、Class 表达式

与函数一样,类也可以使用表达式的形式定义。

const MyClass = class Me {

  getClassName() {

    return Me.name;

  }

};

上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是Me,Me只在 Class 的内部代码可用,指代当前类。

let inst = new MyClass();

inst.getClassName() // Me

Me.name // ReferenceError: Me is not defined

上面代码表示,Me只在 Class 内部有定义。如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

const MyClass = class { /* ... */ };

采用 Class 表达式,可以写出立即执行的 Class。

let person = new class {

  constructor(name) {

    this.name = name;

  }

 

  sayName() {

    console.log(this.name);

  }

}(‘张三‘);

 

person.sayName(); // "张三"

4、不存在变量提升

类不存在变量提升(hoist),这一点与 ES5 完全不同。

new Foo(); // ReferenceError

class Foo {}

上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

{

  let Foo = class {};

  class Bar extends Foo {

  }

}

上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

5、私有方法

私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。

一种做法是在命名上加以区别。

class Widget {

 

  // 公有方法

  foo (baz) {

    this._bar(baz);

  }

 

  // 私有方法

  _bar(baz) {

    return this.snaf = baz;

  }

 

  // ...

}

上面代码中,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。

class Widget {

  foo (baz) {

    bar.call(this, baz);

  }

 

  // ...

}

 

function bar(baz) {

  return this.snaf = baz;

}

上面代码中,foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。

还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

const bar = Symbol(‘bar‘);

const snaf = Symbol(‘snaf‘);

 

export default class myClass{

 

  // 公有方法

  foo(baz) {

    this[bar](baz);

  }

 

  // 私有方法

  [bar](baz) {

    return this[snaf] = baz;

  }

 

  // ...

};

上面代码中,bar和snaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。

6、私有属性

与私有方法一样,ES6 不支持私有属性。目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。

class Point {

  #x;

 

  constructor(x = 0) {

    #x = +x; // 写成 this.#x 亦可

  }

 

  get x() { return #x }

  set x(value) { #x = +value }

}

上面代码中,#x就表示私有属性x,在Point类之外是读取不到这个属性的。还可以看到,私有属性与实例的属性是可以同名的(比如,#x与get x())。

私有属性可以指定初始值,在构造函数执行时进行初始化。

class Point {

  #x = 0;

  constructor() {

    #x; // 0

  }

}

之所以要引入一个新的前缀#表示私有属性,而没有采用private关键字,是因为 JavaScript 是一门动态语言,使用独立的符号似乎是唯一的可靠方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用@表示私有属性,ES6 没有用这个符号而使用#,是因为@已经被留给了 Decorator。

还有其他属性:

Class 的取值函数(getter)和存值函数(setter

Class Generator 方法

Class 的静态方法

 

2、简介(   class的继承  

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

class Point {

}

 

class ColorPoint extends Point {

}

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

class ColorPoint extends Point {

  constructor(x, y, color) {

    super(x, y); // 调用父类的constructor(x, y)

    this.color = color;

  }

 

  toString() {

    return this.color + ‘ ‘ + super.toString(); // 调用父类的toString()

  }

}

上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。

class Point { /* ... */ }

 

class ColorPoint extends Point {

  constructor() {

  }

}

 

let cp = new ColorPoint(); // ReferenceError

上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this。

如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

class ColorPoint extends Point {

}

 

// 等同于

class ColorPoint extends Point {

  constructor(...args) {

    super(...args);

  }

}

另一个需要注意的地方是,在子类的构造函数中,只有调用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; // 正确

  }

}

上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

下面是生成子类实例的代码。

let cp = new ColorPoint(25, 8, ‘green‘);

 

cp instanceof ColorPoint // true

cp instanceof Point // true

上面代码中,实例对象cp同时是ColorPoint和Point两个类的实例,这与 ES5 的行为完全一致。

3Object.getPrototypeOf()

Object.getPrototypeOf方法可以用来从子类上获取父类。

Object.getPrototypeOf(ColorPoint) === Point

// true

因此,可以使用这个方法判断,一个类是否继承了另一个类。

4super 关键字

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

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

class A {}

 

class B extends A {

  constructor() {

    super();

  }

}

上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B。

作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

class A {}

 

class B extends A {

  m() {

    super(); // 报错

  }

}

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

class A {

  p() {

    return 2;

  }

}

 

class B extends A {

  constructor() {

    super();

    console.log(super.p()); // 2

  }

}

 

let b = new B();

上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()。

这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

class A {

  constructor() {

    this.p = 2;

  }

}

 

class B extends A {

  get m() {

    return super.p;

  }

}

 

let b = new B();

b.m // undefined

上面代码中,p是父类A实例的属性,super.p就引用不到它。

如果属性定义在父类的原型对象上,super就可以取到。

class A {}

A.prototype.x = 2;

 

class B extends A {

  constructor() {

    super();

    console.log(super.x) // 2

  }

}

 

let b = new B();

上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

ES6 规定,通过super调用父类的方法时,super会绑定子类的this。

class A {

  constructor() {

    this.x = 1;

  }

  print() {

    console.log(this.x);

  }

}

 

class B extends A {

  constructor() {

    super();

    this.x = 2;

  }

  m() {

    super.print();

  }

}

 

let b = new B();

b.m() // 2

上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()会绑定子类B的this,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)。

还有:extends 的继承目标

实例的 __proto__ 属性(子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。)

原生构造函数的继承(语言内置的构造函数,通常用来生成数据结构。)

Mixin 模式的实现(将多个类的接口“混入”(mix in)另一个类。它在 ES6 的实现如下。)

 

、增强的对象字面量

对象字面量被增强了,写法更加简洁与灵活,同时在定义对象的时候能够做的事情更多了。具体表现在:

  • 可以在对象字面量里面定义原型
  • 定义方法可以不用function关键字
  • 直接调用父类方法

这样一来,对象字面量与前面提到的类概念更加吻合,在编写面向对象的JavaScript时更加轻松方便了。

//通过对象字面量创建对象

var human = {

    breathe() {

        console.log(‘breathing...‘);

    }

};

var worker = {

    __proto__: human, //设置此对象的原型为human,相当于继承human

    company: ‘freelancer‘,

    work() {

        console.log(‘working...‘);

    }

};

human.breathe();//输出 ‘breathing...’

//调用继承来的breathe方法

worker.breathe();//输出 ‘breathing...’

 

、字符串模板

字符串模板相对简单易懂些。ES6中允许使用反引号 ` 来创建字符串,此种方法创建的字符串里面可以包含由美元符号加花括号包裹的变量${vraible}。如果你使用过像C#等后端强类型语言的话,对此功能应该不会陌生。

//产生一个随机数

var num=Math.random();

//将这个数字输出到console

console.log(`your num is ${num}`);

 

、解构

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

以前,为变量赋值,只能直接指定值。

let a = 1;

let b = 2;

let c = 3;

ES6 允许写成下面这样。

let [a, b, c] = [1, 2, 3];

上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

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

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

foo // 1

bar // 2

baz // 3

 

let [ , , third] = ["foo", "bar", "baz"];

third // "baz"

 

let [x, , y] = [1, 2, 3];

x // 1

y // 3

 

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

head // 1

tail // [2, 3, 4]

 

let [x, y, ...z] = [‘a‘];

x // "a"

y // undefined

z // []

如果解构不成功,变量的值就等于undefined。另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

如果等号的右边不是数组(严格地说,不是可遍历的结构),那么将会报错。

// 报错

let [foo] = 1;

let [foo] = false;

let [foo] = NaN;

let [foo] = undefined;

let [foo] = null;

let [foo] = {};

上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

对于 Set 结构,也可以使用数组的解构赋值。

let [x, y, z] = new Set([‘a‘, ‘b‘, ‘c‘]);

x // "a"

事实上,只要某种数据结构具有 Iterator 接口,都可以用数组形式解构赋值。(默认值:解构赋值允许指定默认值。

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

字符串的解构赋值:字符串被转换成了一个类似数组的对象。

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

用途:(1)交换变量的值;(2)从函数返回多个值;(3)函数参数的定义;(4)提取JSON数据;(5)函数参数的默认值;(6)遍历map结构;(7)输入模块的指定方法

 

五、参数默认值,不定参数,拓展参数

默认参数值

现在可以在定义函数的时候指定参数的默认值了,而不用像以前那样通过逻辑或操作符来达到目的了。

function sayHello(name){

        //传统的指定默认参数的方式

        var name=name||‘dude‘;

        console.log(‘Hello ‘+name);

}

//运用ES6的默认参数

function sayHello2(name=‘dude‘){

        console.log(`Hello ${name}`);

}

sayHello();//输出:Hello dude

sayHello(‘Wayou‘);//输出:Hello Wayou

sayHello2();//输出:Hello dude

sayHello2(‘Wayou‘);//输出:Hello Wayou

、不定参数

不定参数是在函数中使用命名参数同时接收不定数量的未命名参数。这只是一种语法糖,在以前的JavaScript代码中我们可以通过arguments变量来达到这一目的。不定参数的格式是三个句点后跟代表所有不定参数的变量名。比如下面这个例子中,…x代表了所有传入add函数的参数。

//将所有参数相加的函数

function add(...x){

        return x.reduce((m,n)=>m+n);

}

//传递任意个数的参数

console.log(add(1,2,3));//输出:6

console.log(add(1,2,3,4,5));//输出:15

、let与const 关键字

1、可以把let看成var,只是它定义的变量被限定在了特定范围内才能使用,而离开这个范围则无效,for循环的计数器,就很合适使用let命令。(例:)

{

  let a = 10;

  var b = 1;

}

 

a // ReferenceError: a is not defined.

b // 1

下面的代码如果使用var,最后输出的是10。

 

var a = [];

for (var i = 0; i < 10; i++) {

  a[i] = function () {

    console.log(i);

  };

}

a[6](); // 10

上面代码中,变量i是var命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是10。

如果使用let,声明的变量仅在块级作用域内有效,最后输出的是6。

var a = [];

for (let i = 0; i < 10; i++) {

  a[i] = function () {

    console.log(i);

  };

}

a[6](); // 6

上面代码中,变量i是let声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

for (let i = 0; i < 3; i++) {

  let i = ‘abc‘;

  console.log(i);

}

// abc

// abc

// abc

上面代码正确运行,输出了3次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。let不允许重复声明不存在变量提升。

2、块级作用域

ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

第一种场景,内层变量可能会覆盖外层变量。

var tmp = new Date();

 

function f() {

  console.log(tmp);

  if (false) {

    var tmp = ‘hello world‘;

  }

}

 

f(); // undefined

上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量升,导致内层的tmp变量覆盖了外层的tmp变量。

第二种场景,用来计数的循环变量泄露为全局变量。

var s = ‘hello‘;

 

for (var i = 0; i < s.length; i++) {

  console.log(s[i]);

}

 

console.log(i); // 5

上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

3、ES6 的块级作用域

let实际上为 JavaScript 新增了块级作用域。

function f1() {

  let n = 5;

  if (true) {

    let n = 10;

  }

  console.log(n); // 5

}

上面的函数有两个代码块,都声明了变量n,运行后输出5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是10。

 

const则很直观,用来定义常量,即无法被更改值的量。const一旦声明变量,就必须立即初始化,不能留到以后赋值。const的作用域与let命令相同:只在声明所在的块级作用域内有效。const命令声明的常量也是不提升。

4、本质

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

 

 

 

以上是关于ES6(简介及常用)-上的主要内容,如果未能解决你的问题,请参考以下文章

ES6常用语法简介

ES6 常用总结——第一章(简介letconst)

ES6常用语法简介import export

es6常用数组操作及技巧汇总

ES6--ClassModule及常用特性

es6 属性及常用新属性汇总