JavaScript基于原型的继承机制和类

560 阅读8分钟

类的概念

宏观

  • 宏观上,我们可以定义:类是一种面向对象设计模式,比如迭代器模式、观察者模式、工厂模式、单例模式...
  • 很多语言提供了面向类的原生语法,ES6也提供了类似语法,但是这并不意味着JavaScript实际上有类JavaScript
  • JavaScript只有对象,并不存在可以被实例化的“类"。对象会通过原型链被委托关联起来,模拟类的行为。

JavaScript微观

  • 类:一组对象从一个原型对象继承属性
  • 类的标识是原型对象,而不是构造函数,两个构造函数的prototype属性可能指向同一个对象

创建类的方法

  1. 定义一个原型对象,然后反复用Object.create()创建继承它的对象
  2. 使用构造函数,定义其prototype属性,new调用构造器-构造函数的prototype属性将被用作新对象的原型【构造器模式】
  3. ES6的类语法,实际是构造函数prototype的语法糖

prototype

ES6的类语法实际是构造函数prototype的语法糖,接下来围绕-最基础的类定义机制 展开,即上文提到的创建类的方法2,联系下文-构造器模式进行理解

【一】、几乎所有对象都有原型,但是只有少数对象有prototype(函数对象/构造函数),有prototype属性的对象为所有其他对象定义了原型。

  • Object.prototype没有原型
  • 对象字面量和new Object创建的对象的原型对象是Object.prototype
  • Object.create(null)新创建的对象也没有原型,没有继承任何方法。

p.s注意:实际编程中,不要直接把对象传给库函数,防止对象被第三方库意外修改,而是传递一个继承对象,可以使用Object.create()生成

function objectCreate(o){//object.create的单参数
    function F(){}
    F.prototype=o;//
    return new F();//传入一个原型,返回一个根据原型创建的对象实例
}

【二】、有原型(一部分浏览器实现为__proto__)的一定是对象,有prototype原型的一定是函数,普通函数既有__proto__又有prototype(箭头函数、异步函数、生成器函数、Function.prototype.bind()除外)。

  • 每个普通函数,自动拥有一个prototype对象属性,支持手动赋值
  • prototype对象有一个不可枚举的constructor属性指向与之关联的构造函数。
  • 在创建函数时,也会创建其prototype对象,同时自动给prototype.constructor属性赋值。
  • 只有prototype对象才实际拥有constructor属性,其他对象都是通过原型链间接获得。

注意:如果使用对象字面量重写原型对象,意味着constructor属性会被改写指向Object,这时就不能通过constructor来识别类型。

const F = function () {};
const proto = F.prototype;
const constr = proto.constructor;
constr === F; // true
const obj = new F();
obj.constructor === obj.__proto__.constructor; // true

【三】、_实例的原型指向,作为对象的内部属性是不能直接访问的,但是为了查看对象原型:

  • Firefox和Chrome内核的JavaScript引擎中提供了这个__proto__非标准访问器
  • ECMA新标准引入了标准对象原型访问器Object.getPrototype(object)、Object.setPrototype(object)、

JavaScript的一些早期浏览器实现了通过__proto__暴露对象的prototype对象的读写,虽然被废弃但是存有代码,所以ES标准要求所有浏览器的JavaScript实现必须支持它。

让我们实验一下

function A(){};
let a=new  A();
console.log(A.prototype.constructor);//ƒ A(){}
console.log(a.constructor);//ƒ A(){}
console.log(a instanceof  A);//true

修改实例的原型对象,相当于切断与构造器的连接,重新定义a.constructor

a.__proto__ = {};//修改实例的隐式原型对象,相当于切断与构造器的连接
console.log( A.prototype.constructor);//ƒ A(){}
console.log(a.constructor);//ƒ Object() { [native code] }
console.log(a instanceof A);//false

又或者修改构造器的原型,注意这里是构造器方法,constructor会改变

A.prototype={
	id:7
};//只影响后面的,不影响前面已经生成的
console.log(A.prototype.constructor);//ƒ Object() { [native code] }
console.log(a.constructor);//ƒ A(){} 前面已经生成的不受影响【脚本语言】
console.log(a instanceof A);//false
let aa=new  A();
console.log(aa.constructor);//ƒ Object() { [native code] }之后实例化的生效
console.log(aa instanceof  A);//true

r instanceof Range 并非检查r是否通过Range构造函数初始化,而是检查原型链上是否有Range.prototype 区别:isPrototypeOf,待思考

class-like 创建相似对象=>类

构造器模式

JavaScript中的new和面向类的语言完全不同,在JavaScript中构造函数只是一些使用new操作符时被调用的普通函数

构造函数不能直接访问prototype对象的属性 A.b1

new操作符调用构造函数:

  1. 在内存中创建一个新对象
  2. 新对象的原型被赋值为构造函数的prototype【仅有new调用构造器时会进行这一步】
  3. 构造函数中的this指向新对象
  4. 执行构造函数的代码,为新对象添加属性
  5. 如果构造函数没有返回对象,就返回新对象(参看盗用构造器模式)
//除了声明函数也可以写成表达式
function A () {
  // return {b1:'in a'} // 覆盖了构造函数生成的对象,并没有获取到原型对象
  this.b1 = 'in a'
}

A.prototype = {
  b1:'in pro',
  proKey:'only in pro',
}

const a = new A();
console.log(a.b1); // 'in a'
console.log(a.proKey); // 'only in pro'

盗用构造器模式

实例的constructor的指向是通过原型对象间接获得的

只有在new调用的时候才会自动继承prototype属性。而盗用构造器是使用apply方法借用构造器,不会继承prototype,c.constructor指向B。

function A() {
    this.o = 'a';

}
A.prototype.say = function(){
    console.log("hhh");
}
function B() {
    A.call(this);
    // return new A(); // constructor指向A
}

const c =new B();
console.log(c); // Bfunction A() {
    this.o = 'a';

}
A.prototype.say = function(){
    console.log("hhh");
}
function B() {
    A.call(this);
    // return new A(); // constructor指向A
}

const c =new B();
console.log(c); // B{o:'a'}

原型链继承

原型对象不采用构造函数默认的原型,而是另一个类的实例

function A() {
    this.o = 'a';
}
A.prototype.say = function(){
    console.log("hhh");
}

function B() {}
B.prototype = new A();
console.log(B.prototype.constructor); // A
const c =new B();
console.log(c);// B
console.log(c.constructor); // 指向A因为B.prototype.constructor并不指向B,而是指向A
c instanceof B; // true
c instanceof A; // true
A.prototype.isPrototypeOf(c); // true
B.prototype.isPrototypeOf(c); // true

寄生虫模式(最有效

相比原型模式,将父类实例赋值给子类原型,寄生虫直接用父类原型创建一个原型对象,再手动绑定constructor关系

两个参数:子类构造器和父类构造器

function inheritPrototype(subT,superT){
    let prototype=Object.create(superT.prototype);//创建父类原型的副本对象给子类的prototype
    prototype.constructor=subT;//绑定明确的constructor
    subT.prototype=prototype;//绑定
}

工厂模式

理解:类只是一种设计模式

function createPerson(name,age){
    let o=new Object();//显式创建对象
    o.name=name;
    o.age=age;
    o.sayName=function(){
        console.log(this.name);
    };
    return o;
}
let p1=createPerson("Greg",27);
console.log(p1);

ES6 class语法

前文围绕JavaScript模拟class-like的行为,ES6中引入class(背后的实质仍然是原型和构造函数)

类的实质:使用立即执行函数【所以类声明定义都不能提升(函数声明可以提升)】在框定一个命名空间 TS官方提供的编译运行环境

image.png

  • 采用对象字面量简写形式定义方法,即 a:function(){} a(){}
  • 如果类不需要任何初始化,可以省略constructor关键字及其方法体,解释器会隐式为你创建一个空构造函数。
class Range {
    constructor (from,to) { 
//添加到this上的全部都会成为实例成员
        this.from = from;
        this.to = to;
    }
// 定义的字段不在原型上,可能位于实例或者类
    static share =0;
    each='ones';
//在类块中定义的所有方法都会定义在类的原型上
    includes (x) {
        return this.from <= x && x <= this.to;
    }

// 可以使用字符串、符号或者计算的值作为键
    *[Symbol.iterator]() { // 这个生成器方法让 类的对象支持迭代语法
        for(let x = Math.ceil(this.from); x <= this.to; x++) {
            yield x;
        }
    }

//静态,定义在类本身上
     static locate(){
        console.log("class",this);
    }
}

//为已有类的原型添加一个 方法
Range.prototype.say=function (){
  console.log('hh');
}

const r = new Range(0,4);
for (const p of r) {
    console.log(p);
}

类内部定义的function在原型上,但是变量在实例上?
ES6标准的类,只允许创建方法(获取方法、设置方法、生成器)和静态方法,还没有定义字段的语法。如果想要在类实例上定义字段(实例属性),必须在方法中进行。静态字段,必须在类体之外,在定义类之后定义。

但是扩展类语法在发展,支持定义实例和静态字段的标准化在发展,浏览器逐渐支持定义公有实例字段,且定义公有实例字段的语法在使用React和Babel转义的JavaScript程序、typescript中非常常用。

子类

  1. 只能在派生类的构造函数静态方法中使用super
  2. 在静态方法中使用super调用父类的静态方法
  3. 不能单独引用关键字super,用么调用构造函数super();要么引用静态方法
  4. 构造函数,只有先调用super() ,才可以使用this
  5. 如果在派生类中显示定义了构造器,要么必须调用super(),要么返回一个对象
class SuperT{
    identify(id){
        console.log(id,this);//this的值反映调用相应方法的实例或者}
    s类
    tatic identifyClass(id){
        console.log(id,this);//this的值反映调用相应方法的实例或者类
    }
}
//继承
class  SubT extends SuperT{
     constructor(){
       //调用父类构造器
        super(); // 父类构造器内部可以通过new.target 获得 子类构造器
        //必须在调用父类构造器之后才能引用this,否则抛出ReferenceError
        console.log('subT constructor',this);     
    }
}

let son=new SubT();
let father=new SuperT();

son.identify('son');//son SubT{}
father.identify('father');//father SuperT{}
SuperT.identifyClass('父类');//父类 class SuperT{}
SubT.identifyClass("子类");//子类 class SubT{}

JavaScript提倡委托来关联对象而不是继承,推荐在自己的类中创建另一个类的实例,然后在需要的时候委托实例去完成。

类对生成器和迭代器的支持

class P{
    //在原型上定义生成器方法
    *createNameIterator(){
        yield 'n1';
        yield 'n2';
    }
    //也可以在类上定义 static *name
}//

let iter=new P().createNameIterator();
console.log(iter.next().value);//n1
console.log(iter.next().value);//n2

JavaScript 对象

描述对象的属性

属性分为:数据属性、访问器属性-访问器属性不包含数据值,通常包含getter、setter函数

采用内部特性来描述属性的特征, 属性描述符是ES5开始支持的语法

  • Configurable是否可以通过delete删除、是否可以修改特性,是否更改为数据/访问器属性、默认true
  • Enumerable是否可以通过for-in循环返回,默认true
  • 【数据特有】Writable
  • 【数据特有】Value
  • 【访问器特有】Get,默认undefined
  • 【访问器特有】Set,默认undefined

Object.getOwnPropertyDescriptor()方法可以获取指定属性的属性描述符(只可以获取自己的属性,即实例不能通过调用该方法知道自己的原型属性)

修改对象的属性

访问器属性不能直接定义,在支持Object.defineProperty()的浏览器中可以通过其修改特性,还可以多个描述符一次性定义多个属性:

let person={};
Object.defineProperty(person,"name",{
	writable:false,
	value:"hh"
	});
//定义多个属性
let book={};
Object.defineProperty(book,{
	year:{//数据属性
 		value:2017
 	},
 	edition:{//访问器属性
 	   get(){
			return this.year;
		},
	   set(newyear){
	   if(newyear>2017){
	   		this.year=newyear;
	   	}}
 	}
})

访问对象的属性

  • in操作符返回,是否可以通过对象访问属性(原型、实例)// 一个存在但是值为undefined的值 使用操作符in,返回true
  • 实例名.hasOwnPrototype("属性名“);//检测属性是否属于实例
  • 属性 !==undefined 注意: 属性 !==undefined 不能区别undefined和null,但是in可以
  • Object.keys(对象名);//可枚举的实例属性名称
  • Object.getOwnPrototypeNames(对象名);//包括不可枚举的实例属性名称
  • Object.getOwnPrototypeNames-Symbol(对象名);//符号为键的属性
  • Object.values()、Object.entries()两个静态方法接收一个对象,返回他们实例对象上的内容的数组

修改对象的原型

Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)

function A() {
    this.o = 'a';

}
A.prototype.say = function(){
    console.log("hhh");
}
Object.setPrototypeOf(A.prototype, {addF(){
    console.log("new");
}});
function B() {
    
    return new A();
}

const c =new B();
console.log(c);

注意区别重写和修改、新增

Sub.prototype=new Super();//原型,继承方法 Sub.prototype.sayAge=function(){ console.log(this.age); };//原型链上的方法共享,新增

原型链的属性设置和屏蔽

给一个对象设置属性,原型链上有同名的属性时候,服从原型对象的属性的描述符 只有当writable:true才会在当前对象上添加一个新的屏蔽属性(hasOwnProperty返回true)

我们尽量避免在原型链的不同级别中使用相同的命名

image.png