JavaScript进阶-对象进阶
构造函数

什么是构造函数
构造函数是用于创建和初始化对象的特殊函数,构造函数通过 new 关键字调用,返回一个新创建的对象。
构造函数具有以下特点:
- 函数名称:构造函数通常以大写字母开头,以便与普通函数进行区分。
- 创建对象:构造函数内部使用
this
关键字引用新创建的对象。 - 属性和方法:构造函数可以定义对象的属性和方法,这些属性和方法会被每个对象实例共享。
- 初始化对象:构造函数可以在对象创建时对其进行初始化,为对象设置初始值。
示例:
js
function Person(name, age) {
this.name = name;
this.age = age;
}
// 使用构造函数创建对象实例
var person1 = new Person("Alice", 25);
var person2 = new Person("Bob", 30);
console.log(person1.name); // 输出: "Alice"
console.log(person2.age); // 输出: 30
new 关键字
new
是JavaScript 中用于创建对象实例的关键字。
new
调用一个构造函数的时候,他执行以下步骤
- 创建一个空对象,
new
关键字会创建一个新的空对象,该对象将作为构造函数的执行上下文 - 将构造函数的原型赋值给新对象的
__proto__
属性:新对象会继承构造函数的的原型对象,通过__proto__
属性与原型链建立连接 - 将新对象作为构造函数的执行上下文(this):构造函数内部使用
this
关键字引用新创建的对象,可以通过this
关键字设置新对象的属性和方法 - 执行构造函数的代码:构造函数内部的代码会执行,可以对新对象进行初始化操作
- 返回新对象:如果构造函数内部没有显式返回其他对象,那么
new
表达式将返回新创建的对象
示例:
js
function Person(name, age) {
this.name = name;
this.age = age;
}
// 使用 new 关键字调用构造函数创建对象实例
var person = new Person("Alice", 25);
console.log(person.name); // 输出: "Alice"
console.log(person.age); // 输出: 25
在上面的示例中,Person
是一个构造函数,通过 new Person("Alice", 25)
创建了一个新的对象实例 person
。构造函数内部的代码将使用 this
关键字设置新对象的属性值,最终返回这个新对象。
需要注意的是,new
关键字在创建对象实例时,会自动将新对象与构造函数的原型对象建立关联,形成原型链。这样,新对象可以访问构造函数原型上的属性和方法。
原型和原型链
学习大纲

什么是原型?
原型是 JavaScript 对象的一个内部属性,它指向另一个对象,即该对象的原型。每个对象都有原型,除了 null
。原型是通过 __proto__
属性来访问的。
js
const person = {
name: "Alice",
age: 25,
};
console.log(person.__proto__); // Output: {}
什么是原型链?
原型链是由一系列对象组成的链式结构,每个对象都有一个指向其原型的引用。当访问对象的属性或方法时,如果对象自身没有定义,则会沿着原型链向上查找,直到找到该属性或方法或到达原型链的末端。
js
const person = {
name: "Alice",
age: 25,
};
const student = {
grade: "A",
};
student.__proto__ = person;
console.log(student.name); // Output: Alice
console.log(student.grade); // Output: A
console.log(student.toString()); // Output: [object Object]
他们的关系是什么?
原型和原型链的关系是通过原型链将对象连接起来形成继承关系。每个对象的原型都是它的父级对象,可以访问父级对象的属性和方法。通过原型链,可以实现属性和方法的继承、代码的复用,以及对对象进行扩展和修改。
如图:

上图中,我们通过构造函数 prototype
指向了他的原型对象,通过 new
关键字创造出实例对象,实例对象通过 __proto__
指向原型对象。原型对象可以通过 constructor
访问构造函数。
对象的继承
学习大纲

什么是继承
继承是面向对象编程中的一种机制,它允许一个对象获取另一个对象的属性和方法。通过继承,子类可以继承父类的特性,包括属性和方法,从而实现代码的复用和扩展。
作用是什么
继承的作用是实现代码的重用和扩展。它使得我们可以通过定义一个通用的父类,然后在子类中继承这个父类,从而减少重复的代码编写。继承还能够建立对象之间的层次关系,通过子类可以访问和重写父类的属性和方法,从而实现功能的定制和扩展。
几种继承方式
-
原型链继承
通过将子类的原型指向父类的实例来实现继承。子类可以访问父类原型上的属性和方法,但不能访问父类实例上的属性和方法
示例
jsfunction Parent() { this.name = "Parent"; } Parent.prototype.sayHello = function() { console.log("Hello, I am " + this.name); }; function Child() {} Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; var child = new Child(); child.sayHello(); // Output: Hello, I am Parent
-
构造函数继承
通过在子类构造函数中调用父类构造函数来实现继承。子类可以访问父类构造函数内部定义的属性和方法,但不能访问父类原型上的属性和方法
示例
jsfunction Parent() { this.name = "Parent"; } Parent.prototype.sayHello = function() { console.log("Hello, I am " + this.name); }; function Child() { Parent.call(this); } var child = new Child(); child.sayHello(); // Output: Hello, I am Parent
-
组合继承
结合了原型链继承和构造函数继承的方式。通过在子类构造函数中调用父类构造函数来继承属性,然后将子类的原型指向父类的实例来继承方法
示例
jsfunction Parent() { this.name = "Parent"; } Parent.prototype.sayHello = function() { console.log("Hello, I am " + this.name); }; function Child() { Parent.call(this); } Child.prototype = new Parent(); Child.prototype.constructor = Child; var child = new Child(); child.sayHello(); // Output: Hello, I am Parent
-
寄生继承
在组合继承的基础上进行改进,通过创建一个中间函数来继承父类的属性和方法,然后返回该函数作为子类的构造函数
示例
jsfunction Parent() { this.name = "Parent"; } Parent.prototype.sayHello = function() { console.log("Hello, I am " + this.name); }; function createChild() { var child = Object.create(Parent.prototype); child.name = "Child"; return child; } var child = createChild(); child.sayHello(); // Output: Hello, I am Child
-
组合+寄生继承
组合+寄生继承是一种综合利用构造函数继承和原型链继承的方式,以实现更灵活和高效的继承机制,既能够继承父类的实例属性,又能够共享父类的原型属性和方法,避免了原型链继承中多个实例共享同一个原型对象的问题,也避免了构造函数继承中重复创建实例属性的问题
示例
jsfunction Parent(name) { this.name = name; } Parent.prototype.sayHello = function() { console.log("Hello, I am " + this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } // 使用Object.create创建一个中间对象,继承父类原型 function inheritPrototype(child, parent) { var prototype = Object.create(parent.prototype); prototype.constructor = child; child.prototype = prototype; } inheritPrototype(Child, Parent); // 添加子类特有的方法 Child.prototype.sayAge = function() { console.log("I am " + this.age + " years old"); }; var child = new Child("Child", 10); child.sayHello(); // Output: Hello, I am Child child.sayAge(); // Output: I am 10 years old
深拷贝和浅拷贝
学习大纲

浅拷贝

浅拷贝是创建一个新的对象或数组,并将原始对象或数组的引用复制给新的对象或数组。这意味着新的对象或数组与原始对象或数组共享相同的内存地址,其中的引用类型数据仍指向同一块内存。因此,修改新对象或数组中的引用类型数据会影响原始对象或数组。
示例
js
// 浅拷贝示例
const obj = { name: 'John', age: 30 };
const shallowCopy = Object.assign({}, obj);
shallowCopy.age = 40;
console.log(obj); // { name: 'John', age: 30 }
console.log(shallowCopy); // { name: 'John', age: 40 }
深拷贝

深拷贝则是创建一个全新的对象或数组,并将原始对象或数组的所有属性或元素逐个复制到新的对象或数组中。这意味着新的对象或数组与原始对象或数组完全独立,互不影响。即使原始对象或数组中存在引用类型数据,深拷贝也会为其创建新的内存空间。
示例
js
// 深拷贝示例
const obj = { name: 'John', age: 30, hobbies: ['reading', 'coding'] };
// 使用JSON.parse和JSON.stringify进行深拷贝
const deepCopy = JSON.parse(JSON.stringify(obj));
deepCopy.hobbies.push('swimming');
console.log(obj); // { name: 'John', age: 30, hobbies: ['reading', 'coding'] }
console.log(deepCopy); // { name: 'John', age: 30, hobbies: ['reading', 'coding', 'swimming'] }
使用JSON.parse
和JSON.stringify
进行深拷贝有一些缺点:
-
丢失函数和特殊对象的信息:
JSON.stringify
在序列化过程中会忽略函数和特殊对象(如Date对象)的信息,因为它们不是有效的JSON数据类型。在使用JSON.parse
进行反序列化时,这些函数和特殊对象会丢失其原有的功能和属性。 -
无法处理循环引用:如果待拷贝的对象存在循环引用,即对象中某个属性引用了自身或形成了循环链结构,使用
JSON.stringify
和JSON.parse
进行深拷贝会导致堆栈溢出或死循环。 -
无法处理特殊属性:
JSON.stringify
无法序列化对象的非枚举属性、Symbol属性和使用get
访问器定义的属性。在反序列化后,这些属性将不再存在于拷贝对象中。 -
对象方法丢失:使用
JSON.stringify
和JSON.parse
进行深拷贝后,拷贝对象中的方法将丢失,只保留了数据属性。
综上所述,虽然JSON.parse
和JSON.stringify
是常见的实现深拷贝的方法,但在某些特定场景下可能会存在缺点。如果需要处理函数、特殊对象、循环引用等情况,或者需要完全保留对象的所有属性和方法,可能需要使用其他深拷贝的方法或自行实现深拷贝函数。
手写深拷贝基础版
js
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj; // 基本数据类型和null直接返回
}
let clone;
if (Array.isArray(obj)) {
clone = [];
for (let i = 0; i < obj.length; i++) {
clone[i] = deepClone(obj[i]);
}
} else {
clone = {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
clone[key] = deepClone(obj[key]);
}
}
}
return clone;
}
// 示例使用
const obj = { name: 'John', age: 30, hobbies: ['reading', 'coding'] };
const deepCopy = deepClone(obj);
deepCopy.hobbies.push('swimming');
console.log(obj); // { name: 'John', age: 30, hobbies: ['reading', 'coding'] }
console.log(deepCopy); // { name: 'John', age: 30, hobbies: ['reading', 'coding', 'swimming'] }