面向对象的英文Object-Oriented 俗称OO
面向对象语言有一个标志,他们都有类的概念。通过类可以创建任意多个具有相同属性和方法的对象
在学习这篇文章之前推荐先学习 Object类型和Function类型
对象与属性
1.Object类型
对象是Object类型,所以无论用什么方法创建它,对象都有Object类型里面默认的属性和方法。
1.对象
对象的定义:无序属性的集合,其属性可以包含基本值,对象,函数。每个对象都是基于基本引用类型创建的
创建对象的方法:
1.创建Object对象的实例:
var penson1 = new Object()
penson1.name = '小米'
console.log(penson1);//{name: '小米'}
2.对象字面量
var penson2 = {
name:'小米'
}
console.log(penson2)//{name: '小米'}
这两种创建方式都定义了一个相同的属性,属性在创建时会带一些特征值,js通过这些特征值来定义他们的行为
2.属性
JavaScript 提供了一个内部数据结构,用于描述对象的值,控制其行为,例如该属性是否可写、可读、可配置、是否可修改以及是否可枚举等。这个内部数据结构被称为属性描述符。
每个属性都有自己对应的属性描述符,保存该属性的元信息。
js中的属性可以分为两种:数据属性和访问器属性
1.数据属性:具有属性的值
数据属性默认特性一共有四个:[[Configurable]]、[[Enumerable]]、[[Writable]]、[[Value]]
- [[Configurable]] :表示能否通过delete删除属性从而重新定义属性,能否修改属性的特性(修改成四个中的其他个),能否把属性修改为访问器属性,默认值为true
- [[Enumerable]]:能否通过for-in循环返回属性,默认值为true
- [[Writable]]:能否修改属性的值(是否能修改value),默认值为true
- [[Value]]:包含这个属性的数据值(value),默认值为undefined
在这里创建了person对象,里面的属性叫name,值为’小米’,也就是说[[value]]特性被设置为小米,而其他三个默认为true
var person = {
name:'小米'
}
2.访问器属性
用setter和getter函数(这两个函数不是必须的)描述的属性
在读取访问器属性时会调用getter函数。这个函数负责返回有效的值
在写入访问器属性时,会调用setter函数传入这个值,这个函数负责处理数据
访问器属性也是四个特性,但是它不包含数据值:
[[Configurable]]、[[Enumerable]]
[[set]] :写入属性时调用,默认值Undefined
[[get]] :读取属性时调用,默认值Undefined
- 修改数据属性和访问访问器属性
Object.defineProperty()方法:
接收三个参数:对象名、属性名、和一个描述符对象(特性和所对应的值)
// 创建一个book对象
var book = {
// 定义了两个默认属性
_year :2004,
edition:1
}
// 方法中的参数:对象名 访问器属性名 getter和setter函数
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
alert(book.edition)
访问器属性最常用的方法是设置一个属性的会导致其他属性发生变化
- 定义多个属性
Object.defineProperties()方法
:将数据属性和访问器属性一起定义
接收两个对象参数:
第一个对象参数:在其上定义或修改属性的对象。
第二个对象参数:要定义其可枚举属性或修改的属性描述符的对象。 - 读取属性的特性
Object.getOwnPropertyDescriptor()方法
:通过调用这个方法读取属性的特性值
接收两个参数:
需要读取的属性的对象
目标对象内属性的名称
创建对象
通过Object构造函数和对象字面量的方式创建单个对象,利用同一个构造函数创建多个对象就会无可避免的产生很多同样的属性和方法
首先先了解下工厂模式
工厂模式:
工厂模式是一种设计模式,抽象了具体对象创建过程,人们并不知道对象创建的具体过程。
工厂模式解决了创建多个相似对象的问题,将他们封装成函数,每次调用这个函数传入不同的参数,来反复调用
function creatAnimal(name,age) {
var o = new Object();
o.name = name;
o.age = age;
o.eat = function(){
this.name + "会吃饭"
}
return o
}
var Dog = creatAnimal('小狗','2')
console.log(Dog);
var Cat = creatAnimal('小猫','3')
console.log(Cat);
工厂模式解决了多个相似对象的创建问题,但是没有解决怎么知道对象是什么类型的问题
下面引出构造函数
构造函数
- 为什么会有构造函数:
学js时应该注意,js中没有类,需要用构造函数来表示类,而构造函数所创建的对象,用面向对象的概念来说就是创建实例,用js来表示的话类和对象就都有了 - 知道为什么会有构造函数之后,应该了解构造函数是什么:
构造函数本身是一个函数是为了创建新对象而产生的,用来创建特定类型的对象
构造函数分为两种:原生构造函数和自定义构造函数。
原生构造函数:例如Object、Function、Array,在运行时会自动出现在运行环境中,所创建的对象带有他们原生的属性和方法
自定义构造函数:可以自定义属性和方法
利用this.
将属性添加到函数里
下面是一个自定义构造函数的例子,自定义 构造函数里面的属性和方法
// 创建动物类用构造函数模拟
function Animal(name, age) {
this.name = name;
this.age = age;
this.eat = function () {
console.log(this.name + "会吃饭");
}
}
// 创建狗实例
var Dog = new Animal('小狗', '2')
Dog.eat()
console.log(Dog);
// 创建猫实例
var Cat = new Animal('小猫', '3')
Cat.eat()
console.log(Cat);
与工厂模式不同的是:
没有显式的创建对象
直接将属性和方法赋值给this对象
没有return因为直接将属性添加到函数上
如何区分构造函数和普通函数:
- 构造函数的函数名首字母大写
- 用new实例化对象的函数都是构造函数
如何知道使用的是什么类型的构造函数(检测对象类型):利用instance of
使用构造函数的缺点 :每定义一个函数就会实例化一个函数对象, 一旦new一个对象,就必然会在内存中生成新的的区域来存储,所以Dog.eat == Cat.eat为false
不同实例上的同名函数是不相等的
function Animal(name, age) {
this.name = name;
this.age = age;
this.eat = new Function(this.name + "会吃饭")
//=this.eat = function () {console.log(this.name + "会吃饭");}
}
在内存中的存储结构图为:
创建两个完成相同任务的function实例,会很浪费内存
解决办法:可以将函数转移到构造函数外部,成为一个全局作用域中共享的函数
// 创建动物类构造函数
function Animal(name, age) {
this.name = name;
this.age = age;
}
var eat = function () {
console.log(this.name + "会吃饭");
}
// 创建狗实例
var Dog = new Animal('小狗', '2')
// 创建猫实例
var Cat = new Animal('小猫', '3')
console.log(Dog.eat == Cat.eat);//true
eat包含的是一个指向函数的指针,这样就解决了两个函数做同一件事情的问题
但是随之而来又有一个问题:
1.定义在全局作用域的函数实际上只能被某个对象调用,只有调用才能执行并不是哪个对象都有
2.如果对象要定义很多方法,那么就要定义很多个全局函数,那么自定义的构造函数的意义在哪,这个自定义的引用类型就并没有封装性可言
使用原型模式解决以上这些问题
原型模式
我们创建的每一个函数里面都有一个prototype(原型)属性,这是一个指针,指向一个对象
这个对象是:原型对象,包含由特定类型的所有实例共享的方法和属性
-
只要创建一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象
-
在默认情况下,原型对象都会获得一个constructor属性,这个属性指向构造函数,
创建自定义的构造函数后,其原型对象默认只会取constructor属性,其他的方法都是从Object继承来的 -
创建实例后,实例内部将包含内部属性也算是一个指针,指向构造函数的原型对象,这个属性叫[[prototype]],如何访问[[prototype]]属性,利用__proto__属性来访问
-
构造函数People(), People.prototype指向原型对象,其自带属性construtor又指回了People,即People.prototype.constructor==People.
-
实例对象person由于其内部指针__proto__指向了原型对象,所以可以访问原型对象上的其他方法。
这样我们就可以将属性和方法添加到原型对象中
function Person() {
}
Person.prototype.name = '小米'
Person.prototype.age = '20'
Person.prototype.sayName = function(){
alert(this.name)
}
var person1 = new Person()
console.log(person1);
var person2 = new Person()
console.log(person2);
直接将方法和属性添加到[[prototype]]属性中,构造函数变成了空函数,调用构造函数创建新对象,两个对象访问同一组属性和方法
对多个对象实例共享原型所保存方法和属性的原理:
首先搜索对象实例本身,如果有想要找的属性和方法就返回该属性的值,如果没有就往原型对象中查找
向实例中添加一个同名属性name,会添加在函数中,但是不会影响原型对象上的name,原型对象上的name依然存在
person1.name = '大米'
console.log( person1.name );//大米
可以利用hasOwnProperty()方法
检测属性存在实例中还是原型中,只有给定属性存在实例中才会返回true
for in 循环时返回的是所有能够通过对象访问的,包括实例中的属性和原型中的属性
- 原型的动态性
可以随时为原型添加属性和方法,所修改的内容能够立即在所有对象实例中反应出来,但是如果重写原型对象就等于切断了构造函数与最初原型之间的联系 - 原型对象的问题
它省略了为构造函数传递初始化参数这一环节,结果所有实例在默认情况下都将取得相同的属性值,这不是最严重的问题,最严重的问题是:如果我们在原型对象中声明一个数组属性,然后通过另一个实例去添加或删除数组里面的内容,其他实例里面的数组属性也将被改变,而事实上实例应该拥有属于自己的全部属性
而如何解决这个问题呢,也就是修改一个属性,其他实例上的属性也被修改
组合使用构造函数模式和原型模式
创建自定义类型最常用的方法是组合使用这两个方法,可以解决以上问题。用构造模式定义实例属性,原型模式用于定义方法和共享的属性,这样每个实例都将有自己的属性,但同时又共享着对方法的引用,这是用来定义引用类型的默认模式
动态原型模式
通过检查某个应该存在的方法是否有效,来决定是否需要初始化原型
寄生构造函数模式
创建一个函数,该函数的作用仅仅是封装创建对象的代码,然后再返回新创建的对象。除了使用new操作符传递实参以外,这个模式和工厂模式其实是一样的,建议在可以使用其他模式的情况下不要使用这种模式
function Person(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 friend = new Person('小米','18','玩游戏');
friend.sayName()//小米
稳妥构造函数模式
稳妥对象指的是没有公共属性,而且其方法也不引用this对象,适合在安全执行环境下使用
继承
许多oo语言都支持两种继承方式:接口继承和实现继承
接口继承只继承方法签名,实现继承继承实际的方法
由于函数没有签名,在ECMAScript中无法实现接口继承
ECMAScript只支持实现继承,实现继承主要依靠原型链
实现继承的主要方法是原型链
原型链
基本思想是让一个引用类型继承另一个引用类型的属性和方法
- 本质上是原型对象成为另一个类型的实例
function Father(){
this.prototype = true
}
Father.prototype.getValue = function(){
return this.prototype
}
function Son (){
this.Sonprototype = false
}
Son.prototype = new Father();
Son.prototype.getSonValue = function(){
return this.Sonprototype
}
var sun = new Son()
console.log(sun.getValue());//true
例如这个代码,最后一句调用的是sun.getValue(),就需要从sun实例身上找有没有这个方法,而它本身是没有但是继承了Son的,因为它是Son构造函数的实例。
而Son声明的时候也没有这个方法,但是它继承了Father的。
原型链的本质就是通过继承一层一层的向上找,实例中总会有__proto__指针指向构造函数中的prototype
- 所以引用类型默认都继承了object,而这个继承也是通过原型链实现的,所有函数的默认原型都是Object的实例
- 可以使用两种方式来确定原型与实例之间的关系
instanceof操作符
console.log(sun instanceof Object);
console.log(sun instanceof Father);
console.log(sun instanceof Son);//返回的都是true
我们可以说sun是他们三个中任何一个的实例
isPrototypeOf()
只要在原型链中出现过得原型,都可以说是该原型链所派生的实例的原型
- 子类有时候需要覆盖父类中的方法,要先添加这个原型方法再重写。原型链实现继承不能通过字面量写原型方法,这样会重写原型方法