JavaScript-原型、原型链详解
一、构造函数
在 JavaScript 中,构造函数是一种特殊的函数,用于创建和初始化对象,它就像一个 “对象模板”。通过 new 关键字调用构造函数时,会创建一个新对象,并将构造函数中的属性和方法 “绑定” 到这个新对象上。
1.构造函数基础示例
// 定义一个构造函数 Star
function Star(uname, age) {// 通过 this 为新创建的对象添加属性this.uname = uname; this.age = age;
}// 使用 new 关键字创建对象实例
const ldh = new Star('刘德华', 18);
const zxy = new Star('张学友', 19); console.log(ldh.uname); // 输出: 刘德华
console.log(zxy.age); // 输出: 19
在这个例子中,Star
是构造函数,每次通过 new Star(...)
创建实例(ldh
、zxy
)时,都会为实例添加 uname
和 age
属性。
2.构造函数的 “方法重复” 问题
如果尝试为构造函数添加方法,会出现内存浪费的问题:
function Star(uname, age) {this.uname = uname;this.age = age;this.sing = function() { // 每个实例都会新建一个 sing 函数console.log('我会唱歌');};
}const ldh = new Star('刘德华', 18);
const zxy = new Star('张学友', 19);console.log(ldh.sing === zxy.sing); // 输出: false(两个不同的函数)
每个实例(ldh
、zxy
)都有独立的 sing
函数,在内存中重复创建相同逻辑的函数,造成浪费。此时,** 原型(prototype
)** 机制就可以解决这个问题 —— 将方法定义在构造函数的原型上,让所有实例共享,避免重复创建。
二、原型(原型对象)的引入
JavaScript 规定,每个构造函数都有一个 prototype
(原型)属性,指向另一个对象(即原型对象)。我们可以把不变的方法挂载到这个原型对象上,让所有实例共享这些方法,避免重复创建。
修改代码如下:
function Star(uname, age) {this.uname = uname;this.age = age;
}// 将 sing 方法定义在 Star 的原型对象上
Star.prototype.sing = function() {console.log('我会唱歌');
};const ldh = new Star('刘德华', 18);
const zxy = new Star('张学友', 19);console.log(ldh.sing === zxy.sing); // true,现在共享同一个函数
此时,所有通过 Star 构造函数创建的实例(ldh、zxy)都会通过原型链共享 Star.prototype.sing 方法,不再重复创建,节约了内存。
1.构造函数this的指向
结论:构造函数里面的this就是实例对象
function Star(uname) {console.log(this)this.uname = uname
}
const ldh = new Star('刘德华')
验证:
2.构造函数里的原型对象的this指向
结论:原型对象里面的函数this指向还是实例对象
<script>let thatfunction Star(uname) {this.uname = uname}Star.prototype.sing = function () {that = thisconsole.log('唱歌')}const ldh = new Star('刘德华')ldh.sing()console.log(that === ldh)</script>
注意:
<script>let that; // 全局变量,初始值为 undefinedfunction Star(uname) {this.uname = uname; // 构造函数中的 this 指向实例,但未赋值给 that}Star.prototype.sing = function () {that = this; // 只有调用 sing 方法时,this 才会指向调用它的实例console.log('唱歌');};const ldh = new Star('刘德华');// ldh.sing(); // 注释掉这行时,sing 方法未被调用,that 保持初始值 undefinedconsole.log(that === ldh); // 输出 false 的原因:
</script>
当 ldh.sing()
被注释时(未调用):
sing
方法从未执行,其中的that = this
代码块没有机会运行- 全局变量
that
保持初始值undefined
(声明时未赋值) - 最终比较
undefined === ldh
为false
(类型和值都不相等)
3.原型对象的constructor 属性
注意:每个原型对象里面都有个construtor属性,该属性指向该原型对象的构造函数
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><script>function Star() {}const ldh = new Star();console.log(Star.prototype)console.log(Star.prototype.constructor === Star);</script>
</body></html>
⑴.constructor使用场景
如果有多个对象的方法,我们可以给原型对象采取对象形式赋值。但是这样就会覆盖构造函数原型对象原来的内容,这样修改后的原型对象 constructor 就不再指向当前构造函数了。
原型对象的定义方式
第一个示例(逐个添加原型方法)
第一个看成方法
此方法不会丢失 constructor
指向
<script>function Star() { }Star.prototype.sing = function () {console.log('唱歌');};Star.prototype.dance = function () {console.log('跳舞');};console.log(Star.prototype);
</script>
- 原型对象引用不变:通过 prototype 属性逐个添加方法时,始终操作的是构造函数默认的原型对象(初始包含 constructor 属性)。
- 保留 constructor 指向:原型对象的 constructor 仍然指向 Star 构造函数,因为未覆盖整个原型对象。
- 打印结果:Star.prototype 会包含 constructor、sing、dance 三个属性。
第二个示例(整体替换原型对象)
第二个看成对象
<script>console.log(Star.prototype); // ①function Star() { }Star.prototype = {sing: function () {console.log('唱歌');},dance: function () {console.log('跳舞');}};console.log(Star.prototype); // ②
</script>
- 原型对象引用改变:直接将 prototype 赋值为一个全新的对象,覆盖了默认的原型对象。
- 丢失 constructor 指向:新对象没有 constructor 属性,此时 Star.prototype.constructor 会指向 Object(默认构造函数),除非手动添加 constructor: Star。
三、对象原型
答:
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><script>function Star() {}const ldh = new Star()// 对象原型__proto__ 指向 改构造函数的原型对象console.log(ldh.__proto__)console.log(ldh.__proto__ === Star.prototype)// 对象原型里面也有constructor 指向 构造函数 Starconsole.log(ldh.__proto__.constructor === Star)</script>
</body></html>
注意区分原型对象和对象原型
四、原型继承
1.引入
看以下代码
function Woman() {this.eyes = 2this.head = 1}const red = new Woman()console.log(red)function Man() {this.eyes = 2this.head = 1}const pink = new Man()console.log(pink)
它们除了名字不同,里面都有相同的属性,可以抽取出来
2.封装-抽取公共部分
把男人和女人公共的部分抽取出来放到Person里面,然后公共的部分放原型对象上
const Person = {eyes: 2,head: 1}function Woman() {}Woman.prototype = Personconst red = new Woman()console.log(red)console.log(Woman.prototype)function Man() {}Man.prototype = Personconst pink = new Man()console.log(pink)
存在一个问题:
解决:
3.Woman.prototype和Man.prototype都指向堆中同一块地址
在 JavaScript 中,对象是按引用传递的。当将同一个对象 Person 直接赋值给 Woman.prototype 和 Man.prototype 时,两者会共享同一个对象的引用,因此它们的原型确实指向堆内存中的同一块地址。以下是对代码的详细分析:
⑴. 原型赋值的本质是引用传递
const Person = { eyes: 2, head: 1 }; // 定义一个对象(堆内存中创建)// 原型赋值(传递引用而非复制对象)
Woman.prototype = Person; // Woman 的原型指向 Person 对象
Man.prototype = Person; // Man 的原型也指向同一个 Person 对象
Person
是一个普通对象,存储在堆内存中。Woman.prototype
和Man.prototype
本质上是两个指针,它们被赋值为Person
对象的内存地址。- 结论:两者的原型指向同一块堆内存地址,可通过
===
验证:
console.log(Woman.prototype === Man.prototype); // 输出 true
⑵.实例与原型的关系
const red = new Woman(); // red 的 __proto__ 指向 Woman.prototype(即 Person)
const pink = new Man(); // pink 的 __proto__ 指向 Man.prototype(即 Person)console.log(red.__proto__ === pink.__proto__); // 输出 true(均指向 Person)
- 两个实例的原型链最终都会追溯到同一个
Person
对象,因此它们会共享Person
中定义的所有属性(eyes
、head
)。
⑶.潜在问题:共享原型对象的副作用
由于 Woman.prototype
和 Man.prototype
指向同一个对象,对其中一个原型的修改会影响另一个:
// 为 Woman 的原型添加新方法(实际修改的是 Person 对象)
Woman.prototype.speak = function() { return "Hello"; };// Man 的原型也会自动拥有该方法
console.log(pink.speak()); // 输出 "Hello"(意外共享)
这种行为可能导致预期外的继承关系污染,尤其是在需要独立原型的场景下(如 Woman
和 Man
应属于不同类别)。
⑷.解决方法
把上面的Person写成构造函数,然后类似Java多态的写法;Woman.prototype = new Person()
子类的原型=new 父类
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title>
</head><body><script>function Person() {this.eyes = 2this.head = 1}function Woman() {}Woman.prototype = new Person()Woman.prototype.constructor = Womanconst red = new Woman()console.log(red)console.log(Woman.prototype)// 为 Woman 的原型添加新方法(实际修改的是 Person 对象)Woman.prototype.speak = function () { return "Hello"; };// Man 的原型也会自动拥有该方法function Man() {}Man.prototype = new Person()const pink = new Man()console.log(pink)console.log(pink.speak()); // 输出 "Hello"(意外共享)</script>
</body></html>
此时,再用Man调用speak方法就报错了