Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
alanchanzm opened this issue Feb 22, 2019 · 77 comments
Open
Labels

Comments

@alanchanzm
Copy link

alanchanzm commented Feb 22, 2019

来源:Understanding ECMAScript 6

  1. 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;
  }
}
  1. 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();
  1. 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); // []
  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
  1. 必须使用 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'
  1. 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
@labike
Copy link

labike commented Feb 22, 2019

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

@alanchanzm
Copy link
Author

alanchanzm commented Feb 22, 2019

@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;
    }
  }
}

@alanchanzm alanchanzm changed the title 第 7 期:ES5/ES6 的继承除了写法以外还有什么区别?解答与一个疑惑 第 7 期:ES5/ES6 的继承除了写法以外还有什么区别? Feb 22, 2019
@labike
Copy link

labike commented Feb 23, 2019

@alanchanzm 我觉得不对吧

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

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

@alanchanzm
Copy link
Author

alanchanzm commented Feb 23, 2019

@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 */}
}

@XueSeason
Copy link

@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

@alanchanzm
Copy link
Author

@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) []

@xiaofengqqcom123
Copy link

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

10 similar comments
@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@xiaofengqqcom123
Copy link

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

@wd2010
Copy link

wd2010 commented Mar 5, 2019

@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

@MingShined
Copy link

MingShined commented Mar 6, 2019

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回调方法调用父类。

@XueSeason
Copy link

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

@MingShined
Copy link

@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('陈先生'); // 陈先生

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

@zgw010
Copy link

zgw010 commented Mar 10, 2019

刚好今天在看红宝书,顺便放下自己总结的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);
}

@Jesse121
Copy link

Jesse121 commented Apr 3, 2019

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

@MingShined
Copy link

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

@wmy1992
Copy link

wmy1992 commented Apr 15, 2020

@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生成的顺序不同导致的

@wubing0324
Copy link

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

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

@rxdxxxx
Copy link

rxdxxxx commented May 29, 2020

对了贴两张容易记住的图吧
image
这就是传说中es6入门里那句:子类实例的__proto__属性的__proto__属性指向父类实例的__proto__属性

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

@soraly
Copy link

soraly commented Jun 14, 2020

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

原型链继承

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 寻址到父类

@wxthahha
Copy link

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

@1939108122
Copy link

最重要的一点是继承机制完全不同,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直接写在类里就行。

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

@SnailOwO
Copy link

@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

@myf117
Copy link

myf117 commented Jun 24, 2020

@alanchanzm 我觉得不对吧

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

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

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

@SnailOwO
Copy link

SnailOwO commented Jun 24, 2020

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

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

@SnailOwO
Copy link

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

原型链继承

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....

@yeesunday
Copy link

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

@afishhhhh
Copy link

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)?

@whosesmile
Copy link

whosesmile commented Dec 10, 2020

@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);

@afishhhhh
Copy link

@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

@zyzhangyu
Copy link

@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的继承有很多方式,不仅仅只有原型式继承,还有构造函数继承、符合继承、寄生式继承等等,所以你的这个回答并不准确。

很关键的一点 很核心

@GitHdu
Copy link

GitHdu commented Dec 23, 2020

@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());

@andyyxw
Copy link

andyyxw commented Jan 16, 2021

我觉得忽略了一点,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)

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

@Luz-Liu
Copy link

Luz-Liu commented Mar 16, 2021

@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{}

@j-joker
Copy link

j-joker commented Apr 5, 2021

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

@raozhanping
Copy link

@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()

@afishhhhh
Copy link

@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

@sikichan
Copy link

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

@SnailOwO
Copy link

SnailOwO commented Nov 23, 2021

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

@Owen-MS
Copy link

Owen-MS commented Mar 30, 2022

区别:

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

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

@zane0904
Copy link

@alanchanzm我觉得不对

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

班级会过去段代码就说不! 班级

let、const、import、class声明的变量不存在变量提升
这段代码执行的时候首先在ATS抽象与解析阶段只会把全局var Foo先进行变量提升+声明赋值undefined,ATS完事会把
function() {
this.foo = 21;
}赋值给全局的Foo
接着在解析{...}内的代码时候遇到了const class 定义的变量 这个时候{...}代码块会形成一个单独的作用域
代码执行过程中遇到以下情况会生成单独的作用域(var 无视此规则)
1.判断体、循环体、代码块遇到{ }时,并且代码块内出现了let、const、class声明的变量
那么当前的执行上下文有const class就会形成独立的块级作用域,当在执行到 const foo = new Foo();的时候由于class并不会变量提升,而且在ATS解析阶段就已经知道,当前代码块中一定会出现Foo的一个变量,那在你log就会报错提示在初始化之前不允许使用,而不是报 is not defined
块级内的const foo = new Foo() 跟外层的根本没关系,这个题就是一个块级作用域的问题
let a = 1
{
let a = 2
console.log(a)
}
console.log(a)
这样是不是更好理解?

@HeyShinner
Copy link

区别:

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

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

帖子第一条答案完美避开了核心区别😂(真是一点都没提到啊……)

@1242793152
Copy link

Uncaught ReferenceError: Cannot access 'Foo' before initialization at
class不会提升生命变量

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests