各位观众老爷们,晚上好!我是你们的老朋友,今天咱们聊聊JavaScript里让人又爱又恨的Prototype链。这玩意儿就像个家族族谱,搞明白了就能看清对象间的血缘关系,用起来也就能更加得心应手。准备好,咱们这就开始寻根溯源!
开场白:JavaScript的世界,万物皆对象
在JavaScript里,几乎所有东西都是对象。你说“123”是个数字?它也是Number对象。你说“Hello”是个字符串?它也是String对象。就连函数,那也是Function对象。既然都是对象,那就得有个“爹”,也就是从哪里继承来的。这个“爹”的概念,就是Prototype链的核心。
第一幕:__proto__
,对象的私生子
首先,咱们来认识一位低调的大佬:__proto__
。这玩意儿,读作“dunder proto”,或者叫“双下划线proto”。它像个秘密通道,连接着一个对象和它的原型对象。
-
__proto__
的本质: 任何对象(除了null
)都有__proto__
属性。这个属性指向的是创建该对象的构造函数的原型对象(prototype)。简单来说,就是告诉我们这个对象“遗传”自哪个“家族”。 -
代码实战:
let myObject = {}; // 创建一个空对象 console.log(myObject.__proto__); // 输出: [object Object] (指向Object.prototype) console.log(myObject.__proto__ === Object.prototype); // 输出: true
这段代码告诉我们,即使是一个空对象,也有
__proto__
属性,并且它指向Object.prototype
。这意味着,myObject
继承了Object.prototype
上的所有属性和方法。 -
重要提示:
__proto__
是一个非标准属性,虽然主流浏览器都支持,但官方更推荐使用Object.getPrototypeOf()
和Object.setPrototypeOf()
来获取和设置对象的原型。但是__proto__
对于理解原型链的原理至关重要。
第二幕:prototype
,构造函数的正牌夫人
接下来,咱们认识另一位举足轻重的人物:prototype
。 这位可不是随便的对象都有的,她是构造函数的专属。
-
prototype
的本质:prototype
是一个属性,它属于构造函数。 它的值是一个对象,这个对象包含了所有由此构造函数创建的实例所共享的属性和方法。 换句话说,prototype
是实例对象的“公共基因库”。 -
代码实战:
function Person(name) { this.name = name; } Person.prototype.sayHello = function() { console.log("Hello, my name is " + this.name); }; let person1 = new Person("Alice"); let person2 = new Person("Bob"); person1.sayHello(); // 输出: Hello, my name is Alice person2.sayHello(); // 输出: Hello, my name is Bob console.log(person1.__proto__ === Person.prototype); // 输出: true
在这个例子中,
Person.prototype
定义了一个sayHello
方法。所有通过new Person()
创建的实例(比如person1
和person2
)都共享这个方法。 注意person1.__proto__
指向了Person.prototype
,这是连接实例和构造函数原型的关键。 -
深入理解: 构造函数通过
prototype
属性来定义实例对象可以继承的属性和方法。 实例对象通过__proto__
属性来访问其构造函数的prototype
对象。
第三幕:Prototype链,血脉相连的家族
现在,把 __proto__
和 prototype
这两个概念串起来,就能理解Prototype链了。
-
Prototype链的本质: 当你试图访问一个对象的属性时,JavaScript引擎会首先在该对象自身查找。 如果找不到,它会沿着该对象的
__proto__
属性指向的原型对象上查找。 如果还是找不到,就继续沿着原型对象的__proto__
向上查找,直到找到目标属性或者到达原型链的顶端(null
)。 这种沿着__proto__
向上查找的链条,就是Prototype链。 -
图解Prototype链:
假设我们有以下代码:
function Animal(name) { this.name = name; } Animal.prototype.sayName = function() { console.log("My name is " + this.name); }; function Dog(name, breed) { Animal.call(this, name); // 调用父构造函数 this.breed = breed; } // 设置Dog的原型为Animal的实例 Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; // 修正constructor指向 Dog.prototype.bark = function() { console.log("Woof!"); }; let myDog = new Dog("Buddy", "Golden Retriever"); myDog.bark(); // 输出: Woof! myDog.sayName(); // 输出: My name is Buddy
那么,
myDog
的Prototype链是这样的:myDog
对象本身: 拥有name
和breed
属性。myDog.__proto__
指向Dog.prototype
: 拥有bark
属性和constructor
属性。Dog.prototype.__proto__
指向Animal.prototype
: 拥有sayName
属性和constructor
属性。Animal.prototype.__proto__
指向Object.prototype
: 拥有toString
,valueOf
等通用方法。Object.prototype.__proto__
指向null
: 原型链的终点。
当我们调用
myDog.bark()
时,JavaScript引擎首先在myDog
对象自身查找bark
属性,找不到。 然后在myDog.__proto__
(也就是Dog.prototype
) 上找到bark
方法,于是执行。当我们调用
myDog.sayName()
时,JavaScript引擎首先在myDog
对象自身查找sayName
属性,找不到。 然后在myDog.__proto__
(也就是Dog.prototype
) 上查找sayName
方法,找不到。 接着在Dog.prototype.__proto__
(也就是Animal.prototype
) 上找到sayName
方法,于是执行。 -
Object.create()
的妙用:Object.create(Animal.prototype)
创建了一个新对象,并将该对象的__proto__
设置为Animal.prototype
。 这是一种安全地继承原型属性的方法,避免直接修改Animal.prototype
带来的副作用。 -
constructor
属性的修正: 由于我们使用了Object.create()
来设置Dog.prototype
,导致Dog.prototype.constructor
指向了Animal
,而不是Dog
。 因此,我们需要手动修正Dog.prototype.constructor = Dog;
,以确保constructor
属性的正确指向。
第四幕:原型继承的各种姿势
掌握了Prototype链的原理,就能灵活运用各种原型继承的方式了。
-
原型链继承: 直接将子构造函数的
prototype
设置为父构造函数的实例。function Parent() { this.property = 'parent property'; } Parent.prototype.getParentProperty = function() { return this.property; }; function Child() { this.childProperty = 'child property'; } Child.prototype = new Parent(); // 关键:将Child的原型设置为Parent的实例 Child.prototype.constructor = Child; // 修正constructor指向 Child.prototype.getChildProperty = function() { return this.childProperty; }; let child = new Child(); console.log(child.getParentProperty()); // 输出: parent property console.log(child.getChildProperty()); // 输出: child property
优点: 简单易懂。
缺点:
- 子类型的所有实例都会共享父类型实例的属性。 如果父类型实例的属性是引用类型,那么一个子类型实例修改了该属性,会影响到所有子类型实例。
- 创建子类型实例时,无法向父类型构造函数传递参数。
-
借用构造函数继承(也叫伪造对象或经典继承): 在子构造函数内部调用父构造函数,使用
call
或apply
方法。function Parent(name) { this.name = name; } function Child(name, age) { Parent.call(this, name); // 关键:借用Parent构造函数 this.age = age; } let child = new Child('Alice', 10); console.log(child.name); // 输出: Alice console.log(child.age); // 输出: 10
优点: 可以在子类型构造函数中向父类型构造函数传递参数。
缺点:
- 只能继承父类型的实例属性和方法,不能继承父类型原型上的属性和方法。
- 每个子类型实例都有一份父类型实例属性的副本,造成内存浪费。
-
组合继承: 结合原型链继承和借用构造函数继承的优点。
function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); // 借用构造函数继承属性 this.age = age; } Child.prototype = new Parent(); // 原型链继承方法 Child.prototype.constructor = Child; // 修正constructor指向 Child.prototype.sayAge = function() { console.log(this.age); }; let child1 = new Child('Alice', 10); child1.colors.push('black'); console.log(child1.colors); // 输出: ['red', 'blue', 'green', 'black'] child1.sayName(); // 输出: Alice child1.sayAge(); // 输出: 10 let child2 = new Child('Bob', 12); console.log(child2.colors); // 输出: ['red', 'blue', 'green'] child2.sayName(); // 输出: Bob child2.sayAge(); // 输出: 12
优点: 既可以继承父类型的实例属性和方法,又可以继承父类型原型上的属性和方法。
缺点: 父类型构造函数会被调用两次,造成一定的性能浪费。
-
寄生式组合继承: 对组合继承的优化,避免了父类型构造函数被调用两次。
function Parent(name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.sayName = function() { console.log(this.name); }; function Child(name, age) { Parent.call(this, name); this.age = age; } // 关键:使用寄生式继承来避免调用Parent构造函数 Child.prototype = Object.create(Parent.prototype); Child.prototype.constructor = Child; Child.prototype.sayAge = function() { console.log(this.age); }; let child1 = new Child('Alice', 10); child1.colors.push('black'); console.log(child1.colors); child1.sayName(); child1.sayAge(); let child2 = new Child('Bob', 12); console.log(child2.colors); child2.sayName(); child2.sayAge();
优点: 避免了父类型构造函数被调用两次,性能更好。
缺点: 代码稍微复杂一些。
-
ES6的
class
继承: ES6 引入了class
关键字,提供了更简洁的语法糖来实现继承。class Parent { constructor(name) { this.name = name; } sayName() { console.log(this.name); } } class Child extends Parent { constructor(name, age) { super(name); // 调用父类的constructor this.age = age; } sayAge() { console.log(this.age); } } let child = new Child('Alice', 10); child.sayName(); // 输出: Alice child.sayAge(); // 输出: 10
优点: 语法更简洁,更易于理解。
缺点: 本质上仍然是基于原型链的继承,只是语法糖而已。
第五幕:总结与升华
Prototype链是JavaScript中实现继承的关键机制。 理解 __proto__
和 prototype
的关系,以及Prototype链的查找过程,能够帮助我们更好地理解JavaScript的面向对象编程。 虽然继承的方式有很多种,但寄生式组合继承和ES6的 class
继承是目前比较推荐的方式。
表格总结各种继承方式:
继承方式 | 优点 | 缺点 |
---|---|---|
原型链继承 | 简单易懂 | 1. 子类型实例共享父类型实例的属性;2. 无法向父类型构造函数传递参数 |
借用构造函数继承 | 可以在子类型构造函数中向父类型构造函数传递参数 | 1. 无法继承父类型原型上的属性和方法;2. 每个子类型实例都有一份父类型实例属性的副本 |
组合继承 | 既可以继承父类型的实例属性和方法,又可以继承父类型原型上的属性和方法 | 父类型构造函数会被调用两次 |
寄生式组合继承 | 避免了父类型构造函数被调用两次,性能更好 | 代码稍微复杂一些 |
ES6的class 继承 |
语法更简洁,更易于理解 | 本质上仍然是基于原型链的继承 |
收尾:实战演练,融会贯通
好了,今天的讲座就到这里。希望大家通过今天的学习,能够对JavaScript的Prototype链有更深入的理解。 记住,理论学习只是第一步,更重要的是在实际项目中运用这些知识。 只有在实践中不断探索,才能真正掌握JavaScript的精髓。
下次再见!