• javascript面向对象完全指北


    ES5的面向对象机制

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

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

    
    function fn(name){
        this.name = name;
        return {b:'bb'}
    }
    let a = new fn('aa')
    console.log(a); // {b: 'bb'}
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    function fn(name){
        this.name = name;
    }
    let a = new fn('aa')
    console.log(a); // fn {name: 'aa'}
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    function Person(name){
        this.name = name;
    }
    let p = Person('flten');
    console.log(p); // undefined 
    console.log(window.name); // flten
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

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

    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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

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

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

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

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

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

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

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

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

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

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

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

    Object.prototype.__proto__ // null
    
    • 1

    结果是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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    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(); // 👶🏻
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    对象继承

    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
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

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

    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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    寄生式组合继承

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    ES6的面向对象机制

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

    class Fn{}
    typeof Fn; // 'function'
    
    • 1
    • 2

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

    class Person {
    	getName(){}
    	getAge(){}
    }
    
    let p = new person();
    p.getAge === Person.prototype.getAge // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    class Person {
    	getName(){}
    	getAge(){}
    }
    for(key in Person){console.log(key)}; // undefined
    for(key in Person.prototype){console.log(key)}; // undefined
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    function PersonFn() {}
    PersonFn.prototype.getName = function(){}
    PersonFn.prototype.getAge = function(){}
    for(key in PersonFn.prototype){console.log(key)}; // getName、getAge
    
    • 1
    • 2
    • 3
    • 4

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

    class Person {
    	getName(){}
    	getAge(){}
    }
    
    let p1 = new Person();
    p1.constructor.name === Person; // true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    class Person {
    	constructor(){
    		return {}
    	}
    	getName(){}
    	getAge(){}
    }
    
    let p2 = new Person();
    p2.constructor.name === Person; // false
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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

    class Person {
    	getName(){}
    	getAge(){}
    }
    
    Person(); // Uncaught TypeError: Class constructor Person cannot be invoked without 'new'
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    function PersonFn() {}
    PersonFn() // 可以正常调用
    
    • 1
    • 2

    类方法内部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')
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    可以在构造函数中给方法绑定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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

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

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

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    模拟抽象类

    class staticClass {
    	constructor(){
    		if(new.target === staticClass){
    			throw new Error('staticClass是抽象类不能被实现')
    		}
    	}
    }
    
    let s1 = new staticClass(); // Error: staticClass是抽象类不能被实现
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    如果子类不显式声明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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    (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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    (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();
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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

    子类的继承机制:

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

    class A {}
    class B extends A{}
    B.__proto__ === A // true
    
    • 1
    • 2
    • 3

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

    class A {}
    class B extends A{}
    B.prototype.__proto__.constructor.name === 'A' // true
    
    • 1
    • 2
    • 3
  • 相关阅读:
    Redis基本数据类型:List
    聊聊超卖
    SSM - Springboot - MyBatis-Plus 全栈体系(十四)
    Node.js环境配置级安装vue-cli脚手架
    每天五分钟机器学习:通过查准率和召回率绘制P-R曲线
    详细讲解Linux内存泄漏检测实现原理与实现
    vSphere 7.0 主机卸载NFS存储失败故障处理
    egg框架的使用
    何为整型提升(实例)
    C语言自定义类型详解 —— 结构体、枚举、联合体
  • 原文地址:https://blog.csdn.net/qq_32925031/article/details/128162019