ES6 新特性梳理系列丨Class

ES6 新特性梳理系列文章将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!


JavaScript 与 ECMAScript

JavaScript 诞生于1995年,设计者是就职于 Netscape 公司的工程师 Brendan Eich。它是一门仅用10天就完成设计的编程语言,但至今为止已对业界保持26年的影响,且势头愈发强劲

1996年 Netscape 将 JavaScript 提交给ECMA,希望它可以成为“标准化一个通用的、跨平台的、中立于厂商的脚本语言的语法和语义标准”,1997年 ECMA 确定将 JavaScript 作为浏览器脚本语言的标准,并为之重命名为 ECMAScript,所以通常来讲我们将 ECMAScript 视为 JavaScript 的标准,而 JavaScript 则是 ECMAScript 的实现及扩展

1997年-1999年连续发布了ES1-ES3发布,2000年开始酝酿新版本的升级内容,中间因标准委员会意见未能达成一致,只做了部分功能的小范围升级及支持,于2009年12月发布了过渡版 ECMAScript 5.0,2011年6月发布 ECMAScript 5.1 并成为 ISO 国际标准

2013年3月 ECMAScript 6 草案冻结,2013年12月 ECMAScript 草案发布,2015年6月 ECMAScript 6 正式通过,成为新的国际标准。ES6 泛指自2015年升级为 ECMAScript 6.0 后的所有子版本,如:ES2015-ES2020,等同于ES6.0-ES6.5,这是 JavaScript 走向企业级编程语言的强势升级

不断升级的标准与实现,对于开发效率及产品质量起到强有力的支撑,接下来我们开始梳理ES6的新特性吧!


ES6 之前通过原型来模拟类

在 ES6 之前,我们一般通过 prototype 原型来模拟创建一个类:

// 定义一个函数
function A(x, y) {
  this.x = x;
  this.y = y;
}


// 在 Fn 的原型链上添加一个 toString 方法
A.prototype.toString = function () {
  return '(' + this.x + ', ' + this.y + ')';
};


// 创建一个 A 实例
var a = new A (1, 2);
// 调用实例上的方法
  console.log(a.toString()) // (1, 2)

通过class定义类

//定义类
class A{
  constructor(x, y) {
    this.x = x;
    this.y = y;
  }


  toString() {
    return '(' + this.x + ', ' + this.y + ')';
  }
}
// 创建一个 A 实例
var a = new A(1, 2);
// 调用实例上的方法
console.log(a.toString()) // (1, 2)

constructor 是类中的构造方法,this 关键字代表实例对象。通过 new 关键字创建一个类的实例时,constructor 方法自动调用,然后返回类的实例对象。

一个类里面必须有 constructor 方法,自己没有定义的话,ES6 会默认添加一个方法体为空的 constructor 方法。


类的继承

class B extends A{
  constructor(x,y,color){
    super(x,y)
    this.color = color
  }
  toString() {
    // 调用父类的 toString()
    return this.color + ' ' + super.toString(); 
  }
}

上面代码表示 B 类继承了 A 类所有属性和方法,通过 super 可以调用 A 中的构造函数和方法。

class A{ /* ... */ }


class B extends A{
  constructor() {
  }
}


let b = new B(); // ReferenceError

子类必须在 constructor 方法中调用 super 方法,否则新建实例时会报错。这是因为子类没有自己的 this 对象,而是继承父类的 this 对象,然后对其进行加工。如果不调用 super 方法,子类就得不到 this 对象。

如果子类没有定义 constructor 方法,这个方法会被默认添加,并且在方法内部通过 super 继承父类的构造函数。也就是说,不管有没有显式定义,任何一个子类都有 constructor 方法。

另外,super 调用的顺序必须在 constructor 方法调用 this 语句的上面,这是因为子类实例是基于父类实例的,只有 super 方法执行完以后,才能得到父类实例。


this指向问题


类的方法内部的 this 默认指向类的实例,在类中调用没有问题,但是一旦在外面调用,就会导致 this 指向错误,调用失败。

class Test{
  fn1(name = '张三') {
    this.fn2(`Hello ${name}`);
  }
  fn2(text) {
    console.log(text);
  }
}


const test = new Test();
const { fn1 } = test;
fn1(); // TypeError: Cannot read property 'fn2' of undefined

上面是因为 ES6 的 class 默认开启严格模式,fn1 函数里的 this,会根据是否是严格模式,而指向 undefined 或者 window 对象。所以这里的 this 指向的是 undefined,而 undefined 没有属性 fn2, 导致找不到该属性而抛出TypeError 错误。

解决上面的方法有以下三种:

(1)通过 constructor 方法,给 test 添加一个属性,属性名为 fn1,属性值为fn1 函数,并且通过 bind() 方法,将 this 指向绑定到实例对象上。

class Test{
  constructor() {
    this.fn1 = this.fn1.bind(this);
  }
  fn1(name = '张三') {
    this.fn2(`Hello ${name}`);
  }
  fn2(text) {
    console.log(text);
  }
}


const test = new Test();
const { fn1 } = test;
fn1(); // hello 张三

(2)通过箭头函数,实际就是上面 bind 方法的简写形式,本质也是改变 fn2 的 this 指向,让 this 指向实例自身

class Test{
  constructor() {
    this.fn1 = (name = '张三')=>{
      this.fn2(`Hello ${name}`)
    }
  }
  fn2(text) {
    console.log(text);
  }
}


const test = new Test();
const { fn1 } = test;
fn1(); // hello 张三

(3) 通过 Proxy,当使用方法的时候,改变 this 的指向。

class Test{
  fn1(name = '张三') {
    this.fn2(`Hello ${name}`);
  }
  fn2(text) {
    console.log(text);
  }
}
 function changeFnThisProxy(target) {
   const m = WeakMap()
   const handler  = {
      get(target, key) {
        // 拿到用户要读取的值
        const val = Reflect.get(target, key)
        // 要获取的不是函数就直接返回 val
        if (typeof val !== 'function') return val
        // 要获取的是函数,就改变 this 指向
        if (!m.has(val)) {
          // 使用 bind() 改变运行函数的 this 为拦截的实例对象
          m.set(val, val.bind(target))
        }
        return m.get(val)
      }
    }
    const proxy = new Proxy(target, handler)
    return proxy
 }
const test = new Test();
const { fn1 } = changeFnThisProxy(test);
fn1(); // hello 张三

super 关键字

super 关键字既可以当做函数调用,也可以当做对象使用。

可以在子类中通过调用 super 方法,来继承父类的构造函数。并且只能在子类的 constructor 方法内部使用,在其他地方使用会报错。

class A {}


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

当 super 作为对象使用时,指向父类的原型对象。

class A {
  fn(){
    console.log('123')
  }
}


class B extends A {
  fn2(){
    super.fn()
  }
}
let b = new B()
b.fn2()  // '123'

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

class A {
  constructor() {
    this.a = 123;
  }
}


class B extends A {
  fn2(){
    console.log(super.a)
  }
}


let b = new B();
b.fn2() // undefined

静态方法

在一个方法前面加上 static 关键字,那么这个方法就不会被实例继承,只能通过类来调用。

class A{
  static print() {
    return 'hello';
  }
}
A.print() // 'hello'


var a= new A();
a.print()
// TypeError: a.print is not a function

静态方法也可以被子类继承,也可以被 super 关键字调用。

class A{
  static print() {
    return 'hello';
  }
}
class B extends A{
  static print2(){
    let a = super.print()
    console.log(a + '123')
  }
}
console.log(B.print())  // 'hello'
B.print2() // 'hello123'

静态属性和实例属性

类自身的属性叫做静态属性,而不是指实例上面的属性。

在 ES6 中定义实例属性:

class A {
  constructor(props) {
    this.x = 1   // x 就是 A 类的实例属性
  }
}

在 ES6 中定义静态属性:

class A{}
A.prop = 1
console.log(A.prop) // 1

在 ES7 中定义实例属性:

class A{
  x = 1;   // x 是 A 类的实例属性,可通过 A 的实例 new A() 调用x
  constructor() {
    console.log(this.x); // 1
  }
}

在 ES7 中定义静态属性:

class A{
  // 在实例属性前面加上 static关键字,就变成A类的静态属性了
  static x = 1; 
  constructor() {
    console.log(A.x); // 1
  }
}

ES6 新特性梳理系列文章将在本号持续发布,一起查漏补缺学个痛快!若您有遇到其它相关问题,非常欢迎在评论中留言讨论,达到帮助更多人的目的。若感本文对您有所帮助请点个赞吧!

叶阳辉

HFun 前端攻城狮

往期精彩:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值