1、对象的属性
给对象添加属性非常的简单如下所示:
var person={
userName:'zhangsan'
}
如果想修改属性的特性,可以通过Object.defineProperty()
来完成。
var person = {
userName: "zhangsan",
};
Object.defineProperty(person, "userName", {
writable: false,
});
person.userName = "lisi"; //无法完成值的修改
console.log(person.userName); //zhangsan
我们可以给Object.defineProperty
添加getter()
函数和setter( )
函数,这两个函数可以实现对象的私有属性,私有属性不对外公布,如果想要对私有属性进行读取和写入,可以通过getter()
函数和setter( )
函数。
var person = {
_age: 20, // _age表示私有属性
};
Object.defineProperty(person, "age", {
get: function () {
return this._age;
},
//在给私有属性赋值的时候,完成对应的校验功能
set: function (value) {
if (value >= 18) {
this._age = value;
console.log("可以浏览该网站");
} else {
console.log("不可以浏览该网站");
}
},
});
console.log(person.age); //20
person.age = 12;
console.log(person.age); //20
person.age = 30;
console.log(person.age); // 30
关于Object.defineProperty
更详细的内容,可以参考vue
响应式原理的课程。
2、属性访问方式的区别
我们知道访问对象中的属性,有两种方式。
第一种方式:通过‘.’来访问。
第二种方式:通过‘[ ]’来访问属性。
两种方式有什么区别呢?
第一:使用方括号来访问属性,可以借助于变量来实现。
var person = {
userName: "zhangsan",
};
var myName = "userName";
console.log(person[myName]);
第二:使用方括号来访问属性,也可以通过数字来做属性。
var person = {
};
person[1] = "hello";
console.log(person[1]);
3、创建对象有哪几种方式
字面量方式创建对象
var userInfo = {
userName: "zhangsan",
userAge: 18,
getUserInfo: function () {
console.log(this.userName + ":" + this.userAge);
},
};
userInfo.getUserInfo();
字面量创建对象比较简单,但是问题也比较突出,每次只能创建一个对象,复用性比较差,如果需要创建多个对象,代码冗余比较高。
通过工厂模式创建对象
工厂模式是一个比较重要的设计模式,该模式提供了一个函数,在该函数中完成对象的创建。
function createUser(userName, userAge) {
var o = new Object();
o.userName = userName;
o.userAge = userAge;
o.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
return o;
}
var user1 = createUser("wangwu", 20);
var user2 = createUser("lisi", 20);
console.log(user1.userName + ":" + user2.userName);
通过工厂模式创建对象,解决了字面量创建对象的问题,也就是当创建多个相似对象的时候代码重复的问题。
但是问题是,所创建的所有对象都是Object
类型,无法进一步的区分对象的具体类型是什么。
通过构造函数创建对象
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
this.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
}
var p = new Person("zhangsan", 19);
p.sayHi();
构造函数创建对象的优点:解决了工厂模式中对象类型无法识别的问题,也就是说通过构造函数创建的对象可以确定其所属的类型。
但是通过构造函数创建对象的问题:
在使用构造函数创建对象的时候,每个方法都会在创建对象时重新创建一遍,也就是说,根据Person
构造函数每创建一个对象,我们就会创建一个sayHi
方法,但它们做的事情是一样的,因此会造成内存的浪费。
通过原型模式创建对象
我们知道,每个函数都有一个prototype
属性,这个属性指向函数的原型对象,而所谓的通过原型模式创建对象就是将属性和方法添加到prototype
属性上。
function Person() {
}
Person.prototype.userName = "wangwu";
Person.prototype.userAge = 20;
Person.prototype.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
var person1 = new Person();
person1.sayHi();
var person2 = new Person();
console.log(person1.sayHi === person2.sayHi); // true
通过上面的代码,我们可以发现,使用基于原型模式创建的对象,它的属性和方法都是相等的,也就是说不同的对象会共享原型上的属性和方法,这样我们就解决了构造函数
创建对象的问题。
但是这种方式创建的对象也是有问题的,因为所有的对象都是共享相同的属性,所以改变一个对象的属性值,会引起其他对象属性值的改变。而这种情况是我们不允许的,因为这样很容易造成数据的混乱。
function Person() {
}
Person.prototype.userName = "wangwu";
Person.prototype.userAge = 20;
Person.prototype.arr = [1, 2];
Person.prototype.sayHi = function () {
console.log(this.userName + ":" + this.userAge);
};
var p1 = new Person();
var p2 = new Person();
console.log(p1.userName);
p2.userName = "zhangsan";
console.log(p1.userName); //wangwu,基本数据类型不受影响
p1.arr.push(3);
console.log(p1.arr); // [1,2,3]
console.log(p2.arr); // [1,2,3]
//引用类型受影响
组合使用构造函数模式和原型模式
通过构造函数和原型模式创建对象是比较常用的一种方式。
在构造函数中定义对象的属性,而在原型对象中定义对象共享的属性和方法。
//在构造函数中定义对象的属性
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
}
//在原型对象中添加共享的方法
Person.prototype.sayHi = function () {
return this.userName;
};
var p = new Person("zhangsan", 21);
var p1 = new Person("lisi", 22);
console.log(p1.sayHi());
console.log(p.sayHi());
// 不同对象共享相同的函数,所以经过比较发现是相等的。
console.log(p.sayHi === p1.sayHi);
//修改p对象的userName属性的值,但是不会影响到p1对象的userName属性的值
p.userName = "admin";
console.log(p.sayHi());
console.log(p1.sayHi());
通过构造函数与原型模式组合创建对象的好处就是:每个对象都有自己的属性值,也就是拥有一份自己的实例属性的副本,同时又共享着方法的引用,最大限度的节省了内存。
使用动态原型模式创建对象
所谓的使用动态原型模式创建对象,其实就是将所有的内容都封装到构造函数中,而在构造函数中通过判断只初始化一次原型。
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
if (typeof this.sayHi !== "function") {
console.log("abc"); //只输出一次
Person.prototype.sayHi = function () {
console.log(this.userName);
};
}
}
var person = new Person("zhangsan", 21);
var person1 = new Person("zhangsan", 21);
person.sayHi();
person1.sayHi();
通过上面的代码可以看出,我们将所有的内容写在了构造函数中,并且在构造函数中通过判断只初始化一次原型,而且只在第一次生成实例的时候进行原型的设置。这种方式创建的对象与构造函数和原型混合模式创建的对象功能上是相同的。
4、对象拷贝
拷贝指的就是将某个变量的值复制给另外一个变量的过程,关于拷贝可以分为浅拷贝与深拷贝。
针对不同的数据类型,浅拷贝与深拷贝会有不同的表现,主要表现于基本数据类型和引用数据类型在内存中存储的值不同。
对于基本数据类型,变量存储的是值本身,
对于引用数据类型,变量存储的是值在内存中的地址,如果有多个变量同时指向同一个内存地址,其中对一个变量的值进行修改以后,其它的变量也会受到影响。
var arr=[1,23,33]
var arr2=arr
arr2[0]=10;
console.log(arr) // [10, 23, 33]
在上面的代码中,我们把arr
赋值给了arr2
,然后修改arr2
的值,但是arr
也受到了影响。
正是由于数据类型的不同,导致在进行浅拷贝与深拷贝的时候首先的效果是不一样的。
基本数据类型不管是浅拷贝还是深拷贝都是对值的本身的拷贝。对拷贝后值的修改不会影响到原始的值。
对于引用数据类型进行浅拷贝,拷贝后的值的修改会影响到原始的值,如果执行的是深拷贝,则拷贝的对象和原始对象之间相互独立,互不影响。
所以,这里我们可以总结出什么是浅拷贝,什么是深拷贝。
浅拷贝:如果一个对象中的属性是基本数据类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址,也就是拷贝后的内容与原始内容指向了同一个内存地址,这样拷贝后的值的修改会影响到原始的值。
深拷贝:如果一个对象中的属性是基本数据类型,拷贝的也是基本类型的值,如果属性是引用类型,就将其从内存中完整的拷贝一份出来,并且会在堆内存中开辟出一个新的区域存来进行存放,而且拷贝的对象和原始对象之间相互独立,互不影响。
浅拷贝
下面我们先来看一下浅拷贝的内容
var obj = {
a: 1, arr: [2, 3], o: {
name: "zhangsan" } };
var shallowObj = shallowCopy(obj);
function shallowCopy(src) {
var dst = {
};
for (var prop in src) {
if (src.hasOwnProperty(prop)) {
dst[prop] = src[prop];
}
}
return dst;
}
obj.o.name = "lisi";
console.log(shallowObj.o.name); //lisi,值受到了影响
obj.arr[0] = 20;
console.log(shallowObj.arr[0]); //20,值受到了影响
obj.a = 10;
console.log(shallowObj.a); // 1,值没有收到影响
除了以上方式实现浅拷贝以外,还可以通过ES6
中的Object.assign()
函数来实现,该函数可以将源对象中的可枚举的属性复制到目标对象中。
var obj = {
a: 1, arr: [2, 3], o: {
name: "zhangsan" } };
var result = {
};
//将obj对象拷贝给result对象
Object.assign(result, obj);
console.log(result);
obj.a = 10;
console.log(result.a); // 1,不受影响
obj.arr[0] = 20;
console.log(result.arr[0]); //20 受影响
obj.o.name = "lisi";
console.log(result.o.name); // lisi 受影响
深拷贝
下面,我们来看一下深拷贝内容
这里,我们可以使用
JSON.parse(JSON.stringify());
来实现深拷贝。
JSON.stringify()
可以将对象转换为字符串
JSON.parse()
可以将字符串反序列为一个对象
var obj = {
a: 1, arr: [2, 3], o: {
name: "zhangsan" } };
var str = JSON.stringify(obj);
var resultObj = JSON.parse(str);
obj.a = 10;
console.log(resultObj.a); // 1 不受影响
obj.arr[0] = 20;
console.log(resultObj.arr[0]); // 2 不受影响
obj.o.name = "lisi";
console.log(resultObj.o.name); // zhangsan 不受影响
以上通过JSON
对象,虽然能够实现深拷贝,但是还是有一定的问题的。
第一:无法实现对函数的拷贝
第二:如果对象中存在循环引用,会抛出异常。
第三:对象中的构造函数会指向Object
,原型链关系被破坏。
function Person(userName) {
this.userName = userName;
}
var person = new Person("zhangsan");
var obj = {
fn: function () {
console.log("abc");
},
// 属性o的值为某个对象
o: person,
};
var str = JSON.stringify(obj);
var resultObj = JSON.parse(str);
console.log("resultObj=", resultObj); // 这里丢失了fn属性。因为该属性的值为函数
console.log(resultObj.o.constructor); //指向了Object,导致了原型链关系的破坏。
console.log(obj.o.constructor); // 这里指向Person构造函数,没有问题
下面我们再来看一下循环引用的情况:
var obj = {
userName: "zhangsan",
};
obj.a = obj;
var result = JSON.parse(JSON.stringify(obj));
以上的内容会抛出异常。
自己模拟实现深拷贝
这里,我们实现一个简单的深拷贝,当然也可以使用第三方库中的方法来实现深拷贝,例如:可以使用jQuery
中的$.extend()
在浅拷贝中,我们通过循环将源对象中属性依次添加到目标对象中,而在深拷贝中,需要考虑对象中的属性是否有嵌套的情况(属性的值是否还是一个对象),如果有嵌套可以通过递归的方式来实现,直到属性为基本类型,也就是说,我们需要将源对象各个属性所包含的对象依次采用递归的方式复制到新对象上。
function clone(target) {
if (typeof target === "object") {
let objTarget = {
};
for (const key in target) {
//通过递归完成拷贝
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan",
a: {
a1: "hello",
},
};
var result = clone(obj);
console.log(result);
以上就是一个最简单的深拷贝功能,但是在这段代码中我们只考虑了普通的object
,还没有实现数组,所以将上面的代码修改一下,让其能够兼容到数组。
function clone(target) {
if (typeof target === "object") {
//判断target是否为数组
let objTarget = Array.isArray(target) ? [] : {
};
for (const key in target) {
objTarget[key] = clone(target[key]);
}
return objTarget;
} else {
return target;
}
}
var obj = {
userName: "zhangsan"