Skip to Content

面试导航 - 程序员面试题库大全 | 前端后端面试真题 | 面试

Javascript原型链

Prototype Chain

In ECMAScript, objects are data structures composed of key-value pairs. Each key (also called property name) is a string, and the value can be any type of data (such as primitive values, functions, or even other objects). Objects are one of the core structures in ECMAScript for storing and organizing data.

Methods of Creating Objects

In JavaScript, there are two common ways to create objects:

  1. Literal syntax.
const person = { running() { console.log('I can do what you can do'); }, name: 'moment', age: 7, dance() { console.log('I can dance too'); }, };
  1. Constructor syntax
// Create a new instance of Object, then add properties and methods to the instance const person = new Object(); person.name = 'moment'; person.age = 18; person.running = function () { console.log('I can swim, can you?'); };

In the above two examples, every time we create an object, we need to create properties like name, age, and a running method for it. Is there a way to reduce some of this repetitive code? The answer is yes. We can extract the methods into a function and then assign them to objects.

const running = (info) => { console.log(info); }; const person = new Object(); person.name = 'moment'; person.age = 18; person.running = running; const object = { running, name: 'moment', age: 7, dance() { console.log('I can dance too'); }, }; // It seems we can only encapsulate common methods, properties cannot be dynamically assigned values, they can only be fixed values person.running('We all can swim'); // We all can swim object.running('We all can swim'); // We all can swim

Factory Functions

A Factory Function is a regular function that returns a new object instance with certain properties and methods. Factory functions are typically used to create multiple similar objects without using classes and the new keyword. It’s a common pattern in JavaScript for implementing object creation and encapsulation.

function createObject(name, age, info) { const obj = new Object(); obj.name = name; obj.age = age; obj.running = function () { console.log(info); }; return obj; } const person = createObject('moment', 18, 'I can run'); const student = createObject('supper', 16, 'I can run, and I am younger than you'); person.running(); // I can run student.running(); // I can run, and I am younger than you

Through the factory pattern, we can quickly create many similar objects without duplicate code. However, a new problem arises: objects created by the factory pattern belong to Object and cannot be distinguished by object type, which is why the factory pattern is not widely used.

Constructor Functions

The constructor function pattern is a common way to create objects, using a function to define an object template. Object instances created using constructor functions can inherit properties and methods from the constructor. Constructor functions are typically used with the new keyword to create object instances.

Basic Form of Constructor Pattern

A constructor function is a regular function, usually capitalized (to distinguish it), and used with the new keyword to create object instances. The purpose of constructor functions is to set properties and methods for each instantiated object.

function Person(name, age, info) { this.name = name; this.age = age; this.running = function () { console.log(info); }; } const person = new Person('moment', 18, 'I can run'); const student = new Person('supper', 16, 'I can run, and I am younger than you'); person.running(); // I can run student.running(); // I can run, and I am younger than you

To create a Person instance, you should use the new operator. When using the new keyword, the constructor function performs the following steps:

  1. Creates a new object {} in memory.

  2. The internal [[Prototype]](proto) property of this new object is assigned to the constructor’s prototype property, i.e., person.__proto__ = Person.prototype.

  3. The this inside the constructor is assigned to this new object (i.e., this points to the new object this = person).

  4. Executes the code inside the constructor. For example, person.proto.name='moment'.

  5. If the constructor returns a non-null object, returns that object. Otherwise, returns the newly created object.

The execution process of new can be referenced in the following code:

function myNew(func, ...args) { // Check if it's a function if (typeof func !== 'function') { throw 'First parameter must be a function'; } // Create new object // This object's [[prototype]](implicit prototype __proto__) points to func's prototype object // So instances can access the constructor's prototype obj.constructor === Person const object = Object.create(func.prototype); // The this inside the constructor is assigned to this new object const result = func.apply(object, args); // If the constructor returns a reference type result, return the result after execution // Otherwise return the newly created obj const isObject = typeof result === 'object' && result !== null; const isFunction = typeof result === 'function'; return isObject || isFunction ? result : object; }

Looking at the console output, it’s not hard to see that both objects are instances of Object and Person:

// The instanceof operator is used to check if constructor.prototype exists in the prototype chain of the parameter object // The implicit prototypes of person and student both point to Person's explicit prototype console.log(person.__proto__ === student.__proto__); // true console.log(person instanceof Object); // true console.log(person instanceof Person); // true console.log(student instanceof Object); // true console.log(student instanceof Person); // true // The above code actually executes this example console.log(student.__proto__.__proto__.constructor.prototype === Object.prototype); // true console.log(student.__proto__.constructor.prototype === Person.prototype); // true

While constructor methods are useful, they also have issues. Although both person and student have a running method, these two methods are not instances of the same Function and point to different memory locations.

console.log(student.running === person.running); // false

They’re doing the same thing, but each instance creates its own method, causing unnecessary memory consumption. Is there a way to solve this problem effectively? This is where the prototype pattern comes in.

Prototype Pattern

The Prototype Pattern is a way of creating objects that allows us to create new objects by cloning existing ones, rather than directly instantiating a new object. JavaScript’s Prototype Inheritance is implemented based on the prototype pattern.

In JavaScript, every function (including constructor functions) has a prototype property that points to an object containing properties and methods shared by all instances. All instances created by that constructor will inherit the properties and methods from this prototype object. Using the prototype pattern can avoid creating identical methods in the constructor, thus saving memory.

function Person(name, age) { this.name = name; this.age = age; } // Here, the running method is defined on Person's prototype property // person and student share the same prototype method running, pointing to the same memory space Person.prototype.running = function (info) { console.log(info); }; const person = new Person('moment', 18); const student = new Person('supper', 16); // In constructor functions, this outputs false console.log(person.running === student.running); // true person.running('I can run'); // I can run student.running('I can run, and I am younger than you'); // I can run, and I am younger than you

From the previous new operator, we know that person and student’s implicit prototypes equal Person’s explicit prototype:

  1. First, person and student look for the running method on themselves, but don’t find it.

  2. They then look in the prototype, through person.__proto__ or student.__proto__. Since person.__proto__ = Person.prototype, calling person.running() actually calls Person.prototype.running() method

console.log(person.__proto__ === Person.prototype); // true console.log(student.__proto__ === Person.prototype); // true console.log(Person.prototype.constructor === Person); // true

The specific memory diagram is shown below:

Through the above diagram, the output results of the following code can be understood:

console.log(Person.prototype.constructor === Person); // true console.log(student.__proto__.constructor === Person); // true

Prototype Levels

Before we begin, let’s look at two important diagrams:

Before discussing prototypes, let’s first understand the relationship between Function and Object.

In JavaScript, every JavaScript function is actually a Function object. This means that functions themselves are objects, having properties and methods like objects do.

Almost all objects are instances of Object type, and they inherit properties and methods from Object.prototype. Object.prototype provides basic object functionality such as toString(), hasOwnProperty(), etc.

From a prototype chain perspective, Function inherits from Object, meaning Function.prototype inherits from Object.prototype. This allows functions to not only have their own methods and properties but also access methods and properties from Object.prototype.

From a constructor perspective, Function is also a constructor function used to create new function instances, and Function itself is constructed by Object. This means that Function itself is an instance of Object.

The following code demonstration should help clarify what was explained in the two diagrams above:

function Person(name, age) { this.name = name; this.age = age; } // Here, the running method is defined on Person's prototype property Person.prototype.running = function (info) { console.log(info); }; const obj = {}; const person = new Person('moment', 18); const student = new Person('supper', 16); console.log(obj.__proto__ === Object.prototype); // true console.log(Object.constructor === Function); // true console.log(Function.prototype.__proto__ === Object.prototype); // true console.log(Function.constructor === Function); // true console.log(Person.__proto__.constructor.__proto__ === Function.prototype); // true console.log(student.__proto__.__proto__.constructor.__proto__ === Function.prototype); // true console.log(Function.constructor.__proto__ === Function.prototype); // true console.log(Object.constructor === Function.constructor); // true console.log(Object instanceof Function); // true console.log(Function instanceof Object); // true console.log(Function.prototype.__proto__ === Object.prototype); // true

In the code above, all objects and functions are constructed by Function, meaning they all inherit from Function.prototype, while Function.prototype itself inherits from Object.prototype.

Through a series of prototype chain checks, the code verifies these inheritance relationships. For example, both Object.constructor and Function.constructor point to Function, reflecting that Object is constructed by Function. At the same time, functions themselves, being objects, have their own prototype chain, giving them both Function characteristics and inherited Object methods.

In this code, by examining __proto__ and constructor properties, we further understand how functions and objects are interconnected through the prototype chain. For instance, Function.prototype.__proto__ points to Object.prototype, and Object and Function are connected not only through constructors but also through the prototype chain forming inheritance relationships. Through these checks, the code reveals that Function is the “ancestor” of all constructor functions and objects, while also being an instance of Object itself.

Prototype Lookup Mechanism

The prototype lookup mechanism works as follows: First, JavaScript looks for the required property or method on the instance object itself. If found on the instance, that property or method is returned. If not found, it continues searching on the object’s prototype (proto). If the property or method exists in the prototype, it is returned. If still not found in the prototype, it continues searching up the prototype chain until reaching Object.prototype. If found on Object.prototype, that property or method is returned. If the property or method is not found anywhere in the prototype chain, undefined is returned.

Note that a TypeError is only thrown when attempting to call a non-existent function; otherwise, the lookup typically returns undefined when nothing is found.

function Person(name, age) { this.name = name; this.age = age; this.sayName = function () { console.log('I am on the instance'); }; this.memory = 'I belong to the instance'; } // Here, the running method is defined on Person's prototype property Person.prototype.running = function () { console.log('I am a method on the prototype'); }; Object.prototype.awesome = 'This is true'; Person.prototype.memory = 'I belong to the prototype'; const person = new Person('moment', 18); console.log(person.name); // from instance console.log(person.memory); // from instance person.sayName(); // from instance person.running(); // from prototype console.log(person.awesome); // This is true console.log(person.nonexistent); // undefined

The entire lookup flow is as follows:

person -> Person.prototype -> Object.prototype -> null

Drawbacks of the Prototype Pattern

While the prototype pattern has the advantage of saving overhead when creating new objects, it also has some drawbacks in practical applications.

Sharing Reference Type Data

When using the prototype pattern, if you define reference type data (such as arrays, objects, etc.) on the prototype object (e.g., Person.prototype), all instances will share this data. This can lead to data pollution or unintended side effects.

function Person(name) { this.name = name; } Person.prototype.hobbies = ['reading', 'traveling']; // reference type data const person1 = new Person('Alice'); const person2 = new Person('Bob'); person1.hobbies.push('swimming'); console.log(person1.hobbies); // ["reading", "traveling", "swimming"] console.log(person2.hobbies); // ["reading", "traveling", "swimming"] // affected person2 data

In this example, hobbies is defined on Person.prototype as an array, which is shared by all instances. If you modify person1.hobbies, it will also affect person2.hobbies. This shared reference type data behavior can lead to unpredictable side effects.

If you want to avoid sharing reference type data, you can initialize reference type data in the constructor or use closures in the prototype to create private data.

function Person(name) { this.name = name; this.hobbies = []; // initialize in constructor }

Prototype Chain Lookup Performance Issues

The prototype chain lookup in JavaScript is the core mechanism for object inheritance. However, in some cases, it can lead to performance issues. Whenever an object’s property is accessed, JavaScript will traverse the prototype chain until the property is found or the end of the prototype chain Object.prototype is reached.

If the inheritance chain is too deep, each lookup will increase the lookup time, leading to performance degradation. To avoid performance issues, you should simplify the inheritance hierarchy and avoid excessive nested inheritance. Using Object.create() can explicitly specify the prototype to reduce the lookup hierarchy. Caching frequently accessed properties or methods can also effectively improve performance by reducing repeated prototype chain lookups.

Prototype Chain Inheritance

The Prototype Chain Inheritance is the primary way to implement inheritance in JavaScript, using the prototype chain mechanism. Each JavaScript object has a prototype object (__proto__), through which sub-objects can access methods and properties from parent objects. The core idea of prototype chain inheritance is to allow sub-classes to access properties and methods from parent classes through the prototype chain.

Its construction principle is that each function object has a prototype property, pointing to an object containing shared instance properties and methods. Each instance object has an implicit __proto__ property, pointing to its constructor’s prototype through the prototype chain, allowing instances to access properties and methods from the constructor’s prototype. Through this mechanism, sub-class instances can inherit properties and methods from parent class prototypes, achieving inheritance.

function Student() {} function Teacher() {} Teacher.prototype.running = function () { console.log('teacher'); }; Teacher.prototype.teach = function () { console.log('teach'); }; const teach = new Teacher(); Student.prototype = teach; const student = new Student(); student.running = function () { console.log('I was modified'); }; const smallStudent = new Student(); smallStudent.running(); // teacher inherited from Teacher prototype student.running(); // I was modified from instance itself

Through a diagram to show the relationship between two instances and two constructors and their corresponding prototypes:

When using the prototype chain to implement inheritance, the prototype of the subclass actually becomes an instance of the parent class. This causes the parent class’s instance properties and methods to become prototype properties and methods of the subclass. This results in the subclass instances sharing properties and methods on the parent prototype, which may cause shared data issues. Another issue is that the subclass cannot directly pass parameters to the parent constructor when instantiating, which affects the parent constructor’s initialization.

function Animal(name) { this.name = name; // parent class instance property } Animal.prototype.sayHello = function () { console.log(`${this.name} says hello!`); // parent class method }; function Dog(name, breed) { Animal.call(this, name); // unable to pass breed to Animal constructor this.breed = breed; // subclass unique attribute } // Use prototype chain inheritance Dog.prototype = Object.create(Animal.prototype); Dog.prototype.constructor = Dog; const dog1 = new Dog('Buddy', 'Golden Retriever'); dog1.sayHello(); // "Buddy says hello!" (inherited from Animal method) const dog2 = new Dog('Max', 'Bulldog'); console.log(dog1.name); // "Buddy" console.log(dog2.name); // "Max" // Problem 1: Prototype Chain Shared Attribute dog1.name = 'Rocky'; // modify dog1 name console.log(dog1.name); // "Rocky" console.log(dog2.name); // "Max" (dog2 also affected, modified prototype attribute) // Problem 2: Unable to Pass breed Parameter to Parent const dog3 = new Dog('Rex'); // only passed name console.log(dog3.breed); // undefined (no breed passed)

Borrowing Constructor

The Borrowing Constructor is a way to inherit in JavaScript, using call() or apply() in the subclass constructor to inherit the parent class constructor. Its basic idea is to allow subclass instances to inherit parent class instance properties. The borrowing constructor does not change the prototype chain; it only ensures that subclass instances have parent class properties through calling the parent constructor.

Its working principle is as follows:

  1. Subclass constructor: In the subclass constructor, use call() or apply() to call the parent constructor, and set this to the current subclass instance.

  2. Parent constructor: The parent constructor is used to set instance properties (not methods), and call() or apply() allows the subclass to get parent instance properties.

function Teacher(nickname, age, height) { this.nickname = nickname; this.age = age; this.height = height; } function Student(nickname, age, height) { Teacher.call(this, nickname, age, height); this.hobby = ['singing', 'dancing', 'rapping']; } Teacher.prototype.running = function () { console.log('teacher'); }; Teacher.prototype.teach = function () { console.log('teach'); }; const student = new Student('moment', '18', '1.59 meters'); console.log(student.height); // 1.59 meters console.log(student.hobby); // ["singing", "dancing", "rapping"]

The borrowing constructor will have some problems:

  1. Unable to inherit parent prototype methods: The borrowing constructor only inherits parent class constructor defined instance properties, not parent class prototype methods (e.g., running).

  2. Unable to share parent prototype methods: Each time the subclass constructor is called, it creates parent class constructor defined instance properties, unable to share parent prototype methods, which may lead to memory waste.

The borrowing constructor is a simple inheritance method, allowing subclass instances to obtain parent class instance properties through calling parent constructor, but unable to inherit parent class prototype methods. To better inherit all properties and methods of parent class, usually need to combine prototype chain inheritance.

Combining Inheritance

The Combining Inheritance is a common inheritance pattern in JavaScript, combining the advantages of Prototype Chain Inheritance and Borrowing Constructor Inheritance, and effectively avoiding their drawbacks. The Combining Inheritance can inherit both parent class instance properties and parent class prototype methods.

The implementation steps of the Combining Inheritance are as follows:

  1. Call parent class constructor: In the subclass constructor, call the parent constructor (call()) to inherit parent class instance properties.

  2. Set subclass prototype object: Through Object.create() to create a new object as the subclass prototype object, and point it to parent class prototype to inherit parent class prototype methods.

  3. Repair constructor: Since Object.create() changes the constructor pointer, manually repair the constructor pointer to point to subclass constructor.

function Teacher(nickname, age, height) { this.nickname = nickname; this.age = age; this.height = height; } Teacher.prototype.running = function () { console.log('teacher'); }; Teacher.prototype.teach = function () { console.log('teach'); }; function Student(nickname, age, height) { Teacher.call(this, nickname, age, height); // inherit Teacher instance properties this.hobby = ['singing', 'dancing', 'rapping']; // student unique attribute } // Use prototype chain inheritance Teacher method Student.prototype = Object.create(Teacher.prototype); Student.prototype.constructor = Student; // repair constructor pointer // Create Student instance const student = new Student('moment', '18', '1.59 meters'); console.log(student.height); // 1.59 meters console.log(student.hobby); // ["singing", "dancing", "rapping"] student.running(); // teacher student.teach(); // teach

The drawback of the Combining Inheritance is that the parent constructor is executed twice when instantiating the subclass, once in call() for inheriting instance properties, and once in Object.create() for inheriting prototype methods. This can lead to unnecessary parent constructor execution.

Parasitic Combination Inheritance

To solve performance issues, parasitic combination inheritance (Parasitic Combination Inheritance) can be used, which is an optimization of the Combining Inheritance.

function inheritPrototype(superType, children) { const prototype = new Object(superType.prototype); // create object prototype.constructor = children; // enhance object children.prototype = prototype; // assign object } function Teacher(nickname, age, height) { this.nickname = nickname; } function Student(nickname) { Teacher.call(this, nickname); this.hobby = ['singing', 'dancing', 'rapping']; } inheritPrototype(Student, Teacher); Teacher.prototype.running = function () { console.log('teacher will run'); }; Student.prototype.running = function () { console.log('student will run'); }; const student = new Student('moment'); student.running(); // student will run console.log(student.hobby); // ['singing', 'dancing', 'rapping'] console.log(student.nickname); // comment

Through parasitic combination inheritance, the performance issues caused by executing the parent constructor twice can be solved by avoiding unnecessary parent constructor execution.

Summary

The Prototype Chain Inheritance is the core mechanism for object inheritance in JavaScript, through the prototype chain, sub-classes can inherit parent class instance properties and prototype methods. Through the Combining Inheritance, sub-classes not only inherit parent class instance properties but also share parent class prototype methods, avoiding memory waste. However, the Combining Inheritance has the problem of executing the parent constructor twice, leading to performance degradation. To solve this problem, parasitic combination inheritance optimizes performance by reducing unnecessary parent constructor execution, achieving a more efficient inheritance method.

Last updated on:
Copyright © 2025Moment版权所有粤ICP备2025376666