JS的面向对象编程设计
最近阅读了《JS高级程序设计3》,刚看完第六章面向对象,于是将大体的核心概念整理总结一下。
1.理解对象
属性类型
ECMAScript中有两种属性:数据属性和访问器属性。
- 数据属性
configurable:可配置性,表示能否通过delete删除属性,默认为false,
(注意:该属性的修改不可逆一旦把configurable设为false,则表示该属性变为不可配置的了,此时不可再做任何属性修改,否则会报错);
Enumerable: 可遍历性,表示能否通过for in遍历属性,默认为false;
writable: 可修改性,表示能否修改属性值,默认为false;
value: 表示属性的数据值;
- 访问器属性
configurable:可配置性,默认为true
Enumerable: 可遍历性,表示能否通过for in遍历属性,默认为true
get:获取属性值的方法,默认为undefined
set:设置属性值的方法,默认为undefined
定义属性
- 定义单个属性:
Object.defineProperty(对象名,属性名,属性定义)
- 定义多个属性:
Object.defineProperties(对象名,{
属性名1:属性定义,
属性名2:属性定义,
.......
})
读取属性
Object.getOwnPropertyDescriptor(对象名,属性名) // 返回的是一个对象
2. 创建对象
工厂模式
概念:通过函数封装对象共有的属性和方法,避免创建多个相似对象的问题
示例:
function Person(name) {
var o = new Object();
o.name = name;
o.sayName = function() {
alert(this.name);
};
return o;
}
var person = Person('测试');
问题:工厂模式创建的对象无法得知其类型。
构造函数模式
概念:定义函数,通过new操作符创建对象(任何函数,通过new操作符创建,都是构造函数)
示例:
function Person(name) {
this.name = name;
this.sayName = function() {
alert(this.name);
};
}
var person = new Person('测试');
问题:构造函数中定义的方法在每个实例上都会被重新创建一次(函数也是对象)
原型模式
理解原型对象
-
只要创建了一个新函数,就会为该函数创建一个prototype属性,这个属性指向函数的原型对象,每个原型对象都会自带一个constructor属性,这个属性指向该函数本身。
-
当调用构造函数创建一个新实例时,该实例将会包含一个指针(内部属性)[[Prototype]],指向构造函数的原型对象。
-
当在对象实例中声明一个与原型中的属性名相同的属性时,这个属性就会屏蔽原型中对应的属性,但不会修改原型中的属性,通过delete方法可以删除对象实例属性。
上述描述可以举个例子:
function Person() {}
Person.prototype.name = "测试";
Person.prototype.age = 18;
Person.prototype.job = "学生";
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayname();
我们可以依据上述的例子画一个大概的概念图:
判断对象之间是否存在原型关系: isPrototypeOf()
获取对象的原型: Object.getPrototypeOf(object)
判断一个属性是否在实例中: hasOwnProperty(属性名)
判断一个属性是否在原型中或实例中: name(属性名) in object(实例)
如何判断属性是原型中的属性?
!object.hasOwnProperty(name) && (name in object)
返回一个包含所有可枚举属性的字符串数组: Object.keys()
获取所有属性(包括不可枚举属性): Object.getOwnPropertyNames()
原型的动态性
1.对原型对象的所有修改都能立即在实例对象上体现出来,即使修改是在声明实例之后。
function Person() {}
var p = new Person();
Person.prototype.name = '哈哈哈';
console.log(p.name); // 哈哈哈
2.如果重写了整个原型对象,则切断了现有原型与原本实例的联系,原本实例指向的还是最初的原型(实例指针仅指向原型,不指向构造函数)
function Person() {}
var p = new Person();
Person.prototype = {
constructor: Person,
name: '哈哈哈'
};
console.log(p.name); // undefined
原型模式的问题
- 所有实例在默认情况下都取得相同的属性值。
- 属性共享,对于引用类型的属性,实例会共享属性(当其中一个实例修改了原型中的属性时,其他实例也会一起被影响)。
function Person() {}
Person.prototype = {
constructor: Person,
colors: ["red"]
};
const p1 = new Person();
const p2 = new Person();
p1.colors.push("blue");
console.log(p1.colors); // ["red", "blue"]
console.log(p2.colors); // ["red", "blue"]
组合构造函数和原型模式(最常用)
概念:用构造函数定义实例属性,用原型定义方法和共享属性。
示例:
function Person(name) {
this.name = name;
}
Person.prototype = {
constructor: Person, // 重写原型对象需要指明原型对象的构造函数,原因上文有指出
sayName: function() {
alert(this.name);
};
};
var p = new Person("测试");
动态原型模式
概念:通过在构造函数中判断某个方法是否已存在,来决定是否需要初始化原型。
示例:
function Person(name) {
this.name = name;
if (typeof this.sayName !== 'function') {
Person.prototype.sayName = function() {
alert(this.name);
};
}
}
var p = new Person("测试");
稳妥构造函数(适合在某些安全环境下工作)
概念:创建对象的实例方法不引用this,也不使用new调用构造函数
示例:
function Person(name,year,job){
var o = new Object();
// 这里可以添加私有变量和方法
o.sayName = function () {
alert(name); // name值仅能通过sayName()方法访问
}
return o;
}
var p = Person("测试"); // p是一个稳妥对象:没有公共属性,方法也不引用this的对象
p.sayName();
3.继承
原型链继承
示例:
function Father() {
this.colors = ["red"];
}
father.prototype.getFather = function () {
return this.colors;
}
function Son() {}
// 继承父类型
son.prototype = new father();
const son1 = new Son();
const son2 = new Son();
son1.colors.push("blue");
console.log(son1.getFather()); // ["red", "blue"]
console.log(son2.getFather()); // ["red", "blue"]
问题:
- 包含引用类型值的原型属性会被所有实例共享,当其中一个实例修改了原型中的引用类型属性时,其他实例也会一起被影响。
- 没办法在不影响其他实例的情况下,向父类型的构造函数传参。
借用构造函数继承
概念:通过在子函数内部调用父函数的构造函数实现继承。
示例:
function Parent(name) {
this.colors = ["red"];
this.name = name;
}
function Child() {
// 可以向父类型的构造函数传参
Parent.call(this, '测试');
}
const c = new Child();
c.colors.push('black');
// ["red", "black"]
console.log(c.colors);
const p = new Parent();
// ["red"] 避免了属性共享的问题
console.log(p.colors);
问题:
- 因为函数方法都定义在构造函数里,导致方法无法复用
- 父类型原型对子类型不可见
组合继承(最常用)
概念:结合原型链继承和借用构造函数继承。
示例:
function Parent(name){
this.name = "测试";
this.age = 12;
};
Parent.prototype.say = function() { console.log(this.age) };
// 子类继承父类
function Child(name){
// 继承属性
Parent.call(this, name); // 第二次调用Parent
this.age = 13;
}
// 继承方法
Child.prototype = new Parent(); // 第一次调用Parent
Child.prototype.constructor = Child;
问题:
- 父类型的构造函数会被调用两次
原型式继承
概念:基于已有的对象创建新对象
示例:
/** 1. 不使用Object.create() **/
function createObj(obj) {
funcion F() {}
F.prototype = obj;
return new F();
}
var Person = {
name: '测试'
};
var p = createObj(Person);
/** 2. 使用Object.create() **/
var Person = {
name: '测试'
};
var p = Obejct.create(person, {
name: {
value: '修改测试数据'
}
);
问题:就像使用原型模式创建对象一样,引用类型的属性会被共享。
寄生式继承
概念:结合原型式继承,在内部通过某种方式增强对象。
示例:
function createObj(obj) {
funcion F() {}
F.prototype = obj;
return new F();
}
function createPerson(original) {
// 方式1:
// const clone = createObj(original);
// 方式2:
const clone = Object.create(original);
clone.sayHi = function() {
alert("hi");
};
}
var Person = {
name: '测试'
};
var p = createPerson(Person);
p.sayHi(); // hi
问题:和构造函数模式继承一样,因为方法在函数内部定义,导致方法无法复用。
寄生组合式继承(最理想)
概念:通过借用构造函数继承属性,通过原型链的混成形式继承方法(本质就是不再通过调用父类型的构造函数创建实例来指定子类型的原型,而只需创建一个父类型的原型的副本)。
示例:
function initialPrototype(son, father) {
// 1.创建一个父类型的原型副本(创建对象)
const prototype = Object.create(father.prototype);
// 2.指定副本的构造函数(增强对象)
prototype.constructor = son;
// 3. 将该原型副本赋给子类型的原型(指定对象)
son.prototype = prototype;
}
function Parent(name){
this.name = "测试";
this.age = 12;
};
Parent.prototype.say = function() { console.log(this.age) };
// 子类继承父类
function Child(name){
// 继承属性
Parent.call(this, name);
this.age = 13;
}
// 继承方法(替换原本的 Child.prototype = new Parent() )
initialPrototype(Child, Parent);