练武不练功,到头一场空,JavaScript 原生对象,被业务应用层的研发容易忽略的一些底层细节。
对象的属性类型
属性有两种类型:数据类型和访问器类型。
- 数据属性包含一个值,比如自己定义的 name,sex,age 等,而对象里有一个内置的 [[Put]] 方法,它的默认行为是创建数据属性;
- 访问器属性不包含值而是定义了一个当属性被读取时调用的函数(称为 getter)和一个当属性被写入时调用的函数(称为 setter)。
访问器属性仅需要 getter
和 setter
两者中的任意一个,当然也可以两者都有。
在对象字面形式中定义访问器属性有特殊的语法:
例:访问器类型 :
var person1 = { |
使用场景:当你希望对象的赋值操作会触发一些行为或读取的值需要通过计算所需的返回值得到时,访问器属性会非常有用!
补充:利用访问器属性 实现表单双向数据绑定
var txt = document.getElementById('txt'), |
属性特征:(数据属性)
数据属性和访问器属性的特征
通用特征
有两个属性特征是数据和访问属性都具有的。
- [[Enumerable]] (可枚举的):决定了你是否可以遍历该属性;
- [[Configurable]] (可配置的):决定了该属性是否可配置;
配置的意思就是:你可以用delete
删除一个可配置的属性,或者随时更改它,开放的!所有自己申明的对象属性默认都是可枚举、配置的!
改变属性特征:Object.defineProperty() ===> 译(定义属性的意思)
使用 Object.defineProperty
方法如果不指定,除了 value
其他都时 false
- 拥有该属性的对象(属性所在的对象);
- 属性名;
- 包含需要设置的特征的属性描述对象;
🔔 注意:参数三:3) 是一个对象{} 这个对象里面有四个属性:
第一个属性:value
指定的第二个参数的值 (可选)
第二个属性:enumerable
是否可枚举
第三个属性:configurable
是否可配置 (配置是指 可否删除? 可否重新定义) 类似于总开关
第四个属性:wriable
是否可写入 就是是否可以更改属性值等
🔔 注意:configurable 修改为 false 以后 除了 wriable
其他都是不可逆的,不可重新配置的
栗:
var person1 = { |
总结:可配置属性的这个 Object.defineProperty()
这个方法 是一次性的!在定义对象的时候,只起一次效果!不存在覆盖情况!🔔 最后注意 如果使用了属性类型的 get
和 set
方法就不能使用 writable
和 value
的任意一个 否则报错!
数据属性的特征
数据属性额外拥有两个访问器属性不具备的特征
- [[Value]] : 包含属性的值,当你在对象上创建属性时该特征被自动赋值,匹配属性的值,键值对的值,对象中所有的属性的值都保存在 [[Value]] 中,哪怕该值是一个函数。
- [[Wriable]]:该特征是一个布尔值,指示该属性是否可以写入。所以属性默认是可写的,除非另外指定
通过这两个额外特征,数据属性相当于拥有完整的四个属性特征;就是Object.defineProperty()
方法的第三个参数里面的四个完整的参数!
所以常规的一个对象创建,并给这个对象添加属性以及属性值(就是添加键值对的过程)
显式的是这样子:
var person1 = { |
隐式的是这样子:
var person1 = {} // 这里还有var person1 = new Object(); |
注意一点:当 Object.defineProperty()
被调用时,它首先先检查属性是否存在,如果不存在,将根据属性描述对象指定的特征创建!如果使用 Object.defineProperty()
方法给对象添加属性以及值,如果不针对参数进行设置,那么它们将默认都为 false
不可枚举、不可配置、不可修改!
访问器属性特征
访问器属性也有两个额外特征。访问器属性不需要存储值,因为也就不需要(没有) [[Value]] 和 [[Wriable]]。取而代之的是[[Get]] 和 [[Set]],内含 getter
和 setter
函数。和对象字面形式的 getter
和 setter
一样,仅需要定义其中一个特征就可以创建一个访问器属性 。
🔔 注意:如果试图创建一个同时具有数据特征和访问器特征的属性,将会得到一个错误!
在属性类型笔记里面的例子 可以改写为如下:
var person1 = { |
🔔 注意:这里传给 Object.defineProperty()
的属性描述对象中的 get,set
关键字属性,它们是包含函数的数据属性,在这里不能使用属性类型笔记中的那种对象字面形式的特殊设置方法 set(){}
,get(){}
。
🔔 注意:这里访问器属性如果不在 Object.defineProperty()
方法中设置枚举以及配置为 true
默认任何调用 Object.defineProperty()
方法的对象,里面指定的第二个参数属性,都是不可枚举,不可配置的!
和对象字面字面形式定义的访问器属性一样,在严格模式下试图写入没有 setter
的访问器属性会抛出错误,在非严格模式下会失效(什么也不弹出),试图读取一个没有 getter
的访问器属性则总是返回 undefined。
获取(读取)属性特征
Object.getOwnPropertyDescriptor()
方法
此方法只可用于自有属性,接收两个参数:
- 对象;
- 属性名;
如果属性存在,则返回一个属性描述对象,内含 4 个属性:configurable
和enumerable
;另外两个属性则根据属性类型决定,看是数据属性还是访问器属性!
🔔 注意:如果是访问器属性,这个对象属性有 configurable
、enumerable
、get
、set
;
如果是数据属性,这个个对象属性有 configurable
、enumerable
、writable
和 value
。
栗:
var person1 = { |
这里,属性 name
作为对象字面形式的一部分被定义。调用 Object.getOwnPropertyDescriptor()
方法返回的属性描述对象具有 4 个属性,因为上面是数据属性,所以返回 enumerable
,configurable
,writable
和 value
,即使它们从没有没 Object.defineProperty()
显式定义。
定义多重属性
如果使用 Object.defineProperties() 方法而不是 Object.defineProperty() 方法,可以为一个对象同时定义多个属性。
Object.defineProperties() 接收两个参数:
- 需要改变的对象;
- 一个包含所有属性信息的对象;(对象的属性与第一个参数对象要添加或修改的一一对应)
栗:
var person1 = {} |
🔔 注意:上面代码 name 是对象里面属性的名字,设置有两种方式,一种利用 for in 循环 让所有属性匹配数据属性,或者如上一个一个去设置,上面仅设置了 name 属性,假设对象当中有 name 属性的话!
继承自 Object.prototype 的方法
所有的对象都继承顶级对象 Object.prototype 下面的 5 种方法
hasOwnPrototype() 方法: 检查是否存在一个给定名字的自有属性(用来判断和过滤继承属性的)
propertyEnumerable() 方法:检查一个自有属性是否可枚举
isPrototypeOf() 方法:检查一个对象是否是另外一个对象的原型对象 (具体可参考笔记)
valueOf() 返回一个对象的值表达 (返回指定对象的原始值)
toString() 返回一个对象的字符串表达式,强大之处能把数组嵌套 N 层的强制转成字符串表达(隐)
这 5 种方法由继承出现在所有对象中,也就是所有对象都可以使用这 5 种方法。
判断某个对象是否是另外一个对象的原型
isPrototypeOf()
以及 Object.getPrototypeOf()
通过 isPrototypeOf() 来确定对象之间是否存在这种关系,本质上讲,如果[[Prototype]] (就是proto) 指向调用 isPrototypeOf()
方法的对象 (Person.prototype
),那么这个方法就返回 true
console.log( |
这里,用原型对象的 isPrototypeOf()
方法测试了 person1 和 person2 ,因为它们内部都有一个指向 Person.prototype
的指针,因此返回 true
isPrototypeOf(需要判断的对象名)
该方法被包含在所有对象中
var obj = {} |
🔔 注意:可以使用 Object.getPrototypeOf()
方法读取 __proto__
也就是 [[Prototype]] 隐式原型属性的值
栗:
var obj = {} |
🔔 注意:上面代码 Object.getPrototypeOf(obj)得到的其实就是 obj 实例对象的 __proto__
拓展:
function Person() {} |
返回的对象实际就是这个对象的原型。
Object.create() 方法
该方法接收接收两个参数:
- 要继承的对象,也就是说,要通过原型链
__proto__
也就是 [[prototype]] 往上查找的对象 - 可选参数:属性的描述对象,描述属性特征!是否枚举?是否可配置?是否可读写?以及 value
如果第二个参数使用了不写,默认为false
栗:
var person = { |
常规对象字面量方式创建对象:
var book = { |
Object.create() 方式创建对象:
var book = Object.create(Object.prototype, { |
🔔 注意:以上两种申明方式具有相同的效果。
- 第一种申明使用对象字面形式来定义一个具有单一属性的
title
的对象,该对象自动继承Objec.prototype
,并且其属性默认为可配置、可枚举、可读写! - 第二种申明使用
Object.create()
显式做了同样的操作,两个 BOOK 行为完全一致!
下面的例子是用来体验 Object.crate() 来对象之间发生继承关系:
栗:
var person1 = { |
当访问一个对象的属性时,javascript 引擎会执行一个搜索过程,如果在对象实例上发现该属性(就是自有属性),该属性值就会被使用。如果对象实例上没有发现该属性,则通过 [[prototype]] 也就是 __proto__
查找,如果也没有,则继续搜索该原型对象的__proto__
,直接原型链顶端,Object.prototype.__proto__
为 null
;
🔔 注意反例:不要使其为 null;
var nakeObj = Object.create(null) |
延申,用来实现继承父类原型的时候可以使用:
// 将父类的原型 赋值缓存到一个变量 |
设置原型继承 ES6 这个方法更加强大 Object.setPrototypeOf(Son.prototype,Person.prototype)
禁止修改对象
对象和属性一样具有指导其他行为的内部特征。其中对象内部 [[Extensible]] (可扩展的)是一个布尔值,它指明该对象本身是否可以被修改。所有创建的默认对象都是可扩展修改的,意味着新的属性可以随时添加。
设置 [[Extensible]] 为 false
,就能禁止新属性的添加,原生 js 提供了 3 种方法!
- 禁止扩展;
- 对象封印;
- 对象冻结;
判断对象是否为可扩展:isExtensible(对象名)
,返回一个布尔值,默认创建的对象为可扩展 true
!
栗:
var person1 = { |
禁止扩展:Object.preventExtensions()
这种方法是利用 Object.preventExtensions()
方法, (防止扩展的意思),创建一个不可扩展的对象,该方法接收一个参数,就是你希望使其不可扩展的对象。一旦在一个对象上使用该方法,就永远不能再给它添加新的属性了,可以利用 Object.isExtensible()
方法来检查 [[Extensible]] 的值。
栗:
var person1 = { |
每一次创建一个对象的时候,都会隐式的检查该对象的 [[Extensible]] 特征,默认是可扩展。以上代码显示的将其变为不可扩展,由于不可扩展,所有上面的 sayName
方法永远无法添加到 person1
上!
对象封印:Object.seal()
对象封印是创建不可扩展对象的第二种方法。
一个被封印的对象是不可扩展的并且其所有属性都不可配置,意味着不能给添加对象新属性,也不能删除属性或改变类型,如果一个对象被封印,只能读写它的属性。
使用 Object.seal()
方法来封印一个对象。当该方法被调用时,[[Extensible]] 特征被置为 false
,其所有属性的[[Configurable]] 特征被置为 false
。
判断对象是否被封印。Object.isSealed(对象名)
,返回一个布尔值,默认是 false
。
var person1 = { |
栗:
var person1 = { |
这段代码封印了 person1
,因此不能再 person1
上面添加或删除属性,所有的封印对象都是不可扩展的对象,此时对 person1
使用 Ojbect.Extensible()
方法将返回 false,并且试图添加 sayName()
会失败,而且,虽然 person1.name
被成功改变为新值,是因为它 writable
为 true
,可以读写,但是删除它确是失败,因为它的数据属性特征 configurable
为 false
,不可配置!
🔔 注意:
- 禁止扩展:只是阻止了扩展功能,无法添加新的属性和方法。
- 封印对象:在阻止扩展功能的前提上,增加了不可配置,不能删除和重新配置
相同点:它们都是可以针对对象自有属性进行 writable 读写的!
对象冻结 Object.fieeze()
创建不可扩展对象的最后一种方法是冻结它。
如果一个对象被冻结,则不能再其上添加或删除属性,不能改变属性类型,也不能写入任何数据属性。
简而言之,被冻结对象是一个数据属性都为只读的被封印对象,所以 Object.isExtensible()
返回为 false
(不可扩展),而 Object.isSealed()
返回 true
(被封印)。
被冻结对象无法解冻!
判断对象是否被冻结:Object.isFrozen(对象名)
var person1 = { |
栗:
var person1 = { |
🔔 注意:被冻结的对象仅仅只是对象在某个时间点上的快照,其用途有限且极少使被使用;使用的时候注意,不可扩展,对象封印,对象冻结的特性来根据需求选择!
冻结对象较于封印对象 多了属性 [[Writable]] 为 false
不可写! 只能读~
冻结/封印/扩展的时候注意是冻结实例对象还是冻结构造函数的原型,但是常规的原型引用的目的就是为了共享,所以原型的冻结是没必要的,一般原生提供的 Object
顶级对象的原型 就是可以提供修改的 只是不能删除(不可配置 configurable
)
模块模式和暴露模块模式
模块模式:(单个对象,字面量对象)
模块模式是一种用于创建拥有私有数据的单件对象的模式。基本做法是使用立即执行函数表达(IIFE)来返回一个对象。IIFE 是一种被定义后立即调用并产生结果的函数表达,该函数表达可以包括任意数量的本地 变量,它们在函数外不可见。因为返回的对象被定义在函数内部,对象的方法可以访问这些数据。(IIFE 定义的所有的对象都可以访问同样的本地变量)以这种方式访问私有数据的方法被称为特权方法。
栗:基本格式
var yourObj = (function () { |
该模式创建了一个匿名函数并立即执行。这种立即执行函数意味着仅存在于被调用的瞬间,一旦执行完毕立即销毁。
模块模式允许使用普通变量作为非公有对象属性。
通过创建闭包函数作为对象方法来操作它们。
闭包函数就是一个可以访问其他作用域外部数据的普通函数。
举例来说:
当你在一个函数中访问一个全局对象,比如网页浏览器的 window 对象,该函数就是在访问其欧用于的变量。
区别是:
在模块模式里,变量是定义在 IIFE(立即执行表达式函数)中,而访问变量的函数也定义在 IIFE 中。
栗:
var person = (function () { |
这段代码使用模块创建了 person
对象。
变量 age
就是该对象的一个私有属性。
它无法被外界直接访问,但可以通过对象方法来操作。
该对象上有有两个特权方法:getAge()
读取变量 age
的值,growOlder()
让 age
自增.
这两个方法都可以直接访问变量 age
,因为它们都定义了同一个 IIFE 内。
暴露模块模式
它将所有的变量和方法都组织在 IIFE 的顶部,然后将它们设置到需要被返回的对象上。
栗:
var person = (function () { |
在暴露模块模式中,age,getAge()
和 growOlder()
都被定义成 IIFE 的本地对象。
然后 getAge()
和growOlder()
函数被设置到返回的对象中,有效地对外界进行了 “暴露”。
也就是说,只能对暴露出来的这些属性和方法进行操作!
构造函数的私有成员
模块模式在定义单 对象的私有属性上十分有效,但对于同样需要私有属性的自定义类型,需要用构造函数类型的模块模式来创建。
栗:
function Person(name) { |
在这段代码中,Person
构造函数有一个本地变量 age
。
该变量被用于 getAge()
和 growOlder()
方法,当你创建 Person
的一个实例时,该实例接收其自身的 age
变量、getAge()
方法和 growOlder()
方法,这种做法在很多方面类似模块模式,构造函数创建一个本地作用域并返回 this
对象。
将方法直接放在对象实例上不如放在原型对象有效,但是如果需要实例私有的数据,这是唯一的手段!
所有实例可共享的私有数据:
就好像它被定义在原型对象里那样,需要结合模块模式和构造函数。
var Person = (function () { |
在这段代码中,InnerPerson
构造函数被定义在一个 IIFE 中。
变量 age
被定义在构造函数外并被两个原型对象的方法使用。
IIFE 返回 InnerPerson
构造函数作为全局作用域里的 Person 构造函数。
最终,Person 的全部实例得以共享 age
变量,所以在一个实例上的改变自动影响另外一个。