对象和数组一样,都是常用的东西。本篇对JS的对象做深入的分析和沉淀
原生JS中的对象
业务开发中,对象是最常见的数据类型。在JS中几乎“所有事物”都是对象
,包括但不限于布尔、数字、字符串、日期、函数、对象等等。
字符串、数值和布尔值的定义
在实际开发中布尔、数字、字符串等类型都是直接赋值,并未用new关键字进行过定义。
var a = 1; // 1
var b = new Number(2); // Number对象
var c = new Object(3); // Number对象
console.log(a + b + c); // 6
直接赋值数字时不是Number对象,仅为数值。
在W3C的JavaScript 对象中文章末尾有一节:
请不要把字符串、数值和布尔值声明为对象!
如果通过关键词 "new" 来声明 JavaScript 变量,则该变量会被创建为对象:
var x = new String(); // 把 x 声明为 String 对象 var y = new Number(); // 把 y 声明为 Number 对象 var z = new Boolean(); // 把 z 声明为 Boolean 对象
请避免字符串、数值或逻辑对象。他们会增加代码的复杂性并降低执行速度。
这就是为什么不用new定义数字、字符串、布尔的原因:增加代码的复杂性并降低执行速度
定义对象变量
常见定义变量的方式:
var obj = { key: 'value' }
上述代码是创建对象最简单的方式,称为对象字面量方式
创建对象。
原生JS还支持以下方式:
var obj = new Object();
obj.key = 'value'
官方描述:
使用new
关键字进行变量定义。和使用字面量定义的方式,字面量有明显的优势:
{}方式具备简易性、可读性和更快执行速度
在ES5中支持以下方式:
var obj = { name: '张三' }
var obj2 = Object.create(obj);
obj2.key = 'value'
引用传递
对象通过引用传递,而非值传递。
基础数据类型是值传递,而非引用传递
// 字符串
var str = '1';
var str1 = str;
str1 = '2';
console.log(str); // 1
console.log(str1); // 2
// 数值
var num = 1;
var num1 = num;
num1 = 2;
console.log(num); // 1
console.log(num1); // 2
// 布尔值
var bool = true;
var bool1 = bool;
bool1 = false;
console.log(bool); // true
console.log(bool1); // false
// 对象(重新赋值)
var obj = { key: 'value' }
var obj1 = obj;
obj1 = { key1: 'value1' };
console.log(obj); // { key: 'value' }
console.log(obj1); // { key1: 'value1' }
// 对象(添加修改值)
var obj = { key: 'value' }
var obj1 = obj;
obj1.key = 'value_copy';
obj1.key1 = 'value1'
console.log(obj); // {key: 'value_copy', key1: 'value1'}
console.log(obj1); // {key: 'value_copy', key1: 'value1'}
ES5对象新增特性
ES5中新增了许多对象的方法,Vue也是基于ES5对象的方法实现的双向绑定。
Getter
和 Setter
JavaScript访问器
Getter
和 Setter
想必都不陌生,Vue2双向绑定原理的知识点,用于数据劫持。
var obj = {
name: '张三',
language: "en",
get lang() {
return this.language.toUpperCase();
},
get hello() {
console.log("I'm " + this.name)
},
set change(value) {
this.name = value
}
}
obj.hello // I'm 张三
console.log(obj.lang) // EN
obj.change = '李四';
obj.hello // I'm 李四
Getter
和 Setter
的特点
- 更简洁的语法(无需执行方法)
- 允许属性和方法的语法相同()
- 确保更好的数据质量(不会改变对象的原属性)
- 有利于后台工作(返回后台处理后的数据)
以现有对象为原型创建对象: Object.create()
以现有对象为原型创建对象
var man = {
sex: 1,
say: function () {
console.log(this.name)
}
};
var person = Object.create(man);
person.name = '张三';
console.log(person); // { name: '张三' }
person.say(); // 张三
原生JS实现create:
function create(proto) {
var obj = {};
obj.__proto__ = proto;
return obj;
}
var man = {
sex: 1,
say: function () {
console.log(this.name)
}
};
var person = create(man);
person.name = '张三';
console.log(person); // { name: '张三' }
person.say(); // 张三
添加或更改对象属性:defineProperty
和 defineProperties
这个方法都不陌生,Vue2就是基于ES5的defineProperty
实现数据劫持的。
defineProperty
用于添加或更改对象属性,允许更改 getter 和 setter
var obj = { name: "张三" };
// 修改属性
Object.defineProperty(obj, "name", { value: "李四" }); // { name: '李四' }
// 添加属性:不可修改、不可枚举、不可配置
Object.defineProperty(obj, "age", {
value: 18
}); // {name: '李四', age: 18}
obj.age = 20; // {name: '李四', age: 18}
// 添加属性:可修改、不可枚举、不可配置
Object.defineProperty(obj, "age", { value: 18, writable: true });
obj.age = 20; // {name: '李四', age: 20}
// 添加属性:可修改、可枚举、不可配置
Object.defineProperty(obj, "age", { value: 18, writable: true, enumerable : true });
obj.age = 20; // {name: '李四', age: 20}
// 添加属性:全部支持
Object.defineProperty(obj, "age", {
value: 18,
writable: true,
enumerable: true,
configurable: true
});
obj.age = 20; // {name: '李四', age: 20}
Object.defineProperty(obj, "age", { value: 18 }); // {name: '李四', age: 18}
上述修改、枚举和配置:
- writable:控制是否可以修改属性值。
- enumerable:控制显式或隐式添加属性。
- configurable:控制是否可以通过
defineProperty
重新定义属性。
通过defineProperty
更改 getter 和 setter
// html
<div id="root"></div>
// js
var obj = {};
var vm = {};
const el = document.getElementById('root');
Object.defineProperty(vm, 'name', {
get: function () {
return obj.name;
},
set: function (value) {
obj.name = value;
el.innerHTML = value
}
})
vm.name = '张三'
console.log(vm);
/*
{
name: "张三"
get name: ƒ ()
set name: ƒ (value)
}
*/
上述例子简单模拟Vue数据绑定到dom的实现,仅模拟Vue数据劫持的核心实现。
defineProperties
方法和defineProperty
类似,区别在于defineProperties
可添加或修改多个属性。
访问属性: Object.getOwnPropertyDescriptor()
返回指定对象上一个自有对应的属性
var obj = {
name: "张三"
};
var result = Object.getOwnPropertyDescriptor(obj, 'name');
console.log(result);
// {value: '张三', writable: true, enumerable: true, configurable: true}
返回元信息,可以在修改属性值前可用于检测,业务中不怎么用。
获取对象的属性名: Object.getOwnPropertyNames()
和bje Object.keys()
var obj = {
name: '张三',
age: 18
};
Object.defineProperty(obj, "sex", { value: 1 });
console.log(Object.getOwnPropertyNames(obj)); // ['name', 'age', 'sex']
console.log(Object.keys(obj)); // ['name', 'age']
通过 defineProperty
添加的属性没有设置可枚举属性,所以Object.keys()
不会返回。
区别:Object.getOwnPropertyNames()
可以获取所有属性名,而 Object.keys()
只能获取枚举的属性名
访问原型: Object.getPrototypeOf()
返回其原型的对象,可用于原型对比
var obj = { name: "张三" };
var obj2 = Object.create(obj);
console.log(Object.getPrototypeOf(obj2) === obj);
// 构造函数拓展
function People() { }
function Robot() { }
var bob = new People();
console.log(Object.getPrototypeOf(bob) === People.prototype); // true
console.log(Object.getPrototypeOf(bob) === Robot.prototype); // false
基于getPrototypeOf
可以检测某个实例是否基于指定原型创建。
保护对象
用于禁止修改对象,保护后,上述命令将不可用。实际使用很少,感兴趣的写写demo吧。
// 防止向对象添加属性
Object.preventExtensions(object)
// 如果属性可以添加到对象,则返回 true
Object.isExtensible(object)
// 防止更改对象属性(不是值)
Object.seal(object)
// 如果对象被密封,则返回 true
Object.isSealed(object)
// 防止对对象进行任何更改
Object.freeze(object)
// 如果对象被冻结,则返回 true
Object.isFrozen(object)
ES6新增特性
ES6对象进行了比较大的升级,拓展了对象的能力。
属性升级:属性简洁表示法和属性名表达式
属性简洁表示法:对象可以直接写入变量和函数,作为对象的属性和方法。
var name = '张三';
var say = function () {
console.log("I'm " + this.name);
}
var obj = { name, say };
console.log(obj); // { name: '张三‘, say: f() }
obj.say(); // I'm 张三
属性名表达式:之前对象的key只能是字符串,现在可以写表达式。
var key = 'name';
var obj = {
[key]: '张三',
['a' + 'g' + 'e']: 18,
['say']: function () {
console.log("I'm " + this.name);
}
}
super关键字
super关键字:类似JS,指向当前对象的原型对象
var man = {
say: function () {
console.log("I'm " + this.name)
}
};
var people = {
name: '张三',
say () {
super.say()
}
}
Object.setPrototypeOf(people, man);
people.say()
super
关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。
扩展运算符、解构赋值(ES8)
这个相信无论你用什么框架都会经常使用的js技能之一。
需要注意的是对象的拓展运算符是在ES8中引入的,并非ES6
var obj = { a: 1, b: 2, c: 3 };
var obj2 = { ...obj, d: 4 };
console.log(obj2); // {a: 1, b: 2, c: 3, d: 4}
var { a, ...c } = obj;
console.log(c); // {b: 2, c: 3}
比较值是否相等:Object.is()
原生JS比较两个值是否相等使用的是==
或===
。
前者会自动转换数据类型,后者的NaN
不等于自身。
Object.is()
方法类似于===
,用来比较两个值是否严格相等。
// NaN
console.log(NaN === NaN); // false
Object.is(NaN, NaN); // true
// +0和-0
console.log(+0 === -0); // true
Object.is(+0, -0); // false
// {};
console.log({} === {}); // false
Object.is({}, {}); // false
区别:+0,-0和NaN的判断结果不一致
原生js实现
function is(x, y) {
if (x === y) {
// 针对+0 不等于 -0的情况
return x !== 0 || 1 / x === 1 / y;
}
// 针对NaN的情况
return x !== x && y !== y;
}
console.log(is(NaN, NaN)) // true
console.log(is(0, 0)) // true
console.log(is(0, -0)) // false
console.log(is({}, {})) // false
合并对象:Object.assign()
这个方法都不陌生,用于对象的合并。
// 基础数据类型
var obj1 = {}
var obj2 = {
key1: 1,
key2: '2',
key3: true
}
Object.assign(obj1, obj2);
console.log(obj1); // {key1: 1, key2: '2', key3: true}
obj2.key3 = false;
console.log(obj1); // {key1: 1, key2: '2', key3: true}
// 复杂数据类型
var obj3 = {}
var obj4 = {
key1: {},
key2: [1, 2, 3],
key3: {
key3_1: [1, 2, 3]
}
}
Object.assign(obj3, obj4);
console.log(JSON.stringify(obj3, null, 4));
// {
// "key1": {},
// "key2": [ 1, 2, 3 ],
// "key3": {
// "key3_1": [ 1, 2, 3 ]
// }
// }
obj4.key2 = []
obj4.key3.key3_1 = {};
console.log(JSON.stringify(obj3, null, 4));
// {
// "key1": {},
// "key2": [ 1, 2, 3 ],
// "key3": {
// "key3_1": {}
// }
// }
总结:
- 如果对象的属性值为基础类型(如string, number),
Object.assign()
为深拷贝 - 如果属性值为对象或其它引用类型,
Object.assign()
为浅拷贝
设置对象的原型对象: setPrototypeOf
Object.setPrototypeOf
方法的作用与__proto__
相同,用来设置一个对象的原型对象(prototype),返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。
var man = {
sex: 1
};
var person = {
name: '张三'
};
Object.setPrototypeOf(person, man);
console.log(person); // { name: '张三' }
注意:ES6以后建议不要再使用__proto__
,使用Object.setPrototypeOf()
(写操作)、Object.getPrototypeOf()
(读操作)、Object.create()
(生成操作)代替。
ES7及后续版本新特性
ES6后的版本更新基本都是完善对象的方法集。
返回对象所有自身属性的描述对象(ES2017):Object.getOwnPropertyDescriptors()
和Object.getOwnPropertyDescriptor
类似,这个是返回对象属性的描述对象(全部)。
var obj = {
id: 1,
name: '张三'
};
Object.defineProperty(obj, 'sex', { value: 1 })
console.log(Object.getOwnPropertyDescriptors(obj));
// {
// id: {
// configurable: true,
// enumerable: true,
// value: 1,
// writable: true
// },
// name: {
// configurable: true,
// enumerable: true,
// value: "张三",
// writable: true,
// },
// sex:{
// configurable: false,
// enumerable: false,
// value: 1,
// writable: false
// }
// }
它会返回每个属性的描述对象
返回对象可遍历对象的键值数组(ES2017):Object.values()
和Object.keys()
类似的方法,返回对象键值数组
var obj = {
id: 1,
name: '张三'
};
Object.defineProperty(obj, 'sex', { value: 1 })
console.log(Object.values(obj)); // [1, '张三']
返回对象可遍历对象的键值对数组(ES2017): Object.entries()
类似Object.keys()
和Object.values()
的集合,返回键值对数组
var obj = {
id: 1,
name: '张三'
};
Object.defineProperty(obj, 'sex', { value: 1 })
console.log(Object.entries(obj)); // [["id",1],["name","张三"]]
将键值对数组转为对象(ES2019): Object.fromEntries()
和Object.entries()
相反的操作,将键值对数组转为对象
console.log(JSON.stringify(Object.fromEntries([["id",1],["name","张三"]])));
// {"id":1,"name":"张三"}
拓展:深拷贝
深拷贝无论是在实际开发中还是面试中都是比较常见的问题,现阶段开发中,大多数情况下都会使用第三方库完成,如lodash
库的deepClone
方法。下面是几种深拷贝的方式:
var obj = {
id: 1,
name: '张三',
tags: ['篮球', '电玩'],
schools: [
{ id: 1, name: '清华大学' },
{ id: 2, name: '清华附中' }
],
say: function () {
console.log(this.name)
}
}
// 未深拷贝时
var obj1 = obj;
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj1)
// 1. 利用JSON实现
var obj2 = JSON.parse(JSON.stringify(obj));
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj2)
// 2. 利用Object.create实现
function clone(data) {
var obj = Object.create(Object.getPrototypeOf(data));
var props = Object.getOwnPropertyNames(data);
props.forEach(function(name) {
Object.defineProperty(obj, name, Object.getOwnPropertyDescriptor(obj, name));
});
return obj;
}
var obj3 = deepCopy(obj);
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj1)
// 2. 原生JS实现
function deepCopy(data) {
var result = Array.isArray(data) ? [] : {};
for (var k in data) {
if (data.hasOwnProperty(k)) {
if ( typeof data[ k ] === 'object' ) {
result[k] = deepCopy(data[k]);
} else {
result[k] = data[ k ]
}
}
}
return result;
}
var obj4 = deepCopy(obj);
obj.tags.push('学霸');
obj.schools.push({ id: 3, name: '清华附小' });
console.log(obj1)
JSON
的方式无法拷贝函数,若没有函数它算最简单的方式,Object.create
是基于ES5的特性实现,比原生的方式简单一些。- 最后是原生JS的实现方式,比前两种方式复杂一些。
相关资料
- W3Chool的JavaScript 对象
- W3Chool的JavaScript ES5
- W3Chool的JS对象教程
- W3Chool的JavaScript ES5 对象方法
- MDN的Object
- 阮一峰老师的ES6教程中的对象的扩展和对象的新增方法
相关试题
new Object()
和{}
定义对象有什么区别?- Vue双向绑定的原理是什么?
- Vue数据劫持的具体实现逻辑是什么?
- 通过某个构造函数创建的变量,如何检测它是这个构造函数的实例?
- ES6中对象新增了哪些新特性?
- 你知道super关键字吗?它和js有什么区别?
- 你知道怎么对比两个对象是否相等吗?
==
和===
有什么区别?Object.is()
和===
有什么区别?Object.create()
和构造函数的区别是什么?- 你知道深拷贝和浅拷贝的区别吗?
- js中浅拷贝的方法有哪些?
- 如何实现对象的深拷贝?
- 实现一个深拷贝函数(笔试)。
- 如何获取对象的全部键名?