一、定义与理解
面向对象的定义:无序属性的集合,其属性可以包含基本值、对象或者函数
创建自定义对象的最简单方式就是创建一个 Object 的实例,然后再为它添加属性和方法
var person = new Object();
person.name = "luckfine";
person.age = 26;
person.job = "Web-Engineer";
person.sayName = function(){
alert(this.name);
};
上述创建了一个名为person的对象,定义了name,age,job三个属性,和一个sayName方法
访问对象属性
1、点语法 :
对象名.属性名
对象名.功能属性名(参数列表)
2、方括号语法
对象名["属性名"]
对象名["功能属性名"](参数列表)
点语法和方括号法的区别?
1、方括号法可以通过变量来访问属性
2、方括号法可以在程序运行中创建和修改属性(for循环
)
属性类型
ECMAScript 中有两种属性:数据属性和访问器属性
1、数据属性:数据属性包含一个数据值的位置,在这个位置可以读取和写入值
Configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。默认值为 true
Enumerable:表示能否通过 for-in 循环返回属性。默认值为 true
Writable: 表示能否修改属性的值。默认值是true
Value:包含这个属性的数据值。读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
Object.defineProperty()
三个参数:属性所在的对象、属性的名字和一个描述符对象
var person = {};
Object.defineProperty(person, "name", {
writable: false,
configurable: false,
value: "luckfine"
});
alert(person.name); //luckfine
person.name = "范小饭";
delete person.name
alert(person.name); //luckfine
// 这个属性的值是不可修改 的,如果尝试为它指定新值,则在非严格模式下,赋值操作将被忽略;在严格模式下,赋值操作将会导 致抛出错误。
//configurable 设置为 false,表示不能从对象中删除属性,可以多次调用 Object.defineProperty()方法修改同一个属性,但在把 configurable 特性设置为 false 之后就会有限制了
2、访问器属性:它们包含一对儿 getter 和 setter 函数,在读取访问器属性时,会调用 getter 函数,这个函数负责返回有效的值;在写入访问器属性时,会调用 setter 函数并传入新值,这个函数负责决定如何处理数据
Configurable:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特 性,或者能否把属性修改为数据属性 默认值为true
Enumerable:表示能否通过 for-in 循环返回属性 默认值为false
Get:在读取属性时调用的函数。默认值为 undefined
Set:在写入属性时调用的函数。默认值为 undefine
Object.defineProperties() :定义多个属性
利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一 个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
})
// book 对象上定义了两个数据属性(_year 和 edition)和一个访问器属性(year)
Object.getOwnPropertyDescriptor()
可以取得给定属性的描述 符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果 是访问器属性,这个对象的属性有 configurable、enumerable、get 和 set;如果是数据属性,这 个对象的属性有 configurable、enumerable、writable 和 value。
var book = {};
Object.defineProperties(book, {
_year: {
value: 2004
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
} }
});
var descriptor = Object.getOwnPropertyDescriptor(book, "_year");
alert(descriptor.value); //2004
alert(descriptor.configurable); //false
alert(typeof descriptor.get); //"undefined"
var descriptor = Object.getOwnPropertyDescriptor(book, "year");
alert(descriptor.value); //undefined
alert(descriptor.enumerable); //false
alert(typeof descriptor.get); //"function"
二、创建对象
2.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 person1 = createPerson("Nicholas", 29, "Software Engineer");
var person2 = createPerson("Greg", 27, "Doctor");
2.2 构造函数模式
function Person(name, age, job){
this.name = name;
this.age = age;
this.job = job;
this.sayName = function(){
alert(this.name);
};
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
与工厂模式相比,构造函数没有显示的创建对象,直接将属性和方法赋值给this对象,没有return语句
要创建person的新实例,必须使用new操作符,以这种方式调用构造函数实例会经历以下步骤
1、创建一个对象
2、将构造函数的作用域赋给新对象(因此 this 就指向了这个新对象);
3、执行构造函数中的代码(为这个新对象添加属性)
4、返回新对象。
instanceof :检测对象类型
// 这个例子中创建的对象,既是Object的实例,同时也是Object的实例,可通过instanceof验证
alert(person1 instanceof Object); //true
alert(person1 instanceof Person); //true
构造函数和函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。不过,构造函数毕竟也是函数,不 存在定义构造函数的特殊语法。任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而 任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样
// 当作构造函数使用
var person = new Person("Nicholas", 29, "Software Engineer"); person.sayName(); //"Nicholas"
// 作为普通函数调用
Person("Greg", 27, "Doctor"); // 添加到window
window.sayName(); //"Greg"
// 在另一个对象的作用域中调用
var o = new Object();
Person.call(o, "Kristen", 25, "Nurse"); o.sayName(); //"Kristen"
构造函数的问题
使用构造函数的主要问题,就是每个方法都要在每个 实例上重新创建一遍
2.3 原型模式
我们创建的每个函数都有一个 prototype(原型)属性,这个属性是一个指针,指向一个对象, 而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。使用原型对象的好处是可以 让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是 可以将这些信息直接添加到原型对象中
function Person(){}
Person.prototype.name = "luckfine";
Person.prototype.age = 29;
Person.prototype.job = "Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
person1.sayName(); //"luckfine"
var person2 = new Person();
person2.sayName(); //"luckfine"
alert(person1.sayName == person2.sayName); //true
在此,我们将 sayName()方法和所有属性直接添加到了 Person 的 prototype 属性中,构造函数 变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属 性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说, person1 和 person2 访问的都是同一组属性和同一个 sayName()函数。
理解原型对象
只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype 属性,这个属性指向函数的原型对象
在默认情况下,所有原型对象都会自动获得一个 constructor (构造函数)属性,这个属性包含一个指向 prototype 属性所在函数的指针
person.prototype 指向了原型对象,而 Person.prototype.constructor 又指回了 Person。 原型对象中除了包含 constructor 属性之外,还包括后来添加的其他属性。Person 的每个实例—— person1 和 person2 都包含一个内部属性,该属性仅仅指向了 Person.prototype;换句话说,它们 与构造函数没有直接的关系。此外,要格外注意的是,虽然这两个实例都不包含属性和方法,但我们却可以调用 person1.sayName()。这是通过查找对象属性的过程来实现的。
isPrototypeOf():判断一个对象是否是另一个对象的原型对象
alert(Person.prototype.isPrototypeOf(person1)); //true
Object.getPrototypeOf():返回的对象实际就是这个对象的原型
alert(Object.getPrototypeOf(person1) == Person.prototype); //true
alert(Object.getPrototypeOf(person1).name); //"luckfine"
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先 从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到, 则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。如果在原型对象中找到了这 个属性,则返回该属性的值。也就是说,在我们调用 person1.sayName()的时候,会先后执行两次搜 索。首先,解析器会问:“实例 person1 有 sayName 属性吗?”答:“没有。”然后,它继续搜索,再 问:“person1 的原型有 sayName 属性吗?”答:“有。”于是,它就读取那个保存在原型对象中的函 数。当我们调用 person2.sayName()时,将会重现相同的搜索过程,得到相同的结果。而这正是多个 8 对象实例共享原型所保存的属性和方法的基本原理。
所以可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我们 在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那我们就在实例中创建该属性,该 属性将会屏蔽原型中的那个属性。
function Person(){}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer"; 12 Person.prototype.sayName = function(){
alert(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例 alert(person2.name); //"Nicholas"——来自原型
hasOwnProperty():可以检测一个属性是存在于实例中,还是存在于原型中。
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();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
person1.name = "Greg";
alert(person1.name); //"Greg"——来自实例 alert(person1.hasOwnProperty("name")); //true
alert(person2.name); //"Nicholas"——来自原型 alert(person2.hasOwnProperty("name")); //false
delete person1.name;
alert(person1.name); //"Nicholas"——来自原型 alert(person1.hasOwnProperty("name")); //false
原型与 in 操作符
in 操作符会在通 过对象能够访问给定属性时返回 true,无论该属性存在于实例中还是原型中。
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();
var person2 = new Person();
alert(person1.hasOwnProperty("name")); //false
alert("name" in person1); //true
person1.name = "Greg";
alert(person1.name); //"Greg" ——来自实例 alert(person1.hasOwnProperty("name")); //true alert("name" in person1); //true
alert(person2.name); //"Nicholas" ——来自原型 alert(person2.hasOwnProperty("name")); //false alert("name" in person2); //true
delete person1.name;
alert(person1.name); //"Nicholas" ——来自原型 alert(person1.hasOwnProperty("name")); //false alert("name" in person1); //true
同时使用 hasOwnProperty()方法和 in 操作符,就可以确定该属性到底是存在于对象中,还是存在于 原型中
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
由于 in 操作符只要通过对象能够访问到属性就返回 true,hasOwnProperty()只s在属性存在于 实例中时才返回 true,因此只要 in 操作符返回 true 而 hasOwnProperty()返回 false,就可以确 定属性是原型中的属性
Object.keys():这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上 反映出来——即使是先创建了实例后修改原型也照样如此
var friend = new Person();
Person.prototype.sayHi = function(){
alert("hi");
};
friend.sayHi(); //"hi"(没有问题!)
尽管可以随时为原型添加属性和方法,并且修改能够立即在所有对象实例中反映出来,但如果是重 写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的 [[Prototype]]指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。 请记住:实例中的指针仅指向原型,而不指向构造函数。
function Person(){}
var friend = new Person();
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
sayName : function () {
alert(this.name);
}
};
friend.sayName(); //error
原型对象的问题
原型模式也不是没有缺点。首先,它省略了为构造函数传递初始化参数这一环节,结果所有实例在 默认情况下都将取得相同的属性值。虽然这会在某种程度上带来一些不方便,但还不是原型的最大问题。 原型模式的最大问题是由其共享的本性所导致的。
function Person(){}
Person.prototype = {
constructor: Person,
name : "Nicholas",
age : 29,
job : "Software Engineer",
friends : ["Shelby", "Court"],
sayName : function () {
alert(this.name);
} };
var person1 = new Person();
var person2 = new Person();
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Court,Van"
alert(person2.friends); //"Shelby,Court,Van"
alert(person1.friends === person2.friends); //true
在此,Person.prototype对象有一个名为friends的属性,该属性包含一个字符串数组。然后, 创建了 Person 的两个实例。接着,修改了 person1.friends 引用的数组,向数组中添加了一个字符 串。由于 friends 数组存在于 Person.prototype 而非 person1 中,所以刚刚提到的修改也会通过 person2.friends(与 person1.friends 指向同一个数组)反映出来。假如我们的初衷就是像这样 在所有实例中共享一个数组,那么对这个结果我没有话可说。可是,实例一般都是要有属于自己的全部 属性的。而这个问题正是我们很少看到有人单独使用原型模式的原因所在。
组合使用构造函数模式和原型模式
创建自定义类型的最常见方式,就是组合使用构造函数模式与原型模式。构造函数模式用于定义实 例属性,而原型模式用于定义方法和共享的属性。结果,每个实例都会有自己的一份实例属性的副本, 但同时又共享着对方法的引用,最大限度地节省了内存。另外,这种混成模式还支持向构造函数传递参 数;可谓是集两种模式之长。
function Person(name, age, job){
this.name = name; 3 this.age = age;
this.job = job;
this.friends = ["Shelby", "Court"];
}
Person.prototype = {
constructor : Person,
sayName : function(){
alert(this.name);
}
}
var person1 = new Person("Nicholas", 29, "Software Engineer");
var person2 = new Person("Greg", 27, "Doctor");
person1.friends.push("Van");
alert(person1.friends); //"Shelby,Count,Van"
alert(person2.friends); //"Shelby,Count"
alert(person1.friends === person2.friends); //false
alert(person1.sayName === person2.sayName); //true
在这个例子中,实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor 和方 法 sayName()则是在原型中定义的。而修改了 person1.friends(向其中添加一个新字符串),并不 会影响到 person2.friends,因为它们分别引用了不同的数组。
三、继承
3.1 原型链继承
原型链继承:将父类的实例作为子类的原型(利用原型让一个引用类型继承另外一个引用类型的属性和方法。)
function SuperType() {
this.property = true;
this.arr = ['1']
}
function SubType() {}
//继承了SuperType
SubType.prototype = new SuperType();
var instance1 = new SubType();
var instance2 = new SubType();
console.log(instance1.property);//true
console.log(instance2.property);//true
instance1.property = false
console.log(instance1.property);//false
console.log(instance2.property);//true
instance1.arr.push('8')
console.log(instance1.arr);//["1", "8"]
console.log(instance2.arr);//["1", "8"]
优缺点:
1、简单,易于实现
2、修改instance1.arr,instance2.arr也会改变,因为来自原型对象的引用属性是所有实例共享的
3、创建子类实例时,无法向父类构造函数传参数
3.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";
alert(instance.age); //29
核心就是借用父类的构造函数来增强子类实例,等于是把父类的实例属性复制一份给子类实例
优点:
1.解决了子类实例共享父类引用的属性问题
2.创建子类实例时可以向父类构造函数传参
3.可以实现多继承(call多个父类对象)
缺点 :
1.实例并不是父类的实例,只是子类的实例
2.只能继承父类的实例属性和方法,不能继承原型属性/方法
3.无法实现函数的复用,每个子类都有父类实例函数的副本,例子中每个子类都持有一个新的fun函数,函数无法复用,影响性能
3.3 组合继承
组合继承避免了原型链和借用构造函数的缺陷,融合了它们的优点,成为 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);
this.age = age;
}
//继承方法
SubType.prototype = new SuperType();
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
alert(this.age);
};
var instance1 = new SubType("Nicholas", 29);
instance1.colors.push("black");
alert(instance1.colors);//"red,blue,green,black"
instance1.sayName();//"Nicholas";
instance1.sayAge();//29
var instance2 = new SubType("Greg", 27);
alert(instance2.colors);//"red,blue,green"
instance2.sayName(); //"Greg";
instance2.sayAge();//27
组合继承是 JavaScript 最常用的继承模式;不过,它也有自己的不足。组合继承最大的 问题就是无论什么情况下,都会调用两次超类型构造函数:一次是在创建子类型原型的时候,另一次是 在子类型构造函数内部。没错,子类型最终会包含超类型对象的全部实例属性,但我们不得不在调用子 类型构造函数时重写这些属性
优缺点:
优点:
1.不存在引用属性共享的问题
2.可传递参数
3.函数可复用
缺点:
1.子类原型上有一份多余的父类实例属性,因为父类构造函数被调用了两次,内存浪费
3.4 原型式继承
道格拉斯·克罗克福德在 2006 年写了一篇文章,题为 Prototypal Inheritance in JavaScript (JavaScript 10 中的原型式继承)。在这篇文章中,他介绍了一种实现继承的方法,这种方法并没有使用严格意义上的 构造函数。他的想法是借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
// 通过Object.create()
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
alert(person.friends); //"Shelby,Court,Van,Rob,Barbie"
// Object.create()方法的第二个参数与 Object.defineProperties()方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
alert(anotherPerson.name); //"Greg"
alert(person.name) //Nicholas
优缺点:
1.从已有对象衍生新的对象,不需要创建自定义类型
2.原型引用属性会被所有实例所共享,因为是用整个父类对象充当子类的原型对象,所以这个缺陷无法避免
3.无法实现代码的复用
3.5 寄生式继承
寄生式(parasitic)继承是与原型式继承紧密相关的一种思路,并且同样也是由克罗克福德推而广 之的。寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该 函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。以下代码示范了寄生式继承模式。
function object(o){
function F(){}
F.prototype = o;
return new F();
}
function createAnother(original){
var clone = object(original); //通过调用函数创建一个新对象
clone.sayHi = function(){//以某种方式来增强这个对象
alert("hi");
}
return clone;
};
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
alert(anotherPerson.name) //Nicholas
alert(anotherPerson.friends) //"Shelby", "Court", "Van"
anotherPerson.friends.push('luckfine')
alert(anotherPerson.friends) //"Shelby", "Court", "Van".'luckfine'
alert(person.friends) //"Shelby", "Court", "Van".'luckfine'
//这个例子中的代码基于 person 返回了一个新对象——anotherPerson。新对象不仅具有 person 的所有属性和方法,而且还有自己的 sayHi()方法。
优缺点:
优点:1.不需要创建自定义类型
缺点:1.无法实现函数的复用
3.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(){
alert(this.name);
};
function SubType(name, age){
SuperType.call(this, name);
this.age = age;
}
inheritPrototype(SubType, SuperType);
SubType.prototype.sayAge = function(){
alert(this.age);
}
这个示例中的 inheritPrototype()函数实现了寄生组合式继承的最简单形式。这个函数接收两 个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二 步是为创建的副本添加 constructor 属性,从而弥补因重写原型而失去的默认的 constructor 属性。 最后一步,将新创建的对象(即副本)赋值给子类型的原型。
这个例子的高效率体现在它只调用了一次 SuperType 构造函数,并且因此避免了在 SubType. prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf()
优点:比较完美
缺点:比较麻烦