注意: 本文章为 《重学js之JavaScript高级程序设计》系列第五章【JavaScript引用类型】。 关于《重学js之JavaScript高级程序设计》是从新回顾js基础的学习。前端
面向对象的语言有一个标志,那就是它们都有类的概念,而经过类能够建立任意多个具备相同属性和方法的对象。可是,再前面提到过。ES中没有类的概念,所以它的对象也与基于类的语言中的对象有所不一样。web
对象的定义:‘无序属性的集合,其属性能够包含基本值、对象或者函数。’ 严格来说,这就至关于说对象是一组没有特定顺序的值。对象的每一个属性或方法都有一个名字,而每一个名字都映射到一个值。因此咱们能够把 ES 的对象想象成散列表:无非就是一组名值对,其中值能够是数据或函数。segmentfault
每一个对象都是基于一个引用类型建立的,这个引用类型能够是上一章讨论的原生类型,也能够是自定义类型。数组
最简单的方式就是建立一个Object的实例,而后再为它添加属性和方法。函数
let p = new Object()
p.name = 'js'
p.age = 20
p.job = 'jizhe'
p.sayName = function() {
alert(this.name)
}
p.sayName() // js
复制代码
工厂模式:抽象了建立具体对象的过程。考虑到ES中没法建立类,因而就用一种特定的函数来封装以特定接口建立对象的细节。post
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 对象。能够无数次的调用这个函数,而每次它都会返回一个包含三个属性一个方法的对象。工厂模式虽然解决了建立多个类似对象的问题,但却没有解决对象识别的问题(即怎么样找到一个对象的类型)学习
在前面几章介绍过,ES的构造函数能够用来建立特定类型的对象。像Object 和 Array这样原生构造函数,在运行时会自动出如今执行环境中。此外,也能够建立自定义的构造函数,从而定义自定义对象类型的属性和方法。以下:this
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()函数取代了上一小段的函数。除了内容代码相同,还有如下区别:spa
另外若是要建立P实例,必须使用 new 操做符,以这种方式调用构造函数实际上会经历如下4个步骤:prototype
对象的 constructor 属性,最初是用来标识对象类型的。可是,提到检测对象类型,仍是 instanceof 操做符更可靠。
instanceof 判断某个对象是否属于另一个对象的实例
复制代码
优势: 相比于工厂模式,构造函数模式能够将它的实例标识为一种特定的类型。
注意: 若是以这种方式定义的构造函数是定义在 Global对象中的,所以除非另有说明,instaceof 操做符 和 construcotr 属性始终会假设是在全局做用域中查询构造函数。
构造函数与其余函数的惟一区别,就是在于调用它们的方式不一样。不过,构造函数也是函数,不存在定义构造函数的特殊语法。任何函数,只要经过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
复制代码
构造函数虽然好用,但也有缺点。使用构造函数的主要问题就是每一个方法都要在每一个实例上从新建立一遍。
咱们每次建立一个函数的时候都有 一个 prototype 属性,这个属性是一个指针,指向一个对象。而这个对象的用途是包含能够由特定类型的全部实例共享的属性和方法 。若是按照字面意思,那么 prototype 就是经过调用构造函数而建立的那个对象实例的原型对象。使用原型对象的好处是可让全部对象实例共享它所包含的属性和方法。换句话说,没必要再构造函数中定义对象实例的信息,而是能够将这些信息直接添加到原型对象中。
不管何时,只要建立了一个新韩淑,就会根据一组特定的规则为该函数建立一个 prototype 属性, 这个属性指向函数的原型对象。在默认状况下,全部原型对象都会自动得到一个 constructor 构造函数属性,这个属性包含了一个指向 prototype 属性所在函数的指针。经过这个构造函数,咱们还能够继续为原型对象添加其余属性和方法。
建立了自定义的构造函数以后,其原型对象默认只会 取得 constructor 属性; 至于其余方法,则都是从 Object 继承而来的。当调用构造函数建立一个新实例后,该实例的内部将包含一个指针(内部属性),指向构造函数的原型对象。在不少实现中,这个内部属性的名字是 proto ,并且经过脚本能够访问到;而在其余实现中,这个属性对脚本则是彻底不可见的。不过,要明确的真正重要的一点,就是这个链接存在于实例于构造函数的原型对象之间,而不是存在于实例于构造函数之间
另外,每当代码读取某个对象的某个属性时,都会执行一次搜索,目标是具备给定名字的属性。搜索首先从对象实例自己开始。若是在实例中找到了具备给定名字的属性,则返回该属性的值;若是没有找到,则继续搜索指针指向的原型对象,在原型队形中查找具备给定名字的属性。若是在原型对象中找到了这个属性,则返回该属性的值。也就是说咱们调用p.sayName()的时候,会前后执行两次搜索,首先,解析器会问:实例 p 有 sayName 属性吗,若是没有,则再问p的原型有sayName属性嘛,若是有 则读取保存在原型对象中的函数。
虽然能够经过对象实例访问保存在原型中的值,但却不能经过对象实例重写原型中的值。若是咱们在实例中添加了一个属性,而该属性与实例原型中的一个属性同名,那么咱们就在实例中建立该属性,该属性会屏蔽原型中的那个属性。
有两种方式使用 in 操做符:单独使用和在 for-in循环中使用。在单独使用时,in操做符会在经过对象可以访问给定属性时返回 true,不管该属性存在于实例中仍是原型中。 另外因为in操做符只要经过对象可以访问到属性就返回 true ,hasOwnProperty()只在属性存在于实例中时才返回true,所以只要 in 操做符返回 true 而 hasOwnProperty()返回 false,就能够肯定属性时原型中的属性。
**注意:**在使用 for-in 循环时,返回的是可以经过对象访问的、可枚举的属性,其中既包括存在于实例中的属性,也包括存在于原型中的属性。屏蔽了原型中不可枚举属性的实例属性也会在 for-in循环中返回。
为了减小没必要要的输入,以及从视觉上更好的封装原型的功能,常见的做法是用包含一个说有属性和方法的对象字面量来重写整个原型对象。
function P(){
}
P.prototype = {
name: 'tc',
age: 22,
job: 'web',
sayName: function(){
console.log(this.name)
}
}
复制代码
因为在原型中查找值的过程是一次搜索,所以咱们对原型对象所作的任何修改都可以当即从实例上反映出来。尽管能够随时为原型添加属性和方法,而且修改可以当即在全部对象实例中反映出来,但若是是重写整个原型对象,那么状况就不一样了。另外,调用构造函数时会为实例添加一个指向最初原型__proto__指针,而把原型修改成另外一个对象就等于切断了构造函数和最初原型之间的联系。实例中的指针仅指向原型,而不是指向构造函数。
原型模式的重要性不只体如今建立自定义类型方面,就连全部原生的引用类型,都是采用这种模式建立的。全部原生引用类型(Object、Array、String)都在其构造函数的原型上定义了方法。
typeof Array.prototype.sort // function
typeof String.prototype.substring // function
复制代码
经过原生对象的原型,不只能够取得全部默认方法的引用,也能够定义新的方法。能够像修改自定义对象的原型同样修改原生对象的原型,所以能够随时添加方法。
重要:尽管能够这样作,可是并不推荐在产品化的程序中修改原生对象的原型。若是因某个实现中缺乏某个方法,就在原生对象的原型中添加这个方法,那么当在另外一个支持该方法的实现中运行代码时,就可能的致使命名冲突,并且,这样作也可能会意外地重写原生方法
原型模式也有缺点,第1、它省略了为构造函数传递初始化参数这一环节,结果全部实例在默认状况下都将取得相同的属性值。虽然这个在某一程度上带来了不方便,但其最大的问题仍是由其共享的本性所致使的。
在原型中,全部的属性时被不少实例共享的,这种共享对于函数很是合适。对于那些包含基本值的属性也行,可是对于引用类型值的属性来讲,就有问题了。
建立自定义类型的最多见方式,就是组合使用构造函数模式和原型模式。构造函数模式用于定义实例属性,而原型模式用于定义方法和共享的属性。结果每一个实例都会有本身的一份实例属性的副本,但同时又共享着对方法的引用,最大限度地节省了内存。
把全部信息都封装了在构造函数中,而经过构造函数中初始化原型,又保持了同时使用构造函数和原型的有点。也就是说能够经过检查某一个应该存在的方法是否有效,来决定是否初始化原型。
function P(name, age, job) {
this.name = name
this.age = age
this.job = job
if(typeof this.sayName != 'function') {
P.prototype.sayName = fucntion() {
console.log(this.name)
}
}
}
let p = new P('tc', 23, 'web')
p.sayName() // tc
复制代码
上面的代码 只有在 sayName() 方法不存在的状况下才会将它添加到原型中。这段代码只有在初次调用函数时才会执行。事后,原型已经初始化完成,不须要再修改。这样对原型所作的修改,可以当即在全部实例中获得反映。另外if语句检查的能够是初始化以后应该存在的任何属性或方法。
**注意: ** 使用动态原型模式时,不能使用对象字面量重写原型,若是在已经建立了实例的状况下重写原型,那么就会切断现有实例与新原型之间的联系。
建立一个函数,该函数的做用仅仅时封装建立对象的代码,而后再返回新建立的对象。
注意: 寄生构造函数模式返回的对象与构造函数或者与构造函数的原型属性之间没有关系,也就是说,构造函数返回的对象与在构造函数外部建立的对象没有什么不一样。为此,不能依赖 instanceof 操做符来肯定对象类型。
与寄生构造函数模式相似,使用稳妥构造函数模式建立的对象与构造函数之间也没有什么关系,所以 instanceof 操做符对这种对象也没有意义。
欢迎关注 公众号【小夭同窗】
重学js系列
ES6入门系列
Git教程