javascript面向对象完全指北

597 阅读9分钟

这是我参与「掘金日新计划 · 4 月更文挑战」的第 1 天,点击查看活动详情

ES5的面向对象机制

js中的面向对象其实是基于原型的,构造函数其实充当了“类”的角色,因为构造函数可以创建实例。而构造函数其实与普通函数没有区别,只是一个普通函数用new进行调用的时候我们称它为构造函数。

如果在构造函数中显示return,若返回值是一个对象,则该对象会代替新创建的对象实例返回,但如果返回值是一个原始类型,则会被忽略。


function fn(name){
    this.name = name;
    return {b:'bb'}
}
let a = new fn('aa')
console.log(a); // {b: 'bb'}

function fn(name){
    this.name = name;
}
let a = new fn('aa')
console.log(a); // fn {name: 'aa'}

应该始终确保使用new调用构造函数,否则构造函数中的this指向的是全局对象。此时就是在改变全局对象,而不是创建一个新的对象。

function Person(name){
    this.name = name;
}
let p = Person('flten');
console.log(p); // undefined 
console.log(window.name); // flten

构造函数模式的问题:没有消除代码冗余,方法不共享

function Person(name){
    this.name = name;
    this.getName = function(){
        console.log('name',name);
    }
}
let p1 = new Person('aa');
let p2 = new Person('bb');
console.log(p1); // Person {name: 'aa', getName: ƒ}
console.log(p2); // Person {name: 'bb', getName: ƒ}
p1.getName = function(){
    console.log('its change!')
};
p1.getName(); // its change!
p2.getName(); // name bb

查看/判断对象的原型对象

let o = {};

// 调用Object.getPrototypeOf()方法读取对象的[[Prototype]]属性值
// 直接声明的对象的[[Prototype]]属性值指向Object.prototype
Object.getPrototypeOf(o) === Object.prototype; // true

// 检查某个对象是否是另一个对象的原型对象
Object.prototype.isPrototypeOf(o); // true

// 也可以调用对象的__proto__方法查看对象的[[Prototype]]属性值
o.__proto__ === Object.prototype; // true

构造函数、原型对象和对象实例之间的关系:

对象实例和构造函数之间没有直接的关系,但是对象实例和原型对象,原型对象和构造函数之间都有直接联系。也就是对象实例和构造函数之间只有间接关系。如果切断对象实例和原型对象之间的联系,那么也就切断了对象实例和构造函数之间的联系。

(1)函数创建的时候会有同时创建一个prototype属性,指向一个对象,这个对象称为原型对象,也是构造函数实例共享属性/方法的载体。但这种联系是单向的,也就是函数通过prototype可以找到原型对象,但是原型对象如何指向该构造函数呢?

(2)原型对象身上有一个constructor属性指向它的原型对象,但这个属性是可以被修改的,为了保证指向不错乱,一般不去修改它。

(3)实例对象在通过构造函数new Fn()创建时,需要建立它“父类”的关系,但是这个联系是在实例对象与构造函数的原型对象之间,因为共享(继承)的属性都在原型对象上。实例对象是通过一个内部属性[[prototype]]指向它的原型对象的,这个内部属性我们访问不到,但是我们可以通过浏览器提供给我们的__proto__属性去找到它的原型对象。到这里我们就建立了构造函数、原型对象、实例对象三者之间的联系。

(4)事实上所有的对象都有一个内部的[[prototype]]属性,即便是通过对象字面量声明的对象,例如:

let o = {name:'🍄'};
console.log(o.__proto__  === Object.prototype);

也就是说对象字面量方式声明的对象,它的原型对象是Object.prototype。既然说所有对象身上都有[[prototype]]属性,即它们也都有原型对象,那构造函数的原型对象自己本身是否有原型对象呢?答案是肯定的:

function fn(){};
fn.prototype.__proto__ === Object.prototype // true

即构造函数的原型对象的原型对象是Object.prototype。这里可能让你又疑惑,为什么不是fn.prototype.prototype呢?首先prototype是构造函数(其实是任何函数,因为任何函数都可以作为构造函数)身上的属性,对象身上时没有这个属性的,fn.prototype原型对象是一个对象,对象身上没有prototype这个属性,而作为对象它有一个内部的[[prototype]]属性,而这个属性可以通过浏览器提供的__proto__属性可以访问得到。

既然fn.prototype.__proto__的原型对象是Object.prototype,那么Object.prototype这个原型对象也一定有自己的原型对象了,那么是什么呢?

Object.prototype.__proto__ // null

结果是null,多少有些出乎人的意料,但又是容易让人理解的。对象的原型总不能一直无限下去,自然会有一个终结的时候,而Object作为顶级对象,它的原型指向null颇有一番禅意,一路追溯下去发现起点是空。

(4)从上面我们可以看到,可以由fn的原型对象fn.prototype,通过它的__proto__找到了它的原型对象Object.prototype,又通过Object.prototype__proto__找到了最后的null,这多么像一根链条⛓,可以一直向上查找,这就是原型链,对象继承其原型对象,而原型对象继承它的原型对象 ,依次类推。

function Person(name){
    this.name = name;
}
let p = new Person('👨🏻');
console.log(p.__proto__.constructor.name); // Person
console.log(p.__proto__.__proto__.constructor.name);  // Object
console.log(p.__proto__.__proto__.__proto__);  // null
let person1 = {
	name: '👩🏻',
	sayName: function(){
		console.log(this.name);
	}
};

let person2 =  Object.create(person1,{
	name: {
		configurable: true,
		enumerable: true,
		value: '👶🏻',
		writable: true
	}
});

person1.sayName(); // 👩🏻
person2.sayName(); // 👶🏻

f1360349255dd16c6c70230c1ae8fab6.jpg

对象继承

function Rectangle(length,width){
	this.length = length;
	this.width = width;
}

Rectangle.prototype.getArea = function(){
	return this.length *  this.width;
}

function Square(size){
	this.length = size;
	this.width = size;
}

Square.prototype = new Rectangle();
Square.prototype.constructor = Square;

let square = new Square(12,13);
square.getArea(); // 144

console.log(square instanceof Square);    // true
console.log(square instanceof Rectangle); // true
console.log(square instanceof Object);    // true

借用构造函数,实现组合继承。最主要的效率问题就是父类构造函数始终会被调用两次:一次在是创建子类原型时调用,另一次是在子类构造函数中调用

function Rectangle(length,width){
	this.length = length;
	this.width = width;
}

Rectangle.prototype.getArea = function(){
	return this.length *  this.width;
}

function Square(size){
	Rectangle.call(this,size,size);
}

Square.prototype = new Rectangle();
Square.prototype.constructor = Square;

let square1 = new Square(12);  // 144
square1.getArea();

let square2 = new Square(13);  // 169
square2.getArea();

寄生式组合继承

function object(o) {
  function F() {}
  F.prototype = o;
  return new F();
}

function inheritPrototype(subType, superType) {
  let prototype = object(superType.prototype);  // 创建原型对象
  prototype.constructor = subType;              // 修正constructor指向
  subType.prototype = prototype;                // 设置原型对象实现继承
}

function SuperType(name) {
  this.name = name;
  this.colors = ["red", "blue", "green"];
}

SuperType.prototype.sayName = function() {
  console.log(this.name);
};

function SubType(name, age) {
   // 借用父类构造函数
  SuperType.call(this, name);

  this.age = age;
}

// 设置原型,规避父类二次调用
inheritPrototype(SubType, SuperType);

SubType.prototype.sayAge = function() {
  console.log(this.age);
};

let sub = new SubType('flten',25);
console.log(SubType.prototype.constructor.name); // SubType
console.log(sub.sayName());  // flten
console.log(sub.sayAge());   // 25

ES6的面向对象机制

类的数据类型本身就是函数

class Fn{}
typeof Fn; // 'function'

类的所有方法都定义在类的prototype属性上,在类的实例上调用方法,就是调用原型上的方法

class Person {
	getName(){}
	getAge(){}
}

let p = new person();
p.getAge === Person.prototype.getAge // true

类内部定义的方法都是不可枚举的,而构造函数prototype属性上定义的方法都是可枚举的

class Person {
	getName(){}
	getAge(){}
}
for(key in Person){console.log(key)}; // undefined
for(key in Person.prototype){console.log(key)}; // undefined
function PersonFn() {}
PersonFn.prototype.getName = function(){}
PersonFn.prototype.getAge = function(){}
for(key in PersonFn.prototype){console.log(key)}; // getName、getAge

类必须有constructor()方法,若无显式定义则会默认天剑,默认返回实例对象(this)。可以指定返回另一个对象,但这会导致继承上的问题,因此最好不要显式返回。

class Person {
	getName(){}
	getAge(){}
}

let p1 = new Person();
p1.constructor.name === Person; // true
class Person {
	constructor(){
		return {}
	}
	getName(){}
	getAge(){}
}

let p2 = new Person();
p2.constructor.name === Person; // false

类必须使用new在对象实例化时调用,而构造函数可以作为函数直接调用

class Person {
	getName(){}
	getAge(){}
}

Person(); // Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
function PersonFn() {}
PersonFn() // 可以正常调用

类方法内部this默认指向类实例,但单独使用该方法可能导致this指向错误,造成不可预料的问题。

class Person{
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	getName(){
		console.log(this.name)
	}
}

let p1 = new Person('flten');
let {getName} = p1;
getName(); // Uncaught TypeError: Cannot read properties of undefined (reading 'name')

可以在构造函数中给方法绑定this

class Person{
	constructor(name,age){
		this.getName = this.getName.bind(this);
		this.name = name;
		this.age = age;
	}
	getName(){
		console.log(this.name)
	}
}

let p1 = new Person('flten');
let {getName} = p1;
getName(); // flten

类内部默认严格模式,不存在变量提升

静态方法不会被实例继承,只能通过类调用

class Person {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	static getAge(){
		console.log('25');
	}
	getName(){
		console.log(this.name);
	}
}

class Flten extends Person {
	constructor(name,age){
		super(name,age)
	}
}

const p1 = new Flten('flten',25);
p1.getName(); // flten
// 通过子类调用父类的静态方法
Flten.getAge(); // 25
// 使用实例对用静态方法,报错
p1.getAge(); // TypeError: p1.getAge is not a function

模拟抽象类

class staticClass {
	constructor(){
		if(new.target === staticClass){
			throw new Error('staticClass是抽象类不能被实现')
		}
	}
}

let s1 = new staticClass(); // Error: staticClass是抽象类不能被实现

super super作为函数时: (1)子类构造器必须执行一次super函数 这是由于ES5和ES6继承机制的区别所决定的:ES5是新建子类的实例对象this,然后再将父类的属性添加到子类上;ES6是新建父类的实例对象this,再用子类构造函数修饰this,使得父类所有行为都可继承。 (2)super只能用在子类的构造函数中

class Person {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	static getAge(){
		console.log('25');
	}
	getName(){
		console.log(this.name);
	}
}

class Flten extends Person {
	constructor(){}
}

let p1 = new Flten('flten',25); // ReferenceError: Must call super constructor in derived class before accessing 'this' or returning from derived constructor

如果子类不显式声明constructor,则会在默认的constructor里执行super,因此不会报错

class Person {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	static getAge(){
		console.log('25');
	}
	getName(){
		console.log(this.name);
	}
}

class Flten extends Person {}

let p1 = new Flten('flten',25);
p1.getName(); // flten

super作为对象时: (1)super在静态方法中指向父类,方法内部的this指向当前子类,而不是子类实例。这就相当于子类借用父类的静态方法。

class Person {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	static age = 25;
	static getAge(){
		console.log(this.age);
	}
	getName(){
		console.log(this.name);
	}
}

class Flten extends Person {
	constructor(name,age){
		super(name,age)
	}
}

Flten.getAge(); // 25

(2)super在普通方法中指向父类原型对象,方法内部的this指向当前子类实例,定义在父类实例上的方法或属性无法通过super调用。

class Person {
	constructor(name,age){
		this.name = name;
		this.age = age;
	}
	static getAge(){
		console.log(this.age);
	}
	getName(){
		console.log(this.name);
	}
}

class Flten extends Person {
	constructor(name,age){
		super(name,age)
	}
	getAge(){
		// 这里的super指向父类原型对象,但是super.getName方法内部的this是指向子类实例的
		super.getName();
	}
}

let p1 = new Flten('flten',25);
p1.getAge() // flten

(3)如果在普通方法中通过super对某个属性赋值,这时super就是this,赋值属性会变为子类实例属性,而读取super属性时,读取的是父类原型上的属性。

class A {
  constructor() {
    this.x = 1;
  }
}

A.prototype.x = 5;

class B extends A {
  constructor() {
    super();
    this.x = 2;
    // 这里的super指向的是this,即实例对象
    super.x = 3;
    // 这里的super指向的是父类原型,读取的是父类原型上的属性
    console.log(super.x); // 5
    console.log(this.x); // 3
  }
}

let b = new B();

(4)由于对象总是继承其他对象,因此可在任意对象中使用super

子类的继承机制:

子类作为对象,它的原型__proto__指向父类,表示构造函数的继承

class A {}
class B extends A{}
B.__proto__ === A // true

子类作为构造函数,它的原型对象prototype指向父类原型对象,表示方法的继承

class A {}
class B extends A{}
B.prototype.__proto__.constructor.name === 'A' // true