js中prototype属性与继承

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
2
3
4
5
6
7
8
9
function Car(color, displacement) { 
this.color= color ;
this.displacement = displacement;
}
Car.prototype.logMessage = function(){
console.log(this.color,this.displacement);
}
var car = new Car("black","2.4T");
car.logMessage(); //black, 2.4T

这个例子中,给 Car 的 prototype 属性对象添加了 logMessage 方法,这样使用 Car 创建 的 car 对象就可以直接调用 logMessage 方法 。 虽然这里可以使用 car 调用 logMessage 方法, 但是 car 对象本身并不会添加这个方法,只是可以调用而已 。 function 创建的实例对象在调用属性时会首先在自己的属性中查找,如果找不到就会去 function 的 prototype 属性对象中查找 。 但是,创建的对象只是可以调用 prototype 中的属性 。 但是并不会实际拥有那些属性,也不可以对它们进行修改(修改操作会在实例对象中添加一个同名属性) 。 当创建的实例对象定义了同名的属性后就会覆盖 prototype 中的属性,但是原来 prototype 中的属性并不会发生变化,而且当创建出来的对象删除了添加的属性后,原来 prototype 中的属性还可以继续调用,请看下面的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Car(color, displacement) { 
this.color= color ;
this.displacement = displacement;
}
Car.prototype.logMessage = function(){
console.log(this.color,this.displacement);
}
var car = new Car("black","2.4T");
car.logMessage(); //black, 2.4T
car.logMessage= function(){
console.log(this.color);
}
car.logMessage();
delete car.logMessage;
car.logMessage();

在这个例子中,使用 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function log(msg) {
console.log(msg);
}
function Person() {}
Person.prototype.logPerson = function() {
log("person");
}
function Teacher() {
this.logTeacher = function() {
log("teacher");
}
}
Teacher.prototype = new Person();
Teacher.prototype.logPrototype = function() {
log("prototype");
}
var teacher = new Teacher();
teacher.logTeacher();
teacher.logPrototype();
teacher.logPerson();

这个例子中,因为 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
2
```
使用 prototype 时应注意以下三点 。 一是, prototype 是属于 function 类型对象的属性 , prototype 自身的属性可以被 function 创建的 object 类型的实例对象使用,但是 object 类型的实例对象自身并没有 prototype 属性 。 二是,如果要给 function 对象的 prototype 属性赋 予新的值并且又要添加新的属性,则需 要先赋予新值,然后再添加新的属性,否则在赋值时,会将原先添加的属性覆盖掉,例如下 面的代码 。
1
在上述代码中,在执行最后一行代码 teacher.logPrototype ()的时候会报错,这是因为 给 Teacher 的 prototype 属性对象添加了 logPrototype 属性方法后,又将 prototype 赋值为new Person (),而新的 prototype 中并没有 logPrototype 方法,所以调用就会出错,也就 是说 logPrototype 被新的对象覆盖 。 也 nction 创建的对象在调用属性时是实时按 prototype 链依次查找的,而不是将 prototype 中的属性关联到创建的对象本身,因此创建完对象后,再修改 function 的 prototype 也会影响 到创建的对象的调用,例如下面的例子 。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这里的 log 方法是在 teacher 对象已经创建完成后添加的,但是在 teacher 对象中仍然可以使用,也就是说 prototype 中的属性是动态查询的。另外,使用 prototype 除了可以实现继承之外,还可以节约内存,因为无论使用 function 创建多少对象,它们所指向的 prototype 对象在内存中都只有一份。但是,使用 prototype 中的属性比直接使用对象中定义的属性在执行效率上理论来说会低一些。
## 原型链
JavaScript 中的原型链是一种实现继承的机制。每个 JavaScript 对象都有一个内部属性,称为 `Prototype`,通常可以通过 `__proto__` 属性或 `Object.getPrototypeOf()` 方法来访问。这个属性指向另一个对象,即该对象的原型。原型链就是通过这种原型关系连接起来的一系列对象。
### 原型链的基本概念
1. **原型对象**:每个函数都有一个 `prototype` 属性,该属性是一个对象,称为原型对象。当你创建一个函数的实例时,这个实例的 `Prototype` 属性会指向该函数的 `prototype` 属性所指向的对象。
2. **属性查找**:当你尝试访问一个对象的属性或方法时,JavaScript 引擎会首先在该对象本身上查找。如果找不到,就会沿着原型链向上查找,直到找到该属性或到达原型链的末端(即 `null`)。
3. **原型链的末端**:原型链的末端是 `Object.prototype`,它的 `Prototype` 属性是 `null`。所有原型链最终都会指向 `Object.prototype`。
### 示例
```javascript
function Person(name) {
this.name = name;
}
Person.prototype.sayHello = function() {
console.log(`Hello, my name is ${this.name}!`);
};
const person1 = new Person('Alice');
const person2 = new Person('Bob');
console.log(person1.sayHello); // 输出: [Function: sayHello]
console.log(person1.__proto__ === Person.prototype); // true
console.log(person1.__proto__.__proto__ === Object.prototype); // true
console.log(person1.__proto__.__proto__.__proto__); // null

在这个例子中:

  • Person 是一个构造函数。
  • Person.prototype 是一个对象,它有一个 sayHello 方法。
  • person1person2Person 的实例。它们的 Prototype 指向 Person.prototype
  • Person.prototypePrototype 指向 Object.prototype
  • Object.prototypePrototypenull,这是原型链的末端。

原型链的优缺点

  • 优点
    • 实现了代码的复用,多个实例可以共享原型对象上的属性和方法。
    • 提供了一种继承机制,可以通过原型链来实现对象之间的继承关系。
  • 缺点
    • 过长的原型链会导致性能问题,因为属性查找需要沿着原型链逐级向上查找。
    • 如果原型链上的对象被修改,可能会影响所有继承该原型的对象,这可能导致一些难以追踪的错误。

注意事项

  • 避免修改 Object.prototype:修改 Object.prototype 会影响所有对象,因为所有对象的原型链最终都会指向 Object.prototype
  • 使用 Object.create():可以更清晰地创建对象及其原型关系,例如 const obj = Object.create(proto),其中 proto 是新对象的原型。
    通过理解原型链的工作原理,可以更好地进行面向对象编程和对象继承的设计。

对象自身的属性和prototype里的属性有什么区别

在 JavaScript 中,对象自身的属性和 prototype 里的属性有以下主要区别:

一、定义位置不同

  • 对象自身的属性:直接定义在对象实例上。例如:
    1
    2
    3
    4
    let obj = {
    name: "Kimi",
    age: 20
    };
    这里 nameage 就是对象 obj 自身的属性。
  • prototype 里的属性:定义在对象的原型上。在 JavaScript 中,每个函数都有一个 prototype 属性,指向一个对象,这个对象被称为函数的原型对象。当通过构造函数创建对象实例时,实例对象的内部属性 __proto__ 会指向构造函数的 prototype。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    function Person(name, age) {
    this.name = name;
    this.age = age;
    }
    Person.prototype.sayHello = function() {
    console.log("Hello");
    };
    let person1 = new Person("Kimi", 20);
    这里 sayHello 方法是定义在 Personprototype 上的,person1 实例通过 __proto__ 链可以访问到 sayHello 方法。

二、访问方式不同

  • 对象自身的属性:可以直接通过对象实例访问。例如:
    1
    2
    console.log(obj.name); // 输出 Kimi
    console.log(obj.age); // 输出 20
  • prototype 里的属性:通过对象实例访问时,如果实例上没有该属性,会沿着原型链向上查找。例如:
    1
    2
    console.log(person1.sayHello); // 输出 ƒ () { console.log("Hello"); }
    person1.sayHello(); // 控制台输出 Hello
    这里 person1 实例本身没有 sayHello 方法,但是它可以通过原型链找到 Person.prototype 上的 sayHello 方法并调用。

三、内存占用不同

  • 对象自身的属性:每个对象实例都有自己的属性副本。如果创建多个对象实例,每个实例都会有自己的属性占用内存。例如:
    1
    2
    3
    4
    5
    6
    7
    8
    let obj1 = {
    name: "Kimi",
    age: 20
    };
    let obj2 = {
    name: "Moonshot",
    age: 30
    };
    obj1obj2 各自占用内存来存储 nameage 属性。
  • prototype 里的属性:所有通过同一个构造函数创建的实例共享原型上的属性。这意味着无论创建多少个实例,原型上的属性只有一份,节省内存。例如:
    1
    let person2 = new Person("Moonshot", 30);
    person1person2 都可以通过原型链访问到 Person.prototype 上的 sayHello 方法,而 sayHello 方法在内存中只有一份。

四、修改属性的影响不同

  • 对象自身的属性:修改一个实例的自身属性,不会影响其他实例。例如:
    1
    2
    obj1.name = "Kimi Moonshot";
    console.log(obj2.name); // 输出 Moonshot,obj2 的 name 属性不受影响
  • prototype 里的属性:修改原型上的属性,会影响所有通过该构造函数创建的实例。例如:
    1
    2
    3
    Person.prototype.age = 25;
    console.log(person1.age); // 输出 25
    console.log(person2.age); // 输出 25
    但是,如果实例自身已经存在同名属性,则修改原型上的属性不会影响该实例的自身属性。例如:
    1
    2
    3
    4
    person1.age = 22;
    Person.prototype.age = 30;
    console.log(person1.age); // 输出 22,因为 person1 自身有 age 属性
    console.log(person2.age); // 输出 30

五、应用场景不同

  • 对象自身的属性:适用于存储每个实例独有的数据。例如,每个 Person 实例的 nameage 属性都是不同的,应该定义在实例自身上。
  • prototype 里的属性:适用于存储多个实例共享的方法或数据。例如,所有 Person 实例都可以使用 sayHello 方法,这个方法就应该定义在 Personprototype 上,以实现代码复用和节省内存。