深入讲解JavaScript传统构造函数的编写规范:如何用function定义私有属性,为何及如何通过prototype挂载共享方法,new关键字底层执行原理与经典防错实践。
JavaScript传统构造函数怎么写:function定义与prototype挂载
时间:2026-10-30
浏览:614
文章目录
假设你正在维护一个不支持 ES6 类语法的遗留项目,或者在排查底层库的内存泄漏问题:代码中创建了上万个业务对象,如果每个方法都直接在函数体内通过 this.method = function... 赋值,浏览器内存将迅速吃紧。在 JavaScript 发展史中,传统面向对象开发依靠的是一套优雅且经过时间检验的标准范式——用常规 function 定义实例私有属性,用 prototype 挂载共享方法。本文将系统拆解这套传统构造函数的标准写法、内存分配机制与底层运行流程。
一、为什么采用“function属性 + prototype方法”的标准范式
在 ES6 的 class 关键字引入之前,JavaScript 本身没有类的概念。作为一门基于原型的动态语言,构造函数本质上只是一个普通的函数,区别仅在于调用它时是否使用了 new 运算符。
编写构造函数时,开发者面临的核心设计决策是:哪些数据应当由每个实例独占,哪些行为应当由所有实例共用?
- 实例属性(状态):如用户的姓名、年龄、唯一标识符。每个对象互不相同,必须独立保存在各自的内存空间中,因此在
function内部使用this.key = value定义。 - 实例方法(行为):如计算折扣、打印日志、发起校验。方法的执行逻辑完全一致,如果在函数体内声明,每
new一次就会在内存中重新开辟一段空间创建新的闭包函数对象;将方法挂载到构造函数的prototype上,所有实例通过隐式原型链查找,既避免了重复分配内存,又具备极高的执行效率。
二、核心语法:第一步,定义构造函数(function)
编写构造函数时,约定遵循以下编码规则:
- 使用大驼峰命名(PascalCase):构造函数首字母大写(如
User、ShoppingCart),以便与常规普通函数(camelCase)从视觉上形成清晰区分,提醒调用者必须使用new操作符。 - 只负责初始化状态:接收外部传入参数,通过
this.属性名 = 值将状态赋值给当前正在创建的实例。 - 默认不显式写 return 语句:构造函数在配合
new调用时,默认会隐式返回当前新创建的对象。如果显式返回基本类型(如数字、字符串、布尔值),会被new忽略;如果显式返回一个引用类型对象,该返回值将替换掉正在构造的实例。通常保持自然结束即可。
// 构造函数定义:仅初始化私有属性
function User(name, role) {
// 属性属于当前实例自身
this.name = name;
this.role = role || "member";
this.createdAt = new Date();
}
三、核心语法:第二步,挂载共享方法到 prototype
每个函数在被声明创建时,JavaScript 引擎都会为其自动创建一个关联的普通对象,存储在函数的 prototype 属性中。这个对象被称为显式原型对象。
向原型上挂载方法有两种常见的书写方式,其优劣与注意事项截然不同:
1. 逐个挂载属性或方法(官方推荐)
// 逐个在 prototype 上添加方法
User.prototype.greet = function() {
return "你好,我是 " + this.name + ",角色为 " + this.role;
};
User.prototype.isAdmin = function() {
return this.role === "admin";
};
优点:安全可靠,不会破坏系统预置的原型属性,保留了 User.prototype.constructor === User 的原生完整指针关系。
2. 对象字面量直接覆盖(需手动修正 constructor)
有时开发者为了简洁,会选择用对象字面量一次性赋多个方法:
// 错误示范:完全覆盖了默认 prototype 对象
User.prototype = {
greet: function() { /* ... */ },
isAdmin: function() { /* ... */ }
};
// 此时 User.prototype.constructor 变成了 Object,而非 User!
注意陷阱:当用字面量 {} 整体覆盖 prototype 时,新建的对象其原型是 Object.prototype,其原有的 constructor 属性会丢失并指向 Object。若采用此写法,必须手动补充 constructor: User 修正回溯链条。
// 正确的对象字面量写法:显式指回构造函数
User.prototype = {
constructor: User, // 手动补回 constructor 指针
greet: function() {
return "你好,我是 " + this.name;
},
isAdmin: function() {
return this.role === "admin";
}
};
四、底层探秘:new 运算符在背后做了哪四件事
很多初学者容易误以为 this 是在构造函数被声明时就已经确定。事实上,this 的绑定完全取决于调用方式。执行 const user = new User("张三", "admin") 时,引擎在后台依次执行了以下四个操作步骤:
- 创建全新空对象:在内存堆中分配一个新对象
const obj = {}。 - 原型链接建立:将新对象的原型
__proto__指向构造函数的prototype对象(即Object.setPrototypeOf(obj, User.prototype))。 - 执行构造函数并绑定 this:调用构造函数,并将内部上下文
this绑定为这个新对象(即User.apply(obj, arguments))。在此阶段,所有this.xxx = yyy的属性赋值都会写入这个新对象中。 - 判断并返回结果:检查构造函数执行后的返回值。若返回值是对象(引用类型),则返回该对象;否则(无返回值或返回基本类型),默认返回步骤 1 中创建的新对象。
五、防御性编程:防止遗漏 new 操作符
由于传统构造函数本质上就是普通的 function,如果在调用时忘记书写 new,例如直接调用 const user = User("李四"),会产生严重隐患:
- 非严格模式下,
this默认指向全局对象(浏览器中的window,Node.js 中的global),导致变量被污染到全局环境。 - 严格模式(
"use strict")下,this为undefined,直接执行this.name = name会抛出TypeError: Cannot set property 'name' of undefined。 - 函数没有返回值,变量
user的值将变成undefined。
为了让构造函数既能支持 new User(),也能在直接调用 User() 时自动纠正,行业通用的防御性模板如下:
function User(name, role) {
// 方案 A:通用判断当前 this 是否属于 User 的实例
if (!(this instanceof User)) {
return new User(name, role);
}
// 属性初始化
this.name = name;
this.role = role || "member";
}
若在现代环境(ES6 及以上)中使用该写法,还可以利用 new.target 元属性进行更加严谨的原型判定:
function User(name, role) {
// 方案 B:使用 new.target 检查是否通过 new 触发
if (!new.target) {
return new User(name, role);
}
this.name = name;
this.role = role || "member";
}
六、经典案例:基于构造函数与原型的组合继承模式
在理解了基础的 function 定义与 prototype 挂载之后,如何实现面向对象的“继承”?传统的经典方案是组合继承(Combination Inheritance):通过在子类中调用父类构造函数借用属性,并通过 Object.create 继承父类原型上的方法。
// 1. 定义父类构造函数
function Animal(name) {
this.name = name;
}
Animal.prototype.eat = function(food) {
return this.name + " 正在吃 " + food;
};
// 2. 定义子类构造函数(借用构造函数继承属性)
function Dog(name, breed) {
Animal.call(this, name); // 绑定 this,执行父类构造函数
this.breed = breed;
}
// 3. 原型链对接(寄生组合式继承关键步)
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog; // 修正 constructor 指向
// 4. 挂载子类独有方法
Dog.prototype.bark = function() {
return this.name + " 汪汪叫!";
};
// 测试验证
var myDog = new Dog("旺财", "金毛");
console.log(myDog.eat("骨头")); // 示例输出: 旺财 正在吃 骨头
console.log(myDog.bark()); // 示例输出: 旺财 汪汪叫!
console.log(myDog instanceof Dog); // true
console.log(myDog instanceof Animal); // true
七、高频易错点清单
- 切勿使用箭头函数作为构造函数:箭头函数内部没有自己的
this,也没有prototype属性,直接使用new调用箭头函数会触发TypeError: ... is not a constructor报错。 - 原型方法内切忌使用箭头函数:如果原型上的方法声明为
User.prototype.greet = () => { return this.name; },其内部的this会被绑定为外层词法作用域(通常是window或模块作用域),导致无法访问实例对象属性。 - 区分自有属性与原型属性:使用
instance.hasOwnProperty("key")可以判断属性是存在于实例自身,还是继承自原型对象;in操作符只要在当前实例或原型链上存在该属性就会返回true。
八、总结
尽管 ES6 的 class、extends 语法糖已成为当下编写新代码的主流,但其底层运行基石依然是这套基于构造函数与原型的对象系统。牢牢掌握“function 内初始化实例私有属性、prototype 挂载共享公有方法”的核心范式,不仅能帮助你顺畅阅读和重构经典底层库(如 jQuery、Vue 2、Express 等),更能让你在理解 JavaScript 作用域、闭包与原型链时游刃有余。
实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。