第 7 期:ES5/ES6 的继承除了写法以外还有什么区别?
alanchanzm opened this issue · 77 comments
class
声明会提升,但不会初始化赋值。Foo
进入暂时性死区,类似于let
、const
声明变量。
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
@alanchanzm 1. class
声明会提升 . 是不是写错了?
原文: Class declarations, unlike function declarations, are not hoisted.
@alanchanzm 1.
class
声明会提升 . 是不是写错了?
原文: Class declarations, unlike function declarations, are not hoisted.
@labike
原文有问题,class
是会提升的,其表现与let
、const
类似,变量名会进入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 我觉得不对吧
{
const foo = new Foo(); // ReferenceError: Foo is not defined
class Foo {
constructor() {
this.foo = 37;
}
}
}
class会提升这段代码就说不过去!
class
@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 */}
}
@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 哈哈哈,审题不清,这轮面试要挂了。
再补充一点:
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) []
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
因为this生成顺序不同,所以需要在constructor中,需要使用super()
@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
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回调方法调用父类。
@MingShined 什么是面向类的语言?第一次听说,能否详细讲讲。
@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('陈先生'); // 陈先生
以上是我的一些理解,有什么误人之处,希望指出,感激不尽。
刚好今天在看红宝书,顺便放下自己总结的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);
}
@MingShined 在原型链继承中test.age 输出结果应该是24啊,这里手误吧
@Jesse121 感谢这位同学指出。已经修改了
ES5是先创建子类再在父类添加这个方法
ES6是创建子类直接继承父类
@wd2010 我认为讨论ES5/ES6的问题时不应该去参考编译后的结果,因为我觉得ES6的一些特性对于ES5来说本身是无法模拟的,这些特性只能在编译阶段检查一下,看看当前的用法是否有误,是否和这个特性相悖,仅此而已。
例如我们如果只是简单写一条let定义语句,这会直接被编译成var,但不能说因为它被编译成var所以认为它的所有性质和var一样,这显然是错误的
@MingShined 初接触到这个概念是来自《你不知道的Javascript》,很是赞同(不过我感觉既然官方是在推class,那我还是按他们的要求来吧~
之前看到一篇文章是介绍的关于es6的class的。
里面就很详细啊 https://github.com/xiaohesong/TIL/blob/master/front-end/es6/understanding-es6/class.md
先看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
。
浅薄见解,请不吝指教。
@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,所以 类似于以下代码 感觉有问题。
之前看到一篇文章是介绍的关于es6的class的。
里面就很详细啊 https://github.com/xiaohesong/TIL/blob/master/front-end/es6/understanding-es6/class.md
谢谢,的确很全面,哈哈,octotree看这种还是不错的。
@wd2010 想知道babel转成什么:直接把代码复制到babel官网的那个在线运行编译工具界面,就能得到编译后的结果。
最重要的一点是继承机制完全不同,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直接写在类里就行。
class
声明会提升,但不会初始化赋值。Foo
进入暂时性死区,类似于let
、const
声明变量。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
// 我在浏览器上试,实例对象的方法是可枚举的
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))) // [] 实例对象的原型上的不可枚举
我觉得忽略了一点,es6的class继承不仅是对原型实例进行了继承,还对构造方法进行了继承,class本质还是一个构造函数,转码后的实现逻辑还是组合寄生继承。
@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)
我认为 @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,使得父类的所有行为都可以继承。因此可以在原生数据结构的基础上定义自己的数据结构。
以上是我的一知半解,若有问题,请多多指出
@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);
这样创建的对象,没了自己的属性方法。
@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 */} }
不能说你这么理解提升它就是提升,说明你就没有理解提升
@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的继承有很多方式,不仅仅只有原型式继承,还有构造函数继承、符合继承、寄生式继承等等,所以你的这个回答并不准确。
@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
欢迎各位讨论,拍砖
@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只能说是特性,并不是提升吧,毕竟提升理应是可达的。
@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();
@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 的说法
为什么 Object.keys(foo) 就有值呢?
@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;
function Bar() {
this.bar = 42;
}
const bar = Bar(); // it's ok
这个只是写法上可以这样吧?bar 仍然是undefined
@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#Hoisting
@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写法是不会有差别的,同样会继承内置对象
@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生成的顺序不同导致的
class没有变量提升吧,不然这个怎么解释
{ console.log(Foo) // Uncaught ReferenceError: Cannot access 'Foo' before initialization class Foo { constructor() { this.foo = 37; } } }
{ console.log(Bar) //undefined var Bar = function(){} }
先来复习下两者的写法区别:
原型链继承
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 寻址到父类
Foo
没提升吧 提升了没赋值应该是 typeError 不该是 ReferenceError ,ReferenceError说明就没定义
@alanchanzm 1.
class
声明会提升 . 是不是写错了?
原文: Class declarations, unlike function declarations, are not hoisted.@labike
原文有问题,class
是会提升的,其表现与let
、const
类似,变量名会进入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
最重要的一点是继承机制完全不同,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直接写在类里就行。
学到了学到了,小姐姐真厉害~
@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
@alanchanzm 我觉得不对吧
{ const foo = new Foo(); // ReferenceError: Foo is not defined class Foo { constructor() { this.foo = 37; } } }
class会提升这段代码就说不过去!
class
但是calss只提升不会赋值,你的这段代码是默认会提升而且赋值了
ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
Source:阮一峰,ES6
es6 继承调用/不调用super区别在于:
当一个普通的构造函数运行时,它会创建一个空对象作为 this,然后继续运行。
但是当派生的构造函数运行时,与上面说的不同,它指望父构造函数来完成这项工作。
所以如果我们正在构造我们自己的构造函数,那么我们必须调用 super,否则具有 this 的对象将不被创建,并报错。
先来复习下两者的写法区别:
原型链继承
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....
补充一点:类继承是单一继承结构,只有一个父类;而原型继承本质上是组合,它可以有多个父类,且不会产生层级分类这样的副作用。
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)?
@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);
@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
@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的继承有很多方式,不仅仅只有原型式继承,还有构造函数继承、符合继承、寄生式继承等等,所以你的这个回答并不准确。
很关键的一点 很核心
@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());
我觉得忽略了一点,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)
测试了两种继承 子类实例的行为是一致的。
@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{}
这个问题有问题,class本身就是语法糖,最后也是被转化成es5,所以问这个问题有什么意义?
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,所以也没有[[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
@alanchanzm 说class声明会提升但不会初始化赋值是错的. both class declarations and class expressions are not hoisted(类声明和类表达式都不会存在提升) 出自 https://leanpub.com/understandinges6/read#leanpub-auto-class-declarations
- class内部定义的方法是不可枚举的
- 类必须使用new调用,否则会报错
- class 不存在变量提升机制,es6不会把类的声明提升到代码的头部
- class内部是严格模式的!
- new target属性可以实现抽象类
- ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this。
区别:
- ES5的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即"实例在前,继承在后"。
- ES6的继承机制,则是先将父类的属性和方法加到一个空对象上面,然后再将该对象作为子类的实例,即"继承在前,实例在后"。
这也是为什么ES6的继承必须先调用super方法,因为这一步会生成一个继承父类的this的对象,没有这一步就无法继承父类。
(真的是牛鬼蛇神)
@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)
这样是不是更好理解?
区别:
- ES5的继承机制,是先创造一个独立的子类的实例对象,然后再将父类的方法添加到这个对象上面,即"实例在前,继承在后"。
- ES6的继承机制,则是先将父类的属性和方法加到一个空对象上面,然后再将该对象作为子类的实例,即"继承在前,实例在后"。
这也是为什么ES6的继承必须先调用super方法,因为这一步会生成一个继承父类的this的对象,没有这一步就无法继承父类。 (真的是牛鬼蛇神)
帖子第一条答案完美避开了核心区别😂(真是一点都没提到啊……)
Uncaught ReferenceError: Cannot access 'Foo' before initialization at
class不会提升生命变量