什么是面向对象编程?
面向对象编程是一种编程思想。
面向过程:是分析出解决问题需要的步骤,然后编写函数实现每个步骤,最后依次调用函数。讲究依次顺序。
面向对象:是把构成问题的事物拆解为各个对象,而拆解出对象的目的也不是为了实现某个步骤,而是为了描述这个事物在当前问题中的各种行为。
面向对象的特点
- 继承:就是为了代码的复用,从父类上继承出一些方法和属性,子类也有自己的一些属性
- 封装:让使用对象的人不考虑内部实现,只考虑功能使用 把内部的代码保护起来,只留出一些 api 接口供用户使用
- 是不同对象作用于同一操作产生不同的效果。多态的思想实际上是把“想做什么”和“谁去做“分开
对象
一些内置对象
Object
Array
Date
Function
RegExp
….
一. 理解对象
创建对象
1.创建一个Object实例,再为他添加属性和方法
let person = new Object()
person.name = "小明";
person.age = "22";
person.sayName = fucntion(){
console.log(this.name)
}
复制代码
2 对象的字面量
let person = {
name:"小明",
age:22,
sayName:function(){
consoel.log(this.name)
}
}
复制代码
1. 属性类型
在js中有两种属性:数据属性和访问器属性。
1.1 数据属性
数据属性有四个描述行为的特性
`Configurable`:表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。
`Enumerable`:表示能否通过for-in循环返回属性,是否可枚举。
`Writable`:表示能否修改属性的值。
`Value`:表示包含这个属性的数据值。读取属性的时候,从这个位置读,写入属性值的时候,把新值保存在这个位置。
复制代码
而在前面的例子中直接在对象上定义的属性,他们的[[Configurable]]
,[[Enumerable]]
,[[Writable]]
特性都被设置为true
,而[[Value]]
特性被设置为指定的值。例如:
let person = {
name:"小明"
}
复制代码
必须使用Object.defineProperty()
方法。这个方法接受三个参数:属性所在的对象,属性名和一个描述符对象,其中描述符对象的属性必须是:configurable
,enumerable
,writable
,和value
,设置其中的一个或者多个值,可以修改对应的属性。
//不可修改属性值
let person = {
name:"小明",
age:18,
}
Object.defineProperty(person,'name',{
writable:false,
value:"小红"
})
person.name = "小王"
console.log(person.name) //小红
//不可使用delete删除
let person = {
name:"小明",
age:18
}
Object.defineProperty(person,'name',{
configurable:false,
value:"小黄"
})
person.name = "小红"
console.log(person.name) //小红
delete person.name
console.log(person) //{name:"小红",age:18}
复制代码
而且,一旦把属性定义为不可配置的,就不能再把它变回可配置了。此时,再调用 Object.defineProperty()
方法修改除 writable
之外的特性,都会导致错误:
let person = {};
Object.defineProperty(person,'name',{
configurable:false,
value:"小明"
})
//抛出错误
Object.defineProperty(person,'name',{
configurable:true,
value:"小黄"
})
//可以多次调用Object.defineProperty()方法修改同一个属性,但是把configurable的属性设置为false之后就会有所限制了。
复制代码
1.2 访问器属性
getter
和setter
函数
在读取访问器属性时,会调用getter
这个函数,这个函数负责返回有效的值;在写入访问器属性时,会调用setter
函数并传入新的值,这个函数负责如何进行处理数据。
访问器属性有以下4个特性:
`Configurable`:表示能否通过 delete 删除属性从而重新定义属性,能否修改属性的特
性,或者能否把属性修改为数据属性。对于直接在对象上定义的属性,这个特性的默认值为
true。
`Enumerable`:表示能否通过 for-in 循环返回属性。对于直接在对象上定义的属性,这
个特性的默认值为 true。
`Get`:在读取属性时调用的函数。默认值为 undefined。
`Set`:在写入属性时调用的函数。默认值为 undefined。
复制代码
let book = {
_year: 2004, //用于表示只能通过对象方法访问的属性。
edition: 1
};
Object.defineProperty(book, "year", {
get: function () {
return this._year;
},
set: function (newValue) {
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
});
book.year = 2005; //改变值,触发set
console.log(book.edition) //2
复制代码
1.3 定义多个属性
Object.defineProperties()
方法 :对象定义多个属性的方法,利用这个方法可以通过描述符一次定义多个属性。这个方法接收两个对象参数:第一个对象是要添加和修改其属性的对象,第二个对象的属性与第一个对象中要添加或修改的属性一一对应。
let 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 = 2005;
console.log(book.edition); //1
console.log(book.year); //2004
复制代码
1.4 读取属性的特性
Object.getOwnPropertyDescriptor()
方法
可以取得给定属性的描述符。这个方法接收两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有 configurable
、enumerable
、get
和 set
;如果是数据属性,这个对象的属性有 configurable
、enumerable
、writable
和 value
。
let book = {};
Object.defineProperties(book, {
_year: {
value: 2004,
writable:true
},
edition: {
value: 1
},
year: {
get: function(){
return this._year;
},
set: function(newValue){
if (newValue > 2004) {
this._year = newValue;
this.edition += newValue - 2004;
}
}
}
});
book.year = 2005;
//数据属性
let descriptor = Object.getOwnPropertyDescriptor(book,'_year')
console.log(descriptor.value) //2004
console.log(descriptor.configurable) //false
console.log(descriptor.writable) //true
console.log(descriptor.enumerable) //false
console.log(typeof descriptor.get) //undefined
//访问器属性
let seconddescriptor = Object.getOwnPropertyDescriptor(book,'year')
console.log(typeof seconddescriptor.get) //function
console.log(seconddescriptor.enumerable) //false
console.log(seconddescriptor.value) //undefined
复制代码
二.创建对象
虽然 Object
构造函数或对象字面量都可以用来创建单个对象,但这些方式有个明显的缺点:使用同一个接口创建很多对象,会产生大量的重复代码。
2.1 工厂模式
function creatobj(name,age,year){
let obj = new Object();
obj.name = name;
obj.age = age;
obj.year = year;
obj.sayName = function(){
console.log(this.name)
}
return obj
}
let person1 = creatobj('xiaoming',18,1997)
console.log(person1)
person1.sayName()
let person2 = creatobj('xiaohuang',20,2000)
复制代码
函数creatobj()
能够根据接受的参数来构建一个包含所有必要信息的 obj
对象。可以无数次地调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎样知道一个对象的类型)。
2.11 工厂模式的优劣
优点:工厂模式解决了创建多个相似对象的问题。(也就是大量重复的冗余代码)
缺点:没有解决对象识别的问题(即怎样知道一个对象的类型)。只能判断实例的类型是
Object
,但判断不出来是creatobj
。
2.2 构造函数模式
构造函数可用来创建特定类型的对象。像 Object
和 Array
这样的原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。
function Creatobj(name,age,year){
this.name = name;
this.age = age;
this.sayName = function(){
console.log(this.name)
}
}
let person = new Creatobj('xiaoming',18,2000)
person.sayName() //xiaoming
console.log(person.name) //xiaoming
let person2 = new Creatobj('xiaohong',20,1998)
复制代码
在这个例子中,Createobj()
函数取代了createobj()
函数。我们注意到,Createobj()
中的代码除了与 函数取代了createobj()
中相同的部分外,还存在以下不同之处:
- 没有显式地创建对象;
- 直接将属性和方法赋给了 this 对象;
- 没有 return 语句。
另外,值得注意的是,构造函数名一般首字母大写,例如Createobj()
。
2.2.1 new关键字
要创建 Person
的新实例,必须使用 new
操作符。以这种方式调用构造函数实际上会经历以下 几个步骤:
-
创建一个新对象(例如
p1
) -
将新对象的
__proto__
属性指向函数的prototype
(即p1.__proto__
===Fn.prototype
) -
将函数的
this
指向新创建的对象 -
返回新对象
-
- 构造函数没有显式返回值,返回
this
- 构造函数有显式返回值,且是基本类型,比如
number
,string
,那么还是返回this - 构造函数有显式返回值,且是对象类型,比如
{a:1}
,那么返回这个对象
- 构造函数没有显式返回值,返回
具体封装过程详见interview文件夹。
在前面例子的最后,person1
和 person2
分别保存着 Creatobj
的一个不同的实例。这两个对象都有一个 constructor
(构造函数)属性,该属性指向 Creatobj
,如下所示
console.log(person.constructor === Creatobj) //true
console.log(person2.constructor === Creatobj) //true
复制代码
创建的对象即是Object的实例,也是Creatobj的实例,这一点,可以通过instanceof来进行验证:
console.log(person instanceof Creatobj) //true
console.log(person2 instanceof Creatobj) //true
复制代码
2.2.2 将构造函数当作函数
构造函数与其他函数的唯一区别,就在于调用它们的方式不同。
任何函数,只要通过 new 操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过 new 操作符来调用,那它跟普通函数也不会有什么两样。
将前面的例子当作构造函数使用:
var person = new Creatobj("xiaokong",22);
person.sayName(); //xiaokong
复制代码
当作普通函数调用:
Creatobj("xiaokong",22);
window.sayName(); //"xiaokong"
复制代码
在另一个对象的作用域中调用:
let obj = new Object();
Creatobj.call(obj,"xiaokong",22)
obj.sayName() //"xiaokong"
复制代码
这个例子中的前两行代码展示了构造函数的典型用法,即使用 new
操作符来创建一个新对象。接下来的两行代码展示了不使用new
操作符调用Creatobj()
会出现什么结果:属性和方法都被添加给window
对象了。有读者可能还记得,当在全局作用域中调用一个函数时,this
对象总是指向 Global
对象(在浏览器中就是 window
对象)。因此,在调用完函数之后,可以通过 window
对象来调用 sayName()
方法,并且还返回了”xiaokong”。最后,也可以使用 call()
(或者 apply()
)在某个特殊对象的作用域中调用Creatobj()
函数。这里是在对象obj
的作用域中调用的,因此调用后obj
就拥有了所有属性和sayName()
方法.
2.2.3 构造函数模式的优劣
优点:即使改变了某一个对象的属性或方法,不会影响其他的对象(因为每一个对象都是复制的一份),解决了工厂模式中无法判断类型的问题。
缺点:使用构造函数的主要问题,就是每个方法都要在每个实例上重新创建一遍(内存中复制一份),方法在不同实例中无法进行复用,这样就会造成内存的浪费
2.3 原型模式
我们创建的每个函数都有一个 prototype
(原型)属性,这个属性是一个指针,指向一个对象,而这个对象的用途是包含可以由特定类型的所有实例共享的属和方法。
prototype
就是通过调用构造函数而创建的那个对象实例的原型对象。
使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必在构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
function Person(){}
Person.prototype.name = 'xiaoming'
Person.prototype.age = 18
Person.prototype.sayName = function(){
console.log(this.name)
}
let person1 = new Person()
person1.sayName() //'xiaoming'
let person2 = new Person()
person2.sayName() //'xiaoming'
console.log(person1.sayName === person2.sayName) //true
复制代码
我们将 sayName()
方法和所有属性直接添加到了 Person
的 prototype
属性中,构造函数变成了空函数。即使如此,也仍然可以通过调用构造函数来创建新对象,而且新对象还会具有相同的属性和方法。但与构造函数模式不同的是,新对象的这些属性和方法是由所有实例共享的。换句话说,person1
和 person2
访问的都是同一组属性和同一个 sayName()
函数。
2.3.1 理解原型对象
无论什么时候,只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个 prototype
属性,这个属性指向函数的原型对象。
在默认情况下,所有原型对象都会自动获得一个 constructor
(构造函数)属性,这个属性包含一个指向 prototype
属性所在函数的指针。
(Person.prototype.constructor
指向 Person
)
而通过这个构造函数,我们还可继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会取得 constructor
属性;至于其他方法,则都是从 Object
继承而来的。
当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。
以前面使用 Person
构造函数和 Person.prototype
创建实例的代码为例,展示各个对象之间的关系:
Person.prototype
指向了原型对象,而 Person.prototype.constructor
又指回了 Person
。原型对象中除了包含 constructor
属性之外,还包括后来添加的其他属性。Person
的每个实例——person1
和 person2
都包含一个内部属性,该属性仅仅指向了 Person.prototype
;换句话说,它们与构造函数没有直接的关系。
- isPrototypeOf()方法
虽然在所有实现中都无法访问到[[Prototype]]
,但可以通过 isPrototypeOf()
方法来确定对象之间是否存在这种关系。从本质上讲,如果[[Prototype]]
(原型指针)指向调用 isPrototypeOf()
方法的对象(Person.prototype)
,那么这个方法就返回 true
。
console.log(Person.prototype.isPrototypeOf(person1)) //true
console.log(Person.prototype.isPrototypeOf(person2)) //true
复制代码
person1
和 person2
因为它们内部都有一个指向 Person.prototype
的指针,因此都返回了 true
。
- Object.getPrototypeOf()
这个方法返回[[Prototype]]
的值。
console.log(Object.getPrototypeOf(person1) === Person.prototype) //true
console.log(Object.getPrototypeOf(person2).name) //"xiaoming"
console.log(Object.getPrototypeOf(person1)) //Person.prototype
复制代码
这里的第一行代码只是确定 Object.getPrototypeOf()
返回的对象实际就是这个对象的原型。
第二行代码取得了原型对象中 name
属性的值,也就是”xiaoming”。
使用 Object.getPrototypeOf()
可以方便地取得一个对象的原型。
每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。
搜索首先从对象实例本身开始。
如果在实例中找到了具有给定名字的属性,则返回该属性的值;
如果没有找到,则继续搜索指针指向的原型对象,在原型对象中查找具有给定名字的属性。
如果在原型对象中找到了这个属性,则返回该属性的值。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值
。如果我们 在实例中添加了一个属性,而该属性与实例原型中的一个性同名,那我们就在实例中创建该属性,该属性将会屏蔽原型中的那个属性。
function Person(){};
Person.prototype.name = "xiaoming";
Person.prototype.age = "22";
Person.prototype.job = "haha";
Person.prototype.sayName = function(){
console.log(this.name);
}
var person1 = new Person();
person1.name = "huahua";
console.log(person1.name); //"huahua" 来自实例
var person2 = new Person();
console.log(person2.name); //"xiaoming" 来自原型
//中访问 person1.name 时,需要读取它的值,因此就会在这个实例上搜索一个名为 name的属性。
//这个属性确实存在,于是就返回它的值而不必再搜索原型了。
//当以同样的方式访问 person2.name 时,并没有在实例上发现该属性,因此就会继续搜索原型,结果在那里找到了 name 属性。
复制代码
当为对象实例添加一个属性时,这个属性就会屏蔽原型对象中保存的同名属性;换句话说,添加这个属性只会阻止我们访问原型中的那个属性,但不会修改那个属性。即使将这个属性设置为 null
,也只会在实例中设置这个属性,而不会恢复其指向原型的连接。不过,使用 delete
操作符则可以完全删除实例属性,从而让我们能够重新访问原型中的属性。
function Person(){}
Person.prototype.name = "xiaoming";
Person.prototype.age = 29;
Person.prototype.job = "Software Engineer";
Person.prototype.sayName = function(){
console.log(this.name);
};
var person1 = new Person();
var person2 = new Person();
person1.name = "huahua";
console.log(person1.name); //"huahua"——来自实例
console.log(person2.name); //"xiaoming"——来自原型
delete person1.name;
console.log(person1.name); //"xiaoming"——来自原型
复制代码
在这个修改后的例子中,我们使用 delete
操作符删除了 person1.name
,之前它保存的”huahua
“值屏蔽了同名的原型属性。把它删除以后,就恢复了对原型中 name
属性的连接。因此,接下来再调用person1.name
时,返回的就是原型中 name
属性的值。
- hasOwnProperty属性
可以检测一个属性是存在于实例中,还是存在于原型中。
这个方法(不要忘了它是从 Object
继承来的)只在给定属性存在于对象实例中时,才会返回 true
。
function Person(){}
Person.prototype.name = 'xiaoming'
Person.prototype.age = 16
Person.prototype.sayName = function(){
console.log(this.name)
}
let person1 = new Person()
person1.sayName()
let person2 = new Person()
person2.sayName()
console.log(person1.hasOwnProperty("name")) //false
person2.name = 'xiaohong'
console.log(person2.hasOwnProperty("name")) //true
delete person2.name
console.log(person2.hasOwnProperty("name")) //false
复制代码
调用 person1.hasOwnProperty( "name")
时,只有当 person1
重写 name
属性后才会返回 true
,因为只有这时候 name
才是一个实例属性,而非原型属性。
Object.getOwnPropertyDescriptor()
方法只能用于实例属性,要取得原型属性的描述符,必须直接在原型对象上调用Object.getOwnPropertyDescriptor()
方法。
let des = Object.getOwnPropertyDescriptor(person2,"name")
console.log(des) //{ value: 'xiaoming', writable: true, enumerable: true, configurable: true }
let des2 = Object.getOwnPropertyDescriptor(person1,"name")
console.log(des2) //undefined
let des3 = Object.getOwnPropertyDescriptor(Person.prototype,"name")
console.log(des3) //{ value: 'xiaoming',writable:true,enumerable: true,configurable: true}
复制代码
2.3.2 原型与in操作符
有两种方式使用 in
操作符:单独使用和在 for-in
循环中使用。在单独使用时,in
操作符会在通过对象能够访问给定属性时返回 true
,无论该属性存在于实例中还是原型中。
function Person(){}
Person.prototype.name = 'xiaoming'
Person.prototype.age = 16
Person.prototype.sayName = function(){
console.log(this.name)
}
let person1 = new Person()
let person2 = new Person()
person2.name = "xiaohong"
console.log(person1.hasOwnProperty("name")) //false
console.log(person2.hasOwnProperty("name")) //true
console.log("name" in person1) //true
console.log("name" in person2) //true
复制代码
可以根据in的特性来进行封装方法,判断是否可以在原型上进行访问:
function hasPrototypeProperty(object , name){
return !object.hasOwnPrototype(name) && name in object;
}
复制代码
使用:
console.log(hasPrototypeProperty(person1,"name")) //true
console.log(hasPrototypeProperty(person2,"name")) //false
复制代码
- Object.key()方法
要取得对象上所有可枚举的实例属性,可以使用Object.keys()
方法。这个方法接收一个对象作为参数,返回一个包含所有可枚举属性的字符串数组。
function Person(){}
Person.prototype.name = 'xiaoming'
Person.prototype.age = 16
Person.prototype.sayName = function(){
console.log(this.name)
}
let person1 = new Person()
let person2 = new Person()
person2.name = "xiaohong"
person2.age = 18
console.log(Object.keys(person2)) //[ 'name', 'age' ]
console.log(Object.keys(Person.prototype)) //[ 'name', 'age', 'sayName' ]
复制代码
- Object.getOwnPropertyNames()
得到所有实例属性,无论它是否可枚举。
let keys = Object.getOwnPropertyNames(Person.propertype)
console.log(keys) //[ 'name', 'age', 'sayName' ]
复制代码
2.3.3 更简单的原型语法
前面例子中每添加一个属性和方法就要敲一遍 Person.prototype
。为减少不必要的输入,也为了从视觉上更好地封装原型的功能,更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
function Person(){}
Person.prototype = {
name:"xiaoming",
age:18,
sayName:function(){
console.log(this.name)
}
}
复制代码
我们将 Person.prototype
设置为等于一个以对象字面量形式创建的新对象。 最终结果相同,但有一个例外:constructor
属性不再指向 Person
了。
前面曾经介绍过,每创建一个函数,就会同时创建它的 prototype
对象,这个对象也会自动获得 constructor
属性。而我们在这里使用的语法,本质上完全重写了默认的 prototype
对象,因此 constructor
属性也就变成了新 对象的 constructor
属性(指向 Object
构造函数),不再指向 Person
函数。
此时,尽管 instanceof
操作符还能返回正确的结果,但通过 constructor
已经无法确定对象的类型了,如下所示:
Person.prototype = {
name:"xiaoming",
age:18,
sayName:function(){
console.log(this.name)
}
}
let first = new Person()
console.log(first.constructor == Person)
console.log(first.constructor == Object)
console.log(first instanceof Person)
console.log(first instanceof Object)
//first instanceof Person 表示Person.prototype是否存在于参数first的原型链上。
复制代码
如果 constructor
的值真的很重要,可以像下面这样特意将它设置回适当的值。
Person.prototype = {
constructor:Person,
name:"xiaoming",
age:18,
sayName:function(){
console.log(this.name)
}
}
//以上代码特意包含了一个 constructor 属性,并将它的值设置为 Person,从而确保了通过该属性能够访问到适当的值。
复制代码
注意,以这种方式重设 constructor
属性会导致它的[[Enumerable]]
特性被设置为 true
。默认 情况下,原生的 constructor
属性是不可枚举的,因此如你使用兼容 ECMAScript 5 的 JavaScript 引擎,可以试一试 Object.defineProperty()
方法。
Person.prototype = {
name:"xiaoming",
age:18,
sayName:function(){
console.log(this.name)
}
}
Object.definePorperty(Person.prototype,'constructor',{
Enumerable:false,
value:Person
})
复制代码
2.3.4 原型的动态性
由于在原型中查找值的过程是一次搜索,因此我们对原型对象所做的任何修改都能够立即从实例上反映出来——即使是先创建了实例后修改原型也照样如此。
var friend = new Person();
Person.prototype.sayHa = function(){
console.log("hahaha");
}
friend.sayHa(); //"hahaha"
复制代码
以上代码先创建了 Person
的一个实例,并将其保存在 person
中。然后,下一条语句在Person.prototype
中添加了一个方法 sayHa()
。
即使 person
实例是在添加新方法之前创建的,但它仍然可以访问这个新方法。其原因可以归结为实例与原型之间的松散连接关系。当我们调用 person.sayHa()
时,首先会在实例中搜索名为 sayHa
的属性,在没找到的情况下,会继续搜索原型。
因为实例与原型之间的连接只不过是一个指针,而非一个副本,因此就可以在原型中找到新的 sayHa
属性并返回保存在那里的函数。
但如果是重写整个原型对象,那么情况就不一样了。我们知道,调用构造函数时会为实例添加一个指向最初原型的[[Prototype]]
指针,而把原型修改为另外一个对象就等于切断了构造函数与最初原型之间的联系。
请记住:实例中的指针仅指向原型,而不指向构造函数。
function Person(){
}
//此时他的__proto__指针已确定
var friend = new Person();
//此时Person.prototype已经是开辟了新的空间,和原来的已经没有关系
Person.prototype = {
constructor: Person,
name : "xiaoming",
age : 29,
job : "Software Engineer",
sayName : function () {
console.log(this.name);
}
};
friend.sayName(); //error
复制代码
在这个例子中,我们先创建了 Person
的一个实例,然后又重写了其原型对象。然后在调用friend.sayName()
时发生了错误,因为 friend
指向的原型中不包含以该名字命名的属性。
重写原型对象切断了现有原型与任何之前已经存在的对象实例之间的联系;它们引用的仍然是最初的原型。
总结:利用重写原型的方式为函数的prototype属性添加属性有两大问题:1.重写后的constructor属性丢失,不再默认指向构造函数。2.如果在创建实例之后对函数的prototype属性进行重写,那么实例的proto也会丢失
2.3.5 原生对象的原型
所有原生引用类型(Object
、Array
、String
,等等)都在其构造函数的原型上定义了方法。
例如,在 Array.prototype
中可以找到 sort()
方法,而在 String.prototype
中可以找到substring()
方法,如下所示:
console.log(typeof String.prototype.substring); //"function"
console.log(typeof Array.prototype.sort); //"function"
//在原生引用类型中的prototype定义的方法
复制代码
通过原生对象的原型,不仅可以取得所有默认方法的引用,而且也可以定义新方法。可以像修改自String
添加了一个名为 startsWith()
的方法。
定义对象的原型一样修改原生对象的原型,因此可以随时添加方法。下面的代码就给基本包装类型。
let str = '123456'
String.prototype.mystartWith = function(text){
return this.indexOf(text) === -1
}
console.log(str.mystartWith(2)) //false
console.log(str.mystartWith(7)) //true
//判断某个字符是否存在于字符串中
复制代码
function Person(){}
Person.prototype = {
name:"xiaoming",
age:18,
friends:['xiaohua','xiaowan','mingming'],
sayName:function(){
console.log(this.name)
}
}
let first = new Person()
let second = new Person()
second.friends.pop();
console.log(first.friends) //[ 'xiaohua', 'xiaowan' ]
console.log(second.friends) //[ 'xiaohua', 'xiaowan' ]
复制代码
如上面所示:如果定义了引用类型的属性,那么所有的实例将会共享同一个属性,其中一个实例改变,所有的都会改变。
2.3.6 原型模式的优劣
优点:所有的参数和方法只在内存中创建一次,实例化的对象都会指向这个 prototype 对象。
缺点:1.原型中所有属性是被很多实例共享的,对于引用类型值的属性来说,添加或者修改其中一个实例的属性也会改变其他实例。2.省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值。
2.4 组合使用构造函数模式和原型模式
function Person(name,age,friends){
this.name = name;
this.age = age;
this.friends = friends
}
Person.prototype = {
constructor:Person,
sayName(){
console.log(this.name)
}
}
let person1 = new Person('xiaoming',18,['xiaowang','xiaolu','xiangfen'])
let person2 = new Person('xiaohong',20,['xiaoyi','xiaohei','xiangwu'])
person1.friends.pop();
console.log(person1.friends) //[ 'xiaowang', 'xiaolu' ]
console.log(person2.friends) //[ 'xiaoyi', 'xiaohei', 'xiangwu' ]
console.log(person1.name === person2.name) //true
console.log(person1.sayName === person2.sayName) //false
复制代码
实例属性都是在构造函数中定义的,而由所有实例共享的属性 constructor
和方法 sayName()
则是在原型中定义的。而修改了 person1.friends
(向其中添加一个新字符串),并不会影响到 person2.friends
,因为它们分别引用了不同的数组。
2.5 动态原型模式
它把所有信息都封装在了构造函数中,而通过在构造函数中初始化原型(仅在必要的情况下),又保持了同时使用构造函数和原型的优点。换句话说,可以通过 检查某个应该存在的方法是否有效,来决定是否需要初始化原型。
function Person(name,age,friends){
this.name = name;
this.age = age;
this.friends = friends
if(typeof this.sayName != 'function'){
Person.prototype.sayName = function(){
console.log(this.name)
}
}
}
let person1 = new Person('xiaoming',18,['xiaohong','xiaolu','xiaohuang'])
let person2 = new Person('xiaohong',20,['xiaozi','xiaohua'])
person1.sayName() //xiaoming
复制代码
2.6 寄生构造函数模式
除了使用 new
操作符并把使用的包装函数叫做构造函数之外,这个模式跟工厂模式其实是一模一样的。构造函数在不返回值的情况下,默认会返回新对象实例。而通过在构造函数的末尾添加一个 return
语句,可以重写调用构造函数时返回的值。
function Person(name,age,friends){
const obj = new Object();
obj.name = name;
obj.age = age;
obj.friends = friends;
obj.sayName = function(){
console.log(this.name)
}
return obj
}
let person1 = new Person('xiaoming',18,['xiaohua','xianghuang','xiaolu'])
let person2 = new Person('xiaohua',20,['xiaofen','xianggua','xiaotu'])
person2.sayName() //xiaohua
console.log(person1.age) //18
console.log(person1.sayName === person2.sayName) //false
复制代码
2.7 稳妥构造函数模式
可以将前面的函数重写为:
function Person(name, age, job){
//创建要返回的对象
var o = new Object(); 13
//可以在这里定义私有变量和函数
//添加方法
o.sayName = function(){
console.log(name);
};
//返回对象
return o;
}
复制代码
注意,在以这种模式创建的对象中,除了使用 sayName()
方法之外,没有其他办法访问 name 的值。可以像下面使用稳妥的 Person
构造函数。
var friend = Person("Nicholas", 29, "Software Engineer");
friend.sayName(); //"Nicholas"
复制代码
这样,变量 friend
中保存的是一个稳妥对象,而除了调用 sayName()
方法外,没有别的方式可以访问其数据成员。即使有其他代码会给这个对象添加方法或数据成员,但也不可能有别的办法访问传入到构造函数中的原始数据。
三.继承
3.1 原型链继承
其基本思想就是利用原型让一个引用类型继承另一个引用类型的属性和方法。
构造函数,实例和原型的关系:
每个构造函数都有一个原型对象,原型对象都包含一个指向构造函数的指针,而实例都包含一个指向原型对象的内部指针。
而如果让原型对象等于另一个类型的实例,那么此时的原型对象将包含一个指向另一个原型的指针,相应的,另一个原型也包含着一个指向另一个构造函数的指针。假如另一个原型又是另一个类型的实例,那么上述关系依然成立,如此层层递进,就构成了实例与原型的链条,这就是原型链的概念。
其实也就是,将一个包含指向原型的原型指针的实例等于一个原型对象,那么这个原型对象也就具有了指向那个原型的指针。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
确立了继承关系之后再给子类的prototype添加方法,因为本质上是重写了subType.prototype
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
var instance = new SubType();
alert(instance.getSuperValue()); //true
以上代码定义了两个类型:SuperType 和 SubType。每个类型分别有一个属性和一个方法。
它们的主要区别是 SubType 继承了 SuperType,而继承是通过创建 SuperType 的实例,并将该实例赋给SubType.prototype 实现的。
实现的本质是重写原型对象,代之以一个新类型的实例。换句话说,原来存在于 SuperType 的实例中的所有属性和方法,现在也存在于 SubType.prototype 中了。在确立了继承关系之后,我们给 SubType.prototype 添加了一个方法,这样就在继承了 SuperType 的属性和方法的基础上又添加了一个新方法。
复制代码
在上面的代码中,我们没有使用 SubType
默认提供的原型,而是给它换了一个新原型;这个新原型就是 SuperType
的实例。于是,新原型不仅具有作为一个 SuperType
的实例所拥有的全部属性和方法,而且其内部还有一个指针,指向了 SuperType
的原型。最终结果就是这样的:instance
指向 SubType
的原型, SubType
的原型又指向 SuperType
的原型。 getSuperValue()
方法仍然还在SuperType.prototype
中,但 property
则位于 SubType.prototype
中。这是因为 property
是一个实例属性,而 getSuperValue()
则是一个原型方法。既然 SubType.prototype
现在是 SuperType
的实例,那么 property
当然就位于该实例中了。此外,要注意 instance.constructor
现在指向的是 SuperType
,这是因为原来 SubType.prototype
中的 constructor
被重写了的缘故。
3.1.1 默认的原型
所有的引用类型默认都继承了Object
,而这个继承也是根据原型链来实现的。所有函数的默认原型都是Object
的实例,因此默认原型都会包含一个内部指针,指向Object.prototype
。这也是所有自定义类型都会继承toString()
,valueOf()
等默认方法的根本原因。
SubType
继承了 SuperType
,而 SuperType
继承了 Object
。当调用 instance.toString()
时,实际上调用的是保存在 Object.prototype
中的那个方法。
3.1.2 确定原型和实例的关系
instanceof
方法
console.log(instance instanceof Object); //true
console.log(instance instanceof SuperType); //true
console.log(instance instanceof SupType); //true
复制代码
isPrototypef()
方法
console.log(Object.prototype.isPrototypeOf(instance)) //true
console.log(SuperType.prototype.isPrototypeOf(instance)) //true
console.log(SubType.prototype.isPrototypeOf(instance)) //true
复制代码
3.1.3 谨慎的定义方法
子类型有时候需要重写超类型中的某个方法,或者需要添加超类型中不存在的某个方法。但不管怎样,给原型添加方法的代码一定要放在替换原型的语句之后。
function SuperType(){
this.property = true;
}
SuperType.prototype.getSuperValue = function(){
return this.property;
};
function SubType(){
this.subproperty = false;
}
//继承了 SuperType
SubType.prototype = new SuperType();
//添加新方法 再继承后添加新方法,否则会被覆盖
SubType.prototype.getSubValue = function (){
return this.subproperty;
};
//重写超类型中的方法
SubType.prototype.getSuperValue = function (){
return false;
};
var instance = new SubType();
console.log(instance.getSuperValue()); //false
复制代码
在通过原型链实现继承时,不能使用对象字面量创建原型方法。因为这样做就会重写原型链。
function SuperType(){
this.property = true;
}
SuperType.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();
console.log(instance.getSuperValue()); //error!
复制代码
以上代码展示了刚刚把 SuperType
的实例赋值给原型,紧接着又将原型替换成一个对象字面量而导致的问题。由于现在的原型包含的是一个 Object
的实例,而非 SuperType
的实例,因此我们设想中的原型链已经被切断——SubType
和 SuperType
之间已经没有关系了。
3.2 构造函数继承
function SuperType(){
this.colors = ['red','blue','yellow','pink'];
}
function SubType(){
SuperType.call(this)
}
let person1 = new SubType()
let person2 = new SubType()
person1.colors.push('write')
console.log(person1.colors) //[ 'red', 'blue', 'yellow', 'pink', 'write' ]
console.log(person2.colors) //[ 'red', 'blue', 'yellow', 'pink' ]
复制代码
通过使用 call()
方法(或 apply()
方法也可以),我们实际上是在(未来将要)新创建的 SubType
实例的环境下调用了 SuperType
构造函数。这样一来,就会在新 SubType
对象上执行 SuperType()
函数中定义的所有对象初始化代码。结果,SubType
的每个实例就都会具有自己的 colors
属性的副本。
3.2.1 传递参数
function SuperType(name){
this.name = name;
}
function SubType(name){
//继承了 SuperType,同时还传递了参数
SuperType.call(this, name);
//实例属性
this.age = 29;
}
let instance = new SubType('Nicholas');
console.log(instance.name); //"Nicholas";
console.log(instance.age); //29
复制代码
以上代码中的 SuperType
只接受一个参数 name
,该参数会直接赋给一个属性。在 SubType
构造函数内部调用 SuperType
构造函数时,实际上是为 SubType
的实例设置了 name
属性。为了确保SuperType
构造函数不会重写子类型的属性,可以在调用超类型构造函数后,再添加应该在子类型中定义的属性。
3.3 组合继承
使用原型链实现对原型属性和方法的继承,而通过借用构造函数来实现对实例属性的继承。这样,既通过在原型上定义方法实现了函数复用,又能够保证每个实例都有它自己的属性。
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
console.log(this.name)
}
function SubType(name,age){
//继承了 SuperType,同时还传递了参数
SuperType.call(this, name);
//实例属性
this.age = age;
}
SubType.prototype = new SuperType()
SubType.prototype.sayAge = function(){
console.log(this.age)
}
let instance1 = new SubType('Nicholas',29);
instance1.colors.push("black");
console.log(instance1.colors); //[ 'red', 'blue', 'green', 'black' ]
instance1.sayName(); //"Nicholas";
instance1.sayAge(); //29
let instance2 = new SubType('boer',18)
console.log(instance2.colors); //[ 'red', 'blue', 'green' ]
instance2.sayName(); //"boer";
instance2.sayAge(); //18
复制代码
instanceof
运算符用来测试一个对象在其原型链中是否存在一个构造函数的 prototype
属性。
isPrototypeOf()
方法用于测试一个对象是否存在于另一个对象的原型链上。
3.4 原型式继承
这种方法并没有严格意义上的构造函数,这种想法是借助原型可以基于已有的对象创建新对象,同时不必创建自定义类型。
function object(o){
function F(){};
F.prototype = o;
return new F()
}
复制代码
在object()
函数内部,先创建了一个临时性的构造函数,然后将传入的对象作为这个构造函数的原型,最后返回了这个临时类型的一个新实例。从本质上来讲,object()
对传入其中的对象执行了一次浅复制。
let anotherPerson = object(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = object(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
复制代码
要求你必须有一个对象可以作为另一个对象的基础。如果有这么一个对象的话,可以把它传递给object()
函数,然后再根据具体需求对得到的对象加以修改即可。在这个例子中,可以作为另一个对象基础的是person
对象,于是我们把它传入到object()
函数中,然后该函数就会返回一个新对象。这个新对象将person
作为原型,所以它的原型中就包含一个基本类型值属性和一个引用类型值属性。这意味着person.friends
不仅属于person
所有,而且也会被anotherPerson
以及yetAnotherPerson
共享。实际上,这就相当于又创建person
对象的两个副本。
ECMAScript5通过新增Object.create()
方法规范化了原型式继承。这个方法接收两个参数:一个用作新对象原型的对象和(可选的)一个为新对象定义额外属性的对象。在传入一个参数的情况下,Object.create()
与object()
方法的行为相同。
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person);
anotherPerson.name = "Greg";
anotherPerson.friends.push("Rob");
let yetAnotherPerson = Object.create(person);
yetAnotherPerson.name = "Linda";
yetAnotherPerson.friends.push("Barbie");
console.log(person.friends); //"Shelby,Court,Van,Rob,Barbie"
复制代码
Object.create()
方法的第二个参数与Object.defineProperties()
方法的第二个参数格式相同:每个属性都是通过自己的描述符定义的。以这种方式指定的任何属性都会覆盖原型对象上的同名属性。例如:
let person = {
name: "Nicholas",
friends: ["Shelby", "Court", "Van"]
};
let anotherPerson = Object.create(person, {
name: {
value: "Greg"
}
});
console.log(anotherPerson.name); //"Greg"
复制代码
3.5 寄生式继承
寄生式继承的思路与寄生构造函数和工厂模式类似,即创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,最后再像真地是它做了所有工作一样返回对象。
function createAnother(original){
var clone = object(original);//通过调用函数创建一个新对象
clone.sayHi = function(){//以某种方式来增强这个对象
console.log("hi");
};
return clone;//返回这个对象
}
复制代码
在这个例子中,createAnother()
函数接收了一个参数,也就是将要作为新对象基础的对象。然后,把这个对象(original)
传递给object()
函数,将返回的结果赋值给clone
。再为clone
对象添加一个新方法sayHi()
,最后返回clone
对象。可以像下面这样来使用createAnother()
函数:
var person={
name:"Nicholas",
friends:["Shelby","Court","Van"]
};
var anotherPerson = createAnother(person);
anotherPerson.sayHi();//"hi"
复制代码
这个例子中的代码基于person
返回了一个新对象——anotherPerson
。新对象不仅具有person
的所有属性和方法,而且还有自己的sayHi()
方法。
3.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); //第二次调用
this.age = age;
}
SubType.prototype = new SuperType(); //第一次调用
SubType.prototype.constructor = SubType;
SubType.prototype.sayAge = function(){
console.log(this.age);
};
复制代码
在第一次调用 SuperType 构造函数时,SubType.prototype 会得到两个属性:name 和 colors;它们都是 SuperType 的实例属性,只不过现在位于 SubType 的原型中。当调用 SubType 构造函数时,又会调用一次 SuperType 构造函数,这一次又在新对象上创建了实例属性 name 和 colors。于是,这两个属性就屏蔽了原型中的两个同名属性。
实现思路:
不必为了指定子类型的原型而调用超类型的构造函数,我们所需要的无非就是超类型原型的一个副本而已。本质上,就是使用寄生式继承来继承超类型的原型,然后再将结果指定给子类型的原型。
方法一:
function inheritPrototype(subType, superType){
let prototype = object(superType.prototype); //创建对象
prototype.constructor = subType; //增强对象
subType.prototype = prototype; //指定对象
}
方法二:
// 创建一个没有实例方法的类
let Super = function(){};
Super.prototype = superType.prototype;
//将实例作为子类的原型
subType.prototype = new Super();
方法三:
subType.prototype = Object.create(superType.prototype);
复制代码
这个函数接收两个参数:子类型构造函数和超类型构造函数。在函数内部,第一步是创建超类型原型的一个副本。第二步是为创建的副本添加 constructor
属性,从而弥补因重写原型而失去的默认的 constructor
属性。最后一步,将新创建的对象(即副本)赋值给子类型的原型。这样,我们就可以用调用 inheritPrototype()
函数的语句,去替换前面例子中为子类型原型赋值的语句了,例如:
function SuperType(name){
this.name = name;
this.colors = ["red", "blue", "green"];
}
SuperType.prototype.sayName = function(){
aleconsole.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);
};
复制代码
这个例子的高效率体现在它只调用了一次 SuperType
构造函数,并且因此避免了在 SubType.prototype
上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用instanceof 和 isPrototypeOf()
。
总结:
小结:
ECMAScript 支持面向对象(OO)编程,但不使用类或者接口。对象可以在代码执行过程中创建和增强,因此具有动态性而非严格定义的实体。在没有类的情况下,可以采用下列模式创建对象。
-
工厂模式,使用简单的函数创建对象,为对象添加属性和方法,然后返回对象。这个模式后来被构造函数模式所取代。
-
构造函数模式,可以创建自定义引用类型,可以像创建内置对象实例一样使用 new 操作符。不过,构造函数模式也有缺点,即它的每个成员都无法得到复用,包括函数。由于函数可以不局限于任何对象(即与对象具有松散耦合的特点),因此没有理由不在多个对象间共享函数。
-
原型模式,使用构造函数的 prototype 属性来指定那些应该共享的属性和方法。组合使用构造函数模式和原型模式时,使用构造函数定义实例属性,而使用原型定义共享的属性和方法。
JavaScript 主要通过原型链实现继承。原型链的构建是通过将一个类型的实例赋值给另一个构造函数的原型实现的。这样,子类型就能够访问超类型的所有属性和方法,这一点与基于类的继承很相似。原型链的问题是对象实例共享所有继承的属性和方法,因此不适宜单独使用。解决这个问题的技术是借用构造函数,即在子类型构造函数的内部调用超类型构造函数。这样就可以做到每个实例都具有自己的属性,同时还能保证只使用构造函数模式来定义类型。使用最多的继承模式是组合继承,这种模式使用原型链继承共享的属性和方法,而通过借用构造函数继承实例属性。
此外,还存在下列可供选择的继承模式。
-
原型式继承,可以在不必预先定义构造函数的情况下实现继承,其本质是执行对给定对象的浅复制。而复制得到的副本还可以得到进一步改造。
-
寄生式继承,与原型式继承非常相似,也是基于某个对象或某些信息创建一个对象,然后增强对象,最后返回对象。为了解决组合继承模式由于多次调用超类型构造函数而导致的低效率问题,可以将这个模式与组合继承一起使用。
-
寄生组合式继承,集寄生式继承和组合继承的优点与一身,是实现基于类型继承的最有效方式。