第四节:Object对象相关(对象属性、创建、拷贝、重写、继承等)
一. 基础
1. 对象属性有哪几种访问方式?
(1). 通过 . 模式
(2). 通过 [] 模式
{
let obj = {
name: "ypf",
age: 18,
};
// 方式1--通过 . 获取
console.log(obj.name);
// 方式2 通过[] 获取
console.log(obj["age"]); //18
}
2. 对象创建有哪几种方式?[6种]
(1). 字面量
特点:创建对象简单,但是每次只能创建一个对象,创建多个相似对象的时候,存在大量冗余代码。
{
console.log("方式1-字面量");
let userInfo = {
userName: "ypf",
userAge: 18,
getInfo() {
console.log(`${this.userName},${this.userAge}`);
},
};
userInfo.getInfo();
}
(2). 工厂模式
特点:解决了字面量创建多个相似对象代码重复的问题,但是工厂模式创建出来的对象都是object类型,无法进一步确定对象的具体类型。
{
console.log("方式2-工厂模式");
function CreateUser(userName, userAge) {
let o = new Object();
o.userName = userName;
o.userAge = userAge;
o.getInfo = function () {
console.log(`${this.userName},${this.userAge}`);
};
return o;
}
let user1 = CreateUser("ypf1", 18);
let user2 = CreateUser("ypf2", 19);
user1.getInfo();
user2.getInfo();
console.log(typeof user1); //object
}
(3). 构造函数
特点:解决了工厂模式无法知道对象具体类型的问题,但是构造函数的模式,每创建一次对象,内部的方法就会创建一次,而且都是一样的,造成内存浪费。
{
console.log("方式3-构造函数创建");
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
this.getInfo = function () {
console.log(`${this.userName},${this.userAge}`);
};
}
let user1 = new Person("ypf1", 18);
let user2 = new Person("ypf2", 19);
user1.getInfo();
user2.getInfo();
console.log(user1 instanceof Person); //true
console.log(user1.__proto__.constructor); //[Function: Person]
}
(4). 原型模式
特点:解决了构造函数模式中方法多次创建内存浪费问题,但是原型模式中所有实例都共享原型对象上的属性和方法,对于基本的数据类型而言,没有问题;对于函数也没有问题;但是对于引用数据类型,多个实例之间修改会相互影响,这是错误的。
{
console.log("方式4-原型的方式创建");
function Person() {
Person.prototype.userName = "ypf";
Person.prototype.userAge = 18;
Person.prototype.myArray = [1, 2];
Person.prototype.getInfo = function () {
console.log(`${this.userName},${this.userAge}`);
};
}
let user1 = new Person();
let user2 = new Person();
user1.userName = "ypf1";
user1.userAge = 20;
console.log(user2.userName); //ypf, 基本数据类型不受影响
user1.myArray.push(3);
console.log(user2.myArray); //[ 1, 2, 3 ], 引用数据类型受影响
user1.getInfo(); //ypf1,20
user2.getInfo(); //ypf,18
}
(5). 构造函数+原型【推荐】
模式:属性放在构造函数里,方法放在原型对象上。
特点:解决了构造函数模式方法多次被创建占用内存问题; 解决了原型模式引用类型的属性修改相互影响的问题
好处:每个实例都有自己的属性,而且多个实例之间共享方法,最大程度上节约了内存。
{
console.log("方式5-构造函数+原型 (属性通过构造函数,方法通过原型添加)");
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
}
Person.prototype.getInfo = function () {
console.log(`${this.userName},${this.userAge}`);
};
let user1 = new Person("ypf1", 18);
let user2 = new Person("ypf2", 19);
user1.getInfo(); //ypf1,18
user2.getInfo(); //ypf2,19
// 不同实例共享相同的函数,且不相互影响
console.log(user1.getInfo === user2.getInfo); //true
user1.userName = "lmr";
user1.getInfo(); //lmr,18
user2.getInfo(); //ypf2,19 //不受影响
}
(6). 构造函数+动态原型 【效果同上】
模式:属性放到构造函数里,function方法以原型的形式也放在构造函数里,但是做一次判断,仅创建一次即可。
{
console.log("方式6--动态创建原型 【效果同上述方式5】");
function Person(userName, userAge) {
this.userName = userName;
this.userAge = userAge;
console.log(this.getInfo); //第一次是undefined,第二次是 [Function (anonymous)]
if (typeof this.getInfo !== "function") {
console.log("只运行一次哦");
Person.prototype.getInfo = function () {
console.log(`${this.userName},${this.userAge}`);
};
}
}
let user1 = new Person("ypf1", 18);
let user2 = new Person("ypf2", 19);
user1.getInfo(); //ypf1,18
user2.getInfo(); //ypf2,19
// 不同实例共享相同的函数,且不相互影响
console.log(user1.getInfo === user2.getInfo); //true
user1.userName = "lmr";
user1.getInfo(); //lmr,18
user2.getInfo(); //ypf2,19 //不受影响
}
二. 对象拷贝
(参考之前文章: https://www.cnblogs.com/yaopengfei/p/15261698.html )
1. 什么是深拷贝和浅拷贝?
(1). 浅拷贝: 对于基本数据类型,拷贝的是基本类型的值(即原值和新值不会相互影响);对于引用数据类型而言,拷贝是栈中的地址(即拷贝后的内容和原始内容指向同一个地址,修改值会相互影响)。
(2). 深拷贝:对于基本数据类型,拷贝的是基本类型的值(即原值和新值不会相互影响);对于引用数据类型而言,深拷贝是从内存中完整的拷贝出来一份,并且会在堆内存中开辟一个新的空间进行存储(即原值和新值不会相互影响)。
2. 浅拷贝有哪些方式?
(1). let user2 = Object.assign({},user1)
(2). 展开运算符: let user2={...user1}
(3). 第三方库: lodash
3. 深拷贝有哪些方式?
(1). let user2=JSON.parse(JSON.stringify(user1));
剖析弊端:
A. 无法对函数进行拷贝。
B. 如果对象中存在循环引用,会直接报错。
C. 破坏了原有的原型链。
详细代码参考之前文章。
(2). 第三方库: lodash 【推荐】
4. 手写浅、深拷贝代码?
详细代码参考之前文档
三. 对象继承的实现方式
1. 原型链继承
(1). 核心
A. 子类的原型指向父类的实例(从而实现子类调用父类的方法) Dog.prototype = new Animal();
B. 子类的构造函数指向自身 (如果不写,由于上面那句话,导致指向了Animal构造函数,是错误的) Dog.prototype.constructor = Dog;
(2). 优点
A. 实现简单
B. 子类可以直接访问父类原型链中的属性和方法
(3). 缺点
A. 父类中的属性如果是引用类型,子类操作会相互影响。
B. 创建子类实例的时候,无法向父类构造函数中传递参数。
C. 如果要给子类的原型上添加方法,必须放在 Dog.prototype = new Animal(); 这句话之后,否则无效。
{
console.log("1 继承--原型链继承");
function Animal() {
this.superType = "Animal";
this.name = "动物";
this.hobby = ["test1", "test2"];
// 父类的实例方法
this.sleep = function () {
console.log(this.name + "睡觉");
};
}
// 父类的原型方法
Animal.prototype.eat = function (foodName) {
console.log(`${this.name}正在吃${foodName}`);
};
// 子类
function Dog(name) {
this.name = name;
}
// 实现继承需要两步
// 1. 子类的原型指向父类的实例(从而实现子类调用父类的方法)
Dog.prototype = new Animal();
// 2. 子类的构造函数指向自身 (如果不写,由于上面那句话,导致指向了Animal构造函数,是错误的)
Dog.prototype.constructor = Dog;
// 测试
var dog1 = new Dog("二胖");
console.log(dog1.superType); //Animal
dog1.sleep(); //二胖睡觉
dog1.eat("香肠"); //二胖正在吃香肠
// 缺点
var dog2 = new Dog("大胖");
dog1.hobby.push("test3");
console.log(dog2.hobby); //[ 'test1', 'test2', 'test3' ] 相互影响
}
2. 构造函数继承
(1). 核心
A. 在子类构造函数中,通过apply或call,调用父类构造函数,改变父类构造函数中的this指向,同时可以传递参数。 Person.call(this, name);
(2). 优点
A. call
改变了父类中的this
B. 创建子类实例,可以向父类构造函数中传递参数。
(3). 缺点
A. 子类只能继承父类实例上的属性和方法,不能继承父类原型上的属性和方法。
B. 父类构造函数中的实例方法,每创建一个子类,就会创建这样一个实例方法,而这些方法都是相同的,导致占用内存较大。(之前是放在父类原型上实现)
{
console.log("2 继承--构造函数继承");
// 父类
function Person(name) {
console.log("调用了Person构造函数");
this.name = name;
this.hobby = ["test1", "test2"];
this.eat = function () {
console.log(`${this.name}正在eat中`);
};
}
// 父类原型上的方法
Person.prototype.study = function () {
console.log(`${this.name} 正在study中`);
};
// 子类
function Student(id, name) {
this.id = id;
// 实现继承
Person.call(this, name); //改变父类构造中this的指向,传递参数,并调用父类Person构造函数
}
// 测试
let student1 = new Student(01, "ypf1");
let student2 = new Student(02, "ypf2");
student1.hobby.push("test3");
console.log(student1.hobby); //[ 'test1', 'test2', 'test3' ]
console.log(student2.hobby); //[ 'test1', 'test2' ] 引用类型相互间不影响的
// 可以实现向父类中传值
student1.eat(); //ypf1正在eat中
student2.eat();
// 无法调用父类原型中的方法
// student1.study(); //直接报错:student1.study is not a function
}
3. 拷贝继承
(1). 核心
通过for-in获取父类的实例 和 原型上的属性方法,然后通过 hasOwnProperty 方法,区分原型 or 实例,从而给子类的实例 或 原型进行赋值。
(2). 优点
A. 创建子类实例,可以向父类构造函数中传递参数。
B. 子类实例可以继承父类 实例 和 原型上的 属性、方法。
(3). 缺点
A. 父类上的所有属性和方法子类都需要复制拷贝一遍,消耗内存。
{
console.log("3 继承--拷贝继承");
// 父类
function Person(name) {
console.log("调用了Person构造函数");
this.name = name;
this.hobby = ["test1", "test2"];
this.eat = function () {
console.log(`${this.name}正在eat中`);
};
}
// 父类原型上的方法
Person.prototype.study = function () {
console.log(`${this.name} 正在study中`);
};
// 子类
function Student(id, name) {
this.id = id;
// 逐个拷贝父类的属性
let person = new Person(name);
for (const key in person) {
if (person.hasOwnProperty(key)) {
// 实例属性
this[key] = person[key];
} else {
// 原型属性
Student.prototype[key] = person[key];
}
}
}
// 测试
let student1 = new Student(01, "ypf1");
let student2 = new Student(02, "ypf2");
student1.hobby.push("test3");
console.log(student1.hobby); //[ 'test1', 'test2', 'test3' ]
console.log(student2.hobby); //[ 'test1', 'test2' ] 引用类型相互间不影响的
// 可以实现向父类中传值
student1.eat(); //ypf1正在eat中
student2.eat(); //ypf2正在eat中
// 可以调用父类原型中的方法
student1.study(); //ypf1 正在study中
}
4. 组合继承【推荐】
(1). 核心
原型继承+构造函数继承
A. 通过原型继承,将父类原型对象上的属性和方法绑定到子类的原型上。
B. 通过构造函数继承,将父类实例上的属性和方法绑定到子类上。
特别注意:call()
函数完成父类中实例属性和方法的绑定的优先级要高于通过改写子类prototype
(2). 优点
A. 创建子类实例,可以向父类构造函数中传递参数。
B. 子类实例可以继承父类 实例 和 原型上的 属性、方法。
C. 不需要重复拷贝属性和方法。
(3). 缺点
整个继承的过程中,构造函数被调用了两次。
第一次:Person.call(this, name); ,通过call方法,调用了一次构造函数。
第二次:Student.prototype = new Person(); new父类实例的时候,又调用了一次构造函数。
{
console.log("4 继承--组合继承");
// 父类
function Person(name) {
console.log("调用了Person构造函数");
this.name = name;
this.hobby = ["test1", "test2"];
this.eat = function () {
console.log(`${this.name}正在eat中`);
};
}
// 父类原型上的方法
Person.prototype.study = function () {
console.log(`${this.name} 正在study中`);
};
// 子类
function Student(id, name) {
this.id = id;
// 4.1 构造函数继承
Person.call(this, name);
}
// 4.2 原型继承
Student.prototype = new Person();
Student.prototype.constructor = Student;
// 测试
let student1 = new Student(01, "ypf1");
let student2 = new Student(02, "ypf2");
student1.hobby.push("test3");
console.log(student1.hobby); //[ 'test1', 'test2', 'test3' ]
console.log(student2.hobby); //[ 'test1', 'test2' ] 引用类型相互间不影响的
// 可以实现向父类中传值
student1.eat(); //ypf1正在eat中
student2.eat(); //ypf2正在eat中
// 可以调用父类原型中的方法
student1.study(); //ypf1 正在study中
}
5. 寄生式组合继承【推荐】
(1). 核心
原型继承+构造函数继承+引入一个空函数Super
A. 通过原型继承,将父类原型对象上的属性和方法绑定到子类的原型上,这里引入一个空函数Super,作为中转,Super.prototype
的原型指向了Person.prototype
, 最后核心代码是 Student.prototype = new Super() 【此处就不需要调用父类的构造函数了】
B. 通过构造函数继承,将父类实例上的属性和方法绑定到子类上。
(2). 优点
A. 具有之前方案的所有优点。
B. 解决了组合继承构造函数调用两次的问题。
(3). 缺点
无。
{
console.log("5 继承--寄生式组合继承");
// 父类
function Person(name) {
console.log("调用了Person构造函数");
this.name = name;
this.hobby = ["test1", "test2"];
this.eat = function () {
console.log(`${this.name}正在eat中`);
};
}
// 父类原型上的方法
Person.prototype.study = function () {
console.log(`${this.name} 正在study中`);
};
// 子类
function Student(id, name) {
this.id = id;
// 4.1 构造函数继承
Person.call(this, name);
}
// 4.2 原型继承(寄生式)
function Super() {} // 定义Super构造函数
Super.prototype = Person.prototype; //Super.prototype原型对象指向了Person.prototype
Student.prototype = new Super(); //Student.prototype原型对象指向了Super的实例,这样就去掉了Person父类的实例属性。
Student.prototype.constructor = Student;
// 测试
let student1 = new Student(01, "ypf1");
// 可以实现向父类中传值
student1.eat(); //ypf1正在eat中
// 可以调用父类原型中的方法
student1.study(); //ypf1 正在study中
}
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。