MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

JavaScript构造函数与原型链的关系

2024-02-294.2k 阅读

JavaScript构造函数

在JavaScript中,构造函数是一种特殊的函数,用于创建对象。它的命名通常遵循大写字母开头的约定,以便与普通函数区分开来。构造函数通过new关键字调用,会创建一个新的对象实例,该实例继承自构造函数的prototype属性所指向的对象。

构造函数的基本使用

function Person(name, age) {
    this.name = name;
    this.age = age;
}

let person1 = new Person('Alice', 30);
console.log(person1.name); 
console.log(person1.age); 

在上述代码中,Person是一个构造函数,它接受nameage两个参数,并将它们赋值给新创建对象的属性。通过new Person('Alice', 30)创建了一个person1实例,该实例具有nameage属性。

构造函数与普通函数的区别

  1. 调用方式:普通函数直接调用,如functionName();构造函数通过new关键字调用,如new Constructor()
  2. 返回值:普通函数返回其返回语句中的值,如果没有返回语句则返回undefined。构造函数在使用new调用时,即使没有return语句,也会返回一个新创建的对象实例。如果构造函数中有return语句,且返回的是一个对象,则会返回该对象;如果返回的是基本类型(如字符串、数字、布尔值等),则会忽略该返回值,依然返回新创建的对象实例。
function NormalFunction() {
    return 'This is a normal function';
}

let normalResult = NormalFunction();
console.log(normalResult); 

function ConstructorFunction() {
    this.message = 'This is a constructor function';
    return 'Return value from constructor'; 
}

let constructorResult = new ConstructorFunction();
console.log(constructorResult.message); 

在上述代码中,NormalFunction是普通函数,返回字符串。ConstructorFunction作为构造函数,虽然有返回字符串,但使用new调用时,返回的是新创建的对象实例,字符串返回值被忽略。

原型(Prototype)

每个函数都有一个prototype属性,它是一个对象,包含了可以被该函数创建的所有实例共享的属性和方法。当使用构造函数创建对象实例时,实例对象会通过内部的[[Prototype]](在现代JavaScript中可以通过__proto__访问,但__proto__不推荐在生产代码中使用,推荐使用Object.getPrototypeOf()方法)链接到构造函数的prototype对象。

原型对象的作用

  1. 共享属性和方法:通过将属性和方法定义在构造函数的prototype对象上,所有由该构造函数创建的实例都可以共享这些属性和方法,而不需要在每个实例上重复创建,从而节省内存。
function Animal(name) {
    this.name = name;
}

Animal.prototype.speak = function() {
    console.log(this.name +'makes a sound.');
};

let dog = new Animal('Buddy');
let cat = new Animal('Whiskers');

dog.speak(); 
cat.speak(); 

在上述代码中,speak方法定义在Animal.prototype上,dogcat实例都可以调用该方法,因为它们通过[[Prototype]]链接到了Animal.prototype

  1. 实现继承:在JavaScript的原型继承机制中,原型对象起着关键作用。通过修改原型链,可以实现对象之间的继承关系。

原型对象的属性

  1. constructor:每个原型对象都有一个constructor属性,它指向关联的构造函数。
function Car(make, model) {
    this.make = make;
    this.model = model;
}

console.log(Car.prototype.constructor === Car); 

上述代码中,Car.prototype.constructor指向Car构造函数,这使得可以从原型对象追溯到它所属的构造函数。

原型链

原型链是JavaScript实现继承和属性查找的重要机制。当访问一个对象的属性时,如果该对象本身没有这个属性,JavaScript会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端(null)。

原型链的形成

  1. 每个对象都有一个内部的[[Prototype]]属性,它指向该对象的原型对象。对于通过构造函数创建的对象实例,其[[Prototype]]指向构造函数的prototype对象。
  2. 构造函数的prototype对象本身也是一个对象,它也有自己的[[Prototype]],通常指向Object.prototype
  3. Object.prototype[[Prototype]]null,这标志着原型链的结束。
function Shape() {
    this.color = 'black';
}

function Rectangle(width, height) {
    Shape.call(this);
    this.width = width;
    this.height = height;
}

Rectangle.prototype = Object.create(Shape.prototype);
Rectangle.prototype.constructor = Rectangle;

let rect = new Rectangle(10, 20);

console.log(rect.color); 
console.log(rect.width); 

在上述代码中,Rectangle继承自Shaperect实例首先在自身查找属性,找不到时会沿着原型链到Rectangle.prototype查找,再找不到会到Shape.prototype查找,最后到Object.prototype查找。

属性查找过程

  1. 对象自身属性:当访问obj.property时,JavaScript首先检查obj对象本身是否有property属性。如果有,则直接返回该属性的值。
  2. 原型对象属性:如果obj对象本身没有property属性,JavaScript会沿着obj[[Prototype]]链查找。它会检查obj.__proto__(或Object.getPrototypeOf(obj))指向的原型对象是否有property属性。如果有,则返回该属性的值。
  3. 原型链继续查找:如果在当前原型对象中没有找到property属性,JavaScript会继续沿着原型链向上查找,重复上述过程,直到找到属性或者到达原型链顶端(null)。如果到达null还没有找到属性,则返回undefined
function Parent() {
    this.parentProp = 'Parent property';
}

function Child() {
    this.childProp = 'Child property';
}

Child.prototype = Object.create(Parent.prototype);
Child.prototype.constructor = Child;

let childObj = new Child();

console.log(childObj.childProp); 
console.log(childObj.parentProp); 
console.log(childObj.nonExistentProp); 

在上述代码中,childObj实例本身有childProp属性,所以直接返回其值。对于parentPropchildObj本身没有,沿着原型链在Parent.prototype中找到并返回。而nonExistentProp在原型链中都未找到,所以返回undefined

构造函数与原型链的关系

  1. 构造函数创建实例与原型链起点:构造函数通过new关键字创建对象实例,实例对象的[[Prototype]]指向构造函数的prototype对象,这就确定了原型链的起点。构造函数是创建对象的模板,而原型链则为对象提供了共享属性和方法以及实现继承的机制。
function Circle(radius) {
    this.radius = radius;
}

Circle.prototype.getArea = function() {
    return Math.PI * this.radius * this.radius;
};

let circle1 = new Circle(5);

console.log(circle1.__proto__ === Circle.prototype); 

在上述代码中,circle1Circle构造函数创建的实例,circle1.__proto__指向Circle.prototype,这是原型链的起始链接。

  1. 原型链影响属性和方法的继承与查找:构造函数的prototype对象上定义的属性和方法构成了原型链的一部分。当实例对象访问属性或方法时,会沿着原型链查找,这使得构造函数创建的所有实例都能继承和访问prototype对象上的内容。
function Vehicle() {
    this.wheels = 4;
}

Vehicle.prototype.move = function() {
    console.log('The vehicle is moving.');
};

function Car(make, model) {
    Vehicle.call(this);
    this.make = make;
    this.model = model;
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

let myCar = new Car('Toyota', 'Corolla');

myCar.move(); 

在上述代码中,Car继承自VehiclemyCar实例可以调用Vehicle.prototype上的move方法,因为原型链使得Vehicle.prototype成为myCar查找属性和方法的一部分。

  1. 动态修改原型对构造函数实例的影响:可以动态地修改构造函数的prototype对象,这会影响所有现有和未来通过该构造函数创建的实例。
function Person() {
    this.name = 'Default Name';
}

let person1 = new Person();

Person.prototype.sayHello = function() {
    console.log('Hello, my name is'+ this.name);
};

let person2 = new Person();

person1.sayHello(); 
person2.sayHello(); 

在上述代码中,在创建person1实例后,向Person.prototype添加了sayHello方法。person1和之后创建的person2都可以调用这个新添加的方法,因为它们都通过原型链访问Person.prototype

  1. 构造函数作为原型链的构建者:构造函数在原型链的构建中起着核心作用。通过设置构造函数的prototype对象,并利用new操作符创建实例,JavaScript构建了复杂的原型链结构,实现了对象之间的继承关系。不同构造函数之间通过原型链的关联,可以形成多层次的继承体系。
function Animal() {
    this.species = 'Generic Animal';
}

function Mammal() {
    Animal.call(this);
    this.warmBlooded = true;
}

Mammal.prototype = Object.create(Animal.prototype);
Mammal.prototype.constructor = Mammal;

function Dog(name) {
    Mammal.call(this);
    this.name = name;
}

Dog.prototype = Object.create(Mammal.prototype);
Dog.prototype.constructor = Dog;

let myDog = new Dog('Max');

console.log(myDog.species); 
console.log(myDog.warmBlooded); 

在上述代码中,Dog通过Mammal间接继承自AnimalDog构造函数在原型链的构建中,通过设置其prototypeMammal.prototype的关系,以及Mammal.prototypeAnimal.prototype的关系,构建了一条多层的原型链。

理解构造函数和原型链在JavaScript中的重要性

  1. 内存管理与性能:通过原型链共享属性和方法,避免了在每个对象实例上重复创建相同的属性和方法,从而节省了内存。这对于创建大量相似对象的应用场景非常重要,提高了程序的性能。
  2. 代码复用与可维护性:利用构造函数和原型链实现继承,使得代码可以复用。通过在原型对象上定义通用的属性和方法,减少了重复代码,提高了代码的可维护性。当需要修改或扩展某个功能时,只需要在原型对象上进行修改,所有相关的对象实例都会受到影响。
  3. 面向对象编程的基础:构造函数和原型链是JavaScript实现面向对象编程的核心机制。它们提供了对象创建、属性和方法定义以及继承的功能,使得JavaScript可以像传统面向对象语言一样进行编程,虽然其实现方式与基于类的语言有所不同。

实际应用案例

  1. 创建类库和框架:在开发JavaScript类库和框架时,构造函数和原型链常用于创建可复用的组件和对象。例如,流行的JavaScript库如jQuery,通过构造函数创建jQuery对象实例,利用原型链共享方法,实现了对DOM操作等功能的封装和复用。
  2. 模块化开发:在模块化开发中,构造函数和原型链可以用于创建模块内的对象和实现模块之间的继承关系。通过将相关的功能封装在构造函数和其原型对象中,可以实现模块的独立性和可组合性。
  3. 游戏开发:在JavaScript游戏开发中,构造函数和原型链可用于创建游戏对象,如角色、道具等。通过原型链实现对象之间的继承关系,可以方便地管理游戏对象的属性和行为,提高游戏开发的效率。

注意事项

  1. 避免原型污染:由于原型链的共享特性,如果不小心在原型对象上添加了全局属性或方法,可能会导致原型污染,影响其他对象。例如,不要在Object.prototype上随意添加属性,除非是经过深思熟虑的全局扩展。
  2. 正确设置构造函数的prototype:在使用Object.create()等方法设置构造函数的prototype时,要注意同时正确设置constructor属性,以确保原型对象和构造函数之间的正确关联。
  3. 性能考虑:虽然原型链共享属性和方法可以节省内存,但在属性查找时,原型链过长可能会影响性能。在设计对象结构和原型链时,要考虑到属性查找的效率。

总之,理解JavaScript构造函数与原型链的关系是掌握JavaScript面向对象编程的关键。通过合理运用构造函数创建对象和利用原型链实现继承、共享属性和方法,可以编写出高效、可维护的JavaScript代码。无论是开发小型脚本还是大型应用程序,构造函数和原型链的知识都至关重要。