JavaScript 模拟面向对象的内部机制

刨析 JavaScript 模拟面向对象的内部机制


作者李俊才(CSDN: jcLee95 )
邮箱 :291148484@163.com
CSDN 主页blog.csdn.net/qq_28550263…
本文地址blog.csdn.net/qq_28550263…

目 录


1. 构造对象的方法

2. 原型 与 原型链 的概念

3. JavaScript 继承 的内部机制


1. 构造对象的方法

1.1 通过字⾯量构造

let obj = {
  name: "jcLee95", 
  birthday: "07-30"
};

1.2 通过Object构造器构造

let obj = new Object();
obj.name = "jcLee95";
obj.birthday = "07-30";

1.3 通过原型构造

let obj = Object.create({
  name: "jcLee95",
  birthday: "07-30"
});

1.4 函数的构造调用

JavaScript 始终不是一个完全的面向对象编程语言,更多地像是在模拟面向对象地编程方式,原型链、构造调用等都是实现其面向对象地重要技术环节之一。构造调用 在形势上看,是使用 new 语法糖的函数调用,本质上是JavaScript函数的一种调用方式,用于实现对基于类面向对象编程语言中类的实例化过程。

这里说关键字 new是一个 语法糖 是因为,它其实完全就是一些列数据变换过程的简写,仅此而已。你完全可以 自己手动实现一个 new 函数,这在本章之后的内容中会介绍。实事上只要你愿意在其很多面向过程的语言里面也可以来模拟,比如 C语言,这就是至今有大佬认JavaScript 能算面向对象的语言的原因,当然更存在强面向对象语言中一切皆可对象的说法,因为它有很多东西压根就不是对象,如一些类型的字面量。

1.4.1 从ES6 class 构造调用说起

ES6class语法糖中,构造函数名用constructor表示,比如:

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

虽然本质不同,但用法和 Java 等语言中的类比较类似,如果你要创建 Person 类的实例,应该这样子:

let jack = new Person("jcLee95", "07-30");
console.log(jack);

Out[]:

Person { name: 'jcLee95', birthday: '07-30' }

1.4.2 在 ES6规范 出现之前的 构造函数

更长的一段里 JavaScript 中是没有 class 语法糖 的,只能通过原始的 new函数(构造函数)的方式来进行调用。例如:

// 相当于 class 语法中的 constructor 函数。
function Person(name, birthday) {
  this.name = name;
  this.birthday = birthday;
}

这与基于 class 的写法创建了一个一样的对象,因此对他们进行相同的调用将产生一样的效果:

let jack = new Person("jcLee95", "07-30");
console.log(jack);

Out[]:

Person { name: 'jcLee95', birthday: '07-30' }

我们的思路始终是很清晰的那就是 从模拟 class 来,到 class 去,这就是我们先对 ES6 中 class 写法进行介绍的原因。我们对构造函数进行大写的原因在于,这是借用了 Javaclass 的习惯:构造方法 与 类 同名,比如:

// java 中的构造方法
public class Person{
    String name;
    String birthday;
    // 构造方法与类同名
    public Person(String name, String birthday){
        this.name = name;
        this.birthday = birthday;
    }
}

但事实上,在 JavaScript 中,你可以使用 任意合法的标识符 作为构造函数名,虽然我们不推荐这样做。

2. 原型 与 原型链 的概念

2.1 原型

2.1.1 原型的概念

原型(prototype) 是 JavaScript 对象 上的 一个 特殊 属性,用于 共享数据,它被称作 原型对象原型 来源于 JavaScript 函数 ,我们每创建一个函数都有一个原型(prototype)属性 。因此,不论你使用将要把某个任意地 JavaScript 函数用作构造函数,比如以下地写法总是可以的:

function Person() {}
  
Person.prototype.name = "jcLee95";
Person.prototype.birthday = "07-30";

console.log(Person.prototype);

Out[]:

{ name: 'jcLee95', birthday: '07-30' }

2.1.2 关于 this 上下文

我们通过 函数名.prototype 的方式,可以对一个函数 原型对象 中的属性进行修改,但是 只有在使用 构造调用一个函数时,才能将函数体内部的 this动态地绑定到新创建的一个对象的上下文中。

这里的区别在于:

(1)函数在没有发生构造调用的情况下

  • 不会新创建一个对象 {} 作为被被构造出来的对象;
  • this 在全局执行环境中(在任何函数体外部)都指向 全局对象,如在浏览器中, window 对象同时也是全局对象;严格模式下将有所不同,函数内部this会若无赋值,将一直保持为undefined
    例如以下代码:
function Person() {
  console.log("this 1 =",this)
}
Person();

console.log("this 2 = ",this);

在 nodeJS中结果为:
JavaScript 模拟面向对象的内部机制

在浏览器中:
JavaScript 模拟面向对象的内部机制

(2)函数在发生构造调用的情况下

这任然是JavaScript对面向对象语言的模拟的具体实现之一。一旦函数发生了构造调用,this就应该像 Java 等语言的某个中的this一样,将指向本类JavaScript 中,构造调过程中被创建的一个新对象就是模拟 Java 中被声明的一个类所代表的对象,这就不难理解:JavaScript 构造调用中,新对象将作为构造函数中的this的上下文,例如:

// 构造调用中的 this ,指向构造函数数所构造的类实例,它是一个在构造调用过程中创建的新的对象
function Person(name, birthday) {
  this.name = name;
  this.birthday = birthday;
  console.log(this);
}

new Person("jcLee95", "07-30");

也可以使用 class 语法糖:

// 使用 ES6 class 语法糖 的等效方式
class Person {
  constructor(name, birthday) {
    this.name = name;
    this.birthday = birthday;
    console.log(this);
  }
}

new Person("jcLee95", "07-30");

输出的结果都为:
Out[]:

Person { name: 'jcLee95', birthday: '07-30' }

可以看到, this已经不再是普通调用下的那个this

2.1.3 区分 prototype[[Prototype]]__proto__

前面我们已经说过,prototype是任意函数的一个属性,不一定是构造函数上的。

[[Prototype]] 可以认为是一种连接方式,用于链接实例构造函数原型构造调用 过程中创建了一个新的对象,即实例对象。而[[Prototype]] 就是在这个新对象创建后,该对象上用于 指向发生构造调用的函数(构造函数)的原型(即构造函数的prototype 属性)的指针,称作 原型指针

需要指出的是,一些资料不区分地将 prototype[[Prototype]] 都称作原型,这不太准确,也容易导致初学者误解概念。

[[Prototype]]来源于 ECMA-262规范中的定义,但实际在很多浏览器的实现中,为每个对象都提供了一个 __proto__ 的指针,它就相当于 [[Prototype]],如最主流的chrome、Firefox、Safari浏览器。

2.1.4 使用函数的方式实现 new(即所谓手写 new)

篇初已经指出构造调用中的new 不过是一个语法糖,用于模拟面向对象的类。我们介绍到这里已经陆续讲清楚了 JavaScript 中构造调用的各个环节的原理。现在只需要归纳 new 关键字在实际作用在一个函数时发送构造调用的具体步骤:

  • (1)创建一个空的简单JavaScript对象(即{});
  • (2)为步骤1新创建的对象添加属性__proto__,将该属性链接至构造函数的原型对象
  • (3)将(1)中新创建的对象作为this的上下文 ;
  • (4)如果该函数没有返回对象,则返回this

为了加深读者对于这几个过程的理解,我们可以通过函数的形式自己实现一个与关键字 new 有着相同功能的函数:

// `myNew(cstrct, ...params)` be equal to `new cstrct(...params)`
function myNew(cstrct, ...params){
  // 1. create a new JavaScript object obj
  const obj = {};
  
  // 2. Add the attribute `__proto__` to the newly created object (obj),
  // And link this property to the prototype object of the constructor.;
  obj.__proto__ = cstrct.prototype; 
  
  // 3. Take the new object (obj) as the context of this;
  // 3.1 Temporarily hang the constructor on obj.__proto__
  obj.__proto__._func = cstrct;
  
  // 3.2 Execute this constructor to get "this"
  let _ = obj._func(...params);
  
  // 3.3 Delete the temporarily mounted attribute `_func` (otherwise this attribute will be added to the instance)
  delete obj.__proto__._func
  
  // 4. If the function does not return an object, that is, null or undefined, it returns this; otherwise, it returns the object.
  return _ instanceof Object ? _ : obj;
}

你可以编写一个函数,使用自己编写的 new 函数调用试试:

// A function used as a constructor
function Person(name, birthday) {
    this.name = name;
    this.birthday = birthday;
    console.log(this);
}

// 构造调用,与使用 new 返回的效果完全一样
myNew(Person, "jcLee95", "07-30")

Out[]:

Person { name: 'jcLee95', birthday: '07-30' }

2.2 原型链

至此我们已经了解过什么是 原型。那么试想一下,如果我们让一个对象的原型对象(prototype)恰好为另一个类型的实例会怎么样呢?

JavaScript 中⼏乎所有对象都可以访问 其构造器上的原型对象,而 原型对象(prototype)⼜可以访问 它⾃身的构造器上的原型对象(prototype)。以此类推,就像通过原型在原本独立的对象之间手拉着手不再独立,形成了一个 链式结构,即 原型链
JavaScript 中,原型链 是其实现 继承 的关键技术支撑,关于继承的相关内容,我们将在下一章进行介绍。

3. JavaScript 继承 的内部机制

继承 是面向对象编程的基本特点(抽象、封装、继承、多态)。上面讲了这么多,其实主要还是关于对象的创建于原型的。要称得上 面向对象编程,仅有这些是不够的,必须还要有 继承。早期的 JavaScript 的确从传统面向对象语言中借鉴很多,就比如继承。只是由于 JavaScript 自身本没有类的概念,只能通过已有的东西来 模拟继承。

3.1 从 Java 的继承说起

以Java为例,继承的本质作用是 允许创建分层次的类。面向对象编程中的继承表示的是生活中不同事物的所属关系,例如从生物学的意义上看人类是动物的一个子类别,那么可以以动物类所谓人类的父类。

// java 中的继承

// 父类:动物类
public class Animal { 
    private String name;
    // 动物类(作为父类)的构造函数
    public Animal(String myName) { 
        name = myName; 
    } 
    public void eat(){ 
        System.out.println(name+"在干饭"); 
    }
    public void sleep(){
        System.out.println(name+"正在睡觉");
    }
}

// 子类:人类
public class Human extends Animal {
    private String name;
    // 人类(作为子类)的构造函数
    public Human(name){
        // 调用父类的构造函数
        super(name);
    }
}

Java 语言中的 super 和 super()

Java语言中:

  • super 关键字可以来实现对父类成员的访问,用来引用当前对象的父类。
  • super 是一个函数,调用父类中构造器。
  • 子类是继承父类的构造方法的,它只是调用(隐式或显式)。
    • 如果父类的构造器带有参数,则必须在子类的构造器中显式地通过 super 关键字调用父类的构造器并配以适当的参数列表。
    • 如果父类构造器没有参数,则在 子类的构造方法 中不需要使用 super 关键字调用 父类构造方法,系统会自动调用父类的无参构造方法

在本例中, 人类(Human) 继承于 动物类(Animal)

3.2 ES6 class 语法中的继承

我们之所以要先讲 ES6 中基于 class 语法的继承,是因为该语法糖的用法已经更为接近 JavaScript 创造初要模拟继承的初忠。使用 ES6 的 class 改写上面的 Java 代码为 JavaScript 代码,你会发现形式上看起来是很接近的:

// JavaScript 中的继承(使用ES6 class语法)

// 父类:动物类
class Animal { 
  // 动物类(作为父类)的构造函数
  Animal(name) { 
    this.name = name; 
  }

  eat(){ 
    console.log(this.name+"在干饭"); 
  }

  sleep(){
      console.log(this.name+"正在睡觉");
    }
  }
  
// 子类:人类
class Human extends Animal {
  // 人类(作为子类)的构造函数
  Human(String name){
    // 调用父类的构造函数
    super(name);
  }
}

JavaScript(ES6) 中的 super 和 super()

在 ES6 语法规范中:

  • super 关键字可以来实现对父类成员的访问,用来引用当前对象的父类。

  • super 是一个函数,调用父类中构造器。如果父类构造器中含有参数,应该按照父类构造器的参数规则传入 super()函数中。

  • 只能在派生类的构造函数中使用 super(),如果尝试在非 extends 关键字声明的类或函数中使用,则会导致程序抛出错误。

  • 在构造函数中访问 this 之前一定要先调用 super(),它 负责初始化 this,因此如果你在调用 super()前访问 this则会导致程序序抛出错误。
    例如将上面的程序中子了 Human 的构造函数改为:

    class Human extends Animal {
      // 显示声明了子类(派生类)的构造函数,但没有调用 super()
      constructor(name){
      }
    }
    

    这将导致在你运行程序时引发一个 ReferenceError
    ReferenceError: Must call super constructor in derived class before accessing ‘this’ or returning from derived constructor
    意思是在访问 this从派生构造函数 返回之前,必须调用派生类中的 super() 构造函数。
    JavaScript 模拟面向对象的内部机制

  • 如果你不想在一个派生类的构造函数中调用 super(),则唯一的方法是让类的构造函数返回一个对象。

  • 如果你不显式声明派生类的构造函数,派生类将 “继承” 其父类的构造函数作为自己的构造函数。(实际上不是真实意义上的继承,而是在构造时在其原型链上调用了父类的构造函数)因此如果子类不需要对父类的构造函数进行装饰时,你完全可以选择不写子类的构造函数,在使用时只要按照父类的构造格式进行构造调用即可。

虽然我们这里又要提醒,JavaScript 继承背后的机理与 Java 不同。但是可以看到,ES6 中的 class 继承 语法上还是挺像的。

3.3 ES6 之前 JvaScript 中的继承

3.3.1 JavaScript 继承的特点 与 原型链

继承 是面向对象编程中最最重要的特征之一,被认为是 面向对象的基石。 ES6 标准后新增的伪类(class语法糖)已经和一般的面向对象语言写起来非常相似了。 但是毕竟 JavaScript 从一开始就是在模拟面向对象编程语言的行为特点,因此在其历史上也出现过很多种继承的实现方案。在 JavaScript 中提供了函数原型,这样我们可以使用原型链来完成实现继承,但这种继承和其它的语言的继承有内在的不同。

Java 为例,在Java的继承的过程中 子类 能够且仅能够从 父类 中获得 成员变量方法(不包括构造方法)、内部类(包括接口枚举
例如:

// 动物类
public class Animal { 
    private String name;
    
    public Animal(String myName) { 
        name = myName; 
    } 
    public void eat(){ 
        System.out.println(name+" is eating!");  
    }
    public void sleep(){
        System.out.println(name+"is sleeping!"); 
    }
}

// 人类 继承于 动物类,可以获得 Animal 类的成员变量、方法、内部类
public class Human extends Animal {;
    public Human(String name){
        super(name);
    }
}

public class Run{
    public static void main(String[] args) {
        Human a = new Human("Human");
        // 调用人类从动物类继承过来的 eat 方法
        a.eat();
    }
}

编译,运行:
JavaScript 模拟面向对象的内部机制
JavaScript 模拟面向对象的内部机制
可以看到,虽然Human类没有定义eat方法,但是从父类Animal获得了该方法。
Java 的继承中,实际上子类中所获得的方法、成员等,都是父类中相应成员的复制。在子类中重写相应的方法能够覆盖父类中的方法。

那么 JavaScript 呢?还记得这段代码吗:

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

    eat(){
        console.log(this.name + " is eating!")
    }

    sleep(){
        console.log(this.name + " is sleeping!")
    }
}

class Human extends Animal{
    constructor(name,nationality){
        super(name);
        this.nationality = nationality;
    }

    think(){
        console.log(this.name + " is thinking!")
    }
}

let person = new Human("jcLee95", "China");
console.log(person);
person.eat();
person.sleep();
person.think();

看起来简直不能和Java说很像,但是在继承的原理上看:

  • 子类实例要访问父类的方法,实际上是通过子类实例对象 的 原型指针 __proto__指向子类构造函数的原型对象(prototype属性),这个原型对象中,也包含一个原型指针,指向了父类构造函数的原型。
  • 事实上,在 JavaScript 中当代码读取某个对象的某个属性中,有一套这样的查询过程,是该语言比较有特点的地方:
    • 从对象实例本身的属性开始搜索:
      • 如果在实例中找到了具有给定名字的属性,则返回该属性的值;
      • 如果在实例中找不到:则继续收缩 原型指针(__proto__)所指向的原型对象(prototype),在该对象中继续查找具有给定名字的属性,如果找到,则返回该属性的值。

3.3.2 刨析原型链继承的过程:手写其详细步骤

3.3.2.1 父类的函数表示

(1)父类的构造函数
function Animal(name){
  this.name= name;
}
(2)父类的成员
let AnimalPrototypes = {
  eat:function() {
    console.log(this.name + " is eating!")
  },
  sleep:function() {
    console.log(this.name + " is sleeping!")
  }
}

for(let i in AnimalPrototypes) {
  Animal.prototype[i] = AnimalPrototypes[i];
}

说明
从基本功能上看,这种直接给 prototype赋值的方法也可以实现,而且看起来更简单:

Animal.prototype = {
  eat:function() {
    console.log(this.name + " is eating!")
  },
  sleep:function() {
    console.log(this.name + " is sleeping!")
  }
}

但是这会覆盖Animal.prototype原先包含的信息,使得使用Animal作为构造函数创建实例时,看起来像普通对象:
JavaScript 模拟面向对象的内部机制
可以看到这个例子构建的实例,在 NodeJS 中打印成了,{ name: 'animal_instance' },不能很好地表示该对象是通过 new 构建的 Animal 类的实例,这有时候在编程中不方便我们调试。

而使用Animal.prototype[i]这种添加对象中的键值对的方式:
JavaScript 模拟面向对象的内部机制
可以看到,打印的实例对象结果为:

Animal { name: 'animal_instance' }

很直观地显示出了这个对象是 Animal 构造函数经过构造调用而创建的 Animal 类的实例。

3.3.2.2 子类的函数表示

(1)子类的构造函数

子类的构造函数中是需要调用父类的构造函数的,使用 ES6的 class 语法时我们是这样写的:

constructor(name,nationality){
  super(name);
  this.nationality = nationality;
}

可见分为两个部分,第一个部分是父类的构造函数,它需要传入父类构造函数需要使用的参数,这个例子中是name,一般来说父类构造时也需要绑定一些参数,两个构造函数中的 this 应该指向同一上下文,因此在子类中调用父类的构造函数一定有动态更改父类构造函数的 this 到之类构造函数当中,这个国产被隐含在 super(...)之中了。

另外一个部分是子类特有的构造过程,相当于在构造方面子类对父类的扩展。比如这个例子中人类相对于动物类会更关注国籍,因此在完成动物类都有构造后,还要完成人类特有的国籍属性绑定。

本例中,子类(人类)的构造函数可以这样写:

function Human(super_instance, name, nationality){
  super_instance._superConstructor.call(this,name);
  delete super_instance.__proto__._superConstructor; 
  this.nationality = nationality;
}
(2)子类的成员

这里我们仅仅是先声明好子类的成员到一个对象,需要在后续的继承函数中将这个对象中的属性逐个添加到子类实例。

let Human_PropMembers = {
  think : function(){
    console.log(this.name + " is thinking!")
  }
}

3.3.2.3 原型继承函数的实现

在这个函数中我们实现代码会更通用和原始一些,不会直接去使用 new 来创建对象的实例,因为直接使用 new 不仅不好显示继承的完整步骤,也不好处理子类父类都含有需要绑定的参数且参数不同的情况。

function extendsNew(superConstructor, subConstructor, superParams, subPatams, subPropMembers) {
  // Used as the super class instance object to be constructed
  const super_instance = {};

  super_instance.__proto__ = superConstructor.prototype;

  // Temporarily mount the parent class constructor
  super_instance.__proto__._superConstructor = superConstructor;

  // The prototype of the subclass constructor is the parent class instance, that is, let the subclass inherit the parent class. 
  // At this time, this instance of the parent class has not been constructed.
  // That is, the parent class constructor is called, and the parent class constructor will be called in the subclass constructor.
  for(let i in super_instance){
    subConstructor.prototype[i] = super_instance[i];
  }

  // An object used as an instance of the subclass to be constructed. 
  const sub_instance = {};

  sub_instance.__proto__ = subConstructor.prototype;

  // Add subclass members to the prototype of constructor.
  for (let i in subPropMembers) {
    subConstructor.prototype[i] = subPropMembers[i];
  }

  // Constructor temporarily hung on subclasses on subclasses
  sub_instance.__proto__._subConstructor = subConstructor; 

  // Execute subclass constructor to complete other things of subclass instance construction.
  // At this time, it should be called in the constructor of the subclass, and the constructor of the parent class, namely `super(...params)`, must be called first.
  // It is temporarily hung on the instance `super_instance` of the parent class, and should be deleted when it is used up.
  let _ = sub_instance._subConstructor(super_instance, ...superParams, ...subPatams); 
  
  // After the subclass instance is constructed, the constructor is no longer needed on the instance, 
  // so we need to delete the constructor on the subclass instance.
  delete sub_instance.__proto__._subConstructor; 
  return _ instanceof Object ? _ : sub_instance;
}

3.3.2.4 完整代码与测试

// by jcLee95 
// https://blog.csdn.net/qq_28550263/article/details/126373011
// ----------------------------- Define Animal class ----------------------------- 
// class Animal's constructor
// Which will be used as superclass's constructor
function Animal(name){
this.name= name;
}
for(let i in AnimalPrototypes) {
Animal.prototype[i] = AnimalPrototypes[i];
}
// 不建议直接使用 Animal.prototype = {...} 的方式,因为这样会覆盖掉隐藏在 父类构造器上的 对象名
let AnimalPrototypes = {
eat:function() {
console.log(this.name + " is eating!")
},
sleep:function() {
console.log(this.name + " is sleeping!")
}
}
for(let i in AnimalPrototypes) {
Animal.prototype[i] = AnimalPrototypes[i];
}
// ----------------------------- Define Human class ----------------------------- 
// class Human's constructor
// Which will be used as subclass's constructor
function Human(super_instance, name, nationality){
// Execute other operations required for parent class construction first: super(...params);
// 【强调】:这里必须动态更改父类构造函数的 this 上下文与子类的 this 上下文一致
//          否则,在父类构造器中绑定到 this 的数据不会指向子类实例!
// 你可以直接调用 JavaScript 函数的 .call 方法,该方法的第一个参数就是要动态改变的 this 上下文
super_instance._superConstructor.call(this,name);
delete super_instance.__proto__._superConstructor; // Delete temporarily mounted superclass constructor.
// Execute other operations of subclass construction.
this.nationality = nationality;
}
let Human_PropMembers = {
think : function(){
console.log(this.name + " is thinking!")
}
}
// ----------------------------- Make Human extends Animal ----------------------------- 
// Prototype chain inheritance principle (完全手写继承原理)
function extendsNew(superConstructor, subConstructor, superParams, subPatams, subPropMembers) {
// 用作待构造的父类实例对象
const super_instance = {};
// 父类实例的原型指针指向父类构造函数的原型
super_instance.__proto__ = superConstructor.prototype;
// 临时挂载父类构造函数
super_instance.__proto__._superConstructor = superConstructor;
// 子类构造器原型 为 父类实例,也就是让子类继承父类,这时候父类的该实例也还没有完成构造,即调用父类构造函数,而父类构造函数将再子类构造函数中调用
// 不建议使用 subConstructor.prototype = super_instance;,因为这会覆盖掉隐藏在 subConstructor 对象名
for(let i in super_instance){
subConstructor.prototype[i] = super_instance[i];
}
// 用作待构造的子类实例的对象 
const sub_instance = {};
// 子类实例原型指针,指向子类构造器原型
sub_instance.__proto__ = subConstructor.prototype;
// 添加子类成员到构造函数的原型
for (let i in subPropMembers) {
subConstructor.prototype[i] = subPropMembers[i];
}
// 子类实例上临时挂在子类的构造函数
sub_instance.__proto__._subConstructor = subConstructor; 
// 执行子类构造函数,完成子类实例构造的其它事情。这个时候,在子类的构造函数中应该调用,且必须先调用父类的构造函数,即 super(),它被临时挂在在父类的实例 super_instance 上,用完应该删除
let _ = sub_instance._subConstructor(super_instance, ...superParams, ...subPatams); 
// 构造子类实例后,实例上不再需要构造函数,因此删除子类实例上的构造函数
delete sub_instance.__proto__._subConstructor; 
return _ instanceof Object ? _ : sub_instance;
}
let person = extendsNew(Animal, Human, ["jcLee95"], ["China"], Human_PropMembers);
console.log(person);
person.eat();
person.sleep();
person.think();

Out[]:

Human { name: 'jcLee95', nationality: 'China' }
jcLee95 is eating!
jcLee95 is sleeping!
jcLee95 is thinking!

JavaScript 模拟面向对象的内部机制

原文链接:https://juejin.cn/post/7240111396868898876 作者:jcLee95

(0)
上一篇 2023年6月4日 上午10:37
下一篇 2023年6月4日 上午10:47

相关推荐

发表回复

登录后才能评论