对象原型实现继承

在 JavaScript 中,每个对象都有一个原型(prototype),它是一个内置对象 [[prototype]],浏览器中写为 __proto__,指向该对象的原型。

那么指向的这个对象有什么用呢?(为什么JavaScript要偷偷的设计这么一个属性呢?)

  1. 当我们通过引用对象的属性key来获取一个value时,它会触发[[Get]]的操作;
  2. 这个操作会首先检查该对象是否有对应的属性,如果有的话就使用它
  3. 如果对象中没有改属性,那么会访问对象[[prototype]]内置属性指向的对象上的属性;

那么这样的一种“特性”又有什么用呢?那么这个特性就可以帮助我们实现继承了!JavaScript 是一门支持多范式的编程语言,一方面他支持函数式编程开发,同时也支持面向对象的形式开发。而继承就是面向对象一大特性。

对于任何一个对象,获取该原型对象的方法有两种:

  • 方式一:通过对象的 __proto__ 属性可以获取到(但是这个是早期浏览器自己添加的,存在一定的兼容性问题);(开发测试使用)
  • 方式二:通过 Object.getPrototypeOf 方法可以获取到,兼容性更好,推荐使用;(生产环境使用)

判断指定的属性是否在指定的对象或其原型链中可以使用 in 运算法

每个对象都有一个原型,而原型本身也是一个对象,因此可以形成一个原型链。当访问一个对象的属性或方法时,JavaScript 首先在对象本身查找,然后在其原型上查找,如果还没有找到,就会继续在原型的原型上查找,以此类推,直到找到或者到达原型链的末端 null。

const person1 = {
    name: 'heo'
}
const personPrototype = {
    sayMyName() {
        console.log(this.name)
    }
}
Object.setPrototypeOf(person1, personPrototype)
person1.sayMyName()

但是这种形式每次创建一个新的 person都需要手动设置原型(如上),非常麻烦,所以 js 为我们提供了一种更加简单的形式,使用函数的 prototype 实现继承。

函数原型实现继承

  • 继承者对象可以访问原型对象的属性和方法,就好像它自己拥有这些属性和方法一样。
  • 如果继承者对象自身有和原型对象同名的属性或方法,那么它会覆盖原型对象的属性或方法。

在 JavaScript 中,除了对象都具有的上述属性外,所有函数还都有一个 prototype 属性(这个是函数原型,上面的 [[prototype]] 或者 __proto__ 是对象原型),它是一个对象。

那么我们想到是实现继承该怎么办呢?我们就可以使用构造函数。

构造函数实例化的过程(new 的过程):

  1. 创建一个新的空对象
  2. 将函数的 this 绑定到这个对象上
  3. 将函数的 prototype 赋值给新对象的 [[prototype]] 属性
  4. 默认返回这个新对象

因此使用构造函数的 prototype 属性极大简化了手动设置原型对象的继承方式。

// 构造函数
function Person(name) {
  this.name = name;
}

// 在构造函数的 prototype 上添加方法
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

// 使用 __proto__ 访问原型
console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

在这个示例中,Person 是一个构造函数,Person.prototype 是一个对象,我们将方法 sayHello 添加到了 Person.prototype 上。当创建 person1person2 对象时,它们继承了 Person.prototype 的属性和方法,因此可以调用 sayHello 方法。

但是如果我们使用 new 的构造函数拥有显示的返回值呢?

  1. 如果显示的返回一个对象,则不再使用内部创建的 空对象,而是使用返回的对象。
  2. 如果显示的返回一个基本类型值,则被忽略,仍然按照原方式处理。
function Creator() {
    this.value = 1
    return { value: 2 }
}
// 不再使用内部创建的 空对象,而是使用返回的对象。
const ins = new Creator()
console.log(ins.value) // 2

function Creator2() {
    this.value = 1
    return 2
}
// 返回一个基本类型值,则被忽略
const ins2 = new Creator2()
console.log(ins2.value); // 1

类的原型对象:

类的出现主要用于创建对象和使得继承更加清晰。提高代码的可读性和简洁性。

类出现的原因:

  1. 原型链复杂易混淆,学习曲线陡峭,模拟类和继承不够直观
  2. 私有成员实现困难,旧语法难以实现真正私有,需要借助闭包实现
  3. 代码可读性待提高,函数嵌套多,结构不清晰

类是构造函数的语法糖,因此类的实例化过程实际上也是通过构造函数来创建对象的,也是基于原型链的封装,因此类和构造函数都具有原型的概念。

在类中,类的原型可以被称为类的原型对象(class prototype object),它类似于构造函数的原型。类的实例会继承类的原型对象上的方法和属性,类似于使用构造函数创建的对象会继承构造函数的原型上的方法和属性。

class Person {
  constructor(name) {
    this.name = name;
  }

  sayHello() {
    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

console.log(person1.__proto__ === Person.prototype); // true
console.log(person2.__proto__ === Person.prototype); // true

在这个示例中,Person 是一个类,Person.prototype 是类的原型对象,我们在类中定义了方法 sayHello,该方法被添加到了 Person.prototype 上。当创建 person1person2 对象时,它们继承了 Person.prototype 的属性和方法,因此可以调用 sayHello 方法。

总之,类和构造函数都具有原型的概念,类的实例化过程实际上也是通过构造函数来实现的,因此类的原型在类的继承和方法共享方面具有重要作用。


手写 new 和手写 instanceof

/**
 * 模拟实现 new 操作符的函数
 * @param {Function} Constructor 构造函数
 * @param {...any} args 传递给构造函数的参数
 * @return {*} 如果无返回值或者显示返回一个对象,则返回构造函数的执行结果;如果显示返回一个基本类型,则返回构造函数的实例
 */
function myNew(Constructor, ...args) {
    // 1. 创建一个全新的空对象 2. 为这个空对象设置原型(__proto__)
    // 可以使用 {},但是推荐使用 Object.create() 创建对象并设置原型
    const instance = Object.create(Constructor.prototype)

    // 3. 绑定构造函数的this为其新创建的空实例对象,并执行构造函数体
    const result = Constructor.apply(instance, args)

    const isObject = typeof result === 'object' && result !== null
    const isFunction = typeof result === 'function'
    // 4. 如果构造函数返回一个非原始值,则返回这个对象;否则返回创建的新实例对象
    if (isObject || isFunction) return result
    return instance
}

/**
 * 模拟 instanceOf 的实现
 * @param object 实例对象
 * @param Constructor 构造函数(类)
 * @return {boolean}
 */
function myInstanceOf(object, Constructor) {
    // 初始获取对象的原型
    let proto = Object.getPrototypeOf(object)

    while (true) {
        // 遍历到原型链顶端
        if (proto === null) return false
        // 找到匹配的原型
        if (proto === Constructor.prototype) return true
        // 继续向上查找原型链
        proto = Object.getPrototypeOf(proto)
    }
}

为什么修改原型对象后就不能使用constructor判断数据类型,但是可以使用instanceof

现在将原型对象Person.prototype修改为一个新的对象{a:1},这个新的对象的构造函数是 Object,所以 Person.prototype.constructor === Object 是对的,之前 Person.prototype.contrutor === Person 是对的,现在就是错的了。

所以不能使用 construtor 来判断了,但是可以使用 instanceOf。

因为 const p = new Person(‘xx’),p instanceOf Person 的原理是 在 p 以及其原型链找 Person.prototype。我们确实修改了 Person.prototype,但是只能说 Person.prototype 和之前的 Person.prototype 不一样,但是 instanceof 只需要判断 Object.getPrototypeOf(p) 在原型链上递归能否找到 Person.prototype,至于它变没变,无所谓。

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

console.log(Person.prototype.constructor === Person) // true

// 替换整个原型对象
Person.prototype = {
    sayHi() {
        console.log("hi");
    }
};

console.log(Person.prototype.constructor === Object) // true

let p = new Person("Tom");

console.log(p.constructor); // 变成 Object
console.log(p instanceof Person) // true

原型继承关系

原型继承关系终极理解图:

在这里插入图片描述

解释:

// 第一行
function Foo() {}
Foo.prototype = { constructor: Foo }
const f1 = new Foo();
const f2 = new Foo();
f1.__proto__ = Foo.prototype;
f2.__proto__ = Foo.prototype;

// 第二行
// 浏览器默认创建的函数
function Object() {}
Object.prototype = { constructor: Object }
Object.prototype.__proto__ = null;
const o1 = {}
const o2 = new Object() // 同理 o1
o1.__proto__ = Object.prototype;
o2.__proto__ = Object.prototype;
Foo.prototype.__proto__ = Object.prototype;
Function.prototype.__proto__ = Object.prototype;

// 第三行
// 浏览器默认创建的函数
function Function() {}
Function.prototype = { constructor: Function }
function Foo() {} // 原理也是 const Foo = new Function()
Foo.__proto__ = Function.prototype;
Object.__proto__ = Function.prototype;
Function.__proto__ = Function.prototype;

下面是一个简单的示例来说明原型链的概念:

function Tree(height) {
    this.height = height;
}

Tree.prototype.getHeight = function () {
    return this.height;
}

const highTree = new Tree(10);

// highTree.__proto__ === Tree.prototype; // true
// Tree.__proto__ === Function.prototype; // true

// highTree.__proto__.__proto__ === Object.prototype; // true
// highTree.__proto__.__proto__ === Function.prototype  // false
// Object.prototype.__proto__ === null; // true

// Object.__proto__ === Function.prototype; // true
// Function.__proto__ === Function.prototype; // true
// Function.prototype.__proto__ === Object.prototype; // true


将 class 写法转为 function 的写法

class Example {
    constructor(name) {
        this.name = name;
    }

    func() {
        console.log(this.name);
    }
}

// 要求:将 class 写法转为普通构造函数的写法
// 1. class 类内均使用严格模式
'use strict';
function Example(name) {
    // 2. 类的实例必须通过 new 关键字调用 而 function可以直接调用
    if (!new.target) {
        throw new TypeError('Class constructor Example cannot be invoked without new');
    }
    this.name = name;
}
Object.defineProperty(Example.prototype, 'func', {
    value: function () {
        // 4. 不可以直接 new Example.prototype.func() 进行调用
        if (!new.target) {
            throw new TypeError('Example.prototype.func is not a constructor');
        }
        console.log(this.name);
    },
    // 3. class 中的方法成员都是不可枚举的,而function中的方法成员是默认是可枚举的
    enumerable: false
})

原型面试题(总结)

在JavaScript 中,原型(Prototype)是每个JavaScript对象都具有的一个内部属性。这个属性是一个指向另一个对象的引用,这个对象被称为“原型对象”。

具体来说,当我们访问一个对象的属性或方法时,JavaScript引擎首先会检查该对象本身是否具有该属性或方法。如果没有找到,它会顺着原型链(Prototype Chain)向上查找,直到找到为止。

原型链则是指对象之间通过原型属性所形成的链条,这种机制允许 JavaScript对象实现继承和属性查找。

函数的原型,通过new创建出来的对象,会将函数的原型 prototype 赋值给对象的 __proto__.

JavaScript 在设计之初受到了多种编程语言的影响,而原型原型链就是有借鉴Self语言的。

JavaScript中的原型和原型链设计的主要目的是为了实现继承,而继承是面向对象编程的重要特性之一。

JavaScript是一门支持多范式的编程语言,一方面它支持函数式编程来开发,同时也支持面向对象的形式来开发。
而继承是面向对象的一个大特性,一方面可以减少我们编写重复代码,另一方面也是多态的前提。

JavaScript采用的是基于原型的继承模型,这一模型使得对像之间可以共享属性和方法。

原型链提供了一种属性和方法查找机制。
当我们访问一个对象的属性或方法时,JavaScript首先会检查对象本身是否具有该属性或方法。
如果没有找到,它会顺着原型链向上查找,直到找到为止。
这种查找过程通过共享原型对象,实现了对象之间的继承,而无需在每个对象中重复定义属性或方法,从而节省了内存并简化了代码结构。

这也是当我们通过 new 关键字来调用一个构造函数时,构建函数底层帮助我们完成的操作:

在内存中创建一个新的空对象 {}
将函数的this绑定到新对象
这个对象内部的 [[prototype]] 属性会被赋值为该构造函数的 prototype 属性
构造函数会返回这个新对象

其中对象内部的 [[prototype]] 属性会被赋值为该构造函数的prototype 属性就是利用原型设计来完成通过构建函数创建出来的多个对象指向同一个原型。

当然,可以通过回答原型链终极图来进一步阐述整个原型的关系。

在 JavaScript 中,原型链的终点是null 。

当一个对象的_proto_属性为 null 时,这意味着它已经到达了原型链的终点。
通常,所有对象的原型链最终都会追溯到Object.prototype,而Object.prototype._proto._就是null。
因此,nul标志着原型链的结束。

在这里插入图片描述

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐