一篇文章弄懂JS原型
2024-12-18 08:47 阅读(136)

前言

在JavaScript中,原型(Prototype)是一个非常重要的概念,它是实现对象继承的基础。

每个JavaScript对象都有一个内部属性(即原型),指向另一个对象。这个原型对象也可以有一个原型,形成一个原型链(Prototype Chain)。当我们访问一个对象的属性或方法时,JavaScript 引擎首先会检查这个对象是否有该属性或方法。如果没有,它会查找这个对象的原型,直到找到该属性或方法,或者到达原型链的顶端(null)。

构造函数

在JS中,没有类这一概念,复杂类型都是对象类型,那我们要如何进行继承呢?JS的创造者用构造函数来实现继承机制。

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

let person = new Person('John', 18)

上述代码是一个构造函数的示例。在构造函数中通过this给每个实例赋值,这时就会有一个问题,这些实例无法共享公共属性,如果每一个实例对象都去定义一些共有的属性或者方法,那会浪费很多的精力,也会使代码效率更低。因此JS的设计者设计出了原型对象,用来存放构造函数的公共属性以及方法。

JS中自带的构造函数:

String

Number

Boolean

Object

Array



原型

1. 原型 (prototype) 是一个函数被定义出来天生就具有的属性

JS的每个函数在创建时,都会生成一个prototype属性,这个属性指向一个对象,这个对象就是这个函数的原型。

2. 函数原型存在的意义:因为实例对象能访问到函数原型上的属性,所以原型存在的意义就是为了让某一种数据结构拥有更多的方法可用

// 定义一个构造函数
function Car(make, model) {
    this.make = make; // 实例属性
    this.model = model; // 实例属性
}

// 在Car的原型上添加一个方法
Car.prototype.getDetails = function() {
    return `This car is a ${this.make} ${this.model}.`;
};

// 创建多个实例
const car1 = new Car("Toyota", "Camry");
const car2 = new Car("Ford", "Mustang");

// 调用共享的方法
console.log(car1.getDetails()); 
console.log(car2.getDetails()); 

// 为Car的原型添加一个新方法
Car.prototype.startEngine = function() {
    console.log(`The ${this.make} ${this.model}'s engine has started.`);
};

// 调用新增的方法
car1.startEngine(); 
car2.startEngine(); 

就像这个例子一样,我们可以添加很多个函数供实例对象调用。

3. 可以将一些固定的属性提取到原型上,减少重复的代码执行

最近,小米汽车因为它帅气的外表以及强大的性能爆火,订单量非常之大。假如我们用代码来演示每一辆 su7 的生产,每生产一辆,就生产一个实例对象

function Car(name, height, lang, color, owner){
    this.name = 'su7'
    this.height = 1400
    this.lang = 5000
    this.color = color
    this.owner = owner
}

可以用这样的构造函数生产,但是我们思考一下,每一辆车的名字,高度以及长度是不是都是一致的呢?那么我们是不是可以把这些共有的属性写进这个构造函数的原型上,那么就不需要每次都为车辆再次赋予这些属性。


让我们一起看一下修改后的例子:

Car.prototype.name = 'su7'
Car.prototype.height = '1700'
Car.prototype.lang = '5000'

function Car(color, owner){
    this.color = color
    this.owner = owner
}

let car1 = new Car('red', '阿炜')
let car2 = new Car('green', '小朱')
console.log(car1);
console.log(car2);

4. 实例对象无法修改函数原型上的属性

Car.prototype.run = function(){
    console.log('Running');
    
}
function Car(name){
    this.name = 'su7'
}

let car = new Car()
car.run()

car.run = function(){
    console.log('run');
}
car.run()

let car2 = new Car()
car2.run()

我们定义了一个 Car 构造函数,并在它的原型上添加了一个 run 方法。

创建了一个实例 car,并调用了原型上的 run 方法,输出running。

当我们尝试通过 car 实例修改该方法(car.run)时,虽然我们定义了一个新的函数,但这仅仅是给 car 添加了一个同名的方法,而不是修改 Car.prototype.run。

创建了另一个实例 car2,调用了原型上的 run 方法,仍然可以访问原型上的方法,输出running。


对象原型

1. 属性查找过程

当你试图访问一个对象的某个属性时,JavaScript 引擎会进行以下步骤:


检查对象自身:首先,它会检查对象是否具有该属性(即,该属性是否是对象的“直接拥有”属性)。

原型链查找:如果对象自身不具有该属性,JavaScript 会查找该对象的原型([[Prototype]],通常通过 __proto__ 或 Object.getPrototypeOf() 访问)。

继续查找:如果原型中也没有该属性,引擎会继续沿着原型链向上查找,直到找到该属性或到达原型链的顶端(即 null)。


示例:

function Animal() {
    this.legCount = 4;
}

Animal.prototype.getLegCount = function() {
    return this.legCount;
};

const dog = new Animal();

console.log(dog.legCount); // 输出: 4
console.log(dog.getLegCount()); // 输出: 4
console.log(dog.hasOwnProperty('legCount')); // 输出: true
console.log(dog.hasOwnProperty('getLegCount')); // 输出: false

在这个例子中,dog 对象有 legCount 属性,但没有 getLegCount 属性。访问 dog.getLegCount() 时,JavaScript 首先检查 dog 是否有 getLegCount,发现没有,然后查找 dog 的原型 Animal.prototype,在那里找到了该方法。

对象的隐式原型

在 JavaScript 中,每个对象都有一个隐式原型(也称为原型链接或原型链),它与创建这个对象的构造函数的显示原型有关。

当你使用构造函数创建对象时,该对象的隐式原型指向构造函数的显示原型。

示例:

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

Dog.prototype.bark = function() {
    console.log('Woof!');
};

const myDog = new Dog('Buddy');

console.log(myDog.__proto__ === Dog.prototype); // 输出: true
console.log(myDog.bark); // 输出: [Function] (函数本身)
myDog.bark(); // 输出: Woof!

在这个示例中,myDog 是 Dog 的一个实例。myDog 的隐式原型(myDog.__proto__)指向 Dog.prototype。这使得 myDog 可以访问 bark 方法,因为该方法定义在 Dog.prototype 上。

总结


v8在查找对象上的一个属性时,如果该属性不是对象显示拥有的,那么v8就会去对象的原型上查找

对象的隐式原型会被赋值成创建该对象的构造函数的显示原型


那这个时候可能就会有一个问题,所有的对象都拥有隐式原型吗?那一直往上查找原型链,到了顶端那是不是最顶端的对象没有隐式原型呢?

答案是所有的对象都是有隐式原型的,最顶端的隐式原型指向的是null

js 代码解读复制代码

// 创建一个构造函数
function Animal(name) {
    this.name = name;
}

// 在原型上添加一个方法
Animal.prototype.speak = function() {
    console.log(`${this.name} makes a noise.`);
};

// 创建一个实例
const dog = new Animal('Dog');

console.log(dog.name); // 输出: Dog
dog.speak(); // 输出: Dog makes a noise.

// 检查隐式原型
console.log(Object.getPrototypeOf(dog) === Animal.prototype); // 输出: true
console.log(dog.__proto__ === Animal.prototype); // 输出: true
console.log(dog.__proto__.__proto__ === Object.prototype); // 输出: true
console.log(dog.__proto__.__proto__.__proto__); // 输出: null

这个例子中dog.__proto__.__proto__.__proto__最后输出的便是null

new 的原理


创建一个 this 对象

让构造函数中的逻辑正常执行(相当于往 this 对象上添加了属性)

让 this 对象的 proto = 构造函数的 prototype

return this 对象

// 定义一个构造函数
function Person(name, age) {
    // 1. 创建一个 this 对象,模拟 new 的过程
    // 在构造函数内部,this 是自动创建的
    
    // 2. 让构造函数中的逻辑正常执行
    this.name = name; // 将 name 属性添加到 this 对象
    this.age = age;   // 将 age 属性添加到 this 对象
}

// 3. 创建一个实例
const person1 = new Person('Alice', 30);

// 检查 person1 的属性
console.log(person1.name); // 输出: Alice
console.log(person1.age);  // 输出: 30

// 4. 检查 person1 的原型
console.log(person1.__proto__ === Person.prototype); // 输出: true
console.log(person1 instanceof Person); // 输出: true

// 5. 原型上定义一个方法
Person.prototype.greet = function() {
    console.log(`Hello, my name is ${this.name}`);
};

// 调用原型上的方法
person1.greet(); // 输出: Hello, my name is Alice

创建一个 this 对象:当使用 new 关键字调用构造函数 Person 时,JavaScript 会自动创建一个新的空对象,并将这个对象赋值给 this。

让构造函数中的逻辑正常执行:在构造函数内部,使用 this 来添加属性。这里 this.name 和 this.age 会将传入的值绑定到新创建的对象上。

让 this 对象的 __proto__ 指向构造函数的 prototype:新的对象 person1 会有一个隐式原型(可以通过 __proto__ 访问)指向 Person.prototype。这使得 person1 可以访问到定义在构造函数原型上的属性和方法。

返回 this 对象:在默认情况下,构造函数会返回这个新对象,尽管如果构造函数显式返回一个非基本类型的值,那么返回的值会是该值,而不是这个新创建的对象。不过在这个例子中,我们不需要考虑显式返回。


相信大家看到这里,对JS的原型都有了更深的理解,有哪里有错误,也请各位大佬指出。