第 7 题:ES5/ES6 的继承除了写法以外还有什么区别?

发布于 2022-10-16 21:46:18 字数 2625 浏览 173 评论 51

来源:Understanding ECMAScript 6

class 声明会提升,但不会初始化赋值。Foo 进入暂时性死区,类似于 letconst 声明变量。

const bar = new Bar(); // it's ok
function Bar() {
  this.bar = 42;
}

const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
  constructor() {
    this.foo = 42;
  }
}

class 声明内部会启用严格模式。

// 引用一个未声明的变量
function Bar() {
  baz = 42; // it's ok
}
const bar = new Bar();

class Foo {
  constructor() {
    fol = 42; // ReferenceError: fol is not defined
  }
}
const foo = new Foo();

class 的所有方法(包括静态方法和实例方法)都是不可枚举的。

// 引用一个未声明的变量
function Bar() {
  this.bar = 42;
}
Bar.answer = function() {
  return 42;
};
Bar.prototype.print = function() {
  console.log(this.bar);
};
const barKeys = Object.keys(Bar); // ['answer']
const barProtoKeys = Object.keys(Bar.prototype); // ['print']

class Foo {
  constructor() {
    this.foo = 42;
  }
  static answer() {
    return 42;
  }
  print() {
    console.log(this.foo);
  }
}
const fooKeys = Object.keys(Foo); // []
const fooProtoKeys = Object.keys(Foo.prototype); // []

class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。

function Bar() {
  this.bar = 42;
}
Bar.prototype.print = function() {
  console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor

必须使用 new 调用 class

function Bar() {
  this.bar = 42;
}
const bar = Bar(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
}
const foo = Foo(); // TypeError: Class constructor Foo cannot be invoked without 'new'

class 内部无法重写类名。

function Bar() {
  Bar = 'Baz'; // it's ok
  this.bar = 42;
}
const bar = new Bar();
// Bar: 'Baz'
// bar: Bar {bar: 42}  

class Foo {
  constructor() {
    this.foo = 42;
    Foo = 'Fol'; // TypeError: Assignment to constant variable
  }
}
const foo = new Foo();
Foo = 'Fol'; // it's ok

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(51

美男兮 2022-05-04 13:57:31

@MingShined 什么是面向类的语言?第一次听说,能否详细讲讲。

与风相奔跑 2022-05-04 13:57:31

@MingShined 什么是面向类的语言?第一次听说,能否详细讲讲。

我的理解是

JS一直以来没有被正确的理解,由于诞生的时间晚,相比于c、java等一类面向类的语言,JS没有真正意义上的类的概念。加上最早开始使用JS的开发者大多数都是其他类语言的转型,他们不够理解JS这种面向对象的模式,所以只能通过一些笨拙的方式去实现所谓的类,从而实现继承和多态,这种模式就是我们常见的prototype。
实际上无论是es5的prototype模拟类还是es6的语法糖class,都不是真正意义上的类。因为在类的实现中,子类是对父类的完全复制,而js不是,换句话讲,如果我们在改变了js一个父类的方法,继承该父类的子类和所有实例都会发生改变。ES6class的实现,本质上还是通过Object.crete()去关联两者的prototype。
JS的正确用法应该是面向对象,行为委托,而不是模拟类。

以下是面向对象的一个demo

    // 定义父对象
    var parent = {
        getName: function(name) {
            this.name = name;
            return this.showName();
        },
        showName: function() {
            return this.name;
        }
    }

    // 定义子对象
    var children = {
        sendName: function(name) {
            this.getName(name)
        }
    }

    // 通过Object.create关联父子对象
    var children = Object.create(parent);

    children.prototype === parent.prototype // true
    children.getName('陈先生'); // 陈先生

以上是我的一些理解,有什么误人之处,希望指出,感激不尽。

羁拥 2022-05-04 13:57:31

刚好今天在看红宝书,顺便放下自己总结的ES5的继承

// 寄生组合式继承
// 通过借用构造函数来继承属性, 通过原型链来继承方法
// 不必为了指定子类型的原型而调用父类型的构造函数,我们只需要父类型的一个副本而已
// 本质上就是使用寄生式继承来继承超类型的原型, 然后再讲结果指定给子类型的原型
function object(o){ // ===Object.create()
  function F(){};
  F.prototype = o;
  return new F();
}
function c1(name) {
  this.name = name;
  this.color = ['red', 'green'];
}
c1.prototype.sayName = function () {
  console.log(this.name);
}
function c2(name, age) {
  c1.call(this, name)
  this.age = age
}
// 第一步:创建父类型原型的一个副本
// 第二步:为创建的副本添加 constructor 属性, 从而弥补因重写原型而失去的默认的 constructor 属性
// 第三步:将新创建的对象(即副本)赋值给子类型的原型
function inheritPrototype(superType, subType) {
  const prototype = object(superType.prototype);
  prototype.constructor = subType;
  subType.prototype = prototype;
}

inheritPrototype(c1, c2);
// c2的方法必须放在寄生继承之后
c2.prototype.sayAge = function () {
  console.log(this.age);
}
久光 2022-05-04 13:57:31

@MingShined 在原型链继承中test.age 输出结果应该是24啊,这里手误吧

蒲公英的约定 2022-05-04 13:57:31

@Jesse121 感谢这位同学指出。已经修改了

涙—继续流 2022-05-04 13:57:31

function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok
这个只是写法上可以这样吧?bar 仍然是undefined

花开浅夏 2022-05-04 13:57:31

@labike
可能是我们对「提升」的理解不同吧?我理解的「提升」和「赋值」是两个过程。
我拆解一下那个例子:

var Foo = function() { /** pass */ };

{
  // 「块作用域」内可以访问全局变量 Foo
  const foo = new Foo();
}
var Foo = function() { /** pass */ };

{
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  // 如果 class 不会提升的话,new Foo() 应该成功调用
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo{ /** pass */ }
}

类似于以下代码(但不等于):

var Foo = function() { /** pass */ };

{
  let Foo; // 区别在于此处 Foo 已经初始化为 undefined
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  const foo = new Foo(); 
  Foo = class { /** pass */}
}

首先感谢作者,写的很好,受益匪浅。

其次建议作者修改第一条,虽然我理解了你想表达啥,不过也是看了你和其他人的讨论才明白你的意思。

我理解function和class的声明和赋值是不可分割的,这和var不一样,所以你例举的例子本身就不成立,硬生的去拆解声明和赋值这两个流程对class和function没有意义,只对var有意义。

参考MDN:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes

深爱不及久伴 2022-05-04 13:57:31

@XueSeason 哈哈哈,审题不清,这轮面试要挂了。
再补充一点:
ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。

function MyES5Array() {
  Array.call(this, arguments);
}

// it's useless
const arrayES5 = new MyES5Array(3); // arrayES5: MyES5Array {}

class MyES6Array extends Array {}

// it's ok
const arrayES6 = new MyES6Array(3); // arrayES6: MyES6Array(3) []

这点区别应该是call方法导致的,使用prototype写法是不会有差别的,同样会继承内置对象

英雄似剑 2022-05-04 13:57:31

@XueSeason 哈哈哈,审题不清,这轮面试要挂了。
再补充一点:
ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。

function MyES5Array() {
  Array.call(this, arguments);
}

// it's useless
const arrayES5 = new MyES5Array(3); // arrayES5: MyES5Array {}

class MyES6Array extends Array {}

// it's ok
const arrayES6 = new MyES6Array(3); // arrayES6: MyES6Array(3) []

这点区别应该是call方法导致的,使用prototype写法是不会有差别的,同样会继承内置对象

确实是call方法导致的,不知道大家为何都说是this生成的顺序不同导致的

握住我的手 2022-05-04 13:57:31

class没有变量提升吧,不然这个怎么解释
{ console.log(Foo) // Uncaught ReferenceError: Cannot access 'Foo' before initialization class Foo { constructor() { this.foo = 37; } } }

{ console.log(Bar) //undefined var Bar = function(){} }

与风相奔跑 2022-05-04 13:57:31

对了贴两张容易记住的图吧

这就是传说中es6入门里那句:子类实例的__proto__属性的__proto__属性指向父类实例的__proto__属性

这个图不对啊, A类实例的__proto__ 指向的是A类的prototype

苦笑流年记忆 2022-05-04 13:57:31

先来复习下两者的写法区别:

原型链继承

function Hero(name){
    this.name = name;
}
Hero.prototype.sayName = function(){
    console.log('my name is',this.name)
}

function smallHero(){
    this.age = 30
}
smallHero.prototype = new Hero('spiderman');
var s1 = new smallHero()
s1.sayName()  //my name is spiderman;
或者写成:
function smallHero_second(){
    Hero.apply(this,arguments) //此处有缺点只能继承构造函数里的不能继承原型里的方法
    this.age = 22; 
}

ES6继承

class smallHero11 extends Hero{
    constructor(props){
        super(props);
        this.age = 30;
    }
}
var s2 = new smallHero11('superman') 
s2.sayName()  ==>my name is superman

继承的主要区别:

原型链继承里的

smallHero.__proto__ === Function.prototype

ES6的class里的

smallHero11.__proto__ === Hero , 子类可以直接通过 proto 寻址到父类

離殇 2022-05-04 13:57:31
Foo

没提升吧 提升了没赋值应该是 typeError 不该是 ReferenceError ,ReferenceError说明就没定义

@alanchanzm 1. class 声明会提升 . 是不是写错了?
原文: Class declarations, unlike function declarations, are not hoisted.

@labike
原文有问题,class 是会提升的,其表现与letconst类似,变量名会进入TDZ。
看下例:如果没有提升,foo 会是块作用域外的Foo实例。但是由于提升的关系,块作用域内的Foo遮蔽了外层的同名函数。

var Foo = function() {
  this.foo = 21;
};

{
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo {
    constructor() {
      this.foo = 37;
    }
  }
}

这个问题不会这么理解的吧 分析变量foo = new Foo()的时候{}这个块级作用域分析的时候内部有Foo因为变量不提升导致的暂时性死区导致Foo ReferenceError,如果提升但是没有值或者是为undefined,像函数那样调用该是报错TypeError

诗化ㄋ丶相逢 2022-05-04 13:57:31

最重要的一点是继承机制完全不同,es5是先创建子类实例对象的this,然后将父类方法赋到这个this上。es6是先在子类构造函数中用super创建父类实例的this,再在构造函数中进行修改它。
也因此,es5中array,error等原生构造函数无法继承而es6就可以自己定义这些原生构造函数。
(es5中子类无法拿到父类的内部属性,就算是apply也不行,es5默认忽略apply传入的this)。
es5/6还有一些区别:
1.es6的类内部定义的所有方法都不可枚举,这在es5中默认是可枚举的,甚至可不可枚举都可以用defineProperty配置;
2.es6内部默认使用严格模式;
3.类内不存在变量提升,这个跟继承有关,必须保证子类在父类之后定义,如果允许变量提升就乱套了;
4.es5的实例属性只能写在构造函数里,es6直接写在类里就行。

学到了学到了,小姐姐真厉害~

浸婚纱 2022-05-04 13:57:31

@MingShined 什么是面向类的语言?第一次听说,能否详细讲讲。

我的理解是

JS一直以来没有被正确的理解,由于诞生的时间晚,相比于c、java等一类面向类的语言,JS没有真正意义上的类的概念。加上最早开始使用JS的开发者大多数都是其他类语言的转型,他们不够理解JS这种面向对象的模式,所以只能通过一些笨拙的方式去实现所谓的类,从而实现继承和多态,这种模式就是我们常见的prototype。
实际上无论是es5的prototype模拟类还是es6的语法糖class,都不是真正意义上的类。因为在类的实现中,子类是对父类的完全复制,而js不是,换句话讲,如果我们在改变了js一个父类的方法,继承该父类的子类和所有实例都会发生改变。ES6class的实现,本质上还是通过Object.crete()去关联两者的prototype。
JS的正确用法应该是面向对象,行为委托,而不是模拟类。

以下是面向对象的一个demo

    // 定义父对象
    var parent = {
        getName: function(name) {
            this.name = name;
            return this.showName();
        },
        showName: function() {
            return this.name;
        }
    }

    // 定义子对象
    var children = {
        sendName: function(name) {
            this.getName(name)
        }
    }

    // 通过Object.create关联父子对象
    var children = Object.create(parent);

    children.prototype === parent.prototype // true
    children.getName('陈先生'); // 陈先生

以上是我的一些理解,有什么误人之处,希望指出,感激不尽。

应该是 children.proto === parent 吧.
children 和 parent 都是对象,没有prototype属性,你这children.prototype 和 parent.prototype都是undefined

野鹿林 2022-05-04 13:57:31

@alanchanzm 我觉得不对吧

{
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo {
    constructor() {
      this.foo = 37;
    }
  }
}

class会提升这段代码就说不过去!
class

但是calss只提升不会赋值,你的这段代码是默认会提升而且赋值了

奈何桥上唱咆哮 2022-05-04 13:57:31

ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
Source:阮一峰,ES6

es6 继承调用/不调用super区别在于:
当一个普通的构造函数运行时,它会创建一个空对象作为 this,然后继续运行。
但是当派生的构造函数运行时,与上面说的不同,它指望父构造函数来完成这项工作。
所以如果我们正在构造我们自己的构造函数,那么我们必须调用 super,否则具有 this 的对象将不被创建,并报错。

城歌 2022-05-04 13:57:31

先来复习下两者的写法区别:

原型链继承

function Hero(name){
    this.name = name;
}
Hero.prototype.sayName = function(){
    console.log('my name is',this.name)
}

function smallHero(){
    this.age = 30
}
smallHero.prototype = new Hero('spiderman');
var s1 = new smallHero()
s1.sayName()  //my name is spiderman;
或者写成:
function smallHero_second(){
    Hero.apply(this,arguments) //此处有缺点只能继承构造函数里的不能继承原型里的方法
    this.age = 22; 
}

ES6继承

class smallHero11 extends Hero{
    constructor(props){
        super(props);
        this.age = 30;
    }
}
var s2 = new smallHero11('superman') 
s2.sayName()  ==>my name is superman

继承的主要区别:

原型链继承里的

smallHero.__proto__ === Function.prototype

ES6的class里的

smallHero11.__proto__ === Hero , 子类可以直接通过 proto 寻址到父类
很受用,之前都没发觉到function xxx 其实是等同于 new Function ,所以proto指向的是Function 的constructor....

眉目亦如画i 2022-05-04 13:57:31

补充一点:类继承是单一继承结构,只有一个父类;而原型继承本质上是组合,它可以有多个父类,且不会产生层级分类这样的副作用。

孤独岁月 2022-05-04 13:57:31

JavaScript相比于其他面向类的语言,在实现继承时并没有真正对构造类进行复制,当我们使用var children = new Parent()继承父类时,我们理所当然的理解为children ”为parent所构造“。实际上这是一种错误的理解。严格来说,JS才是真正的面向对象语言,而不是面向类语言。它所实现的继承,都是通过每个对象创建之初就存在的prototype属性进行关联、委托,从而建立练习,间接的实现继承,实际上不会复制父类。

ES5最常见的两种继承:原型链继承、构造函数继承

1.原型链继承

    // 定义父类
    function Parent(name) {
        this.name = name;
    }

    Parent.prototype.getName = function() {
        return this.name;
    };

    // 定义子类
    function Children() {
        this.age = 24;
    }

    // 通过Children的prototype属性和Parent进行关联继承

    Children.prototype = new Parent('陈先生');

    // Children.prototype.constructor === Parent.prototype.constructor = Parent

    var test = new Children();

    // test.constructor === Children.prototype.constructor === Parent

    test.age // 24
    test.getName(); // 陈先生

我们可以发现,整个继承过程,都是通过原型链之间的指向进行委托关联,直到最后形成了”由构造函数所构造“的结局。

2.构造函数继承

    // 定义父类
    function Parent(value) {
        this.language = ['javascript', 'react', 'node.js'];
        this.value = value;
    }
    
    // 定义子类
    function Children() {
    	Parent.apply(this, arguments);
    }

    const test = new Children(666);

    test.language // ['javascript', 'react', 'node.js']
    test.value // 666

构造继承关键在于,通过在子类的内部调用父类,即通过使用apply()或call()方法可以在将来新创建的对象上获取父类的成员和方法。

ES6的继承

    // 定义父类
    class Father {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }

        show() {
            console.log(`我叫:${this.name}, 今年${this.age}岁`);
        }
    };

    // 通过extends关键字实现继承
    class Son extends Father {};

    let son = new Son('陈先生', 3000);
    
    son.show(); // 我叫陈先生 今年3000岁

ES6中新增了class关键字来定义类,通过保留的关键字extends实现了继承。实际上这些关键字只是一些语法糖,底层实现还是通过原型链之间的委托关联关系实现继承。

总结

区别于ES5的继承,ES6的继承实现在于使用super关键字调用父类,反观ES5是通过call或者apply回调方法调用父类。

实在不明白为什么第二个构造函数继承可以被叫做继承,因为一个叫 Parent 一个叫 Children?还是因为 Parent.call(this)?

撩起发的微风 2022-05-04 13:57:31

@afishhhhh 第二个例子作者没写全,这里的Child只继承了Parent类的实例属性和方法,但是没有说父类原型怎么处理,当然如果Parent本身就没有定义原型,这个例子也是没问题的。

// 定义父类
function Parent(value) {
  this.language = ["javascript", "react", "node.js"];
  this.value = value;
}

// 如果Parent也定义了prototype
Parent.prototype = {
  getValue() {
    return this.value;
  },
};

// 定义子类
function Children() {
  Parent.apply(this, arguments);
}

// 这里要继承父类的原型
Children.prototype = Object.create(Parent.prototype);
瑾兮 2022-05-04 13:57:31

@labike
可能是我们对「提升」的理解不同吧?我理解的「提升」和「赋值」是两个过程。
我拆解一下那个例子:

var Foo = function() { /** pass */ };

{
  // 「块作用域」内可以访问全局变量 Foo
  const foo = new Foo();
}
var Foo = function() { /** pass */ };

{
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  // 如果 class 不会提升的话,new Foo() 应该成功调用
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo{ /** pass */ }
}

类似于以下代码(但不等于):

var Foo = function() { /** pass */ };

{
  let Foo; // 区别在于此处 Foo 已经初始化为 undefined
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  const foo = new Foo(); 
  Foo = class { /** pass */}
}

你说提升和赋值是两个过程,但是变量名进入TDZ的表现我认为并不代表提升。提升体现在函数预编译过程中就是提取变量名和以及赋值,是在一起的,只不过非function的变量值为undefined。TDZ只能说是特性,并不是提升吧,毕竟提升理应是可达的。

关于提升,TDZ 这方面的东西我觉得可以通过词法环境的相关内容来解释,这样是比较清楚的,而且关于 TDZ 我没有在规范里找到,所以我理解为 TDZ 是为了帮助我们理解而提出来的一个术语。
afishhhhh/blog#10

白衬杉格子梦 2022-05-04 13:57:31

@alanchanzm 答了很多,而且很有帮助,但是离题了。
问题是继承的差异。

class Super {}
class Sub extends Super {}

const sub = new Sub();

Sub.__proto__ === Super;

子类可以直接通过 proto 寻址到父类。

function Super() {}
function Sub() {}

Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var sub = new Sub();

Sub.__proto__ === Function.prototype;

而通过 ES5 的方式,Sub.proto === Function.prototype
es5的继承有很多方式,不仅仅只有原型式继承,还有构造函数继承、符合继承、寄生式继承等等,所以你的这个回答并不准确。

很关键的一点 很核心

眼角的笑意。 2022-05-04 13:57:31

@XueSeason 哈哈哈,审题不清,这轮面试要挂了。
再补充一点:
ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。

function MyES5Array() {
  Array.call(this, arguments);
}

// it's useless
const arrayES5 = new MyES5Array(3); // arrayES5: MyES5Array {}

class MyES6Array extends Array {}

// it's ok
const arrayES6 = new MyES6Array(3); // arrayES6: MyES6Array(3) []

用es5要实现内置对象的继承

function MyDate() {
  // Date 上的方法只能由 Date 的实例调用,所以new MyDate的时候要返回一个date对象
  // const date = new Date(...arguments)
  const date = new (Function.prototype.bind.apply(Date, [null].concat(Array.prototype.slice.call(arguments))));
  
  Object.setPrototypeOf(date,MyDate.prototype);
  return date;
}



Object.setPrototypeOf(MyDate.prototype, Date.prototype)


MyDate.prototype.getTime = function() {
  const year = this.getFullYear();
  const month = this.getMonth() + 1;
  const day = this.getDate();
  return  `${year}-${month}-${day}`;
}

const newDate = new MyDate();
console.log(newDate.getTime());
掩耳倾听 2022-05-04 13:57:31

我觉得忽略了一点,es6的class继承不仅是对原型实例进行了继承,还对构造方法进行了继承,class本质还是一个构造函数,转码后的实现逻辑还是组合寄生继承。

没错,敲代码验证了一下:

ES5的寄生组合式继承:

function Foo(age){
    this.age = age
    this.balls = [1,2,3]
}
Foo.prototype.getAge = function(){
    return this.age
}
function Bar(name, age){
    Foo.call(this, age)
    this.name = name
}
Bar.prototype = Object.create(Foo.prototype)
Bar.prototype.constructor = Bar
Bar.prototype.getName = function(){
    return this.name
}

const b1 = new Bar('b1', 18)
const b2 = new Bar('b2', 20)

对应的ES6 class的继承:

class Foo {
    constructor(age){
        this.age = age
        this.balls = [1,2,3]
    }
    getAge(){
        return this.age
    }
}
class Bar extends Foo {
    constructor(name, age){
        super(age)
        this.name = name
    }
    getName(){
        return this.name
    }
}

const b1 = new Bar('b1', 18)
const b2 = new Bar('b2', 20)

测试了两种继承 子类实例的行为是一致的。

街角迷惘 2022-05-04 13:57:31

@labike
可能是我们对「提升」的理解不同吧?我理解的「提升」和「赋值」是两个过程。
我拆解一下那个例子:

var Foo = function() { /** pass */ };

{
  // 「块作用域」内可以访问全局变量 Foo
  const foo = new Foo();
}
var Foo = function() { /** pass */ };

{
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  // 如果 class 不会提升的话,new Foo() 应该成功调用
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo{ /** pass */ }
}

类似于以下代码(但不等于):

var Foo = function() { /** pass */ };

{
  let Foo; // 区别在于此处 Foo 已经初始化为 undefined
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  const foo = new Foo(); 
  Foo = class { /** pass */}
}

class不会提升,文档没问题。而且TDZ和变量提升好像没关系,只是ES6的一个规定而已。
不过既然你放在一起举例子,那第二个例子我把class改成let,你瞅瞅是不是好理解点

var a = 1
{
    console.log(a)  // Uncaught ReferenceError: Cannot access 'a' before initialization
    let a = 2      
}

class不会提升,文档没问题。而且TDZ和变量提升好像没关系,只是ES6的一个规定而已。
不过既然你放在一起举例子,那第二个例子我把class改成let,你瞅瞅是不是好理解点

var a = 1
{
    console.log(a)  // Uncaught ReferenceError: Cannot access 'a' before initialization
    let a = 2      
}

众所周知 let是不会被提升的,而且你第二个例子的报错信息写错了,应该是Uncaught ReferenceError: Cannot access 'Foo' before initialization而不是ReferenceError: Foo is not defined,因为触发了TDZ。

“如果 class 不会提升的话,new Foo() 应该成功调用”
你这句话……emmm
1.如果class被提升,那么const foo = new Foo() 的报错提示应该是 TypeError: Foo is not a function因为此时Foo的值为undefined
2.这个跟let是类似的,class没有被提升,所以报错为ReferenceError: Foo is not defined

   // var a会被提升,所以执行a()时报TypeError的错误,因为由于提升,可以找到a这个变量,只是调用的时候错误了
   a()  // Uncaught TypeError: a is not a function
   var a = function() {}

  // let a不会被提升,所以执行a()报 ReferenceError,不会提升,根本找不到a这个变量
  a()   // Uncaught ReferenceError: a is not defined
  let a = function() {}

  // 和上面一样,class Foo{} 不会被提升
  const foo = new Foo() // Uncaught ReferenceError: Foo is not defined
  class Foo{}
蓝梦月影 2022-05-04 13:57:31

这个问题有问题,class本身就是语法糖,最后也是被转化成es5,所以问这个问题有什么意义?

梅窗月明清似水 2022-05-04 13:57:31

@alanchanzm

  1. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
  this.bar = 42;
}
Bar.prototype.print = function() {
  console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor

这个结论似乎和class没有关联,关键在于声明class方法时使用了=>(箭头函数):

class A {
   static a = function() {}
   b = function() {}
}
const a = new A()
const result = new a.b()
那小子欠揍 2022-05-04 13:57:31

@alanchanzm

  1. class 的所有方法(包括静态方法和实例方法)都没有原型对象 prototype,所以也没有[[construct]],不能使用 new 来调用。
function Bar() {
  this.bar = 42;
}
Bar.prototype.print = function() {
  console.log(this.bar);
};

const bar = new Bar();
const barPrint = new bar.print(); // it's ok

class Foo {
  constructor() {
    this.foo = 42;
  }
  print() {
    console.log(this.foo);
  }
}
const foo = new Foo();
const fooPrint = new foo.print(); // TypeError: foo.print is not a constructor

这个结论似乎和class没有关联,关键在于声明class方法时使用了=>(箭头函数):

class A {
   static a = function() {}
   b = function() {}
}
const a = new A()
const result = new a.b()

class 上只有静态方法和原型方法不存在 prototype

雪若未夕 2022-05-04 13:57:31

@alanchanzm 说class声明会提升但不会初始化赋值是错的. both class declarations and class expressions are not hoisted(类声明和类表达式都不会存在提升) 出自 https://leanpub.com/understandinges6/read

转瞬即逝 2022-05-04 13:57:31
  1. class内部定义的方法是不可枚举的
  2. 类必须使用new调用,否则会报错
  3. class 不存在变量提升机制,es6不会把类的声明提升到代码的头部
  4. class内部是严格模式的!
  5. new target属性可以实现抽象类
  6. ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
我恋#小黄人 2022-05-04 13:57:31

区别:

  • ES5的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即"实例在前,继承在后"。
  • ES6的继承机制,则是先将父类的属性和方法加到一个空对象上面,然后再将该对象作为子类的实例,即"继承在前,实例在后"。

这也是为什么ES6的继承必须先调用super方法,因为这一步会生成一个继承父类的this的对象,没有这一步就无法继承父类。
(真的是牛鬼蛇神)

叹沉浮 2022-05-04 13:57:30

因为this生成顺序不同,所以需要在constructor中,需要使用super()

挽袖吟 2022-05-04 13:57:30

@alanchanzm 答了很多,而且很有帮助,但是离题了。

问题是继承的差异。

class Super {}
class Sub extends Super {}

const sub = new Sub();

Sub.__proto__ === Super;

子类可以直接通过 proto 寻址到父类。

function Super() {}
function Sub() {}

Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var sub = new Sub();

Sub.__proto__ === Function.prototype;

而通过 ES5 的方式,Sub.proto === Function.prototype

@XueSeason 我好像记得es6 class Sub extends Super {} 在babel解析中是这样的

function Super(){}
let Sub = Object.create(Super)

Sub.__proto__ === Super;//true
血之狂魔 2022-05-04 13:57:30

JavaScript相比于其他面向类的语言,在实现继承时并没有真正对构造类进行复制,当我们使用var children = new Parent()继承父类时,我们理所当然的理解为children ”为parent所构造“。实际上这是一种错误的理解。严格来说,JS才是真正的面向对象语言,而不是面向类语言。它所实现的继承,都是通过每个对象创建之初就存在的prototype属性进行关联、委托,从而建立练习,间接的实现继承,实际上不会复制父类。

ES5最常见的两种继承:原型链继承、构造函数继承

1.原型链继承

    // 定义父类
    function Parent(name) {
        this.name = name;
    }

    Parent.prototype.getName = function() {
        return this.name;
    };

    // 定义子类
    function Children() {
        this.age = 24;
    }

    // 通过Children的prototype属性和Parent进行关联继承

    Children.prototype = new Parent('陈先生');

    // Children.prototype.constructor === Parent.prototype.constructor = Parent

    var test = new Children();

    // test.constructor === Children.prototype.constructor === Parent

    test.age // 24
    test.getName(); // 陈先生

我们可以发现,整个继承过程,都是通过原型链之间的指向进行委托关联,直到最后形成了”由构造函数所构造“的结局。

2.构造函数继承

    // 定义父类
    function Parent(value) {
        this.language = ['javascript', 'react', 'node.js'];
        this.value = value;
    }
    
    // 定义子类
    function Children() {
    	Parent.apply(this, arguments);
    }

    const test = new Children(666);

    test.language // ['javascript', 'react', 'node.js']
    test.value // 666

构造继承关键在于,通过在子类的内部调用父类,即通过使用apply()或call()方法可以在将来新创建的对象上获取父类的成员和方法。

ES6的继承

    // 定义父类
    class Father {
        constructor(name, age) {
            this.name = name;
            this.age = age;
        }

        show() {
            console.log(`我叫:${this.name}, 今年${this.age}岁`);
        }
    };

    // 通过extends关键字实现继承
    class Son extends Father {};

    let son = new Son('陈先生', 3000);
    
    son.show(); // 我叫陈先生 今年3000岁

ES6中新增了class关键字来定义类,通过保留的关键字extends实现了继承。实际上这些关键字只是一些语法糖,底层实现还是通过原型链之间的委托关联关系实现继承。

总结

区别于ES5的继承,ES6的继承实现在于使用super关键字调用父类,反观ES5是通过call或者apply回调方法调用父类。

风月客 2022-05-04 13:57:27

因为this生成顺序不同,所以需要在constructor中,需要使用super()

掌心的温暖 2022-05-04 13:57:27

因为this生成顺序不同,所以需要在constructor中,需要使用super()

断桥再见 2022-05-04 13:57:27

因为this生成顺序不同,所以需要在constructor中,需要使用super()

谁与争疯 2022-05-04 13:57:27

因为this生成顺序不同,所以需要在constructor中,需要使用super()

天涯沦落人i 2022-05-04 13:57:25

因为this生成顺序不同,所以需要在constructor中,需要使用super()

不乱于心 2022-05-04 13:57:25

因为this生成顺序不同,所以需要在constructor中,需要使用super()

始终不够 2022-05-04 13:57:16

因为this生成顺序不同,所以需要在constructor中,需要使用super()

陪你到最终〆 2022-05-04 13:57:12

因为this生成顺序不同,所以需要在constructor中,需要使用super()

烟酉 2022-05-04 13:57:11

因为this生成顺序不同,所以需要在constructor中,需要使用super()

一身软味 2022-05-04 13:56:59

因为this生成顺序不同,所以需要在constructor中,需要使用super()

权谋诡计 2022-05-04 13:55:47

@XueSeason 哈哈哈,审题不清,这轮面试要挂了。
再补充一点:
ES5 和 ES6 子类 this 生成顺序不同。ES5 的继承先生成了子类实例,再调用父类的构造函数修饰子类实例,ES6 的继承先生成父类实例,再调用子类的构造函数修饰父类实例。这个差别使得 ES6 可以继承内置对象。

function MyES5Array() {
  Array.call(this, arguments);
}

// it's useless
const arrayES5 = new MyES5Array(3); // arrayES5: MyES5Array {}

class MyES6Array extends Array {}

// it's ok
const arrayES6 = new MyES6Array(3); // arrayES6: MyES6Array(3) []
失退〃 2022-05-04 13:52:33

@alanchanzm 答了很多,而且很有帮助,但是离题了。

问题是继承的差异。

class Super {}
class Sub extends Super {}

const sub = new Sub();

Sub.__proto__ === Super;

子类可以直接通过 __proto__ 寻址到父类。

function Super() {}
function Sub() {}

Sub.prototype = new Super();
Sub.prototype.constructor = Sub;

var sub = new Sub();

Sub.__proto__ === Function.prototype;

而通过 ES5 的方式,Sub.__proto__ === Function.prototype

梦冥 2022-05-04 13:47:49

@labike
可能是我们对「提升」的理解不同吧?我理解的「提升」和「赋值」是两个过程。
我拆解一下那个例子:

var Foo = function() { /** pass */ };

{
  // 「块作用域」内可以访问全局变量 Foo
  const foo = new Foo();
}
var Foo = function() { /** pass */ };

{
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  // 如果 class 不会提升的话,new Foo() 应该成功调用
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo{ /** pass */ }
}

类似于以下代码(但不等于):

var Foo = function() { /** pass */ };

{
  let Foo; // 区别在于此处 Foo 已经初始化为 undefined
  // 「块作用域」内无法访问全局变量 Foo,因为它被本作用域内的 Foo 遮蔽了
  const foo = new Foo(); 
  Foo = class { /** pass */}
}
暮凉 2022-05-04 09:28:10

@alanchanzm 我觉得不对吧

{
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo {
    constructor() {
      this.foo = 37;
    }
  }
}

class会提升这段代码就说不过去!
class

挽清梦 2022-05-04 09:27:28

@alanchanzm 1. class 声明会提升 . 是不是写错了?
原文: Class declarations, unlike function declarations, are not hoisted.

@labike
原文有问题,class 是会提升的,其表现与letconst类似,变量名会进入TDZ。
看下例:如果没有提升,foo 会是块作用域外的Foo实例。但是由于提升的关系,块作用域内的Foo遮蔽了外层的同名函数。

var Foo = function() {
  this.foo = 21;
};

{
  const foo = new Foo(); // ReferenceError: Foo is not defined
  class Foo {
    constructor() {
      this.foo = 37;
    }
  }
}
久而酒知 2022-05-04 07:00:39

@alanchanzm 1. class 声明会提升 . 是不是写错了?
原文: Class declarations, unlike function declarations, are not hoisted.

~没有更多了~

关于作者

錯遇了你

暂无简介

0 文章
0 评论
23 人气
更多

推荐作者

謌踐踏愛綪

文章 0 评论 0

开始看清了

文章 0 评论 0

高速公鹿

文章 0 评论 0

alipaysp_PLnULTzf66

文章 0 评论 0

热情消退

文章 0 评论 0

白色月光

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文