1. 对象基础
2. 继承
- 继承是面向对象编程中讨论最多的话题
- 很多面向对象语言都支持两种继承:
- 接口继承
- 实现继承
- 前者只继承方法签名,后者继承实际的方法。
- 接口继承在 ECMAScript 中是不可能的,因为函数没有签名。
- 实现继承是 ECMAScript 唯一支持的继承方法,而这主要是通过原型链实现的。
2.1 原型链
ECMA-262 把原型链定义为 ECMAScript 的主要继承方法。
- 其基本思想就是通过原型继承多个引用类型的属性和方法。
- 重温一下构造函数、原型和实例的关系:
- 每个构造函数都有一个原型对象,原型对象有一个属性指回构造函数,而实例有一个内部指针指向原型。- 如果原型是另一个类型的实例呢?那就意味着,这个原型本身有一个内部指针指向另一个原型,相应的另一个原型也有一个指针指向另一个构造函数。- 这样就是实例和原型之间构造了一条原型链。- 这就是原型链的基本构想。
简单来说,对象实例都是由构造函数内部的对象原型来创造的,对象原型相当于一类实例对象的模板,由构造函数包含,但原型对象也可能是其他构造函数内原型对象的实例对象,这样就构成了两个构造函数之间的关系
在这里仅仅浅析一下什么是原型链,关于原型链的问题还是在有所了解后再深入。
3. 类
ECMAScript 6 新引入的 class 关键字具有正式定义类的能力。类(class)是 ECMAScript 中新的基础性语法糖结构,因此刚开始接触时可能会不太习惯。虽然 ECMAScript 6 类表面上看起来可以支持正式的面向对象编程,但实际上它背后使用的任然是原型和构造函数的概念。
3.1 定义类
与函数类型相似,定义也有两种主要方式:类声明和类表达式。这两种方式都使用 class 关键字加大括号:
// 类声明class Person {}// 类表达式const Animal = class {};
- 与函数表达式类似,类表达式在它们被求值前也不能引用。
- 不过,与函数定义不同的是,虽然函数声明可以提升,但类定义不能 ```javascript console.log(fn); // undefined var fn = function() {}; // fn为变量,只提升变量不提示函数 console.log(fn); // function() {}
console.log(fn1); // fn1() {} function fn1() {} // 函数声明提升 console.log(fn1); // fn1() {}
console.log(aClass); // undefined var aClass = class {}; // 类声明不进行提升,但声明的变量名提升 console.log(aCalss); // class {}
console.log(otherClass); // ReferenceError:otherClass is not defined class otherClass {} // 类表达式声明都不会提升 console.log(otherClass); // claa otherClass
另一个跟函数声明不同的地方是,函数受函数作用域限制,而类受块作用域限制:```javascript{function newFun() {}class oClass {}}console.log(newFun); // newFun() {}console.log(oClass); // ReferenceError:oClass is not defined
和 let 一样的 都限制在块{}括号内部存在
3.2 类的构成
- 类可以包含构造函数方法、实例方法、获取函数、设置函数和静态方法,但这些都不是必需的。空的类定义照样有效。
- 默认情况下,类定义中的代码都在严格模式下执行。
- 与函数构造函数一样,多数编程风格都是建议类名的首字母要大写,以区别通过它创建的实例(比如,通过 class Foo {} 创建实例 foo): ```javascript // 类可以定义为空 class Foo ()
// 有构造函数的类,有效 class Bar { constructor() {} }
// 有获取函数的类,有效 class Baz { get myBaz() {} }
// 有静态方法的类,有效 class Qux { static myQux() {} }
类表达式的名称是可选的。在把类表达式赋值给变量后,可以通过 name 属性取得类表达式的名称 字符串。但不能在类表达式作用域外部访问这个标识符。```javascriptlet Person = class PersonName { // 类声明创建类,并将类赋值给一个变量identify() { // 里面包含一个函数console.log(Person.name, personName.name);}}let p = new Person();p.identify(); // PersonName PersonName 类赋值给p 含有此函数可在外部运行console.log(Person.name); // PersonNameconsole.log(PersonName); // ReferenceError:PersonName is not defined
外部调用类名无法调用,需要将类赋值给变量,通过变量.name的方式来获取类名
3.3 类构造函数
- constructor 关键字用于在类定义块内部创建的构造函数
- 方法名 constructor 会告诉解释器,在使用 new 操作符创建类的新实例时,应该调用这个函数
- 构造函数的定义不是必需的,不定义构造函数相当于将构造函数定义为空函数。
实例化
使用 new 操作符实例化上面代码中 Person 的操作等于使用 new 调用其构造函数。唯一可感知的不同之处就是,JavaScript 解释器知道使用 new 和 类,意味着应该使用 constructor 函数进行实例化。使用 new 调用类的构造函数会执行如下操作。
(1)在内存中创建一个新对象。
(2)这个新对象内部的[[prototype]]指针被赋值为构造函数的 prototype 属性。
(3)构造函数内部的 this 被赋值为这个新对象(即 this 指向新对象)。
(4)如果构造函数返回非空对象,则返回该对象;否则,返回刚创建的新对象。
class Animal {} // 创建一个类class Person { // 创建第二个类constructor() {console.log('person ctor');}}class Vegetable { // 创建第三个类constructor() {this.color = 'orange';}} // 使用 new 操作符实例化类let a = new Animal();let p = new Person(); // person ctorlet v = new Vegetable();console.log(v.color); // orange 类实例化后可在外部调用类的属性
类实例化时传入的参数会用作构造函数的参数。如果不需要参数,则类名后面的括号也是可选的:
class Person {constructor(name) {console.log(arguments.length);this.name = name || null; // 或判断符,只有两个都为false才结束,输出第二个表达式null}}let p1 = new Person; // 0 0=falseconsole.log(p1.name); // nulllet p2 = new Person(); // 0console.log(p2.name); // nulllet p3 = new Person('jake'); // 1=true 或运算符不在判断第二个表达式直接赋值给nameconsole.log(p3.name); // Jake
function Person() {}class Animal {}// 把 window 作为 this 来构建实例let p = Person();let a = Animal();// TypeError: class constructor Animal cannot be invoked without 'new'
类构造函数没有什么特殊之处,实例化之后,它会成为普通的实例方法(但作为类构造函数,任然要使用 new 调用)。因此,实例化之后可以在实例上引用它:
class Person {}// 使用类创建一个新实例let p1 = new Person();p1.constructor();// TypeError: Class constructor Person cannot be invoked without 'new'// 使用对类构造函数的引用创建一个新实例let p2 = new p1.construtor();
把类当成特殊函数
ECMAScript 中没有正式的类这个类型。从各方面来看,ECMAScript 类就是一种特殊函数。声明一个类之后,通过 typeof 操作符检测类标识符,表明它是一个函数:
class Person {}console.log(Person); // class Person {}console.log(typeof Person); // function
类标识符有 prototype 属性,而这个原型也有一个 constructor 属性指向类自身:
class Person {}console.log(Person.prototype); { constructor:f() }console.log(Person === Person.prototype.constructor); // true
与普通构造函数一样,可以使用 instanceof 操作符检查构造函数原型是否存在于实例的原型链中:
class Person {}let p = new Person();console.log(p instanceof Person); // true
类是 JavaScript 的一等公民,因此可以像其他对象或函数引用一样把类作为参数传递:
// 类可以像函数一样在任何地方定义,比如在数组中let classList = [class {constructor(id) {this.id_ = id;console.log('instance ${this.id_}');}}];function creatInstance(classDefinition, id) {return new classDefinition(id);}let foo = createInstance(classList[0],3141); // instance 3141
与立即调用函数表达式相似,类也可以立即实例化:
let p = new class Foo {constructor(x) {console.log(x);}}('bar'); // barconsole.log(p); // foo{}
3.4 类 继承
ECMAScript 6 新增特性中最出色的一个就是原生支持了类继承机制。虽然类继承使用的是新语法,但背后依旧使用的是原型链。
继承基础
ES6 类支持单继承。使用 extends 关键字,就可以继承任何拥有[[Construct]]和原型的对象。很大程度上,这意味着不仅可以继承一个类,也可以继承普通的构造函数(保持向后兼容):
class Vehicle {}// 继承类class Bus extends Vehicle {}let b = new Bus(); // 类的实例化console.log(b instanceof Bus); // trueconsole.log(b instanceof Vehicle); // truefunction Person() {}继承普通构造函数class Engineer extends Person {}let e = new Engineer();console.log(e instanceof Engineer); // trueconsole.log(e instanceof Person); // true
注意 extends 关键字也可以在类表达式中使用,因此let Bar = class extends Foo {}是有效的语法。
构造函数、HomeObject 和 super()
派生类的方法可以通过 super 关键字引用它们的原型。这个关键字只能在派生类中使用,而且仅限于类构造函数、实例方法和静态方法内部。在类构造函数中使用 super 可以调用父类构造函数。
class Vehicle {constructor() {this.haEngine = true;}}class Bus extends Vehicle {constructor() {// 不要在调用 super() 之前引用this,否则会抛出 ReferenceErrorsuper(); // 相当于 super.constructor()console.log(this instanceof Vehicle); // trueconsole.log(this); // Bus { hasEngine: true }}}new Bus(); //类实例化
在静态方法中可以通过 super 调用继承的类上定义的静态方法:
class Vehicle {static identify() {console.log('vehicle');}}class Bus extends Vehicle { // extebds 继承父元素static identify() {super.identify(); // super 调用继承的}}Bus.identify(); // vehicle
注意 ES6 给类构造函数和静态方法添加了内部特性
[[HomeObject]],这个特性是一个指针,指向定义该方法的对象。这个指针是自动赋值的,而且只能在 JavaScript 引擎内部访问。super 始终会定义为[[HomeObject]]的原型
在使用 super 时要注意几个问题
super 只能在派生类(由已存在的类派生出的新类称为 派生类 ,又称为子类。)构造函数和静态方法中使用
class Vehicle {constructor() {super();// SyntaxError: 'super' keyword unexpected here}}
调用 super() 会调用父类构造函数,并将返回的实例赋值给 this。
class Vehicle {}class Bus extends Vehicle {constructor() {super();console.log(this instanceof Vehicle);}}new Bus(); //true
super() 的行为如同调用构造函数,如果需要给父类构造函数传参,则需要手动传入。
class Vehicle {constructor(licensePlate) {this.licensePlate = licensePlate;}}class Bus extends Vehicle {construtor(licensePlate){super(licensePlate);}}console.log(new Bus('1337H4X')); //Bus { licensePlate:'1337H4X'}
如果没有定义构造函数,在实例化派生类时会调用 super(),而且会传入所有传给派生类的参数
class Vehicle {constructor(licensePlate) {this.licensePlat = licensePlate;}}class Bus extends Vehicle {}console.log(new Bus('1337H4X')); //Bus {licensePlate:'1337H4X'}
在类构造函数中,不能在调用 super() 之前引用 this。
class Vehicle {}class Bus extends Vehicle {constructor() {console.log(this);}}new Bus();// ReferenceError: Must call super constructor in derived class// before accessing 'this' or returning from derived constructor
如果在派生类中显示定义了构造函数,则要么必须在其中调用 super(),要么必须在其中返回一个对象。
class Vehicle {}class Car extends Vehicle {}calss Bus extends Vehicle {constructor() {super();}}console.log(new Car()); // Car {}console.log(new Bus()); // Bus {}console.log(new Van()); // {}
继承内置类型
ES6类为继承内置引用类型提供了顺畅的机制,开发者可以方便地扩展内置类型:
class SuperArray extends Array {shuffle() {// 洗牌算法for(let i = this.length - 1; i > 0; i++) {const j = Math.floor(Math.random() * (i + 1));[this[i],this[j]]= [this[j],this[i]];}}}let a = new SuperArray(1,2,3,4,5);console.log(a instanceof Array); // trueconsole.log(a instanceof SuperArray); // trueconsole.log(a); // [1,2,3,4,5]a.shuffle();console.log(a); // [3,1,4,5,2]
有些内置类型的方法会返回新实例。默认情况下,返回实例的类型与原始实例的类型是一致的:
class SuperArray extends Array {}let a1 = new SuperArray(1,2,3,4,5);let a2 = a1.filter(x => !!(x%2));console.log(a1); // [1,2,3,4,5]console.log(a2); // [1,3,5]console.log(a1 instanceof SuperArray); // trueconsole.log(a2 instanceof SuperArray); // true
如果向覆盖这个默认行为,则可以覆盖 Symbol.species 访问器,这个访问器决定在创建返回的 实例时使用的类:
class SuperArray exrends Array {static get [Symbol.species]() {return Array;}}let a1 = new SuperArray(1,2,3,4,5);let a2 = a1.filter(x => !!(x%2));console.log(a1); //[1,2,3,4,5]console.log(a2); //[1,3,5]console.log(a1 instanceof SuperArray); // trueconsole.log(a2 instanceof SuperArray); // true
3.5 实例、原型和类成员
类的语法可以非常方便地定义应该存在于实例上地成员、原型上的成员和类本身上的成员
实例成员
通过 new 调用类标识符(即类名),都会执行类构造函数。这个函数地内部可以为新创建地实例(this)添加“自有”属性。至于添加什么属性是没有限制的,在构造函数执行完毕后,依然可以给已经创建的实例添加新成员(新属性新方法)
每一个实例都对应一个唯一的成员对象,这意味着所有成员不会再原型上共享(无法共享调用)
class Person{constructor(){ //类构造函数// 这个例子先使用对象包装类型定义一个字符串// 为的是在下面测试两个对象的相等性this.name = new String('Jack');this.sayName = () => console.log(this.name);//构造函数内方法this.nicknames = ['Jake', 'J-Dog'] //构造方法内数组}}let p1 = new Person(), //实例化类p2 = new Person();p1.sayName(); // Jackp2.sayName(); // Jackconsole.log(p1.name === p2.name); // falseconsole.log(p1.sayName === p2.sayName); // falseconsole.log(p1.nicknames === p2.nicknames); // falsep1.name = p1.nicknames[0];p2.name = p2.nicknames[1];p1.sayName(); // Jakep2.sayName(); // J-Dog
每个类实例都不是相同的地址存储数据,通过new创建后就无法像对象一样共享
原型方法与访问器
为了在类的实例中共享方法,类定义语法中定义的方法作为原型方法
class Person {constructor() {// 添加到 this 的所有内容都会存在于不同的实例上this.locate = () => console.log('instance');}// 在类块中定义的所有内容都会在类的原型上locate() {console.log('prototype');}}let p = new Person();p.locate(); // instancePerson.Prototype.locate(); // prototype 说明类中直接定义的方法都为原型对象方法
可以把方法定义在类构造函数中或者类块中,但不能在类块中给原型添加原始值或对象作为成员数据:
class Person {name:'Jake' // 属性写在类构造函数constructot里}//// Uncaught SyntaxError: Unexpected token 报错啦
类方法等同于对象属性,因此可以使用字符串、符合、计算的值作为键
const symbolkey = Symbol('symbolkey');class Person {stringKey() {console.log('invoked stringKey');}[symbolKey]() {console.log('invoked symbolkey');}['computed' + 'key']() {console.log('invoked computedKey');}}let p = new Person();p.stringKey(); // invoked stringKeyp[symbolKey](); // invoked symbolKeyp.computedKey(); // invoked computedKey
类定义也支持获取和设置访问器,语法于行为跟普通对象一样:
set name(newName) {this.name_ = newName;}get name() {return this.name_;}let p = new Person();p.name = 'Jake';console.log(p.name); // Jake
静态方法
可以在类上定义静态方法,因为写在类里面的方法都为原型对象内部实例共享的方法,所有静态方法需要在其前面添加 static 关键字作为前缀,在静态成员中,this引用类自身,其他所有都和原型成员一样
class Person {constructor() {//// 添加到 this 的所有内容都会存在于不同的实例上this.locate = () => console.log('instance',this);}// 定义在类的原型对象上locate() {console.log('prototype',this);}// 定义在类本身上static locate() {console.log('class',this);}}let p = new Person();p.locate(); // instance, Person {}Person.prototype.locate(); // prototype, {constructor: ... }Person.locate(); // class, class Person {}
非函数原型和类成员
虽然类定义并不是显示支持在原型或类上添加成员数据,但在类定义外部,可以手动添加
class PersonsayName() {console.log(`${Person.greeting}${this.name}`);}}// 在类上定义数据成员Person.greeting = 'My name is';// 在原型上定义数据成员Person.prototype.name = 'Jake';let p = new Person();p.sayName(); // My name is Jake
