JS中prototype属性与继承
继承方法
继承是 Java 、 C++等基于类的语言中的一个术语。它的含义是子类的对象可以调用父类的属性和方法。基于对象的 ES 语言根本没有类的概念,当然也就不存在基于类的那种继承方式,但是,它可以通过 prototype 属性来达到类似于继承的效果。 prototype 是 ES 中 function 类型对象的一个特殊属性。每个 function 类型的对象都有 prototype 属性, prototype 属性的值是 object 类型的对象。在 FireBug 中可以看到 Object ( Object 本身是 function 类型)的 prototype 属性类型
function 对象中 prototype 属性对象的作用是这样的:在 function 对象创建出的 object 类型对象实例中可以直接调用 function 对象的 prototype 属性对象中的属性(包括方法属性),例如下面的例子 。
1 | function Car(color, displacement) { |
这个例子中,给 Car 的 prototype 属性对象添加了 logMessage 方法,这样使用 Car 创建 的 car 对象就可以直接调用 logMessage 方法 。 虽然这里可以使用 car 调用 logMessage 方法, 但是 car 对象本身并不会添加这个方法,只是可以调用而已 。 function 创建的实例对象在调用属性时会首先在自己的属性中查找,如果找不到就会去 function 的 prototype 属性对象中查找 。 但是,创建的对象只是可以调用 prototype 中的属性 。 但是并不会实际拥有那些属性,也不可以对它们进行修改(修改操作会在实例对象中添加一个同名属性) 。 当创建的实例对象定义了同名的属性后就会覆盖 prototype 中的属性,但是原来 prototype 中的属性并不会发生变化,而且当创建出来的对象删除了添加的属性后,原来 prototype 中的属性还可以继续调用,请看下面的例子
1 | function Car(color, displacement) { |
在这个例子中,使用 Car 直接创建的 car 对象并没有 logMessage 方法,所以第一次调 用 logMessage 方法时会调用 Car 的 prototype 属 性对象中的 logMessage 方 法, 然后给 car 定义了 logMessage 方法,这时再调用 logMessage 方法就会调用 car 自己的 logMessage 方法了 , 最后又删除了 car 的 logMessage 方法,此时调用 logMessage 方法就会再次调用 Car 的 prototype 属 性对象中的 logMessage 方法, 而且 Car 的 prototype 属性对象中的 logMessage 方法的内容也没有发生变化 。
多层继承
function 的 prototype 属性是 object 类型的属性对象,其本身可能也使用 function创建的对象,通过这种方法就可以实现多层继承,例如下面的例子。
1 | function log(msg) { |
这个例子中,因为 Teacher 的 prototype 属性是 Person 创建的实例对象,而使用 Teacher 创建出来的 teacher 对象可以调用 Teacher 的 prototype 属性对象的属性,所以 teacher 对象可以调用 Person 创建的实例对象的属性。又因为 Person 创建的实例对象可以调用 Person 的 prototype 属性对象中的属性,所以 teacher 对象也可以调用 Person 的 prototype 属性对象中的的logPerson 。另外,因为此程序给 Teacher 的 prototype 属性对象添加了 logPrototype 方法,所以 teacher 也可以调用 logPrototype 方法。最后的输出结果如下。
1 | teacher prototype person |
这种调用方法相当于基于类语言中的多层继承,Teacher 创建出来的 teacher 对象在调用属性时会首先在自己的属性中查找 , 如果找不到就会到 Teacher 的 prototype 属性对象的属性中查找,如果还找不到就会到 Person 的 prototype 属性对象的属性中查找,而 Teacher 的 prototype 又由两部分组成 ,一部分是用 Person 创建的 person 对象,另一部分是直接定义 的 logPrototype 方法 。
使用 Prototype 时的注意事项
- todo
在 function 的 prototype 属性对象中默认存在一个名为 constructor 的属性 。 这个属性默认指向 function 方法自身,例如上节例子中 Person 的 prototype 属性对象的 constructor 属性就指向了 Person 。 但是,Teacher 的 prototype 由于被赋予了新的值,因此它的 constructor 属性就不存在(使用 Person 创建的 person 对象自身并没有 constructor 属性) 。 这时,如果调用 teacher.constructor 就 返回 Person 函数 (因为最后会沿着 prototype 找到 person 的 prototype 属性对象的 constructor 属性) 。 为了可以使用 constructor 属性得到正确的构造函数,可以手 动给 Teacher 的 prototype 属性对象的 constructor 属性赋值为 Teacher ,代码如下所示 。
1 | ``` |
1 | 在上述代码中,在执行最后一行代码 teacher.logPrototype ()的时候会报错,这是因为 给 Teacher 的 prototype 属性对象添加了 logPrototype 属性方法后,又将 prototype 赋值为new Person (),而新的 prototype 中并没有 logPrototype 方法,所以调用就会出错,也就 是说 logPrototype 被新的对象覆盖 。 也 nction 创建的对象在调用属性时是实时按 prototype 链依次查找的,而不是将 prototype 中的属性关联到创建的对象本身,因此创建完对象后,再修改 function 的 prototype 也会影响 到创建的对象的调用,例如下面的例子 。 |
1 | 这里的 log 方法是在 teacher 对象已经创建完成后添加的,但是在 teacher 对象中仍然可以使用,也就是说 prototype 中的属性是动态查询的。另外,使用 prototype 除了可以实现继承之外,还可以节约内存,因为无论使用 function 创建多少对象,它们所指向的 prototype 对象在内存中都只有一份。但是,使用 prototype 中的属性比直接使用对象中定义的属性在执行效率上理论来说会低一些。 |
在这个例子中:
Person是一个构造函数。Person.prototype是一个对象,它有一个sayHello方法。person1和person2是Person的实例。它们的Prototype指向Person.prototype。Person.prototype的Prototype指向Object.prototype。Object.prototype的Prototype是null,这是原型链的末端。
原型链的优缺点
- 优点:
- 实现了代码的复用,多个实例可以共享原型对象上的属性和方法。
- 提供了一种继承机制,可以通过原型链来实现对象之间的继承关系。
- 缺点:
- 过长的原型链会导致性能问题,因为属性查找需要沿着原型链逐级向上查找。
- 如果原型链上的对象被修改,可能会影响所有继承该原型的对象,这可能导致一些难以追踪的错误。
注意事项
- 避免修改
Object.prototype:修改Object.prototype会影响所有对象,因为所有对象的原型链最终都会指向Object.prototype。 - 使用
Object.create():可以更清晰地创建对象及其原型关系,例如const obj = Object.create(proto),其中proto是新对象的原型。
通过理解原型链的工作原理,可以更好地进行面向对象编程和对象继承的设计。
对象自身的属性和prototype里的属性有什么区别
在 JavaScript 中,对象自身的属性和 prototype 里的属性有以下主要区别:
一、定义位置不同
- 对象自身的属性:直接定义在对象实例上。例如: 这里
1
2
3
4let obj = {
name: "Kimi",
age: 20
};name和age就是对象obj自身的属性。 prototype里的属性:定义在对象的原型上。在 JavaScript 中,每个函数都有一个prototype属性,指向一个对象,这个对象被称为函数的原型对象。当通过构造函数创建对象实例时,实例对象的内部属性__proto__会指向构造函数的prototype。例如:这里1
2
3
4
5
6
7
8function Person(name, age) {
this.name = name;
this.age = age;
}
Person.prototype.sayHello = function() {
console.log("Hello");
};
let person1 = new Person("Kimi", 20);sayHello方法是定义在Person的prototype上的,person1实例通过__proto__链可以访问到sayHello方法。
二、访问方式不同
- 对象自身的属性:可以直接通过对象实例访问。例如:
1
2console.log(obj.name); // 输出 Kimi
console.log(obj.age); // 输出 20 prototype里的属性:通过对象实例访问时,如果实例上没有该属性,会沿着原型链向上查找。例如:这里1
2console.log(person1.sayHello); // 输出 ƒ () { console.log("Hello"); }
person1.sayHello(); // 控制台输出 Helloperson1实例本身没有sayHello方法,但是它可以通过原型链找到Person.prototype上的sayHello方法并调用。
三、内存占用不同
- 对象自身的属性:每个对象实例都有自己的属性副本。如果创建多个对象实例,每个实例都会有自己的属性占用内存。例如:
1
2
3
4
5
6
7
8let obj1 = {
name: "Kimi",
age: 20
};
let obj2 = {
name: "Moonshot",
age: 30
};obj1和obj2各自占用内存来存储name和age属性。 prototype里的属性:所有通过同一个构造函数创建的实例共享原型上的属性。这意味着无论创建多少个实例,原型上的属性只有一份,节省内存。例如:1
let person2 = new Person("Moonshot", 30);
person1和person2都可以通过原型链访问到Person.prototype上的sayHello方法,而sayHello方法在内存中只有一份。
四、修改属性的影响不同
- 对象自身的属性:修改一个实例的自身属性,不会影响其他实例。例如:
1
2obj1.name = "Kimi Moonshot";
console.log(obj2.name); // 输出 Moonshot,obj2 的 name 属性不受影响 prototype里的属性:修改原型上的属性,会影响所有通过该构造函数创建的实例。例如:但是,如果实例自身已经存在同名属性,则修改原型上的属性不会影响该实例的自身属性。例如:1
2
3Person.prototype.age = 25;
console.log(person1.age); // 输出 25
console.log(person2.age); // 输出 251
2
3
4person1.age = 22;
Person.prototype.age = 30;
console.log(person1.age); // 输出 22,因为 person1 自身有 age 属性
console.log(person2.age); // 输出 30
五、应用场景不同
- 对象自身的属性:适用于存储每个实例独有的数据。例如,每个
Person实例的name和age属性都是不同的,应该定义在实例自身上。 prototype里的属性:适用于存储多个实例共享的方法或数据。例如,所有Person实例都可以使用sayHello方法,这个方法就应该定义在Person的prototype上,以实现代码复用和节省内存。