JavaScript 基础(五):Object
基础系列文章:
JavaScript 基础(一):null 和 undefined
在 JS 中 Object 是一个很重要的数据类型。并且在实际开发中使用频率很高。
但是 Object 也是难点,特别是:原型、原型链、继承、Function 等。
下面就从 Object 的各个方面进行介绍。
一、Property(属性)
Object 是一组数据和功能的集合。创建完成后添加属性和方法(property)。
这些 property 在对象里面是以键值对的形式存储的。
Object 本身也有一些属性,叫属性描述符。
1、数据属性
2、访问器属性
上面两个的具体有:
/** * 可以通过它对定义的属性有更大的控制权,主要有下面几个 * value ---- 获取属性时返回的值 * writable ---- 该属性是否可写 * enumerable ---- 该属性在 fon in 循环中是否会被枚举 * Configurable ---- 该属性是否可被删除 * set() ---- 属性的更新操作 * get() ---- 获取属性操作 * * 分别对应: * 数据属性:value、writable、enumerable、configurable * 访问器属性:get()、set()、enumerable、configurable */
下面是具体的代码展示:
let person = {} // 直接赋值,是数据属性 person.legs = 2 // 这种方式添加的属性,其他描述符都是 true console.log('person descriptors:', Object.getOwnPropertyDescriptors(person)) // legs: { value: 2, writable: true, enumerable: true, configurable: true } // 数据属性形式,这种方式 除了 value 默认 undefined,其他都是默认 false Object.defineProperty(person, 'legs1', { value: 2, writable: true, enumerable: true, configurable: true }) // 访问器属性形式 Object.defineProperty(person, 'legs2', { set: function(v) { return (this.value = v) }, get: function() { return this.value }, enumerable: true, configurable: true }) person.legs2 = 5
在 Vue2.x 中使用的数据拦截就是用的“访问器属性”,在 get、set 里面进行监听操作,进而实现数据动态绑定。
二、Object 属性方法
我们经常用到的 Object 就是我们需要研究的第一个对象。
可以使用 Object 自身的一些方法来查看自身。
console.log(Object.getOwnPropertyDescriptors(Object)) // 查看 Object 自身的全部属性及详情 console.log(Object.getOwnPropertyNames(Object)) // 查看 Object 全部的属性名称列表
属性名称列表打印出来是:
[ 'length', 'name', 'prototype', 'assign', 'getOwnPropertyDescriptor', 'getOwnPropertyDescriptors', 'getOwnPropertyNames', 'getOwnPropertySymbols', 'is', 'preventExtensions', 'seal', 'create', 'defineProperties', 'defineProperty', 'freeze', 'getPrototypeOf', 'setPrototypeOf', 'isExtensible', 'isFrozen', 'isSealed', 'keys', 'entries', 'fromEntries', 'values' ]
属性详情内容比较多,这里就只展示几个,并且 Object 的属性都是用数值属性定义的:
{ length: { value: 1, writable: false, enumerable: false, configurable: true }, name: { value: 'Object', writable: false, enumerable: false, configurable: true }, prototype: { value: {}, writable: false, enumerable: false, configurable: false }, assign: { value: [Function: assign], writable: true, enumerable: false, configurable: true }, getOwnPropertyDescriptor: { value: [Function: getOwnPropertyDescriptor], writable: true, enumerable: false, configurable: true } }
1、属性相关
属性相关的较多,下面就列举出来。
1)、获取属性信息
/** * Object.getOwnPropertyDescriptor(obj,property) * 查看一个对象的某一个属性的详情,也可以一窥内置的对象属性 */ /** * Object.getOwnPropertyDescriptors(obj) * 用于获取对象所有属性的描述 */ /** * Object.getOwnPropertyNames(obj) * 返回当前对象的所有属性名,包括不可枚举的 */ /** * Object.keys(obj) * 返回的是自身可枚举的 */ /** * Object.values(obj) * 用于获取对象自身所有可枚举属性的值 * 顺序和 for in 顺序一致(不同点是,for in 包括继承的属性) */ /** * Object.entries(obj) * 返回的是对象自身的可枚举数据键值对(可迭代) * 顺序和 for in 顺序一致(不同点是,for in 包括继承的属性) */ /** * Object.getOwnPropertySymbols(obj) * 返回当前对象的 Symbol 属性,一个对象初始化后不包含 Symbol 属性 * 只有当赋值了 Symbol 属性才可以取到值 */
2)、设置、赋值属性
/** * Object.assign(target,...sources) * 该方法用于将源对象(sources)的可枚举属性赋值到目标对象(target),可以同时将多个源对象复制给目标对象 * 有同名属性,复制后后面的会覆盖前面的属性 * 只复制自身的可枚举的属性 * 属性类型是 Symbol 的也可以复制 * 对于嵌套类型的对象,是直接替换 * 对于数组,取 length 最大值,并后面覆盖前面 */ let target = { a: 1 } let source1 = { b: 2 } let source2 = { c: 3 } let objAssign = Object.assign(target, source1, source2) console.log('target:', target) console.log('objAssign:', objAssign) console.log(Object.assign([1, 2], [3, 4, 5], [6, 7])) // 打印结果 // target: { a: 1, b: 2, c: 3 } // objAssign: { a: 1, b: 2, c: 3 } // [ 6, 7, 5 ] /** * Object.defineProperty(obj,prop,descriptor) * 用于定义对象的属性 * * Object.defineProperties(obj,props) * 基本功能同上面,但是可以一次定义多个属性 */ let glass = Object.defineProperties( {}, { color: { value: 'transparent', writable: true }, fullness: { value: 'half', writable: true } } )
2、创建对象
Object 属性中有两个方法可以创建新对象。
/** * Object.create(obj,descr) * 创建一个新对象,并设置原型,用属性描述符定义对象的原型属性 * obj:创建新对象的原型 * descr:新对象的属性,以对象形式 */ let person1 = { hi: 'hello' } Object.defineProperty(person1, 'name', { value: 'ZHT', enumerable: true }) // 新创建的 child 的原型是 person1 let child = Object.create(person1, { prop: { value: 1 }, childName: { value: 'ZHT-C', enumerable: true, writable: true } }) /** * Objec.fromEntries(iterable) * 创建一个以给定数组、map或者其他可迭代为属性的新对象 * 是 entries 的逆操作 */ let arr = [['cow','牛牛111'],['pig','佩奇']] let objFromEntries = Object.fromEntries(arr) console.log('objFromEntries:',objFromEntries) // 打印结果:objFromEntries: { cow: '牛牛111', pig: '佩奇' }
3、原型相关
与原型相关就是设置、获取原型
/** * Object.prototype * Object 原型,是一个对象 */ /** * Object.getPrototypeOf(obj) * 获取当前对象的原型 */ /** * Object.setPrototypeOf(obj,prototype) * 设置该对象(obj)的原型为 prototype */
4、其他 Object 属性方法
/** * Object.preventExtensions(obj) * Object.isExtensible(obj) * preventExtensions 用于禁止向对象添加更多属性(不可扩展对象,但是可在其原型中添加)不可逆操作 * isExtensible 检查对象是否可以添加对象 */ /** * Object.seal(obj) * Object.isSealed(obj) * seal 用于密封一个对象,并返回密封后的对象。这个对象的属性不可配置:这种情况下,只能变更现有属性的值, * 不能删除或者重新配置属性的值 * isSeal 判断是否是密封的 * 密封对象 不可扩展 不可逆操作 */ /** * Object.freeze(obj) * Object.isFrozen(obj) * freeze 用于冻结一个对象,冻结后:不可添加属性、删除属性、不能修改属性值,以及配置值 不可逆操作 * isFrozen 用于判断是否被冻结 */ /** * 不可扩展、密封、冻结 对比 */ // 方法名 增(extensible) 删(configurable) 改(writable) // Object.preventExtensions × √ √ // Object.seal × × √ // Object.freeze × × × /** * Object.is(value1,value2) * 用于比较两个值是否严格相等 基本等同于(===) * 有两个地方不一样:+0 不等于 -0,NaN 等于自身 */
三、Prototype(原型)
在 JS 中所有的对象都有原型。
1、Object 原型
Object 原型是一个对象,其属性有:
/** * Object.prototype * Object.prototype.constructor ---- 构造器 * Object.prototype.toString(radix) ---- 返回描述对象字符串,radix 是当为 Number 时进制 * Object.prototype.toLocaleString() ---- 基本同上 * Object.prototype.valueOf() ---- 返回基本类型的 this 值 * Object.prototype.hasOwnProperty(prop) ---- 对象中是否有该属性(原型链上的返回 false) * Object.prototype.isPrototypeOf(obj) ---- 当前对象的原型是不是目标对象(括号中的是当前对象) * Object.prototype.propertyIsEnumerable(prop) ---- prop 是否是当前对象 for in 中的值 */
2、新建对象原型
新建的对象的原型遵循下面的方法获取(没有 prototype):
1)、对于 obj.__proto__ 这个属性,可以读写 obj 的原型,但是这个属性不是在 ES6
可见这个是内部的属性,不是正式对外的 API
2)、所以操作 obj 的原型最好的方法是:Object.getPrototypeOf()、Object.setPrototypeOf()、Object.create()
3)、都是调用的 Object.prototype.__proto__,在 ES6 中推荐的是 setPrototypeOf
四、增强语法
1、计算属性
对象的属性名称可以根据动态生成
const nameKey = 'name' const ageKey = 'age' const jobKey = 'job' let uniqueToken = 0 function getUniqueKey(key){ return `${key}_${uniqueToken++}` } let person = { [getUniqueKey(nameKey)]:'ZHT', [getUniqueKey(ageKey)]:29, [getUniqueKey(jobKey)]:'code' } console.log(person) // { name_0: 'ZHT', age_1: 29, job_2: 'code' }
2、对象解构
解构是ES6的新语法,在使用中可适用的场景很多,这里就简单举例,详细看阮老师的变量的解构赋值
let person1 = { name:'ZHT', age:29 } let {name,age} = person1 // 可以和对象属性同名 let {name:personName,age:personAge} = person1 // 也可以设置别名
五、对象创建
1、对象字面量
对象字面量创建对象,语法上更加的简洁。形式如下:
let obj1 = { name: 'obj1', value: 123 } // 可以继续向对象中添加属性 obj1.age = 12 // 此时新建的对象的原型为空对象,可以显示设置原型 Object.setPrototypeOf(obj1,prototype)
2、new Object
此种方法创建的对象和上面一样,只是写法上面。
同样的,新建的对象原型也是空对象。
3、Object.create
create 方法创建的对象,和上面两种的区别是可以直接指定原型。
具体的使用上面有介绍。
下面是几种批量创建对象的模式
4、工厂模式
工厂模式是软件设计中比较常见的设计模式。用抽象创建特定对象的过程。
function createPerson(name,age,job){ let obj = {} obj.name = name obj.age = age obj.job = job obj.sayName = function(){ console.log(this.name) } return obj } let person1 = createPerson('s',10,'stu')
上面的例子就是把创建 person 对象抽象出来,可以直接调用创建新对象。对于大量、重复创建可以提供效率。
但是工厂模式存在问题,不能解决对象标识问题(即新创建的对象是什么类型)
5、构造函数模式
在 ES 中构造函数是用于创建特定类型对象的。像内置的 Object、Array 都是构造函数,可以在执行环境中直接使用。
同时我们也可以自定义构造函数,以函数的形式给自己的对象类型定义属性和方法。
下面是构造函数模式例子:
function PersonConstructor(name,age,job){ this.name = name this.age = age this.job = job this.sayName = function(){ console.log(this.name) } } const person1 = new PersonConstructor('jike',40,'CEO') const person2 = new PersonConstructor('Robot',39,'CFO') console.log(person1.constructor === PersonCon) // true console.log(person1 instanceof Object) // true console.log(person2 instanceof PersonCon) // true
首先构造函数模式和工厂函数的区别:
1)、没有显示的创建对象
2)、属性和方法直接赋值给了 this
3)、没有 return
看了这些你有没有疑问呢?那为什么能够创建并返回一个新对象呢?
这里就要说到 ES 里面 new + 构造函数 的语法糖的具体内部操作了:
a)、在内存中创建一个新对象
b)、这个新对象的[[prototype]]特性被赋值为构造函数的 prototype 属性(即 __proto__,建议用 Object.getPrototypeOf 取值)
c)、构造函数内部的 this 被赋值为这个新对象(this 指向新对象)
d)、执行构造函数内部代码
e)、如果构造函数有返回非空对象,返回这个对象;如果没有 返回 this(新创建的对象)
所以上面的疑问也就得到了解决。
同时也解决了工厂模式的问题——对象标识。
上面的代码中看出,新建对象的构造函数指向创建它的构造函数。
而且用 instanceof 构造函数、Object 都是 true。这两个都在原型链上。
对于构造函数几点说明:
A)、构造函数也是函数,只是执行上面有点不一样,如果不加 new 和普通函数一样
B)、构造函数当普通函数时要注意 this 的指向问题
C)、存在问题:构造函数中如果有方法的话,创建的每个实例都创建一遍,而且做的事情都一样,但是又有同名不相等的问题
虽然可以放在外面定义,直接指向外面,这样会污染全局,并且方法多了也有问题。
console.log(person1.sayName === person2.sayName) // false
6、原型模式
由于构造函数模式存在一定的问题,所以就有了原型模式。(在这里我们只讲原型模式继承,对原型、原型链暂不做讨论)
function Person(){} // 直接在原型上面添加属性、方法 Person.prototype.name = 'ZHT' Person.prototype.age = 24 console.log(Person.toString()) // 自定义构造函数时,原型对象只会自动获得 constructor 属性,其他的方法继承自 Object // 1、这个 constructor 指回与之关联的构造函数 console.log(Person.prototype.constructor === Person) // true // 根据这个可以一直向上查找,最终源于 Object 原型 console.log(Person.prototype.__proto__ === Object.prototype) // true console.log(Person.prototype.__proto__.constructor === Object) // true console.log(Person.prototype.__proto__.__proto__ === null) // true // 调用构造函数创建一个实例时,实例内部的 [[prototype]] 指向构造函数原型 // 1、在一般实现中 [[prototype]] 是以 __proto__ 暴露出来的 // 2、实例和构造函数原型有直接联系 let personPro = new Person() console.log(personPro.__proto__ === Person.prototype) // true
原型模式也有自己的缺点:
所有的属性、方法都是在原型上面,通过这种方式创建的对象实例共享这些方法。对于有些时候需要相对独立的,原型就有问题了。
7、混合模式
上面的两种:构造函数模式、原型模式 做的都太绝对,相当于两个极端。所以在实际的使用中还是两者的结合。
// 构造函数模式 function Animation(obj){ // 根据传入初始化 this.name = obj.name this.id = obj.id } // 原型模式 Animation.prototype = { // 做相同的事情 eat:function () { console.log(this.name,',开始吃饭了……') } } let dog = new Animation({name:'dog',id:1}) dog.eat()