一文理清 JavaScript 中对象的创建模式与继承模式

一文理清 JavaScript 中对象的创建模式与继承模式

一、前言 :

1. 写作目的

本篇创作的目的是为了梳理 JavaScript 中面向对象这部分内容的知识点, 供自己与博友随时查阅, 温故知新 !

2. 需要具备的知识点

  • -1). 熟悉 isPrototypeOf 检测对象原型方法的使用(一顿铺垫但后面没用到, 就当免费送啦😁)。
    let arr = [];
    console.log(Array.prototype.isPrototypeOf(arr)); // true
    console.log(Array.prototype.isPrototypeOf({})); // false
    
  • -2). 熟悉使用 Object.getProptotypeOf(Xxx) 方法获取参数对象的原型指向而非是使用 __proto__(一顿铺垫但后面没用到, 就当免费送啦😁)。
    function Person() { /* ... */ }
    let tom = new Person();
    console.log(tom.__proto__ === Person.prototype); // true
    console.log(Object.getPrototypeOf(tom) === Person.prototype); // true
    
  • -3). 熟悉 JavaScript 中的原型链部分的知识。
    在这里插入图片描述
    这张图用三句话概括足够了 :
    • a. 每一个构造函数都有一个 prototype 属性指向其原型。
    • b. 每一个对象(函数也是对象)都有一个 __proto__ 属性指向其构造函数的原型。
    • c. 每一个构造函数的原型都有一个 constructor 属性指向其构造函数。
      颇有万剑归宗、叶落归根之意。但实际上对象都是由构造函数创建的, 而不论是对象实例也好还是函数也罢其类型又都是 Object 类型 , 所以这两者属于鸡与蛋 的关系。
  • -4). 熟悉 in、for in、hasOwnProperty、Object.keys、Object.values 等的使用。
    • in 可以检测当前属性(不论对象的私有属性与方法或其指向原型的公有属性与方法), 是否属于该实例。
    • hasOwnProperty 可以检测当前属性( 仅对象的私有属性与方法)是否属于该实例
    function Person(name) {
        this.name = name;   
        Person.prototype.age = 22;         
        Person.prototype.sayHello = function() {  };  
    }
    let alice = new Person("Alice");
    console.log('name' in alice); // true
    console.log('age' in alice); // true
    console.log('sayHello' in alice); // true
    console.log(alice.hasOwnProperty('name')); // true
    console.log(alice.hasOwnProperty('age')); // false
    
    • 基于以上两者封装一个检测某个对象的属性是否属于其指向原型的属性的方法
    Object.prototype.hasProtoProperty = function hasProtoProperty(prop) {
    	return typeof prop === 'string' ? !this.hasOwnProperty(prop) && prop in this : false ;
    }
    alice.hasProtoProperty("sayHello"); // true
    alice.hasProtoProperty("name"); // false
    
    • Object.keys(Xxx) 与 Object.values(Xxx) 就是将对象中所有的可枚举的 key 与 value 归并到一个数组集合中返回。
    var obj = { name: "FruitJ" };
    Object.getPrototypeOf(obj).age = 22;
    console.log(Object.keys(obj)); // ["name"]
    console.log(Object.values(obj)); // ["FruitJ"]
    
    • for in 可以枚举出实例本身和原型上所有的可枚举的属性, Object.keys / values 仅会枚举出实例本身上的所有可枚举属性不包括原型上的。
    var obj = { name: "FruitJ" };
    Object.getPrototypeOf(obj).age = 22;
    for(let key in obj) {
        console.log(`key: ${ key }; val: ${ obj[key] }`);
    }
    /*
    	key: name; val: FruitJ
    	key: age; val: 22
    */
    
  • -5). 熟悉 Object.defineProperty 的使用。
    => 配置对象属性 : Object.defineProperty 可以配置一个对象的属性( 是否可配置(configurable)、是否可枚举(enumerable)、是否可写(writable)、值(value) )
    【1】. configurable 为 true 代表可配置(可删除), 为 false 代表不可配置(不可删除); 默认为 true
	var obj = {
	    name: "FruitJ",
	    age: 22,
	};
	Object.defineProperty(obj, 'age', {
	    configurable: false,
	});
	delete obj.age;
	console.log(obj); // {name: "FruitJ", age: 22}	

【2】.writable 为 true 代表可写(可以修改对象的属性值), 为 false 代表不可写(不可修改对象的属性值); 默认为 true

	Object.defineProperty(obj, 'age', {
		configurable: false,
    	writable: false,
	});
	obj.age = 23;
	console.log(obj); // {name: "FruitJ", age: 22}

【3】. enumerable 为 true 代表可枚举(可以枚举出该对象属性), 为 false 代表不可枚举(不可以枚举出对象的属性值); 默认为 true。

Object.defineProperty(obj, 'age', {
	configurable: false,
   	writable: false,
    enumerable: false,
});
for(let key in obj) {
    console.log(`key: ${ key }; val: ${ obj[key] }`);
}
// Uncaught TypeError: Cannot redefine property: age

【4】. value 可以设置对象指定属性的属性值

let obj = { name: "FruitJ", age: 22 };
Object.defineProperty(obj, 'age', {

    value: 25,
});
console.log(obj); // {name: "FruitJ", age: 25}

=> 配置 setter 与 getter : Object.defineProperty 可以配置一个对象的属性的拦截器(在设置与读取属性值的时候触发)

var obj = {
    name: "FruitJ",
    age: 22,
};

Object.defineProperty(obj, 'name', {

    set(val) {
        console.log(`setter -> ${ val }`); // setter -> XXY
    },get() {
        console.log(`getter return -> wow !!!`); // getter return -> wow !!!
        return "wow !!!";
    }

});

obj.name = "XXY"; // setter -> XXY
console.log(obj); // { name: [Getter/Setter], age: 22 }
obj.name; // getter return -> wow !!!
console.log(obj); // { name: [Getter/Setter], age: 22 }

通过 setter 与 getter 实现简单的 Vue 的双向数据绑定功能(TODO LIST 示例) :

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Object.defineProperty</title>
</head>
<body>
    
    <input type="text" id="input">
    <p id="content">

    </p>
    <script>
       
        // 创建被监听的对象
        let obj = {};
        // 创建临时存储数据的三方对象
        let temp = {};
        // 通过 Object.defineProperty 来设置 setter 与 getter 监听
        Object.defineProperty(obj, 'val', {
            set(val) {
                temp.val = val;
                input.value = val;
                content.innerText = val;
            },get() {
                return temp.val;
            }
        });
       input.addEventListener('input', function() {
           obj.val = this.value;
       }, false);
    </script>
</body>
</html>

效果 :
在这里插入图片描述
至于该案例为什么需要借助三方变量 temp 来临时存储数据, 就是因为不能直接通过 obj.val 来设置 / 获取 值, 否则自己调用自己就会堆栈溢出。

3. 阅前声明

文章主要借鉴了红宝书中的理论与自己的理解, 内附插图便于梳理思路与理解有关这方面设计模式的理念。
如有不足之处, 还请各路大神,路过斧正, 笔者表示感谢 !
了解了上述技能可以开始面向 “对象💕” 之旅啦 !

二、JavaScript 中对象的创建模式

1. ( 单例模式 ) - 日月星辰我独尊

a. 什么是单例 ?
  • 首先单例这个概念不是 JavaScript 中的首创, 其他语言中也都有这个模式, 所以这是一个设计理念、一个思想只不过为了解决实际问题在各个语言中被具体化了而已。
  • 单例模式,属于创建类型的一种常用的软件设计模式。通过单例模式的方法创建的类在当前进程中只有一个实例
    — 来源 • 百度百科
  • JavaScript 中的单例构造起来比较简单, 实际上 var obj = { ... }; 就是在创建一个单例, 因为该实例从任何角度来看就仅有这一个, 别无分号。
  • 单例模式的优点 : 此模式的优点就是建立在这个 "单" 上。在某些应用场景下会产生奇效。譬如说 : 在写某个项目的时候需要一个面向全局的 “工具库” 就可以通过将其设计成为单例来实现, 原因就是只需要暴露这么一个实例就好就没有必要去创建 n 个来用, 想一想如果用一次就创建一个那这个内存占用该是多么恐怖的一件事情 ?
    • 常见的单例模式的影子 :
      • windows 下的任务管理器
      • windows 下的垃圾回收站
      • 数据库的连接池
      • 操作多线程的线程池
  • 单例模式的缺点 : 此模式的缺点也是建立在这个 "单" 上, 如果真的有需求批量的、标准化的构建对象呢 ? 这个单例模式在这一点基本属于完败 !
b. 具体的实现方案 :

以打印 name 【名字】为例 :
普通单例 :

var obj = {
	name: "FruitJ",
	sayName() {
		console.log(`My name is ${ this.name }`);
	},
};
obj.sayName(); // My name is FruitJ

=> 缺点 : 在外部可以直修改 sayName 方法中所需的数据。

var obj = {
	name: "FruitJ",
	sayName() {
		console.log(`My name is ${ this.name }`);
	},
};
// 数据被篡改
obj.name = "病毒代码";
obj.sayName(); // My name is 病毒代码

升级版单例 :
借助闭包模式解决普通单例模式痛点

var obj = (function() {
    let name = "FruitJ";
	function sayName() {
		console.log(`My name is ${ name }`);
	}
	// 将待用方法添加到单例对象上
	return { sayName, }
})();
// 试图篡改数据(未成功)
obj.name = "病毒代码";
obj.sayName(); // My name is FruitJ

=> 这样就可以保证每次调用 sayName 方法获取到的就都是安全正确的值, 实际上相对来说 obj.name 这个恶意篡改的行为已经成功的设置上了, obj 的 name 属性为 "病毒代码", 但是 sayName 方法访问的是内部变量 name 而非是 obj 上的 name 属性。

2. ( 工厂模式 ) - 独乐不如众乐

  • 鉴于通过手动创建一个个普通的对象过于麻烦的问题, 所以借助这个工厂模式就可以批量的生产实例。
  • 原理 : 就是借助函数的封装性, 将构造对象的行为细节隐藏于函数内部并将其返回。
// 构造对象的工厂函数
function factory(name, age, hobby, fun) {
    return { name, age, hobby, fun, }; 
}
// 生产对象的数据源
let names = ["Alice", "Tom", "Blank"];
let ages = [21, 22, 23];
let hobbies = ["打代码", "看电视", "看书"];
// 生产的对象数量
const NUMBER = 3;
// result ...
let instances = [];
// 批量生产
for(let i=  0; i < NUMBER; i++) {
    instances.push(factory(names[i], ages[i], hobbies[i], function() {
        console.log(`name: ${ this.name }; age: ${ this.age }; hobbies: ${ this.hobby }`);
    }));
    instances[i].fun();
    /*
		name: Alice; age: 21; hobbies: 打代码
		name: Tom; age: 22; hobbies: 看电视
		name: Blank; age: 23; hobbies: 看书
	*/
}

console.log(instances); // (3) [{…}, {…}, {…}]

在这里插入图片描述
=> 但是工厂模式也存在弊端 : 就是创建的每个实例所属的 “类” 都是 Object, 这就难以满足一些定制化的场景需求。
在这里插入图片描述

3. ( 构造函数模式 ) - 私有财产神圣不可侵犯

  • 为了解决上面的问题, 可以借助构造函数创建对象的模式来确定所创建实例的所属 “类”。
    需要了解的技能点 :
  • 构造函数在执行的时候实际上会进行如下动作 :
    • 创建一个对象
    • 将构造函数中的 this 指向该对象
    • 初始化该对象的属性与方法
    • 隐式返回该对象
  • 并不是它是构造函数我们才 new 它, 而是我们 new 了它, 它才是构造函数。
  • 如果为构造函数显式的指定了返回值, 这个值是基本类型值不会影响到内部实例的隐式返回, 但如果显式指定的这个返回值是引用类型的则默认会覆盖掉隐式返回的实例, 转而走显式返回。
	function Fun(name) {
        this.name = name;
        return [""];
    }
    let fun = new Fun("XXY");
    console.log(fun); // [ '' ]

通过构造函数模式来构建对象

function Person(name, age) {

    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`Hello ${ this.name }`);
    };
    this.common = function() {
        console.log("我是公共方法 ...");
    };
    this.add = function() {
        console.log(`age: ${ this.age + 1 }`);
    };
}
// 创建实例
let alice = new Person("Alice", 21);
let tom = new Person("Tom", 22);
console.log(alice.common === tom.common); // false
console.log(alice.constructor); // function Person() { ... }

=> 这样虽然解决 工厂模式实例所属 "类" 弊端, 但是还存在着另外一个问题没有解决, 就是不论通过构造函数模式还是工厂模式, 所创建出来的每个实例上都有着一套自己的属性与方法, 那如果其中某些方法的功能较单一可以作为公共方法呢 ? 这样在每个实例上都存在独有的一套会导致额外的内存开销, 所以在构造对象之时为其定义实例之间共享的公有方法已势在必行 !!!
改造后 :

function Person(name, age) {

    this.name = name;
    this.age = age;
    this.sayHello = function() {
        console.log(`Hello ${ this.name }`);
    };
    this.common = function() {
        console.log("我是公共方法 ...");
    };
    this.eatRice = eatRice; // 公有方法
    this.add = function() {
        console.log(`age: ${ this.age + 1 }`);
    };
}

// 公有方法
function eatRice() {
    console.log(`${ this.name }吃米饭 ${ this.age + 1 }`);
}
let alice = new Person("Alice", 21);
let tom = new Person("Tom", 22);
console.log(alice.common === tom.common); // false
console.log(alice.eatRice === tom.eatRice); // true

通过在构造函数外部定义公共方法, 并在构造实例的时候为其初始化 eatRice 属性的时候就将外部的 eatRice 函数的地址拿到了, 所以两个实例上的 eatRice 实际上是一个。
=> 但是这打破了函数的封装性原则, 而且容易造成全局的变量污染, 所以这种方式也不可取。

4. ( 原型模式 ) - 大庇天下寒士俱欢颜

既然构造函数模式上的属性与方法都被分别定义在了每个实例上, 那么为了解决使用某些 "公共方法的需求", 我们可以使用原型模式, 将属性与方法都定义在实例所属 “类” 的原型上, 这样所有的属性与方法就都是共享的了。
需要了解的技能点 :

  • 将属性与方法全部定义在实例所属类的原型上实际上每个实例都共用这套属性与方法, 这也就意味着, 其中一个实例改动可能会影响到其他实例的访问。
function Person() {
    Person.prototype.fruits = ["apple", "banana"];
}

let tom = new Person();
let alice = new Person();
console.log(tom.fruits); // ["apple", "banana"]
console.log(alice.fruits); // ["apple", "banana"]
tom.fruits[0] = "pear";
console.log(tom.fruits); // ["pear", "banana"]
console.log(alice.fruits); // ["pear", "banana"]
  • 如果当前实例上存在与其所属类的原型同名的属性或方法, 当实例访问时默认会走实例本身的属性与方法。
function Person() {
    Person.prototype.fruits = ["apple", "banana"];
}

let tom = new Person();
let alice = new Person();
console.log(tom.fruits); // ["apple", "banana"]
console.log(alice.fruits); // ["apple", "banana"]
alice.fruits = ["西瓜", "柠檬"]; //  在 alice 实例上创建 fruits 属性
tom.fruits[0] = "pear";
console.log(tom.fruits); // ["apple", "banana"]
console.log(alice.fruits); // ["西瓜", "柠檬"]
  • 如果实例所属类的原型被重定向了, 那么可能会造成 constructor 属性丢失与 原来实例上的属性与方法丢失。
function Person() {
    
}
Person.prototype.fruits = ["apple", "banana"];
let tom = new Person();

let new_prototype = {
	foods: ["米", "油", "盐"],
};
// 访问原来原型的 constructor 属性
console.log(tom.constructor); // f Person() {  ... }

// 原型重定向
Person.prototype = new_prototype;

let alice = new Person();

// 访问新原型上的属性
console.log(tom.foods); // undefined
console.log(alice.foods); // ["米", "油", "盐"]

// 访问新原型上的 constructor 属性
console.log(alice.constructor); // ƒ Object() { [native code] }
// 访问原来原型上的属性
console.log(alice.fruits); // undefined

=> 所以需要我们重新指定重定向后的 constructor 属性(但要注意重定向前的 constructor 是不可被枚举的, 如出于严谨角度也应遵照这一点)。

function Person() {
    
}
Person.prototype.fruits = ["apple", "banana"];
let tom = new Person();

let new_prototype = {
	constructor: Person,
	foods: ["米", "油", "盐"],
};

// 不可被枚举
Object.defineProperty(Person.prototype, 'constructor', {
	enumerable: false,
});

// 访问原来原型的 constructor 属性
console.log(tom.constructor); // f Person() {  ... }

// 原型重定向
Person.prototype = new_prototype;

let alice = new Person();

// 访问新原型上的属性
console.log(tom.foods); // undefined
console.log(alice.foods); // ["米", "油", "盐"]

// 访问新原型上的 constructor 属性
console.log(alice.constructor); // f Person() {  ... }
// 访问原来原型上的属性
console.log(alice.fruits); // undefined

通过构造函数模式来构建对象

function Person() {}
Person.prototype.name = "FruitJ";
Person.prototype.sayName = function() {
	
};
let prop = {
	constructor: Person,
	name: "FruitJ",
	sayName() {
		console.log(`My name is ${ this.name }`);	
	}
};

Object.defineProperty(Person.prototype, 'constructor', {
	enumerable: false,
});

Person.prototype = prop;
let tom = new Person();
let alice = new Person();
console.log(tom.sayName === alice.sayName); // true
console.log(tom.constructor); // f Person() { ... }
console.log(alice.constructor); // f Person() { ... }

=> 原型模式的弊端在上面的 需要了解的技能点 部分已经提到过了, 就是使用原型模式的所有属性与方法都是公有的, 如果实例需要有自己的属性与方法呢 ? 所以单靠原型模式也无法解决我们的需求。(构造函数模式是全私有, 原型模式是全公有)。

5. ( 组合构造函数与原型模式 ) - 公私分明

将构造函数模式与原型模式合二为一使用, 这样就会解决前面提到的诸多问题。
每个实例上既有自己的私有属性与方法, 其所属类的原型上又有每个实例共享的公有属性与方法。

function Person(name, age, fruit) {

    this.name = name;
    this.age = age;
    this.fruit = fruit;
    this.doWork = function() {
        console.log(`${ this.name } 正在做所属于他的工作`);
    }
}

let prop = {
    constructor : Person,
    sayName() {
        console.log(`My name is ${ this.name }; ${ this.age } years old like eat fruit are ${ this.fruit.join(" ") }`);
    },
};

Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
});

Person.prototype = prop;

// 创建实例
let tom = new Person("Tom", 22, ["apple", "banana"]);
let alice = new Person("Alice", 23, ["pear", "watermelon"]);
tom.doWork(); // Tom 正在做所属于他的工作
alice.doWork(); // Alice 正在做所属于他的工作

tom.sayName(); // My name is Tom; 22 years old like eat fruit are apple banana
alice.sayName(); // My name is Alice; 23 years old like eat fruit are pear watermelon

console.log(tom.sayName === alice.sayName); // true
console.log(alice.doWork === tom.doWork); // false

tom.fruit[0] = "other fruits ...";
console.log(tom.fruit); // [ 'other fruits ...', 'banana' ]
console.log(alice.fruit); // [ 'pear', 'watermelon' ]

=> 这种组合模式是最常用的, 并且可以解决前面诸多模式的弊端。

6. (动态原型模式) - 再接再厉

  • 其实使用组合构造函数与原型模式来高效的创建实例就已经足够了, 但是其初始化原型的过程都是在构造函数外部进行的, 这也打破了函数的封装性的原则, 但是如果将这个初始化过程放在构造函数内部就还会导致一个问题, 就是每次 new 构造函数创建实例的时候都会走一遍初始化原型的流程, 所以这是个优化点。
  • 动态原型模式就是在初始化原型属性与方法之前, 事先判断, 如果当前实例具备该方法那么就不在原型上再次重复定义, 如果当前实例不具备该方法那么仅在第一次实例化的时候参与判断然后在原型上定义该方法, 下次再实例化其他实例的时候就不会出现重复定义的情况了。这样尽管要创建多个实例, 要 new 多次也没有关系了。
    function Person(name, age) {
        this.name = name;
        this.age = age;

        // 动态初始化原型方法
        if(typeof this.sayName !== 'function') {
            console.log("未给实例初始化 sayName 方法, 即将在其所指原型上进行初始化! ");
            Person.prototype.sayName = function() {
                console.log("prototype function ...");
                console.log(`My name is ${ this.name }`);
            };
        }
    }

let tom = new Person("Tom", 22); // 未给实例初始化 sayName 方法, 即将在其所指原型上进行初始化! 
let alice = new Person("Alice", 21); // 实例化 tom 的时候已经定义 sayName 方法了, 就不再重复定义了。

tom.sayName(); // prototype function ...      My name is Tom
alice.sayName(); // prototype function ...     My name is Alice

=> 这种模式真的是实现了我们高效创建对象的需求了, 所以 组合模式与这个动态原型模式应该是我们今后创建实例的首选方式 !

7. (寄生构造函数模式) - 他山之石可以攻玉

寄生构造函数模式与前面讨论的几种模式的应用场景不同, 笔者认为这个寄生构造函数模式更多的应该是应用于对内置 “类” 的一个增强。在不考虑继承的情况下, 既想使用内置类的属性与方法, 又想对其增强, 但还不想破坏原有的内置类的封装。

function MyArray() {
    // 创建数组实例
    let arr = new Array;
    // 初始化数据
    arr.push.apply(arr, arguments);
    // 添加额外方法
    arr.custom = function() {
        console.log("custom ...");
        console.log(this.join("^"));
    };
    return arr;
}

// 创建实例
let arr = new MyArray(1, 2, 3, 4, 5, 6);
arr.custom(); // custom ... | 1^2^3^4^5^6
console.log(arr); // [ 1, 2, 3, 4, 5, 6, custom: ƒ ]
arr.push(7, 8, 9);
arr.forEach(item => {
    console.log(item); // 1 ~ 9  
});
console.log(arr.constructor); // ƒ Array() { [native code] }
let ary = [1, 2, 3, 4, 5, 6];
ary.custom(); // TypeError: ary.custom is not a function

这样既可以调用原生的数组方法也可以调用增强后的数组方法, 实际上真正有用的是那个原生的数组对象, 只不过借了 MyArray 的壳子, 在其实例上进行扩展。所以这就是所谓的 "寄生"

8. (稳妥模式) - 畏首畏尾

在某些强调安全的场景下, 尽量避免使用 this 与 new 。所以稳妥构造模式就是强调不使用 this 与 new 来构建实例 。

function MyArray() {
    // 创建数组实例
    let arr = new Array;
    // 初始化数据
    arr.push.apply(arr, arguments);
    // 添加额外方法
    arr.custom = function() {
        console.log("custom ...");
        console.log(arr.join("^"));
    };
    return arr;
}

let arr = MyArray(1, 2, 3, 4, 5, 6);
console.log(arr); // [ 1, 2, 3, 4, 5, 6, custom: [Function] ]

arr.custom(); // custom ... | 1^2^3^4^5^6
console.log(arr.constructor); // [Function: Array]

9. 工厂模式、寄生构造函数模式、稳妥模式的异同 :

同 :
三种方式创造出来的实例, 每个实例身上的属性与方法都是独立一套, 缺乏公有属性与方法。
异 :
工厂模式与稳妥模式形式基本一致, 两者之间的划分, 取决于是否使用了 this 或者 new。如果使用了 this 则为工厂模式、如果使用了this / new 则为寄生构造函数模式、如果不使用 new 和 this 则为稳妥模式。

三、 JavaScript 中的 “类” 之间的继承模式

1. 原型链继承

概述 :

  • 原型链继承主要是借助原型的特点和类与实例、实例与实例之间的关系【原型链】来实现的继承继承机制, 基于原型链的思想, 子类的实例想要调取到父类的方法必须让子类的原型指向父类的实例
  • 这样一来子类的实例既可以访问到自己的私有属性与方法也可以沿着作用域链访问到父类的属性与方法。

原型继承面临的问题 :

  • 在子类继承之前, 在原有的原型上定义的属性或者方法在继承之后会丢失, 所以为子类的原型(被继承的那个父类的实例)扩展方法的时候最好在子类继承父类的行为发生之后。
  • 继承之后, 子类实例访问的 constructor 属性指向了父类而不是子类, 所以需要手动的在子类的原型(被继承的那个父类的实例)上, 添加 constructor 属性并让其指向子类, 同时要设置为不可被枚举。
  • 子类实例不论谁操作了其所属类的原型的引用类型数据都会影响到其他实例的访问
// 原型链继承
// 生物类(父类 -> 基类)
function Biology(features, fruits) {

    this.features = features;
    this.fruits = fruits;
}
// 在生物原型上定义呼吸的方法
Biology.prototype.breathing = function () {
    console.log("呼吸 ...");
};

// 人类(子类)
function Person(name, age) {

    this.name = name;
    this.age = age;
}

// 在人类的原型上定义 action【动作】方法
// 问题一 : 在子类继承之前, 在原有的原型上定义的属性或者方法在继承之后会丢失
/* Person.prototype.action = function() {
    console.log("软件研发 ...");
}; */

// 子类继承父类
Person.prototype = new Biology("高智能", ["apple", "banana"]);

// 在人类的原型上定义 action【动作】方法
Person.prototype.action = function () {
    console.log("软件研发 ...");
};

// 实例化子类实例
let tom = new Person("TOM", 21);
// 调用子类方法
tom.action(); // 软件研发 ...
// 调用父类方法
tom.breathing(); // 呼吸 ...
// 问题二、继承之后, 子类的 constructor 属性指向了父类而不是子类
console.log(tom.constructor); // [Function: Biology]
console.log(Person.prototype.constructor); // [Function: Biology]

Person.prototype.constructor = Person;

console.log(tom.constructor);

for (let key in Person.prototype) {
    console.log(`key: ${key}-> ${Person.prototype[key]}`);
    /*
        key: features-> 高智能
        key: action-> function() {
            console.log("软件研发 ...");
        }
        key: constructor-> function Person(name, age) {

            this.name = name;
            this.age = age;
        }
        key: breathing-> function() {
            console.log("呼吸 ...");
        }
    */
}

// 继承之后的并且还原子类原型的 constructor 属性之后应该将其置为不可被枚举
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
});

let alice = new Person('Alice', 22);
console.log(tom.fruits); // [ 'apple', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
// 问题三、子类实例不论谁操作了其所属类的原型的引用类型数据都会影响到其他实例的访问
tom.fruits[0] = 'pear';

// 更改 fruits 后
console.log(tom.fruits); // [ 'pear', 'banana' ]
console.log(alice.fruits); // [ 'pear', 'banana' ]

原型链继承 : 示例代码配图 :
在这里插入图片描述

2. 借用构造函数

概述 :
鉴于原型链继承中对于子类实例操作子类原型上的引用类型值的时候出现的问题, 借用构造函数继承方案可以借助父类的构造函数来使子类的实例 “继承” 其预定义的属性与方法。
原理 :
在子类的构造函数实例化对象的时候通过 apply 或者 call 来调用父类的构造函数, 并更新父类的构造函数中的 this 为当前正在实例化的子类的实例, 这样一来子类的实例就会取得父类上所定义的属性与方法。
问题 :
子类实例借用父类构造函数实现 "继承" 实际上会把 “继承” 的结果都挂载到自身实例上。
这种方式虽然可行, 但是无法继承定义在父类原型上的属性与方法, 再者与用构造函数模式创建实例一样, “继承” 过来的属性与方法都是私有的, 就谈不上函数复用了。

// 借用构造函数继承
// 生物类(父类 -> 基类)
function Biology(fruits) {

    this.fruits = fruits;
}

// 在生物原型上定义呼吸的方法
Biology.prototype.breathing = function () {
    console.log("呼吸 ...");
};

// 人类(子类)
function Person(name, age) {

    this.name = name;
    this.age = age;

    // 继承父类(通过 apply 或者 call 更换 this 指向来实现继承)
    Biology.call(this, ["apple", "banana"]);
}

// 实例化子类实例
let tom = new Person("Tom", 21);
let alice = new Person("Alice", 22);

console.log(tom.fruits); // [ 'apple', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
tom.fruits[0] = "pear"; 
console.log(tom.fruits); // [ 'pear', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
console.log(tom); // Person { name: 'Tom', age: 21, fruits: [ 'pear', 'banana' ] }
tom.breathing(); // TypeError: tom.breathing is not a function

借用构造函数继承 : 示例代码配图 :
在这里插入图片描述

3. 组合继承

概述 :
组合继承实际上是将 原型链继承借用构造函数继承, 组合起来实现的一种继承方式。
这种继承方式完美的解决了之前的继承方式的不足, 可以保证 ·”公私分明“ 。·.

// 组合继承
// 生物类(父类 -> 基类)
function Biology(features, fruits) {

    this.features = features;
    this.fruits = fruits;
}

// 在生物原型上定义呼吸的方法
Biology.prototype.breathing = function () {
    console.log("呼吸 ...");
};

// 人类(子类)
function Person(name, age) {

    this.name = name;
    this.age = age;
    // 组合继承之继承父类的私有属性与方法
    Biology.call(this, ...["高智能", ["apple", "banana"]]);
}

// 组合继承之继承父类的公有(原型)属性与方法 
Person.prototype = new Biology();

// 设置 constructor 属性
Person.prototype.constructor = Person;
Object.defineProperty(Person.prototype, 'constructor', {
    enumerable: false,
});

// 为子类原型添加方法
Person.prototype.action = function() {
    console.log("软件研发 ...");
};

// 创建实例
let tom = new Person("TOM", 21);
let alice = new Person("Alice", 22);
tom.action(); // 软件研发 ...
tom.breathing(); // 呼吸 ...
alice.action(); // 软件研发 ...
alice.breathing(); // 呼吸 ...

console.log(tom.fruits); // [ 'apple', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
tom.fruits[0] = "pear";
console.log(tom.fruits); // [ 'pear', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]

console.log(tom instanceof Person); // true

组合继承 : 示例代码配图 :
在这里插入图片描述

4. 原型式继承

概述 :

  • 原型式继承是通过 Object.create 来实现继承的, Object.create 方法在执行的时候就会创建一个子类的实例并将其 __proto__ 属性指向父类的实例【此步骤为继承】。
  • 实际上该方法与原型链继承差不多, 只不过这个原型式继承没有自己构建子类而是通过 Object.create 返回来一个实例, 所以笔者将其 constructor 属性指向了 Object
  • Object.create 是如何实现继承的 ? 下面笔者手写了一版 Object.prototype.create 的实现, 并将其定义在 Object.prototype 上命名为 myCreate, 也就是说 Object.create 方法里面要做的事情才是我们想要做的, 知晓了原理完全可以抛弃 Object.create 自己实现一个也能跑的通 。

原型式继承面临的问题 :

  • 继承之后 constructor 属性的指向问题。
  • 子类实例不论谁操作了其所属类的原型的引用类型数据都会影响到其他实例的访问。
// 原型式继承

// 手动实现 Object.create 方法
function create(o, config) {
    function Foo() { }
    Foo.prototype = o;
    let instance = new Foo();
    if(typeof config === "object" && config !== null) {
        Object.defineProperties(instance, config);
    }
    return instance;
}
// 暴露在 Object 原型上
Object.prototype.myCreate = create;

// 生物类(父类 -> 基类)
function Biology(features, fruits) {

    this.features = features;
    this.fruits = fruits;
}
// 在生物原型上定义呼吸的方法
Biology.prototype.breathing = function () {
    console.log(`${ this.name }正在呼吸 ...`);
};

// 人类(子类)
function Person(name, age) {

    this.name = name;
    this.age = age;
}

// 定义充当子类原型的父类实例
let baseInstance = new Biology("高智能", ["apple", "banana"]);

// 提供配置信息的工厂函数
function ConfigFactory(name, age) {// 私有属性与私有方法的配置信息
    return {
        name: { value: name, enumerable: true, },
        age: { value: age, enumerable: true, },
        action: { value: function() { console.log(`${ this.name }正在进行软件研发 ...`); }, enumerable: true, },
    };
}

// 继承并获取实例
let tom = Object.myCreate(baseInstance, new ConfigFactory("TOM", 21));
let alice = Object.myCreate(baseInstance, new ConfigFactory("Alice", 22));
// 重置子类原型的 constructor 属性的指向
Object.defineProperty(baseInstance, 'constructor', {
    enumerable: false,
    value: Object,
});

console.log(tom); // { name: 'TOM', age: 21, action: [Function: value] }
console.log(tom.constructor); // [Function: Object]
console.log(tom.constructor === baseInstance.constructor); // true
console.log(alice); // { name: 'Alice', age: 22, action: [Function: value] }
console.log(tom.fruits); // [ 'apple', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
tom.fruits[0] = "pear";
console.log(tom.fruits); // [ 'pear', 'banana' ]
console.log(alice.fruits); // [ 'pear', 'banana' ]
tom.breathing(); // TOM正在呼吸 ...
alice.action(); // Alice正在进行软件研发 ...
console.log(tom.action === alice.action); // false

原型式继承 : 示例代码配图 :
在这里插入图片描述
再贴一遍手写 Object.create 的代码 :

// 手动实现 Object.create 方法
function create(o, config) {
    function Foo() { }
    Foo.prototype = o;
    let instance = new Foo();
    if(typeof config === "object" && config !== null) {
        Object.defineProperties(instance, config);
    }
    return instance;
}

Object.prototype.myCreate = create;

5. 寄生式继承

概述 :

  • 寄生式继承实际上就是在原型式继承的基础上在外边包了一层函数, 在该函数内部可以实现对将要继承的对象增强(但实际上这种方式与 Object.create 方法相比就略显鸡肋了, Object.create 的 2 参明明可以自己为实例添加私有属性来实现增强, 笔者感觉这个寄生式继承是一个鸡肋, 但了解还是要了解)。
    原型继承面临的问题 :
  • 由于寄生式继承只是在原型式继承的基础上包了一层函数, 所以其面临的主要问题与原型式继承基本相同。
// 寄生式继承(实际上就是在原型式继承的基础上再封装一层)
// 生物类(父类 -> 基类)
function Biology(features, fruits) {

    this.features = features;
    this.fruits = fruits;
}
// 在生物原型上定义呼吸的方法
Biology.prototype.breathing = function () {
    console.log(`${ this.name }正在呼吸 ...`);
};

let baseInstance = new Biology("高智能", ["apple", "banana"]);

function ConfigFactory(name, age) {// 私有属性与私有方法的配置信息
    return {
        name: name,
        age: age,
        action() { console.log(`${ this.name }正在进行软件研发 ...`); },
    };
}

// 获取子类实例的方法
function obtainInstance(o, config) {
    console.log(config);
    let instance = Object.create(o);
    for(let key in config) {
        instance[key] = config[key];
    }
    return instance;
}

// 创建实例

let tom = obtainInstance(baseInstance, new ConfigFactory("TOM", 21));
let alice = obtainInstance(baseInstance, new ConfigFactory("Alice", 22));

console.log(tom.fruits); // [ 'apple', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
tom.fruits[0] = "pear";
console.log(tom.fruits); // [ 'pear', 'banana' ]
console.log(alice.fruits); // [ 'pear', 'banana' ]

console.log(tom.action === alice.action); // false
tom.action(); // TOM正在进行软件研发 ...
alice.breathing(); // Alice正在呼吸 ...

**寄生式继承 : 示例代码配图 : **
在这里插入图片描述

6. 寄生组合式继承

概述 :

  • 寄生组合式继承就是组合式继承的升级版, 组合继承是通过将 原型链继承借用构造函数继承组合起来的继承 也即通过借用构造函数式继承将父类的私有属性与方法拷贝一份 mixin 到子类的实例中也作为子类的私有属性与方法, 然后通过原型链继承通过中间的父类实例来获取使用父类原型属性 / 方法的权限, 但是在这继承的整个过程分别调用了两次父类的构造函数(通过借用构造函数式继承初始化子类实例的私有属性与方法时以及通过原型链继承获取父类的原型属性 / 方法的使用权限时)。
  • 针对组合继承的调用两次构造函数的行为, 寄生组合式继承基于组合式继承的思想将 借用构造函数继承寄生式继承 进行聚合, 在实现了组合式继承的全部功能的同时仅需要调用一次父类的构造函数。
  • 相对于上述的继承方式而言, 寄生组合式继承算得上是比较好用的了。
// 寄生组合式继承
// 生物类(父类 -> 基类)
function Biology(features, fruits, eat) {

    this.features = features;
    this.fruits = fruits;
    this.eat = eat;
}
// 在生物原型上定义呼吸的方法
Biology.prototype.breathing = function () {
    console.log(`${ this.name }正在呼吸 ...`);
};

// 人类(子类)
function Person(name, age) {

    this.name = name;
    this.age = age;
    this.privateFunc = function() {
        console.log(`${ this.name }: 这是我自己的私有方法 ...`);
    };
    // 组合继承之继承父类的私有属性与方法(借助借用构造函数式继承)
    Biology.call(this, ...["高智能", ["apple", "banana"], function() { console.log(`${ this.name }正在吃饭 ...`); }]);
}

function extendsToPrototype(baseClass, subClass) {
    let instance = Object.create(baseClass.prototype);
    Object.defineProperty(instance, 'constructor', {
        enumerable: false,
        value: Person,
    });
    subClass.prototype = instance;
}
// 组合继承之继承父类的公有属性与方法(寄生式式继承)
extendsToPrototype(Biology, Person);

Person.prototype.action = function() {
    console.log(`${ this.name } 正在编写程序`);
};

// 创建实例
let tom = new Person("TOM", 21);
let alice = new Person("Alice", 22);

tom.action(); // TOM 正在编写程序
alice.breathing(); // Alice正在呼吸 ...
console.log(tom.action === alice.action); // true
console.log(tom.breathing === alice.breathing); // true
tom.eat(); // TOM正在吃饭 ...
console.log(tom.eat === alice.eat); // false
alice.privateFunc(); // Alice: 这是我自己的私有方法 ...
console.log(tom.privateFunc === alice.privateFunc); // false

console.log(tom.fruits); // [ 'apple', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]
tom.fruits[0] = "pear";
console.log(tom.fruits); // [ 'pear', 'banana' ]
console.log(alice.fruits); // [ 'apple', 'banana' ]

寄生组合式继承 : 示例代码配图 :
在这里插入图片描述

四、总结

  • 创建实例的模式 : 首选 单例模式, 如果需要创建多个实例首选 组合构造函数与原型模式动态原型模式
  • 类间的继承模式 : 首选寄生组合式继承模式组合继承模式
  • 按需选择: 实际开发中设计模式的选择还是需要根据实际需求来选择与定制, 谈到这就又要回归到基础了, 万丈高楼平地起嘛 !

五、后语 :

  • 本篇文章写到这里算是暂时告一段落了, 设计模式是个抽象概念, 笔者曾一度去想要 “背” 某个模式但发现反而落入下乘, 这个东西不能拿各种线条去描绘它, 时刻提醒自己这只是一种想法,一个思想 …
  • 谢谢观赏, 如有谬误谢谢指出, 共同进步 !!!

六、参考资料 :

  • 《红宝书》
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值