我将为您完善构造函数与实例的文档,详细介绍JavaScript中构造函数的工作原理和实例创建过程。
---
title: 构造函数与实例
icon: javascript
order: 2
---
# 构造函数与实例
构造函数是创建和初始化对象的特殊方法。本文将介绍类的构造函数、实例创建过程以及new操作符的工作原理,帮助您理解JavaScript对象实例化的机制。
## 构造函数基础
### 什么是构造函数
构造函数是用于创建和初始化对象的特殊函数。在JavaScript中,构造函数通常以大写字母开头,以区别于普通函数:
```javascript
// 构造函数
function Person(name, age) {
this.name = name;
this.age = age;
}
// 创建实例
const person1 = new Person('张三', 30);
const person2 = new Person('李四', 25);
console.log(person1); // Person {name: '张三', age: 30}
console.log(person2); // Person {name: '李四', age: 25}
构造函数与普通函数的区别
从语法上看,构造函数与普通函数没有区别,区别在于调用方式:
function Person(name) {
this.name = name;
}
// 作为构造函数调用(使用new)
const person = new Person('张三');
console.log(person.name); // 输出:张三
// 作为普通函数调用(不使用new)
Person('李四'); // this指向全局对象(非严格模式下)
console.log(window.name); // 输出:李四(浏览器环境)
在严格模式下,如果不使用new
调用构造函数,this
将是undefined
,导致错误:
'use strict';
function Person(name) {
this.name = name; // TypeError: Cannot set property 'name' of undefined
}
Person('张三'); // 错误
new操作符的工作原理
当使用new
操作符调用构造函数时,会执行以下步骤:
- 创建一个新的空对象
- 将这个对象的原型设置为构造函数的
prototype
属性 - 将构造函数内部的
this
绑定到新创建的对象 - 执行构造函数的代码
- 如果构造函数返回一个对象,则返回该对象;否则,返回新创建的对象
模拟实现new操作符
我们可以通过自定义函数来模拟new
操作符的行为:
function myNew(Constructor, ...args) {
// 1. 创建一个新对象,并链接到构造函数的原型
const obj = Object.create(Constructor.prototype);
// 2. 执行构造函数,并将this绑定到新对象
const result = Constructor.apply(obj, args);
// 3. 如果构造函数返回一个对象,则返回该对象;否则返回新创建的对象
return (typeof result === 'object' && result !== null) ? result : obj;
}
// 使用自定义的new
function Person(name, age) {
this.name = name;
this.age = age;
}
const person = myNew(Person, '张三', 30);
console.log(person); // Person {name: '张三', age: 30}
构造函数的返回值
构造函数通常不需要显式返回值,但如果返回了一个对象,则new
表达式的结果将是该对象,而不是新创建的实例:
function Person(name) {
this.name = name;
// 返回一个对象
return {
customName: name,
sayHello() {
console.log(`你好,${this.customName}`);
}
};
}
const person = new Person('张三');
console.log(person.name); // 输出:undefined(被返回的对象覆盖了)
console.log(person.customName); // 输出:张三
person.sayHello(); // 输出:你好,张三
如果返回的是原始值(如数字、字符串、布尔值等),则会被忽略,仍然返回新创建的实例:
function Person(name) {
this.name = name;
return 123; // 返回原始值,会被忽略
}
const person = new Person('张三');
console.log(person.name); // 输出:张三
ES6类的构造函数
在ES6中,类的constructor
方法就是构造函数:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
sayHello() {
console.log(`你好,我是${this.name}`);
}
}
const person = new Person('张三', 30);
console.log(person); // Person {name: '张三', age: 30}
person.sayHello(); // 输出:你好,我是张三
默认构造函数
如果没有显式定义构造函数,JavaScript会自动添加一个空的构造函数:
class Person {
// 自动添加:constructor() {}
}
// 等同于
class Person {
constructor() {}
}
子类构造函数
在子类的构造函数中,必须先调用super()
,然后才能使用this
:
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name); // 调用父类构造函数
this.breed = breed; // 添加子类特有的属性
}
}
const dog = new Dog('小黑', '拉布拉多');
console.log(dog.name); // 输出:小黑
console.log(dog.breed); // 输出:拉布拉多
如果子类没有定义构造函数,会自动生成一个构造函数,并在其中调用父类的构造函数:
class Animal {
constructor(name) {
this.name = name;
}
}
class Dog extends Animal {
// 自动添加:constructor(...args) { super(...args); }
}
const dog = new Dog('小黑');
console.log(dog.name); // 输出:小黑
实例创建过程
实例的内部结构
当使用构造函数创建实例时,实例会有以下特点:
- 拥有构造函数中添加的属性和方法
- 通过原型链访问构造函数原型上的属性和方法
- 通过
constructor
属性引用回构造函数
function Person(name) {
this.name = name; // 实例属性
}
Person.prototype.sayHello = function() { // 原型方法
console.log(`你好,我是${this.name}`);
};
const person = new Person('张三');
console.log(person.name); // 输出:张三(实例属性)
person.sayHello(); // 输出:你好,我是张三(原型方法)
console.log(person.constructor === Person); // 输出:true
instanceof操作符
instanceof
操作符用于检查一个对象是否是某个构造函数的实例:
function Person(name) {
this.name = name;
}
const person = new Person('张三');
console.log(person instanceof Person); // 输出:true
console.log(person instanceof Object); // 输出:true(所有对象都是Object的实例)
const obj = {};
console.log(obj instanceof Person); // 输出:false
instanceof
的工作原理是检查对象的原型链中是否包含构造函数的prototype
属性:
function myInstanceof(obj, Constructor) {
// 获取对象的原型
let proto = Object.getPrototypeOf(obj);
// 获取构造函数的原型
const prototype = Constructor.prototype;
// 沿着原型链查找
while (proto !== null) {
if (proto === prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
const person = new Person('张三');
console.log(myInstanceof(person, Person)); // 输出:true
console.log(myInstanceof(person, Object)); // 输出:true
console.log(myInstanceof({}, Person)); // 输出:false
构造函数的常见模式
工厂模式
工厂模式是一种创建对象的设计模式,它不使用new
操作符:
function createPerson(name, age) {
return {
name,
age,
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
}
const person1 = createPerson('张三', 30);
const person2 = createPerson('李四', 25);
person1.sayHello(); // 输出:你好,我是张三
工厂模式的缺点是无法识别对象的类型(所有对象都是Object的实例)。
构造函数模式
构造函数模式使用new
操作符创建实例:
function Person(name, age) {
this.name = name;
this.age = age;
this.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
}
const person1 = new Person('张三', 30);
const person2 = new Person('李四', 25);
person1.sayHello(); // 输出:你好,我是张三
console.log(person1 instanceof Person); // 输出:true
构造函数模式的缺点是每个实例都会创建方法的副本,造成内存浪费。
原型模式
原型模式将方法添加到构造函数的原型上,实现方法共享:
function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
const person1 = new Person('张三', 30);
const person2 = new Person('李四', 25);
person1.sayHello(); // 输出:你好,我是张三
console.log(person1.sayHello === person2.sayHello); // 输出:true(方法共享)
组合模式(构造函数+原型)
组合模式结合了构造函数模式和原型模式的优点:
function Person(name, age) {
// 实例属性(每个实例独有)
this.name = name;
this.age = age;
this.friends = []; // 引用类型属性
}
// 共享方法(所有实例共享)
Person.prototype.sayHello = function() {
console.log(`你好,我是${this.name}`);
};
Person.prototype.addFriend = function(friend) {
this.friends.push(friend);
};
const person1 = new Person('张三', 30);
const person2 = new Person('李四', 25);
// 方法共享
console.log(person1.sayHello === person2.sayHello); // 输出:true
// 属性独立
person1.addFriend('王五');
console.log(person1.friends); // 输出:['王五']
console.log(person2.friends); // 输出:[](不受影响)
实例属性与原型属性
实例属性
实例属性是在构造函数中使用this
添加的属性,每个实例都有自己的副本:
function Person(name) {
this.name = name; // 实例属性
}
const person1 = new Person('张三');
const person2 = new Person('李四');
console.log(person1.name); // 输出:张三
console.log(person2.name); // 输出:李四
person1.name = '张三丰';
console.log(person1.name); // 输出:张三丰
console.log(person2.name); // 输出:李四(不受影响)
原型属性
原型属性是添加到构造函数原型上的属性,所有实例共享同一个副本:
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类'; // 原型属性
const person1 = new Person('张三');
const person2 = new Person('李四');
console.log(person1.species); // 输出:人类
console.log(person2.species); // 输出:人类
// 修改原型属性会影响所有实例
Person.prototype.species = '智人';
console.log(person1.species); // 输出:智人
console.log(person2.species); // 输出:智人
属性查找机制
当访问对象的属性时,JavaScript会先查找对象自身是否有该属性,如果没有,则沿着原型链向上查找:
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';
const person = new Person('张三');
// 1. 查找实例属性
console.log(person.name); // 输出:张三(实例属性)
// 2. 查找原型属性
console.log(person.species); // 输出:人类(原型属性)
// 3. 实例属性覆盖原型属性
person.species = '现代人';
console.log(person.species); // 输出:现代人(实例属性)
// 4. 删除实例属性后,会显示原型属性
delete person.species;
console.log(person.species); // 输出:人类(原型属性)
hasOwnProperty方法
hasOwnProperty
方法用于检查属性是否是对象自身的属性(而不是继承的):
function Person(name) {
this.name = name;
}
Person.prototype.species = '人类';
const person = new Person('张三');
console.log(person.hasOwnProperty('name')); // 输出:true(实例自身的属性)
console.log(person.hasOwnProperty('species')); // 输出:false(原型上的属性)
// 添加与原型同名的属性
person.species = '现代人';
console.log(person.hasOwnProperty('species')); // 输出:true(现在是实例自身的属性)
构造函数的静态属性和方法
静态属性
静态属性是直接添加到构造函数上的属性,而不是原型上:
function Person(name) {
this.name = name;
}
// 添加静态属性
Person.species = '人类';
Person.count = 0;
// 在构造函数中更新静态属性
function Person(name) {
this.name = name;
Person.count++; // 更新静态计数器
}
const person1 = new Person('张三');
const person2 = new Person('李四');
console.log(Person.count); // 输出:2
console.log(person1.count); // 输出:undefined(实例无法直接访问静态属性)
静态方法
静态方法是添加到构造函数上的方法,通过构造函数本身调用,而不是通过实例:
function Person(name, age) {
this.name = name;
this.age = age;
}
// 添加静态方法
Person.createAnonymous = function() {
return new Person('匿名用户', 0);
};
Person.isAdult = function(person) {
return person.age >= 18;
};
// 使用静态方法
const anonymous = Person.createAnonymous();
console.log(anonymous.name); // 输出:匿名用户
const person = new Person('张三', 30);
console.log(Person.isAdult(person)); // 输出:true
在ES6类中,可以使用static
关键字定义静态方法:
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
// 实例方法
sayHello() {
console.log(`你好,我是${this.name}`);
}
// 静态方法
static createAnonymous() {
return new Person('匿名用户', 0);
}
static isAdult(person) {
return person.age >= 18;
}
}
const anonymous = Person.createAnonymous();
console.log(anonymous.name); // 输出:匿名用户
const person = new Person('张三', 30);
console.log(Person.isAdult(person)); // 输出:true
实例检测与类型判断
constructor属性
每个实例都有一个constructor
属性,指向创建该实例的构造函数:
function Person(name) {
this.name = name;
}
const person = new Person('张三');
console.log(person.constructor === Person); // 输出:true
// 使用constructor创建新实例
const anotherPerson = new person.constructor('李四');
console.log(anotherPerson.name); // 输出:李四
需要注意的是,constructor
属性是可以被修改的,因此不是绝对可靠的类型检查方法:
function Person(name) {
this.name = name;
}
function Student(name) {
this.name = name;
}
// 重写原型会导致constructor改变
Person.prototype = {
sayHello() {
console.log(`你好,我是${this.name}`);
}
// 注意:这里没有设置constructor属性
};
const person = new Person('张三');
console.log(person.constructor === Person); // 输出:false
console.log(person.constructor === Object); // 输出:true
正确的做法是在重写原型时显式设置constructor
属性:
Person.prototype = {
constructor: Person, // 显式设置constructor
sayHello() {
console.log(`你好,我是${this.name}`);
}
};
// 或者使用Object.defineProperty避免constructor可枚举
Object.defineProperty(Person.prototype, 'constructor', {
value: Person,
enumerable: false
});
Object.prototype.toString方法
Object.prototype.toString
方法可以用来获取对象的内部[[Class]]
属性,是一种更可靠的类型检查方法:
function getType(obj) {
return Object.prototype.toString.call(obj).slice(8, -1);
}
console.log(getType({})); // 输出:Object
console.log(getType([])); // 输出:Array
console.log(getType(new Date())); // 输出:Date
console.log(getType(new RegExp())); // 输出:RegExp
console.log(getType(null)); // 输出:Null
console.log(getType(undefined)); // 输出:Undefined
console.log(getType(42)); // 输出:Number
console.log(getType('hello')); // 输出:String
// 自定义构造函数
function Person() {}
console.log(getType(new Person())); // 输出:Object(注意:自定义构造函数仍然显示为Object)
实例的内存管理
实例的生命周期
JavaScript是一种垃圾回收语言,当对象不再被引用时,会被自动回收:
function createPerson(name) {
return new Person(name);
}
function Person(name) {
this.name = name;
}
let person = createPerson('张三'); // person引用了新创建的Person实例
console.log(person.name); // 输出:张三
person = null; // 移除引用,Person实例可以被垃圾回收
避免内存泄漏
在使用构造函数和实例时,需要注意避免内存泄漏:
function MemoryLeak() {
this.largeData = new Array(10000000).fill('x'); // 占用大量内存
// 错误:在DOM元素上存储对实例的引用
document.getElementById('someElement').leakRef = this;
// 错误:创建循环引用
this.self = this;
}
// 正确:使用完后移除引用
function cleanup(element) {
delete element.leakRef;
}
实际应用示例
创建可重用的UI组件
function Tabs(container) {
this.container = container;
this.tabs = container.querySelectorAll('.tab');
this.panels = container.querySelectorAll('.panel');
this.activeIndex = 0;
// 初始化
this.init();
}
Tabs.prototype.init = function() {
// 绑定事件
this.tabs.forEach((tab, index) => {
tab.addEventListener('click', () => {
this.activate(index);
});
});
// 默认激活第一个标签
this.activate(this.activeIndex);
};
Tabs.prototype.activate = function(index) {
// 更新标签状态
this.tabs.forEach(tab => tab.classList.remove('active'));
this.tabs[index].classList.add('active');
// 更新面板状态
this.panels.forEach(panel => panel.classList.remove('active'));
this.panels[index].classList.add('active');
// 更新当前索引
this.activeIndex = index;
};
// 使用
const tabsContainer = document.querySelector('.tabs-container');
const myTabs = new Tabs(tabsContainer);
数据模型
function Product(id, name, price) {
this.id = id;
this.name = name;
this.price = price;
this.discount = 0;
}
Product.prototype.setDiscount = function(percentage) {
this.discount = percentage;
};
Product.prototype.getDiscountedPrice = function() {
return this.price * (1 - this.discount / 100);
};
Product.prototype.toString = function() {
return `${this.name} - ¥${this.getDiscountedPrice().toFixed(2)}`;
};
// 使用
const product = new Product(1, '笔记本电脑', 5999);
product.setDiscount(10);
console.log(product.toString()); // 输出:笔记本电脑 - ¥5399.10
总结
构造函数和实例是JavaScript面向对象编程的基础。通过本文,我们学习了:
- 构造函数基础:构造函数的定义和使用方式
- new操作符:new操作符的工作原理和模拟实现
- ES6类的构造函数:ES6类中构造函数的特点和使用
- 实例创建过程:实例的内部结构和instanceof操作符
- 构造函数的常见模式:工厂模式、构造函数模式、原型模式和组合模式
- 实例属性与原型属性:属性的查找机制和hasOwnProperty方法
- 构造函数的静态属性和方法:如何定义和使用静态成员
- 实例检测与类型判断:如何判断对象的类型
- 实例的内存管理:实例的生命周期和避免内存泄漏
- 实际应用示例:如何在实际项目中使用构造函数和实例
理解构造函数和实例的工作原理,对于掌握JavaScript的面向对象编程至关重要。无论是使用传统的构造函数模式,还是现代的ES6类语法,这些基础知识都是不可或缺的。