最近在看《你不知道的javaScript》系列书籍,这里做一些记录。
上卷主要分两部分,分别是作用域和闭包,this 和 对象原型。
这篇文章主要介绍对象原型这部分。
对象原型
对象
对象可以通过两种形式定义:声明(文字)形式和构造形式。
对象是JavaScript的基础。在JavaScript中一共有六种主要类型(术语是“语言类型”):
string
number
boolean
null
undefined
object
ES6 新增基本类型 Symbol
注意,简单基本类型(string、boolean、number、null和undefined)本身并不是对象。
null有时会被当作一种对象类型,但是这其实只是语言本身的一个bug,即对null执行typeof null时会返回字符串”object”。实际上,null本身是基本类型
内置对象
有些内置对象的名字看起来和简单基础类型一样,不过实际上它们的关系更复杂
String
Number
Boolean
Object
Function
Array
Date
RegExp
Error
这些内置对象从表现形式来说很像其他语言中的类型(type)或者类(class),比如Java中的String类。
但是在JavaScript中,它们实际上只是一些内置函数。
这些内置函数可以当作构造函数(由new产生的函数调用)来使用,从而可以构造一个对应子类型的新对象。
举例来说:
1 | var strPrimitive = 'I am a string' |
思考下面的代码:
1 | var strPrimitive = 'I am a string' |
使用以上两种方法,我们都可以直接在字符串字面量上访问属性或者方法,之所以可以这样做,是因为引擎自动把字面量转换成String对象,所以可以访问属性和方法。
对象里的内容相关
复制对象
对于JSON安全(也就是说可以被序列化为一个JSON字符串并且可以根据这个字符串解析出一个结构和值完全一样的对象)的对象来说,有一种巧妙的复制方法:
1 | var newObj = JSON.parse(JSON.stringify(someObj)) |
ES6还定义了Object.assign(..)
方法来实现浅复制Object.assign(..)
方法的第一个参数是目标对象,之后还可以跟一个或多个源对象。
它会遍历一个或多个源对象的所有可枚举(enumerable)的自有键(owned key)并把它们复制(使用=操作符赋值)到目标对象,最后返回目标对象,就像这样:
1 | function anotherFunction(){} |
但是需要注意的一点是,由于Object.assign(..)就是使用=操作符来赋值,所以源对象属性的一些特性(比如writable)不会被复制到目标对象。
属性描述符
1 | var myObject = { a: 2 } |
configurable 是类似于其他两个状态的锁。把configurable修改成false是单向操作,无法撤销!
同时 configurable 为 false的话,也无法删除这个属性
要注意有一个小小的例外:即便属性是configurable:false,我们还是可以把writable的状态由true改为false,但是无法由false改为true。
不变性
很重要的一点是,所有的方法创建的都是浅不变性,也就是说,它们只会影响目标对象和它的直接属性。
如果目标对象引用了其他对象(数组、对象、函数,等),其他对象的内容不受影响,仍然是可变的:
1.对象常量
结合writable:false和configurable:false就可以创建一个真正的常量属性(不可修改、重定义或者删除)
2.禁止扩展
如果你想禁止一个对象添加新属性并且保留已有属性,可以使用Object.preventExtensions(..)
1 | var myObject = { a: 2 } |
3.密封
Object.seal(..)会创建一个“密封”的对象,这个方法实际上会在一个现有对象上调用Object.preventExtensions(..)并把所有现有属性标记为configurable:false。
所以,密封之后不仅不能添加新属性,也不能重新配置或者删除任何现有属性(虽然可以修改属性的值)
4.冻结
Object.freeze(..)会创建一个冻结对象,这个方法实际上会在一个现有对象上调用Object.seal(..)并把所有“数据访问”属性标记为writable:false,这样就无法修改它们的值。
这个方法是你可以应用在对象上的级别最高的不可变性,它会禁止对于对象本身及其任意直接属性的修改(不过就像我们之前说过的,这个对象引用的其他对象是不受影响的
get 和 set
1 | var myObject = { |
1 | // 实际的 get 和 set |
存在性
in & hasOwnProperty(..)
in操作符 会检查 属性是否在对象及其原型链中
相比之下,hasOwnProperty(..)
只会检查属性是否在myObject对象中,不会检查 原型链
看起来in操作符可以检查容器内是否有某个值,但是它实际上检查的是某个属性名是否存在。
对于数组来说这个区别非常重要,4 in [2, 4, 6]的结果并不是你期待的True,因为[2, 4, 6]这个数组中包含的属性名是0、1、2,没有4。
可枚举
1 | var myObject = {} |
在数组上应用for..in循环有时会产生出人意料的结果,因为这种枚举不仅会包含所有数值索引,还会包含所有可枚举属性。
最好只在对象上应用for..in循环,如果要遍历数组就使用传统的for循环来遍历数值索引
另外一些可枚举 API
obj.propertyIsEnumerable(..)会检查给定的属性名是否直接存在于对象中(而不是在原型链上)并且满足enumerable:true
Object.keys(..)会返回一个数组,包含所有可枚举属性
Object.getOwnPropertyNames(..)会返回一个数组,包含所有属性,无论它们是否可枚举
Object.keys(..)和Object.getOwnPropertyNames(..)都只会查找对象直接包含的属性
类的继承
非常重要的一点是,我们讨论的父类和子类并不是实例。
父类和子类的比喻容易造成一些误解,实际上我们应当把父类和子类称为父类DNA和子类DNA
1 | function Enemy(fname,speed) { |
多态
同一个函数,不同情况下表现出不同的状态
- 重载: 根据传入参数的不同,会返回不同的结果
- 重写(override): 如果子对象觉得从父对象继承来的成员不好用,可在子对象本地定义同名成员,来覆盖父对象的成员。
如:顶级对象的toString()和 数组的toString()实现不一样的效果
笔试题目: 判断一个对象是不是数组,共有几种方法:
typeof仅能区分原始类型和function,无法进一步区分每种对象的类型
- 判断原型对象:
obj.__proto__==Array.prototype
Array.prototype.isPrototypeOf(obj)
- 判断构造函数:
obj.constructor==Array
obj instanceof Array
obj(是)Array的一个实例吗?
问题: 以上两种方式,检查不严格,可能被篡改 - 判断隐藏的class属性:
Object.prototype.toString.call(obj1)
this->obj1.class Array.isArray(obj)
直接返回bool值
原理同方法3,也是严格的检查
小结
传统的类被实例化时,它的行为会被复制到实例中。类被继承时,行为也会被复制到子类中。
多态(在继承链的不同层次名称相同但是功能不同的函数)看起来似乎是从子类引用父类,但是本质上引用的其实是复制的结果。
JavaScript并不会(像类那样)自动创建对象的副本。
原型
Prototype
JavaScript中的对象有一个特殊的[[Prototype]]内置属性,其实就是对于其他对象的引用。
几乎所有的对象在创建时[[Prototype]]属性都会被赋予一个非空的值。
所有普通的[[Prototype]]链最终都会指向内置的Object.prototype
根据理解总结几点:
1.原型链的尽头(root)是Object.prototype。所有对象均从Object.prototype继承属性。
1 | Object.prototype.__proto__ // null |
2.Function.prototype和Function.proto为同一对象。
1 | Function.prototype === Function.__proto__ // true |
3.Function.prototype直接继承root(Object.prototype)。
1 | // |
1 | // 委托模型 |
类风格的代码强调的是实体与实体之间的关系,委托风格的代码强调的是对象之间的关联关系;
- 本文作者: Jambo
- 版权声明: 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。转载请注明出处!