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 · 48 comments
Open

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

alanchanzm opened this issue Feb 22, 2019 · 48 comments
Labels

Comments

@alanchanzm
Copy link

@alanchanzm 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

This comment has been minimized.

Copy link

@labike labike commented Feb 22, 2019

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

@alanchanzm

This comment has been minimized.

Copy link
Author

@alanchanzm 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

This comment has been minimized.

Copy link

@labike labike commented Feb 23, 2019

@alanchanzm 我觉得不对吧

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

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

@alanchanzm

This comment has been minimized.

Copy link
Author

@alanchanzm 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

This comment has been minimized.

Copy link

@XueSeason XueSeason commented Feb 23, 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

@alanchanzm

This comment has been minimized.

Copy link
Author

@alanchanzm alanchanzm commented Feb 23, 2019

@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

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

10 similar comments
@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@xiaofengqqcom123

This comment has been minimized.

Copy link

@xiaofengqqcom123 xiaofengqqcom123 commented Feb 27, 2019

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

@wd2010

This comment has been minimized.

Copy link

@wd2010 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

This comment has been minimized.

Copy link

@MingShined 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

This comment has been minimized.

Copy link

@XueSeason XueSeason commented Mar 6, 2019

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

@MingShined

This comment has been minimized.

Copy link

@MingShined MingShined commented Mar 6, 2019

@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

This comment has been minimized.

Copy link

@zgw010 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

This comment has been minimized.

Copy link

@Jesse121 Jesse121 commented Apr 3, 2019

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

@MingShined

This comment has been minimized.

Copy link

@MingShined MingShined commented Apr 8, 2019

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

@yerled

This comment has been minimized.

Copy link

@yerled yerled commented May 17, 2019

@MingShined 初接触到这个概念是来自《你不知道的Javascript》,很是赞同(不过我感觉既然官方是在推class,那我还是按他们的要求来吧~

@fooyee

This comment has been minimized.

Copy link

@fooyee fooyee commented Jul 9, 2019

之前看到一篇文章是介绍的关于es6的class的。
里面就很详细啊 https://github.com/xiaohesong/TIL/blob/master/front-end/es6/understanding-es6/class.md

@kexiaofu

This comment has been minimized.

Copy link

@kexiaofu kexiaofu commented Jul 10, 2019

先看ES5的继承(原型链继承)

function a() {
  this.name = 'a';
}

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

function b() {}
b.prototype = new a();

console.log(b.prototype.__proto__ === a.prototype); // true
console.log(b.__proto__ === a); // false
console.log(b.__proto__); // [Function]

ES6继承

class A {
  constructor(a) {
    this.name = a;
  }
  getName() {
    return this.name;
  }
}

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

console.log(B.prototype.__proto__ === A.prototype); // true
console.log(B.__proto__ === A); // true
console.log(B.__proto__); // [Function: A]

对比代码可以知道,子类的继承都是成功的,但是问题出在,子类的 __proto__ 指向不一样。

ES5 的子类和父类一样,都是先创建好,再实现继承的,所以它们的指向都是 [Function]

ES6 则得到不一样的结果,它指向父类,那么我们应该能推算出来,它的子类是通过 super 来改造的。

根据 es6--阮一峰 在class继承里面的说法,是这样子的:

引用阮一峰的 ECMAScript6入门 的class继承篇:

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

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

浅薄见解,请不吝指教。

@doudounannan

This comment has been minimized.

Copy link

@doudounannan doudounannan commented Jul 10, 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 */}
}

所谓 TDZ 指的是在变量声明之前都是 ReferenceError,所以 类似于以下代码 感觉有问题。

@liaodeen

This comment has been minimized.

Copy link

@liaodeen liaodeen commented Jul 12, 2019

之前看到一篇文章是介绍的关于es6的class的。
里面就很详细啊 https://github.com/xiaohesong/TIL/blob/master/front-end/es6/understanding-es6/class.md

谢谢,的确很全面,哈哈,octotree看这种还是不错的。

@AlexZhong22c

This comment has been minimized.

Copy link

@AlexZhong22c AlexZhong22c commented Jul 12, 2019

@wd2010 想知道babel转成什么:直接把代码复制到babel官网的那个在线运行编译工具界面,就能得到编译后的结果。

@song-le-yi

This comment has been minimized.

Copy link

@song-le-yi song-le-yi commented Jul 15, 2019

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

@song-le-yi

This comment has been minimized.

Copy link

@song-le-yi song-le-yi commented Jul 15, 2019

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

@cdLVV

This comment has been minimized.

Copy link

@cdLVV cdLVV commented Jul 18, 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

// 我在浏览器上试,实例对象的方法是可枚举的
class Foo {
constructor() {
this.foo = 42;
}
static answer() {
return 42;
}
print() {
console.log(this.foo);
}
say = () => {
}
}
var obj = new Foo();
console.log(Object.keys(obj)) // ["say", "foo"] 为什么我在浏览器上试,实例的变量和方法是可以枚举
console.log(Object.getPrototypeOf(obj)) // {constructor: ƒ, print: ƒ}
console.log(Object.keys(Object.getPrototypeOf(obj))) // [] 实例对象的原型上的不可枚举

@fyuxiang

This comment has been minimized.

Copy link

@fyuxiang fyuxiang commented Jul 18, 2019

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

@caihaihong

This comment has been minimized.

Copy link

@caihaihong caihaihong commented Jul 21, 2019

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

应该是这样吧?虽然与要解答的问题没多大关系。 Array.call(this, ...arguments)

@zhouxunxunxunxun

This comment has been minimized.

Copy link

@zhouxunxunxunxun zhouxunxunxunxun commented Aug 3, 2019

我认为 @alanchanzm 写的区别,是class语法糖与es5本身的区别,并非继承方面的区别。
我认为继承方面的区别主要有以下几点:
1.es5的继承实质是先创造子类的实例对象,然后将父类的方法添加到this上面。es6则不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后用子类的构造函数修改this.
2.子类B的__proto__属性指向父类A 即B.proto = A. 而es5中的构造函数的__proto__是指向Function.prototyope的。我看的上面也有人详细说明,我就不详细说明了
3.es6允许继承原生构造函数。因为es6先新建父类的实例对象this。然后再用子类的构造函数修改this,使得父类的所有行为都可以继承。因此可以在原生数据结构的基础上定义自己的数据结构。

以上是我的一知半解,若有问题,请多多指出

@jiang2016tao

This comment has been minimized.

Copy link

@jiang2016tao jiang2016tao commented Aug 13, 2019

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

以上是我的一些理解,有什么误人之处,希望指出,感激不尽。
// 通过Object.create关联父子对象 var children = Object.create(parent);
这样创建的对象,没了自己的属性方法。

@tfeng-use

This comment has been minimized.

Copy link

@tfeng-use tfeng-use commented Aug 14, 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 */}
}

不能说你这么理解提升它就是提升,说明你就没有理解提升

@tfeng-use

This comment has been minimized.

Copy link

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

@ShirleyMatrix

This comment has been minimized.

Copy link

@ShirleyMatrix ShirleyMatrix commented Aug 26, 2019

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

什么是Class-Based program language

在早期面向对象的语言中,大部分都是Class-based的语言,比如大名鼎鼎的SmallTalk(是一种面向对象的、动态类型的编程语言,这门语言对后来c++和java等面向对象的语言具有重大的意义)
所谓的class-base,或者说基于类的面向对象语言(也就是这位同学说的面向类的语言),类是对象的生成器,而我们操作的对象都是类的实例,类并不存在于运行环境中,也无法被我们所操作。类的作用就是生成一个对象实例,类保管着所生成对象的方法。

class-based的缺点?

  • 第一是不直观,我们设计类,但是实际操作的却是他的实例。程序员为了一个新增的实体需要先去设计一个抽象的类,然后再将其实例化一个实体。程序员设计的是类,而实际操作的确是类的实例,这增加了认知成本。

  • 第二点是不灵活,当我们想要为某个类一个特殊实例增加一个特殊方法的时候,基于类的语言就需要进行一次继承,将特殊实例的类作为原来类的子类,并进行一次实例化。而假如我们只需要使用这个对象一次(比如ui编程中一个特殊按钮),这样繁琐的操作显然是不能让人接受的。

  • 第三点是对元类的依赖:元类(mate-class)是类的类。(注:这里仅对部分语言而言,比如前文提到的smalltalk,以及后来Ruby,python)关于metaclass,可以看看他的设计者在这里的所说的

所有类都是 Class类 的实例。就像对象的方法被它的类持有一样,Class也持有所有类的方法(比如创建一个新对象的方法)。
当对象接收到消息时,编译器会到对象的类里寻找该方法,当没有找到时,就会到类的父类里寻找,直到到达顶端也就是Class。
这就会遇到一个问题:类的方法会被所有类所共享。假如我们有两个类,class Point 和class Rectangle ,当我们想增加一个的类方法,比如newPointfromTwocoordinates时,这会造成class Rectangle 也拥有该方法(Rectangle.newPointfromTwocoordinates()),这个方法是没有任何意义的。
故此在Smalltalk-80中引入了mateClass,每一个Class都是它的metaClass的实例,所有mateClass都是MateClass类的实例。通过mateclass来定义class独特方法。
mateclass与class如双生子一般,在class创建时被创建。这里由于你不需要再去给metaclass定义特殊方法了,所以问题也就解决了(不过你真的想这么做的话,就需要定义元类的元类,以及进一步,元类的元类的元类……,这就是metaclass这个设计的另一个缺点:meta-regress. )

什么是Prototype-Based program language

在基于类的编程语言暴露出这些问题后,人们开始找寻解决的方法,Prototype-Based ,也就是基于原型的对象,就在这时被提出。

最早的基于原型的编程语言是Self,而JavaScript的设计者正是在阅读Self的设计论文后才采取的基于原型的设计

基于原型的语言的主要思想就是,直接由原本存在的对象来产生新的对象,从而摆脱对类的依赖。

最简单的实现方法就是,直接复制原有的对象。但是这样存在着很多的问题:

  • 第一,缺少对对象的分类,对象之间没有分别。没办法区分一个对象到底是数组还是字符串还是其他的。
  • 第二,缺少一种简单的方法可以同时更新一组对象

在基于类的语言中,上面两种职责都是由类来完成的,那在基于原型的语言中,不存在类,如何对这两点进行支持呢?

答案就是不完全复制对象,而是在新生成对象内部持有一个指向原有对象的引用,当访问新对象的属性或者方法时,如果新对象中不存在这种方法,就通过这个引用去访问生成他的那个对象,或者说原型对象,这样通过更新原型对象,我们就可以同时更新由他生成所有对象。也可以通过各个对象之间原型对象的不同来区分对象。

这就是一个简单的基于原型的继承

有什么好处?

这样设计的语言相对于基于类的语言有什么优势呢?

  • 简洁,你只需要去关心对象的状态和行为。不再需要为了新建一个对象去写一个新类啦
  • 即使在更深层次,它依然很简洁。不在需要元类了
  • 对于具象化的,视觉性的编程更加友好(比如UI,UX编程)
  • 对象可以被单独扩展,更加灵活(想想上面举得例子)

缺点呢?

  • 对于一个原始对象来说,不容易使用原型来描述
  • 效率:基于类的语言直接生成一个对象,而基于原型的对象需要同时存在对象和他的原型对象,并且需要沿着原型链向上查询
  • 原型可能在不经意间被修改

总结

  • 基于原型的语言设计对于基于类的语言设计来说,显得更简洁,易于接受
  • 并且对于视觉性编程更友好,编写效率也更块,也更灵活,这也是JavaScript选择基于原型设计的原因之一
  • 基于原型的设计本来是用来避免基于类的设计的一些缺点的,但是es6啪唧又给倒回去了,又仅限于语法上,没法享受类的优势,还引入了缺点。可能跟设计之初引入大量java元素(比如new操作符)一样是为了让其他人更容易接受这门语言吧。
  • 所以还是多使用面向原型这种继承方式吧,人家好不容易才给你换来这种更简单更灵活的方式,你非要按照基于类的方式去使用的话,无异于是开倒车了

blog原址:blog
欢迎各位讨论,拍砖

@tangzhibao

This comment has been minimized.

Copy link

@tangzhibao tangzhibao commented Oct 14, 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 */}
}

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

@yft

This comment has been minimized.

Copy link

@yft yft commented Oct 23, 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

另外,在 MDN 中,对 Object.create 的 Polyfill 如下:

if (typeof Object.create !== "function") {
    Object.create = function (proto, propertiesObject) {
        if (typeof proto !== 'object' && typeof proto !== 'function') {
            throw new TypeError('Object prototype may only be an Object: ' + proto);
        } else if (proto === null) {
            throw new Error("This browser's implementation of Object.create is a shim and doesn't support 'null' as the first argument.");
        }

        if (typeof propertiesObject != 'undefined') throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");

        function F() {}
        F.prototype = proto;

        return new F();
    };
}

关键逻辑就是

function F() {}
F.prototype = proto;
return new F();
@yinyihui

This comment has been minimized.

Copy link

@yinyihui yinyihui commented Oct 25, 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 */}
}

这个提升的解释有点说不过去哦, 同意@tangzhibao 的说法

@zhgt-xu

This comment has been minimized.

Copy link

@zhgt-xu zhgt-xu commented Nov 7, 2019

为什么 Object.keys(foo) 就有值呢?

@yinzuowen

This comment has been minimized.

Copy link

@yinzuowen yinzuowen commented Nov 20, 2019

@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和parent都没有prototype,应该是children.proto === parent;

@JiangMengLei

This comment has been minimized.

Copy link

@JiangMengLei JiangMengLei commented Jan 11, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
You can’t perform that action at this time.