目录
(2)、Object.defineProperty() 方法
2、为对象定义多个属性—— Object.defineProperties() 方法
3、获取对象属性的描述符—— getOwnPropertyDescriptor() 方法
(1)、constructor 属性、 [[Prototype]] 属性
(2)、 Object.create() 方法规范了原型式继承
一、面向对象基本特征
Javascript是一种基于对象的语言。但它又不是真正的面向对象编程(OOP)语言,因为它的语法中没有class(类)—–es6以前是这样的。所以es5只有使用函数模拟的面向对象。
真正的面向对象编程有以下三个特征:
- 封装:也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
- 继承:通过继承创建的新类称为“子类”或“派生类”。继承的过程,就是从一般到特殊的过程。
- 多态:对象的多功能,多方法,一个方法多种表现形式。
二、理解对象
- 在 JavaScript中,对象是一个无序的集合,其属性可以包含基本值、对象和函数。
- 在 JavaScript中,每一个对象都是给予一个引用类型创建的,这个引用类型可以是原生类型(戳此了解引用类型),也可以是开发者定义的类型。
对象的特征:
- 对象具有唯一标识性:即使完全相同的两个对象,也并非同一个对象。
- 对象有状态:对象具有状态,同一对象可能处于不同状态之下。
- 对象具有行为:即对象的状态,可能因为它的行为产生变迁。
对象具有唯一标识的内存地址,所以具有唯一的标识。
JavaScript 中对象独有的特征是:对象具有高度的动态性,这是因为 JavaScript 赋予了使用者在运行时为对象添改状态和行为的能力。
实际上 JavaScript 对象的运行时是一个“属性的集合”,属性以字符串或者 Symbol 为 key,以数据属性特征值或者访问器属性特征值为 value。
1、对象的属性
JavaScript 的属性被设计成比别的语言更加复杂的形式,它提供了数据属性和访问器属性(getter/setter)两类。
javaScript 对象的三种属性:
- 数据属性:拥有一个确定的值的属性。这也是最常见的属性。
- 访问器属性:通过 getter 和 setter 进行读取和赋值的属性。
- 内部属性:由 JavaScript 引擎内部使用的属性,ES6 之前必须用 Object.getPrototypeOf() 方法来读取和设置。比如:
- 每个对象都有一个内部属性 [[Prototype]]。如果是函数对象,改变其 prototype 属性后,其下所有的实例也会发生改变。
- 数组对象的 length 属性。
- DOM 的 innerHTML,我们赋值时是一个字符串,再取出时,这字符串可能会与原来的不一样, 并且在原元素上生成了不一样的子节点。比如,某一数组,它的长度为10, 当我们设置它为11时,它就会增加一个undefined元素,再设置为9时,就会从后面删掉两个元素。
(1)、数据属性 和 访问器属性
configurable | enumerable | value | writable | get | set | |
数据属性 | Yes | Yes | Yes | Yes | No | No |
访问器属性 | Yes | Yes | No | No | Yes | Yes |
数据属性包括:configurable、enumerable、value 和 writable。
访问器属性包括:configurable、enumerable、get 和 set。
- configrable 当且仅当该属性的configrable为true时,value才能被修改、删除,默认false。
- enumerable 当且仅当该属性的enumerable为true时,才允许该value出现在对象的枚举属性(比如:for...in 或 Object.keys())中,默认false。
- value:该属性对应的值,默认为 undefined。
- writable:当且仅当该属性的writable为true时,value才能被赋值运算符改变,否则报错。默认为 false。
- get:在读取属性时调用的函数,默认值为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
- set:在写入属性时调用的函数,默认值为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
(2)、Object.defineProperty() 方法
- 通过 Object.defineProperty() 方法可以创建一个变量。通过 Object.defineProperty() 方法也可以创建一个常量(当 writable: false 且 configurable: false 时,不可修改,不可重新定义或者删除),类似于 ES6 中的 const 声明的常量。
- 通过 Object.defineProperty() 方法为定义象属性,针对 数据属性 和 访问器属性 各有一套模式,不过,这两套模式不能混合使用(设置set或者get,就不能在设置value和wriable,否则会报错)。
①、用 Object.defineProperty() 方法 操作 数据属性(writable 和 value)
--> writable: true
let Person = {}
Object.defineProperty(Person, 'sex', {
configurable: true,
enumerable: true,
value: '男',
writable:true,
})
console.log('---给person对象写入一个属性', Person);
Person.sex = '女';
console.log('---修改后的person', Person);
结果:
--> writable: false
let Person = {};
Object.defineProperty(Person, 'sex', {
configurable: true,
enumerable: true,
value: '男',
writable:false,
})
console.log('---给person对象写入一个属性', Person);
Person.sex = '女';
console.log('---修改后的person', Person);
结果:
②、用 Object.defineProperty() 方法 操作 访问器属性(getter 和 setter)
var book = {
_year: 2027,
edition: 1
};
Object.defineProperty(book, "year", {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > 2027){
this._year = newValue;
this.edition += newValue - 2027;
}
}
});
book.year = 2029;
console.log(book.edition); // 3
2、为对象定义多个属性—— Object.defineProperties() 方法
var book = {};
Object.defineProperties(book, {
_year: {
writable: true,
value: 2027
},
edition: {
writable: true,
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > 2027){
this._year = newValue;
this.edition += newValue - 2027;
}
}
}
});
3、获取对象属性的描述符—— getOwnPropertyDescriptor() 方法
对象属性的描述符也就是对象的内部属性:configurable、enumerable、value、writable、get 和 set。
var book = {};
Object.defineProperties(book, {
_year: {
value: 2027
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if(newValue > 2027){
this._year = newValue;
this.edition += newValue - 2027;
}
}
}
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
console.log(descriptor.value); // 2027
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
console.log(descriptor.value); // undefined
console.log(descriptor.configurable); // false
console.log(typeof descriptor.get); // "function"
三、创建对象
1、JS 常见的设计模式(★★★★★)
请戳这里:js 设计模式_weixin79893765432...的博客-CSDN博客_js设计模式 阮一峰
2、工厂模式
- 虽然 Object构造函数 或 字面量 都可以创建一个对象,但是这些方式有一个明显的问题:使用同一个借口创建很多对象,会产生大量的的重复代码。为了解决这个问题,出现了 “工厂模式”。
- 工厂模式抽象了创建具体对象的过程。考虑到 JavaScript 在 ES6 之前无法创建类,卡法人员就发明了一种函数,用函数来封装以特定接口创建对象的细节。
- 工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
function createPerson(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job
o.sayName = function(){
console.log(this.name);
};
return o;
}
var person1 = createPerson("marry", 18, "doctor");
var person2 = createPerson("lily", 21, "teacher");
person1.sayName(); // marry
person2.sayName(); // lily
上述代码,函数 createPerson() 能够根据接受的参数来构建一个三个属性一个方法的 person 对象。
3、构造函数模式
- 构造函数始终都应该以一个大写字母开头。
- 除了通过原生构造函数(Object、Array、Number 和 String)来创建特定的对象类型外,我们还可以通过创建自定义的构造函数,从而定义 自定义对象类型 的属性和方法。
- 当创建了自定义的构造函数后,其原型只会取得 constructor 属性,其他方法都是从原生 Object类型 继承而来。
- 构造函数模式 既解决了创建多个相似对象的问题,又解决了对象识别的问题(即怎样知道一个对象的类型)。
(1)、用构造函数模式自定义对象
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person("marry", 18, "doctor");
var person2 = new Person("lily", 21, "teacher");
person1.sayName(); // marry
person2.sayName(); // lily
与工厂模式的案例相比,上述代码中,Person() 函数取代了 createPerson() 函数。他们的不同之处在于:
- Person() 函数没有显式的创建对象;
- Person() 函数直接将属性和方法赋值给了 this 对象;
- Person() 函数没有 return 语句。
(2)、用 new 操作符来创建一个对象时,都经历了什么?
- 创建一个对象;
- 将构造函数的作用域赋给新对象(因此 this 就指向了这个心对象);
- 执行构造函数中的代码(为这个新对象添加属性);
- 返回新对象。
更细致的讲,new 运算接受一个构造器和一组调用参数,实际上做了几件事:
- 以构造器的 prototype 属性(注意与私有字段[[prototype]]的区分)为原型,创建新对象;
- 将 this 和调用参数传给构造器,执行;
- 如果构造器返回的是对象,则返回,否则返回第一步创建的对象。
(3)、检测自定义对象类型
- 通过对象的 constructor 属性来检测自定义对象的类型。不过,一般检测对象类型都用 instanceof 操作符,instanceof 操作符可以检测所有的对象类型,所以它也可以检测自定义对象类型。
- 通过构造函数创建的自定义对象,既是Object 的实例,同时又是 “自定义对象类型” 的实例。
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job
this.sayName = function(){
console.log(this.name);
};
}
var person1 = new Person("marry", 18, "doctor");
var person2 = new Person("lily", 21, "teacher");
console.log(person1.constructor == Person); // true
console.log(person2.constructor == Person); // true
console.log(person1 instanceof Person); // true
console.log(person2 instanceof Person); // true
console.log(person1 instanceof Object); // true
console.log(person2 instanceof Object); // true
上述代码中,为什么 person1 对象 和 person2 对象 既是自定义的 Person 构造函数模式的实例又是 Object 的实例呢?
因为所有对象均继承自 Object。(关于“继承”,请继续往下看。)
4、原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象,这个原型对象储存着原型上的属性和方法。使用原型对象可以让所有的对象实例共享它包含的属性和方法,换句换说,不必再构造函数中定义对象的实例信息,而是可以将这些信息直接添加到原型对象中。
原型系统可以说相当简单,我可以用两条概括:
- 如果所有对象都有私有字段[[prototype]],就是对象的原型;
- 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止。
从 ES6 以来,JavaScript 提供了一系列内置函数,以便更为直接地访问操纵原型。三个方法分别为:
- Object.create 根据指定的原型创建新对象,原型可以是 null;
- Object.getPrototypeOf 获得一个对象的原型;
- Object.setPrototypeOf 设置一个对象的原型。利用这三个方法,我们可以完全抛开类的思维,利用原型来实现抽象和复用。
function Person(){
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.sayName(); // marry
person2.sayName(); // marry
console.log(person1.sayName === person2.sayName); // true
上述代码,我将 sayName() 方法和所有属性直接添加到了 Person 的 prototype 属性中,仍然可以通过构造函数来创建对象,但与构造函数不同的是,新对象的这些属性和方法是有所有实例共享的。
(1)、constructor 属性、 [[Prototype]] 属性
- constructor 属性:在默认情况下,所有原型对象都会自动获得一个 constructor 属性,这个属性是一个指向 prototype 属性所在函数的指针。
- [[Prototype]] 属性:当创建了自定义的构造函数后,其原型只会取得 constructor 属性,其他方法都是从原生 Object类型 继承而来。当调用构造函数创建一个新实例后,该实例内部将包含一个指针(内部属性 [[Prototype]]),指向构造函数的原型对象。借助浏览器可以访问到——浏览器在每个对象上都支持一个属性:__proto__ 属性。通过__proto__ 属性可以访问到 [[Prototype]]。
以 Person 构造函数 和 Person.prototype 创建的实例的代码为例:
function Person(){
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
person1.sayName(); // marry
person2.sayName(); // marry
console.log(Person.prototype.constructor === Person); // true
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person2).name); // "marry"
上水代码中,Person 构造函数、Person 的原型属性以及 Person 现有的两个实例之间的关系如下图:
由上图可知,Person 的每个实例都包含一个内部属性 [[prototype]],该属性指向了 Person.prototype。虽然这两个实例不包含属性和方法,但是由于内部属性 [[prototype]] 属性指向了 Person.prototype,所以我们就可以调用 Person1.sayName() 了。
(2)、确定原型与实例的关系
有 3中方法可以确定原型与实例的关系:
- instanceof 操作符;
- Object.prototype.isPrototypeOf() 方法;
- Object.getPrototypeOf() 方法。
其中 Object.getPrototypeOf() 方法会返回该构造函数的原型的值。
function Person(){
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
}
}
var person1 = new Person();
var person2 = new Person();
console.log(Object.getPrototypeOf(person1) === Person.prototype); // true
console.log(Object.getPrototypeOf(person2).name); // "marry"
console.log(Object.prototype.isPrototypeOf(person1)); // true
console.log(Object.prototype.isPrototypeOf(person2)); // true
console.log(person1 instanceof Object); // true
console.log(person1 instanceof Person); // true
(3)、在读取对象的某个属性时,都发生了什么?
每当代码读取对象的某个属性时,都会先后执行两次搜索,搜索首先从对象实例本身开始,如果在实例中找到了具有给定名字的属性,就返回该属性的值,这就是第一次搜索;如果没找到,就原型对象中继续查找具有给定名字的属性,如果在原型对象中找到了这个属性,就返回该属性的值,还没找到的话,就报错未定义,这就是第二次搜索。
例如,在实例中修改原型中的值
function Person(){
}
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "lily";
console.log(person1.name); // lily
console.log(person2.name); // marry
delete person1.name;
console.log(person1.name); // marry
由上述代码可知,在实例中添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。其原理正是“每当代码读取对象的某个属性时,都会先后在实例中搜索,找不到时再在原型上搜索”。另外,使用 delete 操作符可以完全删除实例属性,从而恢复了对原型中属性的链接,让我们能够重新访问原型中的属性。
(4)、in 操作符在原型中的运用
in 操作符有两种用法:
- for-in:遍历可枚举属性;
- 单独使用:检测对象能否访问到属性,只要对象能够访问到属性就返回 true。
function Person(){
}
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person2.name = "lily";
console.log("name" in person1); // true
console.log("name" in person2); // true
console.log("age" in person1); // false
(5)、hasOwnProperty() 方法
hasOwnProperty() 方法用于检测一个属性是否存在于实例中,如果存在就返回 true。
function Person(){
}
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person2.name = "lily";
console.log(person1.hasOwnProperty("name")); // false
console.log(person2.hasOwnProperty("name")); // true
老版本的 IE 的 DOM Element 是没有 hasOwnProperty 方法的。另外, window 对象也没有 hasOwnProperty 方法的。怎么办?可以考虑用 Object 对象的 hasOwnProperty 试试。
语法:Object.prototype.hasOwnProperty.call(对象名, 属性名);
- call() 方法使用一个指定的 this 值和单独给出的一个或多个参数来调用一个函数(这里的函数指的是 hasOwnProperty())。
function MyObj(name, attr) {
this.name = name;
this.sayHi = function () {
return 'hi boy';
}
}
var obj = new MyObj();
var arr = [];
for (var item in obj) {
if(Object.hasOwnProperty.call(obj, item)){
arr.push(item);
}
}
console.log(arr); // ["name", "sayHi"]
与 Object.keys() 方法的对比详见: js 获取对象内属性的个数以及获取对象的属性和方法(Object.keys()、Object.getOwnPropertyNames()、for...in...对比)_weixin79893765432...的博客-CSDN博客
(6)、Object.keys() 方法
Object.keys() 方法用于获取对象上所有可枚举的实例属性,和 for-in 语句的作用一样。Object.keys() 方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function Person(){
}
Person.prototype.name = "marry";
Person.prototype.sayName = function(){
console.log(this.name);
};
var keys = Object.keys(Person.prototype);
console.log(keys); // ["name", "sayName"]
var p1 = new Person();
p1.name = "lily";
p1.age = 22;
var p1keys = Object.keys(p1);
console.log(p1keys); // ["name", "age"]
上述代码中,Object.keys() 方法基于原型使用时,就返回包含所有原型中属性的数组;基于实例使用时,就返回包含所有实例中属性的数组。
(7)、简化原型语法
用一个对象字面量来表示原型对象,这个对象字面量包含所有原型的属性和方法。
例如:
function Person(){
}
Person.prototype = {
name: "marry",
sayName: function(){
console.log(this.name);
}
};
(8)、原型的动态性
根据需求,随时重写原型,然后创建实例并调用原型,新的原型生效。若在重写之前创建实例,调用重写后的原型属性,会报错。
function Person(){
}
var friend = new Person;
Person.prototype = {
name: "marry",
sayName: function(){
console.log(this.name);
}
};
var friend2 = new Person();
friend2.sayName(); // marry
friend.sayName(); // error
重写原型对象,会切断现有原型与任何之前已经存在的对象实例的联系。
(9)、原生对象的原型
所有原生引用类型(Object、String、Number 等)都在其构造函数的原型上定义了方法,通过访问原生对象的原型,不仅可以区的所有默认方法的引用,而且还可以定义新方法, 可以像修改自定义对象的原型一样修改原生对象的原型。
例如:
String.prototype.startsWith = function(text){
return this.indexOf(text) == 0;
};
var str = "hi boy";
console.log(str.startsWith('hi')); // true
(10)、原型对象的问题
原型中所有属性是被多个实例共享的,对于包含引用类型值的属性来说,通过实例修改引用类型的值时,违背了“实例都是有自己的属性的”这一原则,不但改变了当前实例,还擅自改变了原型对象的该属性的值。
function Person(){
}
Person.prototype = {
name: 'marry',
age: 18,
friends:['lily', 'jock'],
sayName: function(){
console.log(this.name);
}
};
var person1 = new Person();
var person2 = new Person();
person1.age = '21';
person1.friends.push('mack');
console.log(person1.age); // 21
console.log(person2.age); // 18
console.log(person1.friends); // ["lily", "jock", "mack"]
console.log(person2.friends); // ["lily", "jock", "mack"]
上述代码中,我们基于自定义类型 Person 构造函数创建了两个实例,接着,我们修改了 person1.age(基本类型值)和 person1.friends(引用类型值)。最后分别打印实例 person1 和 实例 person2 中的 age 和 friends 的值,发现只有基本类型值遵循了“实例都是有自己的属性的”,person1.age 的修改并没有影响到 Person.prototype 对象中的age的值,所以,person2.age 依然是18。但是通过实例修改引用类型的值时,违背了“实例都是有自己的属性的”这一原则,不但改变了当前实例,还擅自改变了原型对象的该属性的值。
5、通过构造函数和原型的混成模式来创建自定义类型(★)
- 创建自定义类型的最常见方式,就是组合使用构造函数模式和原型模式。
- 构造函数模式用于定义实例的属性,原型模式用于定义方法和共享的属性。于是,每个实例都会有一份实例属性的副本,同时又共享着对方法的引用,最大限度的节省了内存。
- 这种构造函数和原型的混成模式,支持向构造函数传递参数。
例如:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.friends = ["Marry", "Lily"];
}
Person.prototype = {
constructor: Person,
sayName: function(){
console.log(this.name);
}
}
var person1 = new Person("Jack", 18, "Doctor");
var person2 = new Person("Baer", 26, "Teacher");
person1.friends.push("Mike");
console.log(person1.friends); // ["Marry", "Lily", "Mike"]
console.log(person2.friends); // ["Marry", "Lily"]
person1.sayName(); // Jack
person2.sayName(); // Baer
6、动态原型模式
- 动态原型模式把所有的信息都封装在了构造函数中,通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的有点。
- 必要情况下才启动原型:通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
- 如何检查某个应该存在的方法是否有效呢?
- 用 typeof 操作符 和 instanceof 操作符来检测。
比如:
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
if(typeof this.sayName != "function"){
Person.prototype.sayName = function(){
console.log(this.name);
};
}
}
var friend = new Person("marry", 21, "Doctor");
friend.sayName(); // marry
friend.sayName = function(){
console.log('111111');
}
friend.sayName(); // 111111
7、寄生构造函数模式
- 通常在前述的几种模式均不使用的情况下,可以使用寄生构造函数模式。
- 寄生构造函数模式与工厂模式很是相似,唯一不同的地方是:创建实例时,使用 new 操作符。
- 寄生构造函数模式,返回的对象与构造函数或者与构造函数的原型属性没有任何关系。这样就不能依赖 typeof 或者 instanceof 来确定对象的类型了。由于存在这个问题,所以不推荐使用。
function Person(name, age, job){
var o = new Object();
o.name = name;
o.age = age;
o.job = job
o.sayName = function(){
console.log(this.name);
};
return o;
}
var person1 = new Person("marry", 18, "doctor");
person1.sayName(); // marry
构造函数在不返回值的情况下,默认返回实例对象。所以,这里既然 return 了指定的对象,就不返回实例对象了。
8、稳妥构造函数模式
- 稳妥构造函数模式创建的对象,没有公共属性和公共方法,全部私有化。
- 稳妥构造函数模式与寄生构造函数模式类似,但有两点不同:
- 新创建的对象的实例方法不能用 this;
- 不使用 new 操作符调用构造函数。
- 稳妥构造函数模式,返回的对象与构造函数或者与构造函数的原型属性没有任何关系。这样就不能依赖 typeof 或者 instanceof 来确定对象的类型了。由于存在这个问题,所以不推荐使用。
function Person(name, age, job){
var o = new Object();
o.sayName = function(){
console.log(name);
};
return o;
}
var friend = Person("marry", 21, "Doctor");
friend.sayName(); // marry
console.log(friend.name); // undefined
可见,在稳妥构造函数模式下,除了使用 sayName 方法外,没有其他办法访问 name 的值。
四、继承
- 许多面向对象语言都支持两种继承方式:接口继承 和 实现继承。
- 接口继承只继承方法名,而实现继承则继承实际的方法。
- ECMAScript 只支持实现继承,而且其实现继承主要依赖于原型链。
1、原型链的继承
(1)、JS 的原型 与 原型链(★★★★★)
请戳这里:js 原型与原型链_weixin79893765432...的博客-CSDN博客
(2)、原型链的继承实例
// 超类型
function SuPerson(){
this.property = true;
}
SuPerson.prototype.getSuperValue = function(){
console.log(this.property);
}
// 子类型
function Person(){
}
// 继承
Person.prototype = new SuPerson();
var instance = new Person();
instance.getSuperValue(); // true
(3)、原型链的继承的注意事项
- 子类型有时需要覆盖超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但是不管怎样,给原型添加的代码必须放在替换原型的语句之后。
- 在通过原型链实现继承时,不能使用对象字面量创建原型方法,因为用字面量创建创建原型会重写原型链,导致之前的属性无效而报错。上文中有过详解。
(4)、原型链存在的问题
- 原型链虽然可以用来实现继承,但是他也存在一些问题,最主要的问题来自包含引用类型值的原型。上文中详解过:包含引用类型值的原型属性会被所有实例共享。这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
2、原型的继承
(1)、原型的继承的实例
原型式继承,是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
- 原型式继承必须有一个对象可以作为另一个对象的基础。
创建一个函数,比如 object() 函数(注意不是自定义类型哦,自定义类型首字符必须大写的),在 object() 函数内部,先创建一个临时性构造函数,然后将传入的对象作为这个构造函数的原型,最后返回这个临时类型的新实例。本质上,object() 函数对传入的对象做了一次浅拷贝。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
// 参照对象
var person = {
name: "marry",
friends: ["bob", "lily"]
};
var anotherPerson = object(person);
anotherPerson.name = "baer";
anotherPerson.friends.push("jack");
console.log(person.name); // marry
console.log(anotherPerson.name); // baer
console.log(person.friends); // ["bob", "lily", "jack"]
console.log(anotherPerson.friends); // ["bob", "lily", "jack"]
(2)、 Object.create() 方法规范了原型式继承
Object.create() 方法接收两个参数:一个作为新对象原型的对象 和 (可选的)一个为新对象定义额外属性的对象。
①、只传入一个参数时
Object.create() 方法,在只传入一个参数的情况下,与上文中的 object() 函数的原理相同。
// 参照对象
var person = {
name: "marry",
friends: ["bob", "lily"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "baer";
anotherPerson.friends.push("jack");
console.log(person.name); // marry
console.log(anotherPerson.name); // baer
console.log(person.friends); // ["bob", "lily", "jack"]
console.log(anotherPerson.friends); // ["bob", "lily", "jack"]
②、传入两个参数时
Object.create() 方法,在传入两个参数的情况下,第二个参数指定的任何对象的属性,都会覆盖参照对象的同名属性,若不同名就在新对象上新增该属性。
// 参照对象
var person = {
name: "marry",
friends: ["bob", "lily"]
};
var anotherPerson = Object.create(person, {
name: {
value: "tony"
},
age: {
value: 18
},
sayAge: {
value: function(){
console.log(this.age);
}
}
});
anotherPerson.friends.push("jack");
console.log(person.name); // marry
console.log(anotherPerson.name); // baer
console.log(person.friends); // ["bob", "lily", "jack"]
console.log(anotherPerson.friends); // ["bob", "lily", "jack"]
console.log(anotherPerson.age); // 18
anotherPerson.sayAge(); // 18
(3)、 原型式继承的问题
显而易见,原型式继承也存在 “包含引用类型值的属性始终都会共享相应的值” 的问题。
3、构造函数的继承
“借用构造函数” 技术的基本思想是:通过使用 apply() 和 call() 方法,在子类型构造函数内部调用超类型构造函数。
- 借用“构造函数”实现继承:解决了 原型中包含引用类型值所带来的问题。
(1)、借用构造函数实现继承
function SuperType(){
this.colors = ["red", "blue", "green"];
}
function SubType(){
// 继承
SuperType.call(this);
}
var instance1 = new SubType();
var instance2 = new SubType();
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
console.log(instance2.colors); // ["red", "blue", "green"]
“借用构造函数” 技术可以通过 子类型构造函数 向 超类型构造函数 传递参数:
function SuperType(name){
this.name = name;
}
function SubType(){
SuperType.call(this, "marry");
this.age = 18;
}
var instance = new SubType();
console.log(instance.name); // "marry"
console.log(instance.age); // 18
(2)、借用构造函数”实现继承存在的问题
单独使用 “借用构造函数” 存在两个问题:
- “借用构造函数” 的方法都在构造函数中定义,因此函数复用就无从谈起了。
- “借用构造函数” 在超类型的原型中定义的方法,对子类型而言是不可见的,结果所有类型都只能使用构造函数模式。
所以,几乎不单独使用 “借用构造函数”。
4、组合继承
组合继承是将 “借用构造函数” 和 原型链 组合到一块。避免了 “借用构造函数” 和 原型链 的缺陷,融合了他们的优点,是 JavaScript 中最常用的继承模式。
- 解决了包含引用类型值的原型属性会被所有实例共享的问题
- 解决了通过子类型构造函数不能向超类型构造函数传递参数的问题
- 解决了“借用构造函数”不能复用的问题
- 解决了“借用构造函数”原型中定义的方法对子类型不可见的问题
(1)、组合继承的实现
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;
}
// 继承
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
var instance1 = new SubType("marry", 18);
instance1.colors.push("black");
console.log(instance1.colors); // ["red", "blue", "green", "black"]
instance1.sayName(); // marry
instance1.sayAge(); // 18
var instance2 = new SubType();
console.log(instance2.colors); // ["red", "blue", "green"]
(2)、组合继承的问题
组合继承都会调用 2 次超类型构造函数:一次是在创建子类型原型的时候;另一次是在子类型构造函数内部。 那么问题来了:在调用超类型构造函数创建子类型原型时,子类型已经继承了超类型对象的全部属性,但我们却在调用子类型构造函数时重写了这些属性,多此一举。
5、寄生式继承
寄生式继承是基于原型式继承的扩展,与寄生构造函数 和 工厂模式类似,即创建一个仅用于封装继承过程的函数。
(1)、寄生式继承的实现
比如,创建一个仅用于封装继承过程的函数 createAnother(),通过调用 object() 函数生成一个原对象(original)副本的实例对象(clone),然后为其添加它的私有属性和方法,最后作为返回这个加工后的实例对象(clone)。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log("Hi!");
};
return clone;
}
// 参照对象
var person = {
name: "marry",
friends: ["bob", "lily"]
};
var anotherPerson = createAnother(person);
anotherPerson.name = "baer";
anotherPerson.friends.push("jack");
console.log(person.name); // marry
console.log(anotherPerson.name); // baer
console.log(person.friends); // ["bob", "lily", "jack"]
console.log(anotherPerson.friends); // ["bob", "lily", "jack"]
anotherPerson.sayHi(); // Hi!
(2)、寄生式继承的问题
寄生式继承的问题有 2 个:
- 存在 “包含引用类型值的属性始终都会共享相应的值” 的问题。
- 不能做到函数复用而降低了效率,这一点与构造函数模式类似。
6、寄生组合式继承(★)
- 寄生组合式继承,通过 借用构造函数 来继承属性,通过 原型链的混合形式 来集成方法。
- 寄生组合式继承,本质上,就是使用寄生式继承来继承超类型,然后再将结果指定给子类型的原型。
- 寄生组合式继承只需调用超类型一次,解决了组合继承必须调用二次超类型的问题。
function object(o){
function F(){};
F.prototype = o;
return new F();
}
function inheritPrototype(subType, superType){
var prototype = object(superType.prototype);
prototype.constructor = subType;
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);
};
var instance = new SubType("marry", 21);
instance.sayName(); // marry
instance.sayAge(); // 21
在ES6之前,寄生组合式继承,是最理想的继承范式。
【推荐阅读】
函数式编程与面向对象编程https://blog.csdn.net/mChales_Liu/article/details/106530145js 原型与原型链
https://blog.csdn.net/mChales_Liu/article/details/109686177