面向对象关键知识点汇总
先来了解一些基础概念,才能更好的理解知识。
对象的定义
ECMA-262把对象定义为:“无序属性的集合,其属性可以包含基本值、对象或者函数。
数据属性
4个特性
- [[Configurable]]:默认true 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性。
- [[Enumerable]]: 默认true 表示能否通过for-in 循环返回属性
- [[Writable]]: 默认true 表示能否修改属性的值。
- [[Value]]: 默认值为undefined, 属性的数据值,读和写入都在这个位置。
以上属性一般通过Object.defineProperty()方法来修改,接受三个参数:属性所在的对象、属性的名字和一个描述符对象。其中描述符就是上面四个特性。
访问器属性
4个特性
- [[Configurable]]:默认true 表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性。
- [[Enumerable]]: 默认true 表示能否通过for-in 循环返回属性
- 访问器属性不能直接定义,必须使用 Object.defineProperty()来定义。
定义多个属性 以及读取属性的特性
Object.defineProperties()方法
在调用 Object.defineProperty()方法时,如果不指定,configurable、enumerable 和
writable 特性的默认值都是 false。
Object.getOwnPropertyDescriptor()方法
接受两个参数:属性所在的对象和要读取其描述符的属性名称。
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"
工厂模式
用函数来封装以特定接口创建对象的细节
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");
作用:解决了创建多个相似对象的问题
缺陷:没有解决对象识别的问题(即怎么知道一个对象的类型)
构造函数模式
new 一个构造函数通常会经历一下四个步骤
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
- 执行构造函数中的代码(为这个新对象添加属性)
- 返回新对象
由于构造函数内部定义的属性和方法,通过实例化后都有自己的作用域链和辨识符解析。为了让属性和函数能够更好的公用,这时候原型模式就要登场了。
isPrototypeOf()的作用
当调用构造函数创建一个新实例后,该实例的内部包含一个指针(内部属性),指向构造函数的原型对象。ECMA-262中管这个指针叫[[Prototype]]。在脚本中没有标准的方式访问[[Prototype]],但浏览器(Firefox,Safari,chrome)在每个对象上都支持一个属性/proto/;我们要知道的是,这个属性对脚本则是完全不可见的。可以通过isPrototypeOf()方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]指向调用isPrototypeOf()方法的对象(Person.prototype),那么这个方法就返回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();
person1.sayName(); //"Nicholas"
Person.prototype.isPrototypeOf(person1)//true
ECMAScript 5 增加了一个新方法 Object.getPrototypeOf(),在所有支持的实现中,这个方法返回[[Prototype]]的值。例如
Object.getPrototypeOF(person1) === Person.prototype // true
hasOwnProperty()的使用场景
当我们从一个构造函数实例一个对象后,如果在这个实例上创建一个属性或者方法,正好与这个实例的原型上的属性或者方法同名,那么访问时就会屏蔽原型上的属性或者方法。当然这样做不会修改原型上的属性或者方法。如果要恢复指向原型,那么使用delete操作符则可以完全删除实例属性。 使用 hasOwnProperty()方法可以检测一个属性是存在于实例中,还是存在于原型中。不要忘记这个方法是从Object继承来的,所以只在给定属性存在于对象实例中时,才会返回true.
in 操作符的作用
in 操作符只要通过对象能够访问到属性就会返回true. ,hasOwnProperty()只在属性存在于实例中时才返回 true,因此只要 in 操作符返回 true 而hasOwnProperty()返回 false,就可以确定属性是原型中的属性。
function hasPrototypeProperty(object, name){
return !object.hasOwnProperty(name) && (name in object);
}
function Person(){
}
Person.prototype.name = "Nicholas";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
alert(this.name);
};
var person = new Person();
alert(hasPrototypeProperty(person, "name")); //true
person.name = "Greg";
alert(hasPrototypeProperty(person, "name")); //false
for-in的特殊
在使用 for-in 循环时,返回的是所有能够通过对象访问的、可枚举的(enumerated)属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性(即将[[Enumerable]]标记为 false 的属性)的实例属性也会在 for-in 循环中返回,因为根据规定,所有开发人员定义的属性都是可枚举的——只有在 IE8 及更早版本中例外。
Object.keys()
获取对象上所有可枚举的实例属性,返回一个字符串数组。
Object.getOwnPropertyNames()
如果是想得到所有的实例属性,包括不可枚举的,就能使用Object.getOwnPropertyNames() 能够获取到不可枚举的constructor属性。
constructor属性什么情况下会指向Object
function Person(){
}
Person.prototype = {
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
为了减少重复代码,我们经常用一个包含所有属性和方法的对象字面量来重写整个原型对象。代码如上所示。这里会产生一个情况,就是constructor属性不再指向Person。为什么会这样呢?我们知道,每创建一个函数,就会同时创建它的prototype对象,这个对象也会自动获得constructor属性。而我们上面这样的写法,本质上完全重写了默认的prototype对象,因此constructor属性也就变成了新对象的constructor属性(指向Object构造函数),不再指向Person函数。所以为什么我们用instanceof并不能准确的判别实例的构造函数。如下代码
var p = new Person()
console.log(p instanceof Object)//true
console.log(p instanceof Person)//true
console.log(p.constructor === Person)//false
console.log(p.constructor === Object)//true
有下面两种方法将constructor设置回来
function Person(){
}
Person.prototype = {
constructor : Person,
name : "Nicholas",
age : 29,
job: "Software Engineer",
sayName : function () {
alert(this.name);
}
};
以上这种重设constructor属性会导致它的[[Ennumerable]]特性设置为true,所以我们可以用下面这种方法;
//重设构造函数,只适用于 ECMAScript 5 兼容的浏览器
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
});
组合构造函数模式和原型模式
原型模式的缺点就是在共享时属性和方法后,要是修改了内容值,那么同一个构造函数出来的实例就会都受到影响。例如下面代码
function Person(){
}
Person.prototype = {
constructor: Person,
friends : ["Shelby", "Court"]
};
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
所以组合使用构造函数模式与原型模式就是大家普遍用到的方法,我们想要定义自己的属性和方法,那么就在构造函数中定义即可。例如下面代码:
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 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
什么是动态原型模式
意思就在初始化实例的时候就检测判断是否原型中存在你想要的属性或者方法,要是没有,就可以在原型上定义你想要的属性或方法。如一下代码:
function Person(name, age, job){
//属性
this.name = name;
this.age = age;
this.job = job; if (typeof this.sayName != "function"){
Person.prototype.sayName = function(){
alert(this.name);
};
}
}
var friend = new Person("Nicholas", 29, "Software Engineer");
friend.sayName()
什么是寄生构造函数模式
这个模式跟工厂模式几乎是一样的。通过在一个构造函数内部创建一个新的对象(数组、字符串),然后你可以随意在这个定义的新对象上添加属性和方法,最后return这个新的对象。例如一下代码:
function SpecialArray(){
//创建数组
var values = new Array();
//添加值
values.push.apply(values, arguments);
//添加方法
values.toPipedString = function(){
return this.join("|");
};
//返回数组
return values;
}
var colors = new SpecialArray("red", "blue", "green");
alert(colors.toPipedString()); //"red|blue|green"
这样做的优点是我们可以很轻松在原生对象上创建一个自己想要的方法和属性。但是返回的对象与构造函数或者与构造函数的原型属性之间没有关系。这样我们不能通过instanceof操作符确定对象类型。
什么是稳妥构造函数模式
所谓稳妥对象,指的是没有公共属性,而且其方法也不引用this对象。一是新创建对象的实例方法不引用this;二是不使用new操作符调用构造函数。
继承
什么是原型链
我们先来弄懂一句话: 每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。这就是相当于构建一个原型链的基石,整个原型链就是由这样一个基石累计起来。
为什么实例中很少单独使用原型链?
主要有下面两个原因
- 引用类型值的原型属性会被所有实例共享
- 在创建子类型的实例时,不能向超类型的构造函数中传递参数。
比如下面的例子中我们实现简单的继承
function Father(){
this.colors = ["red","blue","green"];
}
function Son(){}
Son.prototype = new Father();
var instance1 = new Son();
instance1.colors.push('black');
console.log(instance1.colors);// red blue,green,black
var instance2 = new Son();
console.log(instance2.colors);// red blue,green,black
上面例子我们可以看见两个实例共享了一个方法,这是我们不愿意看到的地方,那么有什么方法能够解决这个问题呢?
借用构造函数能否解决上面两个问题?
什么是借用构造函数? 就是在子类型构造函数的内部调用超类型的构造函数。如下面代码:
function Father(){
this.colors = ['red','blue','green'];
}
function Son(){
//继承 或者使用apply
Father.call(this)
}
var instance1 = new Son();
instance1.colors.push('black');
console.log(instance1.colors);//red blue,green,black
var instance2 = new Son();
console.log(instance2.colors);// red blue,green
怎么传递参数? 就是在调用父类构造函数时,传递参数。具体见下面代码
function Father(name) {
this.name = name;
}
function Son(){
Father.call(this,'pan');
this.age = 28;
}
var instance = new Son()
console.log(instance.name)//pan
console.log(instance.age)//28
但是借用构造函数也用的不多,为什么呢?
- 方法都在构造函数中定义,函数复用就没有施展的地方了。
- 而且超类型的原型中定义的方法,对子类型而言也是不可见的。
组合继承能够解决什么?
上面两种继承都有自己的缺点和优点,那么我们是否可以取长补短,把两家结合在一起?组合继承就是用来解决这个问题的。思路就是使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,即通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function Father(name) {
this.name = name;
this.colors = ['red','blue','green'];
}
Father.prototype.sayName = function(){
console.log(this.name)
}
function Son(name,age){
Father.call(this,name);
this.age = age;
}
//继承的实现
Son.prototype = new Father();
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('pan',28);
instance1.colors.push('black');
console.log(instance1.colors);// red ,blue,green,black
instance1.sayName();//pan
instance1.sayAge();//28
var instance2 = new Son('lin',26);
console.log(instance2.colors);// red ,blue,green
instance2.sayName();//lin
instance2.sayAge();//26
上面的例子我们看到取长补短的优势,即可以定义自己的属性和方法,又可以使用相同的方法。这是我们常用的继承模式,并且instanceof和isPrototypeOf也能够用于识别基于组合继承创建的对象。
原型式继承
我们先来看一个Object.create()的前身的写法,借助原型可以基于已有的对象创建新对象,同时还不必因此创建自定义类型。
function object(o) {
function F(){}
F.prototype = o;
return new F();
}
在一个function 内部创建一个临时性的构造函数,然后重写这个构造函数的原型,然后在返回这个构造函数的新的实例。不知道你又没有看出什么不好的地方没?从本质上讲,object()对传入其中的对象执行一次浅复制。
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
var yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
我们用新的方法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");
console.log(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"
}
});
console.log(anotherPerson.name); //"Greg"
如果只是简单的继承,那么原型模式就能胜任,但是想要创建构造函数,那么引用类型值的会共享这个坑是不能避免的。
寄生式继承又能解决什么问题?
都是寄生,自然和构造函数的寄生类似,创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增前对象,然后再返回这个对象。
function createAnother(original){
var clone = object(original);
clone.sayHi = function(){
console.log('hi')
}
return clone;
}
接下里我们运用上面这个函数
var person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi(); //"hi"
新对象anotherPerson不仅具有person的所有属性和方法,而且还有自己的sayHi()方法。但是此寄生式继承不能复用函数,所以效率不是很高。
终极boss 寄生组合式继承
我们前面说的组合继承是常用的模式,但是也有一个缺点:那就是会调用两次超类型构造函数。什么意思?我们来看代码;
function Father(name) {
this.name = name;
this.colors = ['red','blue','green'];
}
Father.prototype.sayName = function(){
console.log(this.name)
}
function Son(name,age){
Father.call(this,name);//第二次调用Father()
this.age = age;
}
//继承的实现
Son.prototype = new Father();//第一次调用Father()
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('pan',28);
instance1.colors.push('black');
console.log(instance1.colors);// red ,blue,green,black
instance1.sayName();//pan
instance1.sayAge();//28
上面代码中两个注释很好的反应了这两次构造函数。那么寄生组合式继承就能够很好的优化这个问题。通过Object.create()来很好的实现,只需要修改一处即可。
将Son.prototype = new Father();
替换成
Son.prototype = Object.create(Father.prototype)
高效率的体现就在只调用了一次Father构造函数,可以避免在Son.prototype上面创建不必要的、多余的属性。与此同时,原型链还能保持不变。并且还能正常使用instanceof和isPrototypeOf()。
多继承又如何实现?
上面的寄生组合式继承只是一个单继承,那多继承又该如何实现?
我们可以用混入的方式来实现多个对象的继承
//第一个构造函数
function Father(name) {
this.name = name;
this.colors = ['red','blue','green'];
}
Father.prototype.sayName = function(){
console.log(this.name)
}
//第二个构造函数
function FatherBrother(hobby){
this.hobby = hobby
}
function Son(name,age,hobby){
Father.call(this,name);
FatherBrother.call(this,hobby)
this.age = age;
}
//继承的实现
Son.prototype = Object.create(Father.prototype);
//混合
Object.assign(Son.prototype,FatherBrother.prototype)
//重新指定constructor
Son.prototype.constructor = Son;
Son.prototype.sayAge = function(){
console.log(this.age);
}
var instance1 = new Son('pan',28,'play');
instance1.colors.push('black');
console.log(instance1.colors);// red ,blue,green,black
instance1.sayName();//pan
instance1.sayAge();//28
instance1.hobby;//play
这里关键点是:Object.assign 会把 FatherBrother原型上的函数拷贝到 Son原型上,使 Son 的所有实例都可用 FatherBrother 的方法。
整个创建对象的演变过程
工厂模式(简单的添加属性和方法) -->构造函数模式(优点:可以创建内置对象实例。缺点:成员无法复用。)-->原型模式(结合构造函数模式和原型模式两方的优点,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。)-->原型式继承(优点:不必预先定义。缺点:会存在重写原型)-->寄生式继承(优点:效率高,不必多次调用超类型构造函数。缺点:复用率不高)-->寄生组合式继承(集寄生式继承和组合继承的优点与一身。)
如果大神您想继续探讨或者学习更多知识,欢迎加入QQ一起探讨:854280588
参考文章
- 红皮书第三版