前言
- JavaScript为什么既有原型,也有类思想
- 理清JavaScript中原型、类思想的区别和关联
- 了解object原型中的实用方法
一、什么是对象
在英文中,对象表示一切有形和无形物体的总称
对象具有唯一标识性、状态和行为
- 唯一标识性:指的是即使完全相同的对象也不一样,代码的设计表现是因为他们的内存地址不同
- 状态、行为:在java等语言中用属性表示状态,方法表示行为。
- 而JavaScript中将状态、行为统一抽象为了属性。代码的设计表现是方法、或者属性最终都基于类
属性又被设计为数据属性和访问器属性两种
- 数据属性,特征有以下四种
- value 属性的值
- writeable 是否可以被赋值
- enumerable 是否可以被枚举
- configurable 是否可以被删除,以及writeable、enumerable和configurable是否可以被重置
- 访问器属性
- getter 函数,用于属性值的获取
- setter 函数,用于属性的设置
- enumerable 是否可被枚举
- configurable 是否可以被删除,以及writeable、enumerable和configurable是否可以被重置
- 数据属性和访问器属性的定义和读取分别可以用Object.defineProperty、Object.getOwnPropertyDescriptor()两个方法
综上,对象其实是一个属性的索引结构
- 索引结构是一类常见的数据结构,我们可以把它理解为一个能够以比较快的速度用key来查找value的字典
- 属性以字符串或者Symbol为key,以数据属性特征值或者访问器属性特征值为value
对象的分类
- 宿主对象:由宿主如浏览器提供的document、window等
- 内置对象:
- 固有对象:由标准规定,随着JavaScript运行时创建而自动创建
- 由JavaScript语言提供的对象,固有对象是由标准规定,随着JavaScript运行时而创建而自动创建的对象实例,通常扮演类似基础库的角色
- 具有150+个固有对象,可以参考以下链接www.ecma-international.org/ecma-262/9.…
- 原生对象: 可以由用户通过Array、RegExpd等内置构造器或者特殊语法创建的对象
-
JavaScript的标准提供了30多个构造器,所以原生对象按照构造器分为以下种类
-
-
- 普通对象:由{}语法、Object构造器或者class关键字定义类创建的对象,它能够被原型继承
- 固有对象:由标准规定,随着JavaScript运行时创建而自动创建
二、原型思想构建对象
原型是顺应自然思维的产物。类似于”照猫画虎“,猫就是虎的原型
原型复制操作的两种思路
1. 一个不是真的去复制原型对象,而是去持有原型的引用
2. 另一个是切实的复制对象,从此两个对象再无关联
- 而JavaScript选的是第一种,复制对象时由
__proto__去持有原型的引用
抛开JavaScript用于模拟Java类的复杂语法设置(如new/Function Object/函数的prototype等),原型系统可以总结为:
1. 如果所有对象都有私有字段prototype,就是对象的原型
2. 读一个属性,如果对象本身没有,则会继续访问对象的原型,直到原型为空或者找到为止
利用原型来实现抽象和复用,用下面的代码展示用原型来抽象猫和虎的例子
var cat = {
say(){
console.log("meow~");
},
jump(){
console.log("jump");
}
}
var tiger = Object.create(cat, {
say:{
writable:true,
configurable:true,
enumerable:true,
value:function(){
console.log("roar!");
}
}
})
var anotherCat = Object.create(cat);
anotherCat.say();
var anotherTiger = Object.create(tiger);
anotherTiger.say();
- 通过这种方式就可以通过原始猫、原始虎控制所有的猫和虎
三、类思想构建对象
- 类即是对一类事物属性和行为的抽象总称
- 也是顺着人类思维模式产生的一种抽象,如老虎属于猫科豹属亚种
class Animal { constructor(name) { this.name = name; } speak() { console.log(this.name + ' makes a noise.'); } } class Dog extends Animal { constructor(name) { super(name); } speak() { console.log(this.name + ' barks.'); } } let d = new Dog('Mitzie'); d.speak(); // Mitzie barks.
四、原型和类的关联
为什么JavaScript中既有原型思想,又有类的思想
- 首先JavaScript的创始者选择原型思想,因为原型系统与高动态性语言更加契合,而且多数基于原型的语言提倡运行时的原型修改
- 但是因为java等语言的大火,公司高层要求JavaScript模仿java语言,所以JavaScript中又有了类的思想
类思想和原型思想的区别
- 基于类的编程中,关注分类与类之间关系的开发模型。总是先有类,再从类去实例化一个对象。同时类与类之间再行程组合继承关系
- 基于原型的编程,关注一系列对象实例的行为。而后才去关心如何将这些对象,划分到最近的使用方式相似的原型对象
早起版本“类”的定义是一个私有属性,来表示他们的类。类的概率是比较弱的
- ES5之前用
[[class]]来表示他的类,ES5开始用Symbol.toStringTag代替 - 而且
[[class]]或者Symbol.toStringTag都只能通过Object.prototype.toString()来访问到,返回[Object type],其中type表示它所从属的类
通过构造器来模拟类
function c1(){
this.p1 = 1;
this.p2 = function(){
console.log(this.p1);
}
}
var o1 = new c1;
o1.p2();
function c2(){
}
c2.prototype.p1 = 1;
c2.prototype.p2 = function(){
console.log(this.p1);
}
var o2 = new c2;
o2.p2();
- 第一种是直接在构造器中给this添加属性
- 第二种是修改构造器的prototype属性指向的对象,它是从这个构造器构造出来的所有对象的原型
实际上JavaScript中,类的写法也是由原型运行时来承载的,逻辑上JavaScript认为每个类是有共同原型的一组对象,类的定义方法和属性则会被写在原型对象上
如果用类的思想设计代码时,应该尽量使用class来声明类
- 使用class的语法来定义类,可以令function回归原本的函数语义,这样更明确的区分了定义函数和定义类两种意图
- 比起早期的原型模拟方式,即使使用extends关键字自动设置了constructor,并且会自动调用父类的构造函数,只能更能有效减少语法错误
五、对象的遍历方法
Object.keys()
功能:返回自身可枚举属性的键值数组
- 处理对象时,键值为数值时按照数字的大小自动排序,为非数字时按照插入时间排序
let arr1 = { 100: 'a', 2: 'b', 7: 'c', [Symbol()]:123 };
// console.log(Object.keys(arr1)); // console: ['2', '7', '100']
- ES6中参数为非对象,会强制转换为对象,ES5会出现类型错误
Object.keys("foo");
// TypeError: "foo" is not an object (ES5 code)
Object.keys("foo");
// ["0", "1", "2"] (ES6
- 对比for in不会遍历出原型上的属性
Object.values()
返回自身可枚举属性值的数组
- 可枚举属性顺序和keys()一样
let arr1 = { 100: 'a', 2: 'b', 7: 'c', [Symbol()]:123 };
console.log(Object.values(arr1)); // console: ['b', 'c', 'a']
- ES6会将非对象参数临时转换为对象
console.log(Object.values("foo"));
// ["f", "o", "o"]
Object.entries()
返回自身可枚举属性值的键值对数组
- 可枚举属性的排列顺序和keys()一样
const anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.entries(anObj)); // [ ['2', 'b'], ['7', 'c'], ['100', 'a'] ]
- 不是对象时也会强制转换
console.log(Object.entries('foo')); // [ ['0', 'f'], ['1', 'o'], ['2', 'o'] ]
- 同keys()/values()一样不会遍历出原型上的属性
- 同时Object.keys/values/entries均不能遍历出名为symbol的属性
- 如果需要的话可以使用Reflect.ownKeys(obj)返回一个数组,包含对象自身所有属性,无论键名是否是symbol,也不管是否可枚举
object.keys/values/entries的低版本兼容
主要是通过for in来实现的
- Object.keys低版本兼容
If(!Object.keys)
Object.keys = function(obj){
If(obj !== Object(obj)) throw new TypeError('Object.keys called on a non-object')
var result = []
for(var prop in obj){
if(Object.hasOwnProperty.call(obj,prop)) result.push(prop)
}
return result
}
- Object.values的兼容类似于keys
- Object.entries低版本兼容
If(!Object.entries)
Object.entries = function(obj) {
If(obj !== Object(obj)) throw new TypeError('Object.keys called on a non-object')
var result = []
for(var prop in obj){
if(Object.hasOwnProperty.call(obj,prop)) result.push([prop, obj[prop]])
}
return result
}
object.keys/values/entries底层实现方式
- 属性不超过128个,使用Search Cache,当属性是较为连续的数字时,使用数组,此种方式最快。其它情况使用哈希表,并且数字和字符串的哈希不一样。
- 具体内容较多且较为复杂,详细请参考从chrome源码看JS Object的实现
Object.getOwnPropertyNames()
返回自身可枚举和不可枚举属性名称的字符串数组
- 可枚举属性顺序和keys()一致,不可枚举属性顺序未定义
- 同样ES6会强制转化非对象属性
Object.getOwnPropertyDescriptions()
返回自身可枚举、不可枚举属性描述符对象
- 可枚举属性顺序和keys()一致,不可枚举属性顺序未定义
- 对比Object.getOwnPropertyNames()会将名为symbol的参数也遍历出来
var parent = Object.create(Object.prototype, {
a: {
value: 1,
writable: true,
enumerable: true,
configurable: true
}
});
var child = Object.create(parent, {
b: {
value: 2,
writable: true,
enumerable: true,
configurable: true
},
d: {
value: () => {},
writable: true,
enumerable: true,
configurable: true
},
c: {
value: 3,
writable: true,
enumerable: false,
configurable: true
},
[Symbol()]: {
value: 4,
writable: true,
enumerable: true,
configurable: true
}
});
for(let i in child){
console.log(i)
}
console.log(Object.keys(child))
console.log(Object.getOwnPropertyNames(child))
console.log(Object.getOwnPropertyDescriptors(child))
console.log(Reflect.ownKeys(child))
console.log(Object.getOwnPropertyDescriptors('123'))
六、对象的不可扩展、密封和冻结
Object.preventExtensions()
- 不可扩展之后不能新增属性
- 原有属性仍可以删除
- 可以将属性添加在对象原型上
- 对象的__proto__不能变更
- 通过Object.isExtensiable()判断是否可扩展
Object.seal()
- 在preventExtensions()不能新增的基础上,新特点是不能删除属性
- 可以修改属性值,但已有属性会变得不可配置
- 不会影响原型链上继承的属性,但__proto__的值不能修改
- 通过Object.isSeal()判断是否密封
Object.freeze()
- 在seal()的基础之上,新特性所有的属性无法被修改
- 如果一个属性的值是个独享,则这个属性是可以修改的
- 不会影响原型链上集成度属性,但__proto__的值也不能修改
- 可以通过Object.isFrozen()判断是否冻结
let obj = {
prop: function(){},
foo: 'bar',
ba: {
a:333
}
}
let o = Object.freeze(obj)
console.log(o === obj) //true
obj.ba.a = 222
console.log(Object.getOwnPropertyDescriptors(obj))
obj.foo = 'quux' //无反应
obj.quaxxor = '12312' //无反应
delete obj.prop //无反应
// Object.setPrototypeOf(obj, {x:20}) //报错
// obj.__proto__ = {x:20} //报错
console.log(Object.getOwnPropertyDescriptors(obj))
Object.freeze()和const之间的区别
- const和let唯一的区别是变量的地址值不可变,const申明的对象writeable、configurable属性依然是true,所以const声明的复合类型数据仍然可变
- 所以如果想让对象或者数组无法变更需要freeze
- 但同时freeze仍无法冻结对象下的对象属性,需要使用逐层遍历(递归)冻结,如下所示
const user = {
first_name: 'bolaji',
last_name: 'ayodeji',
contact: {
email: 'hi@bolajiayodeji.com',
telephone: 08109445504,
}
}
console.log(Object.getOwnPropertyDescriptors(user))
let constantize = (obj) => {
Object.freeze(obj);
Object.keys(obj).forEach( (key, i) => {
if ( typeof obj[key] === 'object' ) {
constantize( obj[key] );
}
})
}
constantize(user)
preventExtensible()、seal()、freeze()的使用场景
- 这三个方法可以用来实现不可变数据。而不可变数据正是函数式编程最重要的原则之一。
- 另外不可变对象对于react同样重要,因为React内是通过state/props比较(===)去判断是否render 的,三个等号的比较就要求新的 state 必须是新的引用。
- 在 facebook 的实现的不可变对象库 Immutable 中,Map 是相当于 JavaScript 中的对象。这里可以通过preventExtensible()来实现
function ImmutableMap(object) { Object.keys(object).forEach((key) => { Object.defineProperty(this, key, { value: object[key], writable: false, enumerable: true, configurable: false, }); }); Object.preventExtensions(this); }- 同时在另一个不可变对象库 seamless-immutable 中使用了 JavaScript Object 提供的方法 freeze