JS - 对象的操作办法
在JavaScript这门语言中,数据类型分为两大类:基本数据类型 和 复杂数据类型。基本数据类型包括Number
、Boolean
、String
、Null
、String
、Symbol(ES6 新增)
,而复杂数据类型包括Object
,而所有其他引用类型(Array
、Date
、RegExp
、Function
、基本包装类型(Boolean
、String
、Number
)、Math
等)都是Object
类型的实例对象,因此都可以继承Object
原型对象的一些属性和方法。
# 对象的创建及实例化
对象创建有两种方式,一是直接将一个对象值赋值给目标变量,二是使用声明
var obj = {}
var obj = new Object()
# 对象属性删除
delete
用于删除对象的属性,但不会删该属性的值,如果值引用数为0后,GC会将它回收。这通常用在H5中缓存机制,比如需要对某个缓存值进行清空
var obj = {
a: 1,
b: 2
}
delete obj.a
如果只是想将属性值清空,则直接赋值为空就好了obj.a = ''
# 对象的复制办法(深浅拷贝)
相信大家都了解变量【值类型】和【引用类型】的区别,对象是引用类型的一种,使用变量直接赋值办法得到的只是指向对象值所在堆地址的一个地址引用,这些变量任意一个发生修改都会影响其他的值,然而往往我们想让数据相互独立,也就是实现深浅拷贝过程
浅拷贝,只做了地址引用的复制,并没有得到真正的值,修改任意变量会相互影响;
深拷贝复制真正的值,进行深拷贝的变量之间他们老死不相往来,不会相互影响
| 直接赋值的浅拷贝
直接赋值其实只是对地址引用做了一份复制,所有变量指向了同一个数据源
var obj = { a: 1 };
var copy = obj;
obj.a = 2;
console.log(copy.a) // 2
| 浅拷贝的方法原理
所谓的浅拷贝,就是将源对象的每一个属性都做一个拷贝,并不考虑属性的类型是否为引用类型
function extendCopy (p) {
var c = {},
for (var i in p) {
c[i] = p [i]
}
return c
}
| Object.assign() 实现对象的浅拷贝
assign
意为 转让,分配,在JS中 Object.assign()
方法用作于合并多个对象, 它可以接受多个参数,第一个是目标对象,其他的是源数据,本例中第一个参数为空且只接受了另一个参数实现了复制效果。它由ES6引入。
var origin = { a: 1, b: {b1: 'b1'} }
var assign_copy = Object.assign({}, origin)
// 修改源数据测试对复制变量的影响
origin.a = 2;
origin.b.b1 = 'b1_changed'
// output
console.log(assign_copy); // {a: 1, b: {b1: 'b1_changed'}}
即:对origin
进行修改的时候,顶层属性a
并未跟随着改变,然而二层属性b1
的值则发生了变化,说明属性b
拷贝过来的值,是一个指针(注意b
本身属于顶层属性,效果与a
相同),这也就是为什么有人说Objecrt.assign()
实现的是浅拷贝的原因
另外值得注意的是,对于属性名相同的属性在assign
合并过程中会发生值覆盖。
| 利用JSON对象序列化实现的深拷贝
var Obj = { a: 1, b: undefined, c: () => {} };
var copy = JSON.parse(JSON.stringify(Obj));
console.log(copy) // {a: 1}
这种办法的缺陷在于,JS对象与JSON对象转化过程中,JSON.stringify()
办法会主动丢弃(1)值为undefined
的属性,(2)值为Function
类型的属性,(2)值为Symbol
类型,(4)原型属性。所以经过JSON.parse()
出来的数据与原有的数据可能出现属性丢失。
这种方式常用在H5中将引用类型存放到sessionStorage
或 localStorage
,归功于H5缓存的这应用场景常能规避这种丢失现象
| 递归办法: 深拷贝
其实也好理解,说白了就是浅拷贝的递归。
function deepCopy(p, c) {
var c = c || {};
for (var i in p) {
if (typeof p[i] === 'object') {
c[i] = (p[i].constructor === Array) ? [] : {};
deepCopy(p[i], c[i]);
} else {
c[i] = p[i];
}
}
return c;
}
更完善的写法
var clone = function (obj) {
if(obj === null) return null
if(typeof obj !== 'object') return obj;
if(obj.constructor===Date) return new Date(obj);
var newObj = new obj.constructor (); //保持继承链
for (var key in obj) {
if (obj.hasOwnProperty(key)) { //不遍历其原型链上的属性
var val = obj[key];
newObj[key] = typeof val === 'object' ? arguments.callee(val) : val;
// 使用arguments.callee解除与函数名的耦合
}
}
return newObj;
};
【拓展】constructor
属性返回对创建此对象的数组函数的引用,如下案例
// 构造函数employee如下
function employee(name) {
this.name = name;
}
var bill = new employee('Bill')
console.log(bill.constructor); // function employee(name) { this.name = name }
console.log(bill.constructor === emp[loyee) // true
// 在原型链中, B是A的原型,A是B的构造函数
A.prototype = B
B.constructor = A
# 利用对象构造JS “类”
在ES5中,没有“类”这个概念[ES6已经支持],但是有模拟的办法。JS定义类有如下几个办法:
| 构造函数创建一个类
经典方法,定义一个构造函数作为一个”类“,使用new
关键字生成实例。属性和方法还能定义在构造函数prototype
对象上。原型与自身的区别在于:对象自身的属性和方法只对该对象有效,而原型链中的属性方法共享于所有的实例中
function Animal (name, age) {
this.name = name;
this.age = age;
this.msg = function () {
console.log(this.name + '的年龄是' + this.age);
}
}
Person.prototype.eat = function (food) {
console.log(this.name + 'is eating' + food)
}
var animal1 = new Animal('Jack', 2)
var animal2 = new Animal('Lucy', 5)
animal1.eat('apple') // Jack is eating apple
animal2.eat('peach') // Lucy is eating peach
| Object.create() 创建一个JS‘类’
Object.create()
是ES5 出现的一个替代new
操作的办法。定义一个对象作为一个"类"
// 一个标准的对象类
var animal = {
name: "jack",
getName: function() {
return this.name
},
says: function() {
return this.saying || '';
}
}
// Object.create()定义的一个对象类
var myCat = Object.create(animal);
myCat.name = "heart";
myCat.saying = 'miao';
myCat.getName = function() {
console.log(this.says + ' ' + this.name + this.says)
}
| 极简法封装类
不使用this
和prototype
, 代码编写起来比较简单,用一个自定义对象定义一个“类”,在这个类里面有一个构造函数,用来生成实例
var Dog = {
createFn: function () {
var dog = {}; // 实例对象,作为返回值
dog.name = "bruce";
dog.makeSound = function () { alert('汪汪汪‘); };
return dog;
}
}
这种对象定义类的办法实现继承 只需要 调用父类的createFn
方法即可
var Animal = {
createFn: function () {
var animal = {}
animal.eat = function () {
alert('吃饭啦’);
}
return animal;
}
};
var Dog = {
createFn = new Anumal.createFn();
dog.name = "狗狗"
dog.makeSound = function () {
alert('汪汪');
}
return dog;
}
// 实例应用
var dog = new Dog();
dog.eat() // "吃饭啦"
# 对象的遍历办法
在ES6中,对象被认为是可遍历的,这为我们遍历对象提供了更多的办法
| for...in
循环遍历对象自身的和继承的可枚举属性 (不含Symbol
属性,下同)
如果属性是数字,会对属性遍历做排序处理
var obj = {10: 'a', 2: 'b', 3: 'c'}
for (let x of obj) {
console.log(x, obj[x]);
}
// ---out-put---
// 2, b
// 3, c
// 10, a
| Object.keys(obj)
返回一个数组,它包括自身所有可枚举属性但不包括继承属性
var obj = {10: 'a', 1: 'b', 2: 'c'}
Object.keys(obj).forEach(function(key){
console.log(key, obj[key]);
})
// output
// 1, b
// 2, c
// 10, a
| Object.getOwnPropertyNames(obj)
返回一个数组,它包括自身所有属性
使用方法和特征与Object.keys()
一致
Object.getOwnPropertyNames(obj).forEach(function(key){
console.log(key, obj[key]);
})
| Object.getOwnPropertySymbols(obj)
返回数组,包含对象自身所有属性,包括Symbol
Object.getOwnPropertySymbols(obj).forEach(function(key){
console.log(key, obj[key]);
})
# 结束语
关于对象的知识,还有太多,这里只提到了几个常用的场景,欢迎大家交流