一.前言
这一章个人感觉十分重要,详细剖析了原型和继承,对于对象的理解和创建方法做了多种梳理,在此记录下。文章可能冗长,愿自己秋招顺利。
二.属性类型
我们先了解下ECMAScript定义的两种属性:数据属性和访问器属性。
-
数据属性:数据属性有四个描述其行为的特性。
Configurable:表示能否通过delete删除属性,能否修改属性的特性,能够把属性修改为访问器属性。默认值为true。
Enumerable:表示能否通过for-in循环返回属性。默认值为true。
Writable:表示能否修改属性的值。默认值为true。
Value:表示这个属性的数值是多少。默认值为undefined。以上的四种特性是创建数据属性时默认自带的属性,要想对这些特性进行修改,必须使用Object.defineProperty()方法。
var Person = {}; Object.defineProperty(person,'name',{ writable:false, value:'nico' }); alert(person.name); //'nico' person.name = 'jojo'; alert(person.name) //'nico'
由于我们修改了Writable的值,这个属性不能进行修改,甚至在严格模式下会报错。
var Person = {}; Object.defineProperty(person,'name',{ configurable:false, value:'nico' }); alert(person.name); //'nico' delete person.name alert(person.name) //'nico'
由于我们修改了configurable的值,所以删除数据属性的操作失效,在严格模式下会报错。
注意:将数据属性的configurable值设置为false后,不能进行修改,比如:
var Person = {}; Object.defineProperty(person,'name',{ configurable:false, value:'nico' }); //会报错 var Person = {}; Object.defineProperty(person,'name',{ configurable:true, value:'nico' });
需要注意的是:在调用Object.defineProperty()方法创建一个新属性时,configurable,enumerable,writable值默认都是false。如果只是调用Object.defineProperty()方法修改一个属性时,则没有这种限制。
-
访问器属性
访问器属性不包含数据值,它们包含一对儿getter和setter函数(非必须),
访问器属性有以下四个特性:
Configurable:表示能否通过delete删除属性,能否修改属性的特性,能够把属性修改为访问器属性。默认值为true。
Enumerable:表示能否通过for-in循环返回属性。默认值为true。
Get:在读取属性时调用的函数。默认值为undefined。
Set:在写入属性时调用的函数。默认值为undefined。要想对这些特性进行修改,必须使用Object.defineProperty()方法。
var book = { _year:2020, edition:1 } Object.defineProperty(book,'year',{ get:function(){ return this._year }, set:function(newValue){ this._year = newValue; this.edition += newValue - 2020 } }) book.year = 2021; alert(book.edition); //2
我们在这里定义了一个访问器属性year,getter函数用来返回year的值,setter用来计算确定edition的值。访问器属性的常见方法,设置一个属性的值会导致其他属性的值发生变化。
3.定义多个属性
上面我们是针对单独一个对象的属性进行操作,但是大多数情况下,我们需要操作多个属性,于是就有了Object.defineProperties()这个方法。如下:
var book = {}; Object.defineProperties(book,{ _year:{ writable:true, value:2020 }, edition:{ writable:true, value:1 }, year:{ get:function(){ return this._year; }, set:function(newValue){ if(newValue > 2019){ this._year = newValue this.edition += newValue - 2019 } } } })
4.读取属性的特性
ECMAScript5定义了Object.getOwnPropertyDescriptor()方法,该方法获取两个参数:属性所在的对象和要读取其描述符的属性名称。返回值是一个对象,如果是访问器属性,这个对象的属性有configurable,enumerable,get,set。如果是数据属性,这个对象的属性有configurable,enumerable,writable,value。var book = {}; Object.defineProperties(book,{ _year:{ writable:true, value:2020 }, edition:{ writable:true, value:1 }, year:{ get:function(){ return this._year; }, set:function(newValue){ if(newValue > 2019){ this._year = newValue this.edition += newValue - 2019 } } } }) var descriptor = Object.getOwnPropertyDescriptor(book,'_year') alert(descriptor.value) //2020 alert(descriptor.writable) //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
三.创建对象
我们之前了解到创建对象有两种方法:object构造函数和对象字面量。但是这两种方法都会产生一个问题:大量的重复代码。人们逐渐创建了一下几种模式。
1. 工厂模式
function createPerson(name,age){
var o = new Object()
o.name = 'nico'
o.age = 14
o.sayName = function(){
alert(this.name)
}
return o
}
var person1 = createPerson('jojo',29)
var person2 = createPerson('dio',19)
工厂模式虽然解决了创造多个相似对象的问题,但是却没有解决对象识别的问题——怎么判断一个对象的类型。
2 构造函数模式
function Person(name,age){
this.name = name
this.age = age
this.sayName = function(){
alert(this.name)
}
}
var person1 = new Person('nico',14)
var person2 = new Person('jojo',19)
构造函数的模式和工厂模式有几点不同:
①没有显性的创建对象
②直接将属性和方法赋值给了this对象
③没有return语句。
构造函数我们默认以一个大写字母开头,非构造函数则以一个小写字母开头。
构造函数的方法有几点需要注意:
-
利用构造函数创建的对象都有一个constructor属性,该属性指向person。
alert(person1.constructor == person) //true alert(person2.constructor == person) //true
-
构造函数本身也是函数
任何函数,只要通过new操作符来调用,那它就可以作为构造函数,如果只是普通使用,那它和普通函数也没什么两样。
在上面写的构造函数的基础上:
//作为构造函数使用
var person = new Person('nico',14)
person.sayName() //'nico'
//作为普通函数使用
Person('nico',14)
window.sayName() //'nico'
//在另一个对象的作用域中调用
var o = new Object()
Person.call(o,'dio',14)
o.sayName() //'dio'
- 构造函数的问题
在构造函数中定义函数,每定义一个函数,也相当于实例化一个对象。因此,每个Person()对象实例都有一个不同的Function实例(上述例子为sayName()),因此,不同实例上的函数是不相等的。
alert(person1.sayName == person2.sayName) //false
从意义上说,将函数封装在构造函数中没有很大的必要,我们可以将函数剥离出去:
function Person(name,age){
this.name = name
this.age = age
}
function sayName(){
alert(this.name)
}
var person1 = new Person('nico',14)
var person2 = new Person('jojo',19)
这样person1和person2共享一个函数sayName(),但是这样的话,对象需要定义很多方法和属性,那么就要在全局中写很多类似的函数,就没有封装性可言了,于是我们要介绍下面一种模式来解决。
3.原型模式
我们创建的每一个函数都有prototype(原型)属性。这个属性我们之前提过,是一个指针,指向一个对象,这个对象的用途是包含可以由特定类型的所有实例共享的属性和方法。
function Person(){
}
Person.prototype.name = 'nico'
Person.prototype.age = 14
Person.prototype.sayName = function(){
alert(this.name)
}
var person1 = new Person()
person1.sayName() //'nico'
var person2 = new Person()
person2.sayName() //'nico'
alert(person1.sayName == person2.sayName) //true
构造函数变成了空函数,但是通过prototype直接设置了对象的属性,仍然可以通过构造函数来创建新对象。与构造函数不同的是,新对象的所有属性和方法是共享的。(即新创建的对象调用无论函数还是数据类型都访问的是同一个对象里的函数和数据类型)
如图:创建了一个Person()函数,他的原型对象就是Person.protortype,原型对象默认只会获得constructor属性,其他方法都是从object继承过来的。
我们通过var person1 = new Person(),创建了一个person1新实例,该实例内部包含一个指针,指向构造函数的原型对象,也就是Person.protortype。
这也是我们在chrome,Safari,Firefox网页中见到的_proto_。这也可以说明,person1和person2和Person()构造函数没有直接的联系。
那么如何获取到一个新实例对象的原型呢
alert(object.getPrototypeOf(person1)) //'Person.prototype'
alert(object.getPrototypeOf(person1.name)) //'nico'
object.getPrototypeOf()方法可以很方便的返回对象的原型。
重写原型值出现的问题:我们在新实例中创建与构造函数实例相同的属性名,则会屏蔽原型中的那个属性。这也符合原型链的搜索方式,先查询自身实例的属性,然后往原型链向上不断查询。
function Person(){
}
Person.prototype.name = 'nico'
Person.prototype.age = 14
Person.prototype.sayName = function(){
alert(this.name)
}
var person1 = new Person()
var person1 = new Person()
person1.name = 'dio'
alert(person1.name) //'dio'
alert(person2.name) //'nico'
使用**hasOwnProperty()**可以检测一个属性是否存在于实例中,还是存在于原型中,这个方法只有给定属性在对象实例中才会返回true。
function Person(){
}
Person.prototype.name = 'nico'
Person.prototype.age = 14
Person.prototype.sayName = function(){
alert(this.name)
}
var person1 = new Person()
alert(person1.hasOwnProperty('name')) //false 因为来自原型
person1.name = 'jojo'
alert(person1.name) //'jojo'
alert(person1.hasOwnProperty('name')) //true 因为来自实例
原型与in操作符:in操作符会在通过对象能访问给定属性时返回true。无论该属性在实例中还是原型中。
function Person(){
}
Person.prototype.name = 'nico'
Person.prototype.age = 14
Person.prototype.sayName = function(){
alert(this.name)
}
var person1 = new Person()
var person2 = new Person()
alert('name' in person1) //true
person2.name = 'zhou'
alert('name' in person2) //true
综上,我们要想知道属性到底是存在于某个对象的属性中还是存在与某个对象的原型中。可以综合利用hasOwnProperty()和in操作符。我们自定义一个方法:
function hasPrototype(object,name){
return !Object.hasOwnProperty(name) && (name in object)
}
只需要传入对象和属性,就可以根据返回值判断是不是这个对象的原型上的属性,返回true则是原型上的属性,反之则不是。
要取得对象上所有可枚举的实例属性,可以使用Object.keys()方法。
function Person(){
}
Person.prototype.name = 'nico'
Person.prototype.age = 14
Person.prototype.sayName = function(){
alert(this.name)
}
var keys = Objects.key(Person.prototype)
alert(keys) //[name,age,sayName()]
var p1 = new Person()
p1.name = 'fy'
p1.age = 27
var p1keys = Object.key(p1)
alert(p1keys) //[name,age]
这里key保存的是一个数组,对于person的原型对象来说,可以枚举的实例属性包括name,age,sayName(),对于新创建的对象实例p1来说,可枚举的实例对象包括name,age。
如果你想得到一个对象的所有属性,可以使用Object.getOwnPropertyNames()
var keys = Object.getOwnPropertyNames(Person.property)
alert(keys) //[constructor,name,age,sayName]
我们在上述的例子中,创建一个构造函数的实例对象用来创建新对象,但是我们需要反复书写Person.property,我们更常见的做法是用一个包含所有属性和方法的对象字面量来重写整个原型对象。
function Person(){}
Person.prototype = {
name:'nico',
age:29,
sayName:function(){
alert(this.name)
}
}
这种方法大大减少了创建代码时的代码量,但是有一个例外:constructor属性不再指向Person了。我们在这里使用的语法,完全重写了默认的prototype对象,因此constructor指向新对象的constructor属性(指向Object构造函数),但是instanceof还是可以正确的指向Person,同时也指向Object。
如果constructor很重要,可以在创建原型对象时加上一行代码:
function Person(){}
Person.prototype = {
constructor:Person,
name:'nico',
age:29,
sayName:function(){
alert(this.name)
}
}
原型的动态性:
var friend = new Person()
Person.prototype.sayHi = function(){
alert('hi')
}
friend.sayHi() //'hi'
这里我们先创建Perosn的一个新实例,然后才在原型对象中添加一个sayHi()方法,我们仍然可以访问这个方法。原因很简单:我们调用friend的sayHi()方法时,首先会在自身实例中查找sayHi()方法,没有找到会继续在原型中查找。实例与原型的联系不过是一个指针,所以可以在原型中查找到sayHi(),而这个与代码的书写顺序无关。
但是如果重写原型对象就完全不一样:
function Person(){
}
var friend = new Person()
Person.prototype = {
name:'nico',
age:29,
sayName:function(){
alert(this.name)
}
}
friend.sayName() //报错
为什么会报错呢:我们在创建friend实例时,此时friend指向的是Person的原型对象,但是我们接下来重写了Person的原型对象,此时friend无法指向新的原型对象,也切断了Person和最初的Person的原型对象的联系。所以sayName()无法成功调用新原型对象中的函数,而原来函数中没有定义任务属性和方法(除了constructor),所以自然会报错。
原生对象的原型:我们之前接触过好几个原生对象(Object,Array,String,等等),我们可以像修改自定义对象原型一样修改原生对象的原型。
String.prototype.startSix = function(){
return 666
}
var msg = 'yeah'
alert(msg.startSix()) //'666'
这里我们就在String原生对象的原型对象中自定义了一个函数,我们可以人鱼对字符串调用这个函数。
原型模式的问题:我们在一个构造函数的原型对象中的定义的基本值类型如果想替换可以在新实例中自定义同名的数据进行覆盖。但是引用类型就不是那么方便了,一个新实例将一个引用类型值进行重新赋值(比如数组push一个数据)(基本类型值和引用类型值的区别详见第四章),此时另外一个新实例的此数组也会跟着一同push数据,因为所有实例指向的引用类型是同一个引用类型。但是这并不是我们想要的,我们没能将新实例区别开来。
我们之前提到的构造函数模式,通过的是改变引用类型的执行域(this指向的不同的实例)来区别引用类型,这两种模式各有优劣,我们不妨结合在一起,于是就有了构造函数模式和原型模式结合的方法。
function Person(name,age){
this.name = name
this.age = age
this.firend = ['海绵宝宝','派大星']
}
Person.prototype = {
constructor:Person,
sayName:function(){
alert(this.name)
}
}
var person1 = new Person('nico',14)
var person2 = new Person('mayi',19)
person1.friend.push('章鱼哥')
alert(person1.friend) //['海绵宝宝','派大星','章鱼哥']
alert(person2.friend) //['海绵宝宝','派大星']
alert(person1.friend === person2.friend) //false
alert(person1.sayName === person2.sayName) //true
我们遵循一个规则:实例属性都是在构造函数中定义的,而所有实例共享的属性constructor和方法都是在原型对象中定义。这种模式是ECMAScript中使用最为广泛,认同度最高的一种创建自定义类型的方法。这也是用来定义引用类型的一种默认模式。
除了以上三种模式,还有动态原型模式,寄生构造函数模式和稳妥构造函数模式。详情见书,主要适用于更加安全的环境或者特殊情况。这里不再赘述。