共计 7181 个字符,预计需要花费 18 分钟才能阅读完成。
最近因为在给一个小同学做学习计划,所以也记录一些知识点,便于后面的同学的学习交流。这篇文章是关于 Javascript 的面向对象的程序设计,主要从三个方面来介绍,1. 理解对象属性;2. 理解并创建对象; 3. 理解继承
一、理解对象属性首先我们来理解 Javascript 对象是什么?在 Javascript 中,万物皆对象。其中创建自定义对象的最简单的方式就是创建一个 Object 的实例,如下:
var person = new Object();
person.age = 29;
// 对象字面量的形式:
var person = {
age: 29
};
ECMAScript 中有两种属性:数据属性和访问器属性。
数据属性:其中数据属性有四个描述其行为的特性:Configurable: 表示能都通过 delete 删除属性从而重新定义属性。Enumerable: 表示能否通过 for in 循环返回属性。Writable: 表示能否修改属性的值。Value: 包含这个属性的数据值。要修改属性默认的配置,必须使用 Object.defineProperty(), 这个方法接收三个参数:属性所在的对象,属性的名字和一个描述性对象。比如:
var person = {};
Object.defineProperty(persion,’name’, {
writable: false,
value:’Nicholas’
});
alert(person.name); //Nicholas
person.name =‘Greg’;
alert(person.name); //Nicholas
访问器属性:访问器属性包含一对 setter 和 getter 函数。包含如下 4 个特性:Configurable:能否被 delete 删除属性重新定义。默认值:trueEnumerable:能否被 for-in 枚举。默认值:trueGet:读取属性值。默认值:undefinedSet:写入属性值。默认值:undefined
var dog = {
_age: 2,
weight: 10
}
Object.defineProperty(dog, ‘age’, {
get: function () {
return this._age
},
set: function (newVal) {
this._age = newVal
this.weight += 1
}
})
知道了对象的属性,那么我们创建对象的方式是什么呢?
二、创建对象的方式
创建对象的方式通常有下面几种方式:1、工厂模式我们举个例子:
function createPerson(name, age, job) {
var o = new object();
o.name = name;
o.age = age;
o.job = job;
o.sayName = function () {
alert(this.name);
};
Return o;
}
var person = createPerson(‘Greg’, 27,‘Doctor’);
工厂模式虽然解决了创建多个相似对象的问题,但却没有解决问题识别的问题(即怎样知道一个对象的类型)
2、构造函数模式我们举个例子:**function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = function () {
alert(this.name);
};}var person = new Person(‘Greg’, 27,‘Doctor’);**
构造函数始终都应该以一个大写字母开头,而非构造函数则应该以一个小写字母开头。这里要提的一个属性是 Constructor, 每个 new 出来的实例都有一个 Constructor(构造函数)属性,该属性指向构造函数。对象的 Constructor 属性最初是用来标识对象类型的。但是,提到检测对象类型,还是 instanceof 操作符更好可靠一些。alert(person1 instanceof object); //true
构造函数的问题问题就是,每个方法都要在每个实例上重新创建一遍,当然,可以把函数定义转移到构造函数外部来解决这个问题,如下实例:
**function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.sayName = sayName;
}function sayName() {
alert(this.name);
}var person = new Person(‘Greg’, 27,‘Doctor’);**
那么这里的问题就是:在全局作用域中定义的函数实际上被只能被某个对象调用,这让全局作用域有点名不副实。而更让人无法接受的是:如果独享需要定义很多方法,那么就要定义很多个全局函数。下面我们来看看原型模式是不能解决这个问题。
3、原型模式 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。我们来看下面一个例子来理解这句话:
function Person() {
}
Person.prototype.name =“Nicholas”;
Person.prototype.age = 29;
Person.prototype.job =“Software Engineer”;
Person.prototype.sayName = function() {
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //“Nicholas”
与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。针对这个特性,我们要注意的几个点就是。第一,如果实例的属性与方法与原型的属性和方法同名,那谁的优先级高呢?当然是创建出来的实例的属性和方法的优先级高。第二,实例的属性与方法的修改会影响原型同名的属性和方法吗?不会这里提一下 hasOwnProperty(), 使用 hasOwnProperty() 方法可以检测一个属性是存在于实例中,还是存在原型中。第三,如何判断某个属性是存在原型中?
function hasPrototypeProperty(object, name) {
return !Object.hasOwnProperty(name) && (name in object);
}
hasOwnProperty() 只在属性存在于实例中才返回 true, 因此只要 in 操作符返回 true 而 hasOwnProperty() 返回 false, 就可以确定属性是原型中的属性、这里说下 Constructor, 每创建一个函数,就会同时创建它的 Prototype 对象,这个对象也会自动获得 Constructor 属性。我们来看一个例子:
function Person() {
}
Person.prototype = {
constructor: Person,
name:“Nichloas”,
age: 29,
job:“Software Engineer”,
sayName: function () {
alert(this.name);
}
};
以上代码特意包含了一个 Constructor 属性,并将它的值设置为 Person, 从而确保了通过该属性能够访问到适当的值。但是,以这种方式重设 Constructor 属性会导致它的 Enumerable 特性被设置为 true, 默认情况下原生的 Constructor 属性是不可枚举的。
Object.defineProperty(Person.prototype,“Constructor”, {
enumerable: false,
value: Person
});
原型对象的问题:首先,它省略了为构造函数传递初始化参数这一环节。然后,原型中所有属性是被很多实例共享的,对于包含引用类型值的属性来说,问题比较突出。所以,使用最多的方式使用构造函数和原型模式
4、组合使用构造函数模式和原型模式
function Person(name, age, job) {
this.name = name;
this.age = age;
this.job = job;
this.friends = [“Shelby”,“Court”];
}
Person.prototype = {
constructor: Person,
sayName: function () {
alert(this.name);
}
};
var person = new Person(‘Greg’, 27,‘Doctor’);
知道了创建对象的方式,那么在 Javascript 中我们如何来继承对象呢?
三、继承 1、原型链构造函数,原型和实例的关系:每一个构造函数都也有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。那么,假如我们让原型对象等于另一个类型的实例,结果会怎么样呢?显然,此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型中也包含也包含着一个指向一个构造函数的指针,假如另一个原型又是另一个类型的实例,那么上诉关系依然成立,我们来看下面一个例子.
function SuperType() {
this.property = true;
}
SuerType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
This.subProperty = false;
}
// 继承了 SuperType
SubType.prototype = new SuperType();
SubType.prototype.getSubValue = function () {
return this.subProperty();
};
var instance = new SubType();alert(instance.getSuperValue); // true 这个例子中,SubType 继承了 SuperType,而 SuperType 继承了 Object。如果要确定原型和实例的关系,可以用 instanceOf 操作符与 isPropertyOF() 方法。如 instance instanceOf object; // trueObject.prototype isPropertype(instance); // true
但是通过原型链实现继承时,不能使用对象字面量创建原型方法。
function SuperType() {
this.property = true;
}
SuerType.prototype.getSuperValue = function () {
return this.property;
};
function SubType() {
This.subProperty = false;
}
// 继承了 SuperType
SubType.prototype = new SuperType();
// 使用字面量添加新方法,会导致上一行代码无效
SubType.prototype = {
getSubValue: function () {
return this.subproperty;
},
someOtherMethod: function () {
return false;
}
};
var instance = new SubType();
alert(instance.getSuperValue); // error
接下来,我们来说下原型链继承的问题:第一,最主要的问题来自包含引用类型值的原型。包含引用类型值的原型属性会被所有实例共享,而这也正是为什么要在构造函数中,而不是在原型对象中定义属性的原因。第二,在创建子类型的实例时候,不能向超类型的构造函数中传递参数。
2、借用构造函数
function SuperType(name) {
this.name = name;
}
function SubType() {
// 继承了 SuperType, 同时还传递了参数
SuperType.call(this,“Nicholas”);
this.age = 29;
}
var instance = new SubType();
alert(instance.name); //“Nicholas”
借用构造函数的问题,方法都在构造函数中定义,因此函数复用就无从谈起了。而且,在超类型的原型中定义的方法,对于子类型而言也是不可见的,结果所有类型都只能使用构造函数模式,考虑到这些问题,借用构造函数的技术也是很少单独使用的。
3、组合继承组合继承,指的是将原型链和借用构造函数的技术组合到一块,从而发挥二者之长的一种继承模式。其背后的思路是使用原型链实现对原型属性和放大的绩效,而通过借用构造函数来实现对实例属性的继承。
function SuperType(name) {
this.name = name;
this.colors = [“red”,“blue”,“green”]
}
SuperType.prototype.sayName = function () {
alert[this.name];
};
function SubType(name, age) {
// 继承属性
SuperType.call(this, name);
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType();
SubType.prototype.sayAge = function () {
alert(this.age);
};
var instance = new SubType(“Nicholas”, 29);
instance.colors.push(“black”);
alert(instance.colors); // red, blue, green, black
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 Javascript 中最常见的继承模式。
4、原型式继承
function object(o) {
function F() {}
F.prototype = o;
return new F();
]
var person = {
Name:“Nicholas”,
friends: [“Shelby”,“Court”,“Van”]
};
var anotherPerson = object(person);
anotherPerson.name =“Greg”;
ECMAScipt5 通过新增 Object.create() 方法规范化了原型式继承。接着上面的例子:
var anotherPerson = Object.create(person);
anotherPerson.name =“Greg”;
5、寄生式继承寄生式继承是与原型式继承紧密相关的一种思路,它与工厂模式类似,即创建一个仅用于封装过程的函数,该函数在内部以某种方法来增强对象,最后再像真地是它做了所有工作一样返回对象。例如:
function createAnother(original) {
var clone = object(original);
clone.sayHi = function () {
alert(‘hi’);
};
return clone;
}
var person = {
name:’Nochplas’,
friends: [’Shelby’,‘Court’,‘Van’]
};
var anotherPerson = createAnother(person);
6、组合寄生式继承
组合继承是 Javascript 最常用的继承模式,不过,组合继承最大的问题就是无论什么情况下,都会调用两次超类型构建函数:一次是在创建子类型原型的时候,另一次死在子类型构造函数内部。
function SuperType(name) {
this.name = name;
this.colors = [“red”,“blue”,“green”]
}
SuperType.prototype.sayName = function () {
alert[this.name];
};
function SubType(name, age) {.
// 继承属性
SuperType.call(this, name); // 第二次调用 SuperType()
this.age = age;
}
// 继承方法
SubType.prototype = new SuperType(); // 第一次调用 SuperType()
SubType.prototype.sayAge = function () {
alert(this.age);
};
所谓寄生组合式继承,即通过借用构造函数来继承属性,通过原型链的混成形式来继承方法。不必为了制定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果制定给子类型的原型。如下:
function inheritPrototype (subType, superType) {
var prototype = object (superType.prototype); // 前面的原型式继承 object 方法
prototype.constructor = subtype;
subtype.prototype = prototype;
}
function SuperType(name) {
this.name = name;
this.colors = [“red”,“blue”,“green”]
}
SuperType.prototype.sayName = function () {
alert[this.name];
};
function SubType(name, age) {.
// 继承属性
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(Subtype, SuperType);
SubType.prototype.sayAge = function () {
alert(this.age);
};