深入解析JavaScript原型模式:原理、特性与局限
本文深入解析JavaScript中的原型模式,阐述其如何解决构造函数方法复用问题。详细讲解原型对象、[[Prototype]]指针、属性搜索机制及in操作符的使用。探讨重写原型对现有实例的影响、原生对象原型的修改以及引用类型属性的共享风险,帮助开发者理解并正确使用原型模式。
原型模式概述
一、什么是原型模式
在JavaScript中,创建对象通常采用工厂模式或构造函数模式。然而,构造函数模式存在一个显著缺陷:构造函数内部定义的方法会在每个实例对象中重新创建,导致无法实现方法复用。为了解决这一问题,原型模式应运而生,它通过将实例共享的方法和属性集中存放在一个名为prototype(原型)的属性中,从而优化内存使用。
// 构造函数
function Person() {};
// 原型属性prototype
Person.prototype.name = '张三';
Person.prototype.sayName = function() {
console.log(this.name);
};
let person1 = new Person();
person1.sayName(); //张三
let person2 = new Person();
person2.sayName(); // 张三
console.log(person1.sayName == person2.sayName); //true
理解原型对象与属性搜索
1.理解原型对象
每当创建一个新函数时,JavaScript引擎会根据特定规则为该函数创建一个prototype属性,该属性指向函数的原型对象。在默认情况下,所有原型对象都会自动获得一个constructor(构造函数)属性,这是一个指针,指向定义该原型对象的函数。在自定义构造函数创建后,其原型对象默认仅拥有constructor属性,其他方法均从Object继承而来。
当通过构造函数创建一个新的实例对象后,该实例内部会包含一个内部指针[[Prototype]],指向构造函数的原型对象。这一连接存在于实例与构造函数的原型对象之间,而非实例与构造函数本身之间。
在代码读取对象属性时,JavaScript会执行一次属性搜索,目标是找到具有给定名称的属性。搜索过程首先从对象实例本身开始;若在实例中找到该属性,则返回其值;若未找到,则继续沿着指针指向的原型对象进行搜索。如果在原型对象中找到了同名属性,则返回该属性的值。
虽然可以通过实例访问保存在原型中的值,但不能通过实例直接重写原型中的值。如果在实例中添加一个与原型中同名的属性,该实例属性会自动屏蔽原型中的同名属性,但这并不会修改原型中的属性,只是阻止了对原型属性的访问。使用delete操作符可以完全删除实例属性,从而恢复对原型属性的访问。
原型属性检测与操作
2.原型与in操作符
hasOwnProperty()方法可用于检测一个属性是否存在于实例对象中。
// 构造函数
function Person() {
this.age = 16;
};
Person.prototype.name = "张三";
let person1 = new Person();
console.log(person1.hasOwnProperty('name')); // false
console.log(person1.hasOwnProperty('age')); // true
in操作符的使用分为两种情况:单独使用和在for-in循环中使用。在单独使用时,只要通过对象能够访问给定属性(无论该属性存在于实例中还是原型中),in操作符就会返回true。
// 构造函数
function Person() {}
Person.prototype.name = 'zhang';
let person1 = new Person();
console.log('name' in person1); // true
person1.age = 14;
console.log('age' in person1); // true
结合使用hasOwnProperty()方法和in操作符,可以确定属性是存在于实例中还是原型中。
// 构造函数
function Person() {}
function hasPrototypeProperty(object, name) {
return !object.hasOwnProperty(name) && (name in object);
}
Person.prototype.name = "张三";
let person = new Person();
console.log(hasPrototypeProperty(person, 'name')); // true
console.log(hasPrototypeProperty(person, 'age')); // false
在使用for-in循环时,返回的是所有能够通过对象访问的可枚举属性,其中既包含存在于实例中的属性,也包含存在于原型中的属性。
let o = {
name: 'san',
age: 14,
};
for(let key in o) {
console.log(key);
}
若要获取对象上所有可枚举的实例属性,可以使用Object.keys()方法。该方法接收一个对象作为参数,返回一个包含所有可枚举属性名的字符串数组。
如果希望获取对象上所有实例属性,无论其是否可枚举,可以使用Object.getOwnPropertyNames()方法。
原型语法优化与动态性
3.更简单的原型语法
为了减少冗余输入并在视觉上更好地封装原型功能,常见的做法是使用一个包含所有属性和方法的对象字面量来重写整个原型对象。
// 构造函数
function Person() {};
Person.prototype = {
sayHi: function() {
console.log(hi);
},
name: '张三',
};
通过这种方式,原型对象中的constructor属性将不再指向Person构造函数。如果constructor的值非常重要,可以像下面这样特意将其设置回适当的值。
// 构造函数
function Person() {};
Person.prototype = {
constructor: Person,
sayHi: function() {
console.log(hi);
},
name: '张三',
};
然而,这种方式会导致对象的[[Enumerable]]特性被设置为true。默认情况下,constructor属性是不可枚举的,可以通过Object.defineProperty()方法解决此问题。
// 构造函数
function Person() {};
Person.prototype = {
sayHi: function() {
console.log(hi);
},
name: '张三',
};
Object.defineProperty(Person.prototype, "constructor", {
enumerable: false,
value: Person
}
4.原型的动态性
对原型对象所做的任何修改都能立即反映在实例上。
function Person() {};
var friend = new Person();
Person.prototype.sayHi = function() {
console.log('hi');
};
friend.sayHi(); // hi
但是,如果是重写整个原型对象,情况则有所不同。调用构造函数时,会为实例添加一个指向最初原型的[[prototype]]指针,而将原型修改为另一个对象相当于切断了构造函数与最初原型之间的联系。此时,实例中的指针仅指向原型,而不是指向构造函数。
// 构造函数
function Person() {};
var friend = new Person();
Person.prototype = {
constructor: Person,
sayHi: function() {
console.log(hi);
}
};
friend.sayHi(); // Uncaught TypeError: friend.sayHi is not a function
例如,创建一个Person实例后,又重写了其原型对象。此时调用sayHi()方法会发生错误,因为实例所指向的原型对象已变为一个新对象。重写原型对象切断了现有原型与之前已存在的对象实例之间的联系,因此报错。
原生对象原型与模式局限
5.原生对象的原型
原型模式的重要性不仅体现在创建自定义类型方面,就连所有原生的引用类型都采用这种模式。所有原生引用类型(如Object、Array、String等)都在其构造函数的原型上定义了方法。可以像修改自定义对象的原型一样修改原生对象的原型。
二、原型模式的缺点
对于包含引用类型值的属性,所有实例在默认情况下都会取得相同的属性值。
// 构造函数
function Person() {};
// 原型属性prototype
Person.prototype = {
constructor: Person,
friends: ['张三', '李四'],
}
let person1 = new Person();
let person2 = new Person();
person1.friends.push('王五');
console.log(person1.friends); // ["张三", "李四", "王五"]
console.log(person2.friends); // ["张三", "李四", "王五"]
由于friends存在于Person的原型对象中,person1对friends的修改也会通过person2反映出来。然而,实例对象通常希望拥有属于自己的全部属性。正因为这一共享引用类型的风险,很少有人单独使用原型模式来创建对象。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持本站。
JS处理含Emoji或中文的字符串转数组乱码问题
2026-11-05 16:12:28
JS怎么使用展开运算符合并对象
2026-11-05 14:42:18
ES6+环境下判断JS对象为空的简洁写法
2026-11-05 12:12:23
怎么通过replacer函数自定义JS中JSON的格式化输出
2026-11-05 11:19:44
JS正则表达式基础:字面量与RegExp构造函数的区别及常用元字符
2026-11-05 11:16:58
