注意: 本文章为 《重学js之java script高级程序设计》系列第五章【java script引用类型】。
关于《重学js之java script高级程序设计》是重新回顾js基础的学习。
1. 什么是面向对象
面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是,再前面提到过。ES中没有类的概念,因此它的对象也与基于类的语言中的对象有所不同。
对象的定义:‘无序属性的集合,其属性可以包含基本值、对象或者函数。’ 严格来讲,这就相当于说对象是一组没有特定顺序的值。对象的每个属性或方法都有一个名字,而每个名字都映射到一个值。所以我们可以把 ES 的对象想象成散列表:无非就是一组名值对,其中值可以是数据或函数。
每个对象都是基于一个引用类型创建的,这个引用类型可以是上一章讨论的原生类型,也可以是自定义类型。
2. 创建对象
最简单的方式就是创建一个Object的实例,然后再为它添加属性和方法。
let p = new Object()
p.name = 'js'
p.age = 20
p.job = 'jizhe'
p.sayName = function() {
alert(this.name)
}
p.sayName() // js
3. 工厂模式
工厂模式:抽象了创建具体对象的过程。考虑到ES中无法创建类,于是就用一种特定的函数来封装以特定接口创建对象的细节。
function p(name, age, job) {
let o = new Object()
o.name = name
o.age = age
o.job = job
o.sayName = function() {
alert(this.name)
}
return o
}
let p1 = p('tc', 30, '老宋')
let p2 = p('bd', 22, '百度')
p1.sayName // tc
p2.sayName // bd
函数 p() 能够根据接受的参数来构建一个包含所有必要信息的 Person 对象。可以无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了创建多个相似对象的问题,但却没有解决对象识别的问题(即怎么样找到一个对象的类型)
4. 构造函数模式
在前面几章介绍过,ES的构造函数可以用来创建特定类型的对象。像Object 和 Array这样原生构造函数,在运行时会自动出现在执行环境中。此外,也可以创建自定义的构造函数,从而定义自定义对象类型的属性和方法。如下:
function p(name, age, job) {
this.name = name
this.age = age
this.job = job
this.sayName = function (){
alert(this.name)
}
}
let p1 = new P('tc', 33, 'haha')
let p2 = new P('gg', 32, '小夭同学')
p1.sayName() // haha
p2.sayName() // 小夭同学
在上面的例子中,p()函数取代了上一小段的函数。除了内容代码相同,还有以下区别:
- 没有显式的创建对象
- 直接将属性和方法赋给了this对象
- 没有 retrun 语句
- 另外函数 p 是大写。构造函数始终都以第一个字母大写开头。非构造函数时小写开头。
另外如果要创建P实例,必须使用 new 操作符,以这种方式调用构造函数实际上会经历以下4个步骤:
- 创建一个新对象
- 将构造函数的作用域赋给新对象(因此 this 指向了这个新对象)
- 执行构造函数中的代码(为这个对象添加属性)
- 返回新对象
对象的 constructor 属性,最初是用来标识对象类型的。但是,提到检测对象类型,还是 instanceof 操作符更可靠。
instanceof 判断某个对象是否属于另外一个对象的实例
优点: 相比于工厂模式,构造函数模式可以将它的实例标识为一种特定的类型。
注意: 如果以这种方式定义的构造函数是定义在 Global对象中的,因此除非另有说明,instaceof 操作符 和 construcotr 属性始终会假设是在全局作用域中查询构造函数。
4.1 将构造函数当作函数
构造函数与其他函数的唯一区别,就是在于调用它们的方式不同。不过,构造函数也是函数,不存在定义构造函数的特殊语法。任何函数,只要通过new操作符来调用,那它就可以作为构造函数;而任何函数,如果不通过new 操作符来调用,那它和普通函数也没有射门两样。
// 构造函数调用
let p = new Person('tc', 22, '哈哈哈')
p.sayName // tc
// 普通函数调用
P('gc', 23, 'oo') // 添加到 window
windwo.sayName // gc
// 在另外一个对象的作用域中调用
let o = new Object()
P.call(o, 'new', 33, 'suzhou')
o.sayName // new
4.2 构造函数的问题
构造函数虽然好用,但也有缺点。使用构造函数的主要问题就是每个方法都要在每个实例上重新创建一遍。
5. 原型模式
我们每次创建一个函数的时候都有 一个 prototype 属性,这个属性是一个指针,指向一个对象。而这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法 。如果按照字面意思,那么 prototype 就是通过调用构造函数而创建的那个对象实例的原型对象。使用原型对象的好处是可以让所有对象实例共享它所包含的属性和方法。换句话说,不必再构造函数中定义对象实例的信息,而是可以将这些信息直接添加到原型对象中。
5.1 理解原型对象
无论什么时候,只要创建了一个新韩淑,就会根据一组特定的规则为该函数创建一个 prototype 属性, 这个属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个 constructor 构造函数属性,这个属性包含了一个指向 prototype 属性所在函数的指针。通过这个构造函数,我们还可以继续为原型对象添加其他属性和方法。
创建了自定义的构造函数之后,其原型对象默认只会 取得 constructor 属性; 至于其他方法,则都是从 Object 继承而来的。当调用构造函数创建一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。在很多实现中,这个内部属性的名字是 proto ,而且通过脚本可以访问到;而在其他实现中,这个属性对脚本则是完全不可见的。不过,要明确的真正重要的一点,就是这个连接存在于实例于构造函数的原型对象之间,而不是存在于实例于构造函数之间
另外,每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具有给定名字的属性。搜索首先从对象实例本身开始。如果在实例中找到了具有给定名字的属性,则返回该属性的值;如果没有找到,则继续搜索指针指向的原型对象,在原型队形中查找具有给定名字的属性。如果在原型对象中找到了这个属性,则返回该属性的值。也就是说我们调用p.sayName()的时候,会先后执行两次搜索,首先,解析器会问:实例 p 有 sayName 属性吗,如果没有,则再问p的原型有sayName属性嘛,如果有 则读取保存在原型对象中的函数。
虽然可以通过对象实例访问保存在原型中的值,但却不能通过对象实例重写原型中的值。如果我