一文入门javaScript中的原型与原型链

131 阅读6分钟

原型

在JavaScript中,原型(prototype)是极为重要且独特的概念,对对象创建和继承起着关键作用。它可分为

  • 函数原型(prototype,显示原型)
  • 对象原型(proto,隐式原型)。

理解原型机制有助于深入掌握JavaScript的对象模型,以及对象和继承的设计与使用。

我们通过一些举例来看看~

函数原型

每个函数都有自动生成的prototype属性,它是一个对象,用于存储可被函数所有实例共享的属性和方法。例如:

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');
person1.sayHello(); // 输出:Hello, my name is Alice
person2.sayHello(); // 输出:Hello, my name is Bob

函数原型与实例对象的关系

  1. 属性和方法修改
    • 实例对象可修改继承自函数原型的属性值,但不能直接修改原型上的属性。若直接给原型属性赋值,实际是给实例对象新增同名属性。例如:
function Person(name) {
    this.name = name;
}
Person.prototype.sayHello = function() {
    console.log('Hello, my name is'+ this.name);
};
const person1 = new Person('Alice');
person1.name = 'Charlie';
person1.sayHello = function() {
    console.log('Hi, I am'+ this.name);
};
person1.sayHello(); // 输出:Hi, I am Charlie ,
const person2 = new Person('Bob');
person2.sayHello(); // 输出:Hello, my name is Bob
  • 实例对象无法给原型新增属性,尝试添加只会在实例对象上创建新属性。例如:
function Person(name) {
    this.name = name;
}
const person1 = new Person('Alice');
const person2 = new Person('Bob');
person1.age = 30;
console.log(person1.age); // 输出:30
console.log(person2.age);//underfined
  • 实例对象无法删除原型上的属性,删除操作只影响实例对象自身属性。例如:
function Person(name) {
    this.name = name;
}
Person.prototype.age = 30;
const person1 = new Person('Alice');
const person2 = new Person('Bob');
delete person1.name;
console.log(person1.name); // underfined
console.log(person2.name);//Bob

对象原型

每个对象都有内部属性__proto__,指向该对象的原型,原型可为另一个对象或null。例如:

let person1 = new Person('Alice');
console.log(person1.__proto__ ); // Person.prototype

函数原型与对象原型的关系

函数原型和对象原型关系密切,创建新对象时,对象的__proto__属性会被自动设置为构造函数的prototype属性值。例如:

let person2 = new Person('Bob');
console.log(person2.__proto__ === Person.prototype); // true

constructor属性

在原型链中,constructor属性指向创建对象的构造函数引用。每个对象都有此属性。例如:

function Person(name) {
    this.name = name;
}
const person1 = new Person('Alice');
console.log(person1.constructor === Person); // 输出: true

原型链

原型链的定义与作用

原型链是JavaScript实现

  • 继承
  • 属性查找

的机制。每个对象有原型对象,通过它实现属性和方法继承。访问对象属性或方法时,若对象本身没有,引擎会沿原型链向上查找,直到找到或到达原型链顶端(Object.prototype)。

原型链的继承关系

对象间的继承关系通过原型链实现。例如:

function Animal(name) {
    this.name = name;
}
Animal.prototype.sleep = function() {
    console.log(this.name + " is sleeping");
};
function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
    console.log("Woof! Woof!");
};
var dog1 = new Dog("Buddy", "Labrador");
dog1.sleep(); // 输出: Buddy is sleeping
dog1.bark(); // 输出: Woof! Woof!

此例中,Dog类通过原型链继承了Animal类的属性和方法。

我们先看看这这几个 api

Call 方法

在 JavaScript 中,call是函数的一个原型方法,它的主要作用是在特定的上下文中调用一个函数,并可以显式地指定函数内部this关键字所指向的对象。

在给定的代码中:

function Dog(name, breed) {
    Animal.call(this, name);
    this.breed = breed;
}

这里的Animal.call(this, name)语句的目的是在创建Dog实例时,让Dog构造函数内部的this能够正确地继承Animal构造函数中的属性。

具体来说:

  • this参数:在call方法中,第一个参数指定了函数内部this关键字要指向的对象。在Dog构造函数里,通过传入this,意味着在调用Animal构造函数时,Animal函数内部的this就会指向正在创建的Dog实例对象。这样就能确保Animal构造函数中对this.name的赋值操作,实际上是给当前正在创建的Dog实例设置了name属性。
  • 后续参数:除了第一个参数用于指定this指向外,call方法还可以接收其他参数,这些参数会作为被调用函数(这里是Animal函数)的实际参数传递进去。在上述代码中,name作为参数传递给了Animal构造函数,以便在Animal构造函数内部使用该参数来初始化name属性。

Object.create方法

Object.create()方法用于创建一个新对象,新对象会以传入的对象作为其原型对象。

在代码中:

Dog.prototype = Object.create(Animal.prototype);

这里的作用是让Dog的原型对象(Dog.prototype)继承自Animal的原型对象(Animal.prototype)。这意味着Dog的所有实例都能够继承Animal.prototype上定义的方法,比如sleep方法。通过这种方式,实现了基于原型链的继承机制,使得Dog实例不仅可以访问自身构造函数中定义的属性和方法,还能访问到从Animal原型继承而来的方法。

constructor属性

在 JavaScript 中,每个函数都有一个constructor属性,它指向创建该函数对象的构造函数。

在代码中:

Dog.prototype.constructor = Dog;

当我们使用Object.create(Animal.prototype)来设置Dog.prototype时,新创建的Dog.prototype对象的constructor属性会指向Animal(因为它继承自Animal.prototype,而Animal.prototype.constructor默认指向Animal)。但我们希望Dog.prototypeconstructor属性正确地指向Dog构造函数,所以需要显式地将其设置回来。这样做的好处是保持了对象的构造函数引用的正确性,使得在一些需要通过constructor属性来判断对象类型或者进行相关操作的场景下能够正常工作。

prototype属性

  • 在 JavaScript 中,函数都有一个prototype属性,它是一个对象,用于定义通过该函数创建的实例所共享的属性和方法。当使用new关键字创建一个对象实例时,该实例会自动关联到其构造函数的prototype对象上,通过原型链机制来实现对prototype上属性和方法的继承。
  • 对于Animal函数,Animal.prototype.sleep = function() {...}就是在Animal的原型对象上定义了一个sleep方法,这样所有通过Animal构造函数创建的实例(以及继承自Animal的其他构造函数的实例,如Dog的实例)都能够访问到这个sleep方法。
  • 同样,对于Dog函数,我们通过设置Dog.prototype来定义Dog实例所共享的额外属性和方法,比如bark方法就是定义在Dog的原型对象上,只有Dog的实例能够访问到它。

通过上述这些方法和对属性的操作,实现了Dog类对Animal类的继承关系,使得Dog实例既具有自身特有的属性(如breed)和方法(如bark),又能继承Animal类的属性(如name)和方法(如sleep)。

属性查找过程

访问对象属性或方法时,JavaScript引擎沿原型链向上查找,找不到则返回undefined。

关于对象原型的特殊情况

  1. 除Object本身外的对象原型
    • Object的实例(通过new Object()或对象字面量创建),原型是Object.prototype。例如:
const obj = {};
console.log(obj.__proto__ === Object.prototype); // 输出: true
  • 函数的实例(通过new Function()创建),原型是Function.prototype。例如:
function myFunction() {}
console.log(myFunction.__proto__ === Function.prototype); // 输出: true
  • 数组的实例(通过new Array()创建),原型是Array.prototype。例如:
const arr = [];
console.log(arr.__proto__ === Array.prototype); // 输出: true
  • 其他内置对象(如Date、RegExp、Error等)的实例,原型分别是对应的Date.prototype、RegExp.prototype、Error.prototype等。例如:
const date = new Date();
console.log(date.__proto__ === Date.prototype); // 输出: true
  1. Object.prototype是原型链顶端
    Object.prototype的原型是null。例如:
console.log(Object.prototype.__proto__); // 输出: null
  1. 创建无原型的对象
    使用Object.create(null)可创建无原型的对象。例如:
const noProtoObj = Object.create(null);
console.log(noProtoObj.__proto__); // 输出: undefined

通过对原型与原型链的深入理解,能更好地运用JavaScript进行面向对象编程,提升代码的效率和可维护性。