面向对象编程思想与原型继承

1,786 阅读20分钟

面向对象编程思想

多态——多种形态(一个人多面性,在使用时向外显露类型,趋于抽象。);也就是常说的“鸭子模型”

JavaScript 的“鸭子类型”是指一种动态类型语言的编程风格,通常用于形容在运行时,只要一个对象具有某些方法或属性,就可以认为它是某种类型的实例,即不要求严格的类型一致性,只要形状相似就可以认为是同一类型。

比如,一个“看起来像鸭子、走路像鸭子、叫声像鸭子”,那么我们就可以认为这个对象就是一个鸭子。

在 JavaScript 中,由于语言的动态性,对象的类型是在运行时决定的,而不是在编译时静态确定的。因此,我们可以用同样的方式对待不同形状的对象。(Taro内部实现也有编译时+运行时

理解 JavaScript 的鸭子模型可以帮助我们更好地理解 JavaScript 的灵活性。我们无需强制限制一个对象必须是某个特定类的实例,而是可以根据其形状和行为来组织代码。这在很多场合下非常有用,比如在写函数时接受任意类型的参数,或者实现面向对象的继承时允许多态性。

举个例子,我们可以写一个函数来检查一个对象是否具有某个方法:

function hasPrint(obj) {
//做这个判断时,首先需要确定入参是否为一个对象
/*
typeof obj === "object";
这个判断一定能判断出传人的obj就是一个对象吗?
不能的,因为还有null,null也是对象,但是null没有print方法
return typeof obj === "object" && obj.print;
obj.print一定能确保print方法存在吗?

*/
  return obj && typeof obj.print === 'function';
}

使用 prototype.toString 可以去替代 typeof 的检测,但不可完全替代, typeof 有更大包容性

然后我们可以传入任何对象,只要它具有 print 方法,就可以通过函数的检查。

const obj1 = { print: () => console.log('Object 1') }; 
const obj2 = { someOtherMethod: () => console.log('Object 2') };
const obj3 = { print: () => console.log('Object 3') };
console.log(hasPrint(obj1)); // true
console.log(hasPrint(obj2)); // false
console.log(hasPrint(obj3)); // true

虽然这些对象的类型不同,但是只要它们包含了 print 方法,我们就可以认为它们同属于某个特定类型(比如说“可以打印”的类型)。这就是 JavaScript 中鸭子模型的体现。

设计的本质就是向上抽象

class Person{
    say(){
        //通常在设计时,会屏蔽掉一些不必要的细节,只关心入参及返回值
        console.log("Person");
    }
    
    work(){
        console.log("work");
    }
}
const p=new Person();
console.log(p);

我们可以类比 Java
Java 中:类 → 抽象类 → 接口

面向对象编程的重要特性

  • 封装
  • 继承
  • 多态性

在 JavaScript 中,封装、继承和多态是面向对象编程的核心概念。下面分别介绍这三个概念。

封装(Encapsulation)

封装是指将对象的状态(即属性)和行为(即方法)包装在一起,对外隐藏对象的部分属性和方法,只保留对外公开的接口。这样可以保护对象内部的状态不受外界干扰,从而更好地控制对象的访问和修改。

在以前,我们通常会使用闭包等形式去实现这样一个封装的处理

在 JavaScript 中,常见的封装方式是使用闭包来封装对象的私有属性和方法。例如:

function Counter() {
  let count = 0;

  this.increment = function() {
    count++;
  };

  this.get = function() {
    return count;
  };
}

let counter = new Counter();
console.log(counter.get()); // 输出 0
counter.increment();
console.log(counter.get()); // 输出 1

上面的代码中,Counter 函数使用闭包封装了 count 变量,使其只能通过 increment 和 get 方法进行访问和修改,而不能直接访问或修改。这样可以有效地保护 count 变量的安全性。

继承(Inheritance)

继承是指子类继承父类的属性和方法,并可以在此基础上添加新的属性和方法。继承使得代码的复用和扩展变得更加容易。

当我们设计程序的时候,如果是面向对象的思路,首先需要想到的就是设计基类
eg:
设计个计算器,计算面板+显示屏+外围框架
设计个编辑器,光标cursor、选区 selection、redo/undo manager

//基类
//称之为构造器函数
Function Fruit(name){
    this.name=name;
}
const apple=new Fruit("apple");
const peach=new Fruit("peach");
console.log(apple,peach);

在 JavaScript 中,可以使用原型链实现继承
所以先浅浅回顾一下原型和原型链(在后面再详细补充): image.png js分为函数对象普通对象每个对象都有__proto__属性,但是只有函数对象且非箭头函数才有prototype属性

function Person() {}

Person.prototype.name = "mick"

var person1 = new Person()
var person2 = new Person()

console.log(person1.name) // mick
console.log(person2.name) // mick
//每一个JS对象(除了null)都具有一个属性叫 __proto__ ,这个属性会指向该对象的原型
console.log(person1.__proto__ === Person.prototype) // true
//每一个原型都有一个 constructor 属性指向关联的构造函数
console.log(Person === Person.prototype.constructor) // true

可以看到构造函数Person有一个属性是prototype。其实这个属性指向了一个对象,这个对象正是调用构造函数而创建的实例的原型,也就是person1person2的原型

原型链: 当我们读取实例上的一个属性的时候,如果找不到,就会查找与实例关联的原型中的属性,如果还是找不到,就去找原型的原型,一直找到最顶层为止

function Person() {}
Person.prototype.name = "mick"

var person = new Person()
person.name = "randy"
console.log(person.name) // randy

delete person.name
console.log(person.name) // mick
//当我们删除了name属性的时候,那么在 person的实例 上就找不到name属性了,那就会从 person实例的原型 查找,也就是person.__proto__,也就是Person.prototype查找,找到了`mick`。
//如果`Person.prototype`也没找到
//就要了解原型的原型 Object.prototype
//console.log(Object.prototype.__proto__ === null)  true

那么如何使用原型链实现继承呢:
一个对象的原型对象可以被指定为另一个对象,这样就可以实现属性和方法的继承。假设有两个构造函数 Person 和 Student:

function Person(name) {//其实这个就是一个基类(构造函数)
  this.name = name;
}

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

function Student(name, grade) {
  Person.call(this, name);//call()是将Person的属性绑定到当前(this也就是Student)方法中,让其拥有相同的属性;
  this.grade = grade;
}

Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;

Student.prototype.sayGrade = function() {
  console.log('My grade is ' + this.grade);
};

上面的代码中,Student 构造函数调用了 Person 构造函数,并使用 Object.create() 方法将 Person.prototype 设置为自己的原型,实现对 Person 属性和方法的继承。然后,Student.prototype 添加了自己的新属性gradae和方法 sayGrade

怎么去创建一个没有原型的对象? Object.create(null)可以创建一个没有原型的对象

多态(Polymorphism)

多态是指相同的操作作用于不同的对象上,可以产生不同的结果。在面向对象编程中,多态使用的最重要的便是重写(方法的名字相同,但是参数和实现不同)以及重载(方法的名字相同,但是参数不同)。使用多态可以使得代码更加灵活和可扩展。

在 JavaScript 中,由于语言的动态特性和“鸭子类型”的支持,多态的实现更加自然。一个对象的方法可以被任何对象调用,只要对象具有相同的方法名和参数即可,这就实现了多态性。

function speak(animal) {
  if (animal && typeof animal.speak === 'function') {
    animal.speak();
  }
}

let cat = {
  speak: function() {
    console.log('Meow!');
  }
};

let dog = {
  speak: function() {
    console.log('Woof!');
  }
};

let cow = {};

speak(cat); // 输出 "Meow!"
speak(dog); // 输出 "Woof!"
speak(cow); // 什么也不输出

上面的代码中,speak 函数可以接受任何对象作为参数,只要这个对象具有 speak 方法,就可以调用该方法,实现了多态性。

原型继承

在早期, JavaScript 中实现继承的方式是:原型继承,在现在的新语法中我们很少能看到原型的身影,但是这一特性我们需要理解并掌握。

基本概念

当我们创建 JavaScript 对象时,JavaScript 解释器会自动为这个对象添加一个特殊的属性和方法,它们是 protoprototype。用起来有些容易混淆,我们逐一进行介绍。

proto

proto 是 JavaScript 中每个对象都有的属性,它是一个指向该对象原型指针 。每个 JavaScript 对象都有一个原型,也就是该对象从其父对象继承属性和方法。在创建对象时,__proto__ 会被初始化为其构造函数的原型对象,它可以指向另一个对象,也可以是 null。
例如,以下代码创建了一个名为person的对象,它的原型指向 Object.prototype

let person = {};
console.log(person.__proto__ === Object.prototype); // true

类实例化的对象,__proto__指向这个类构造器的prototype

function Person (name) {
  this.name = name;
}
const p=new Person();
p.__proto__ === Person.prototype; 

const a = {} a 是由Object类构造器实例化来的
对象实例化的形式:字面量

const arr = []; const obj = {};//Array,Object
const arr = new Array(); const obj = new Object()

-> === 的含义是什么,跟 == 的区别是什么?
==的含义是相等,===的含义是完全相同。
==只要求值相等; ===要求值和类型都相等

prototype

prototype 是函数对象中的属性,每个函数均默认拥有一个 prototype 对象(除了Function.prototype.bind() 这个特殊函数),其实例对象可以继承这个 prototype 对象上的属性和方法。

我们可以使用构造函数和 prototype 创建自定义函数,并通过new实例化一个对象。
例如,以下代码创建一个汽车类 Car, 并在其 prototype 上定义了一个 drive 方法:

function Car() {}
Car.prototype.drive = function () {
    console.log('Driving the car!')
}
let myCar = new Car()
myCar.drive() // Driving the car!

注意,在这个例子中,实例对象 myCar 继承了通过 prototype 定义的 drive() 方法。

可以看出,__proto__是一个指向内部 prototype 属性指针的引用,而 prototype 是函数对象的一个属性,指向一个对象,它被称作这个函数的原型对象。简而言之,__proto__ 是访问对象原型链上一层的对象,prototype 是在定义对象构造函数时设置它的原型,以解决继承问题。

原型继承的实现方式

原型继承是 JavaScript 中非常重要的概念。它可以让我们通过继承父对象的属性和方法来创建一个新对象。JavaScript 中的所有对象都有一个原型,而原型也可以拥有自己的原型,形成了一个原型链。下面介绍几种实现原型继承的方式。

原型链继承

原型链继承是最常见的一种实现方式。其基本思想是利用原型链来实现继承

function Parent() {
  this.name = "parent";
}

Parent.prototype.sayName = function() {
  console.log(this.name);
}

function Child() {
  this.name = "child";
}
//缺点:这里创建的Parent只有一个,可以理解为一个单例
Child.prototype = new Parent();
//因为它是单例的,所以有个很大的问题:它的prototype是可以更改的,一旦它更改了,内容会全部变更
//Child.prototype.sayName= {}
//这样sayName就不再是方法了,参数也没法成功传进去

let child = new Child();
child.sayName(); // "child"

在这个例子中,我们定义了一个父构造函数 Parent 和一个子构造函数 Child。通过将 Child.prototype 指向一个父对象的实例,我们实现了子对象继承父对象的属性和方法。现在,子对象 child 具有了父原型上的sayName() 方法。

由此:

Child.__proto__===Parent.prototype;//true
Child.__proto__===Object.prototype;//false

因为object在child的上两层,而由上述我们已经了解到了:__proto__ 是访问对象原型链上一层的对象,所以第二行返回false。

构造函数的借用

借用构造函数也是一种原型继承的方式,该方式的基本思想是通过调用父构造函数并将子构造函数作为上下文来初始化属性

function Parent(name) {
  this.name = name || "default name";
}

Parent.prototype.sayName = function() {
  console.log(this.name);
}
//构造器借用实现继承
function Child(name) {
  this.talk=function(){
      console.log("talk");
  }
  Parent.call(this, name);//这里的this指向的是Child的实例
}

let c = new Child("child1");

c.sayName();// "child1"
c.talk();
//怎么来判断这两个方法哪个是Child自己的方法,还是通过继承来的方法?
//c.hasOwnProperty('sayName'); true
//c.hasOwnProperty('talk'); true
//hasOwnProperty用于判断某个属性是否是对象自身的属性还是原型链上面的属性
//在实例化对象c时,它们会成为a对象自己的属性和方法,而不是继承自原型链,所以上述例子均返回true

在这个例子中,我们定义了一个父构造函数 Parent 和一个子构造函数 Child。在创建子构造函数的实例时,我们使用了 call() 函数来调用父构造函数并将其子构造函数作为上下文来初始化子对象的属性。这样,子对象就继承了父对象的属性。

组合继承

组合继承(也称之为伪经典继承)是基于以上两种方式的一个混成方式,其基本思想是组合使用原型链继承和借用构造函数。

function Parent(name) {
  this.name = name || "default name";
}

Parent.prototype.sayName = function() {
  console.log(this.name);
}

function Child(name) {
//构造器借用
  Parent.call(this, name);
}
//原型继承
Child.prototype = new Parent();

let child1 = new Child("child1");
console.log(child1.name); // "child1"

在这个例子中,我们定义了一个父构造函数 Parent 和一个子构造函数 Child。在创建子构造函数的实例时,我们使用 call() 函数来调用父构造函数并将其子构造函数作为上下文来初始化子对象的属性,然后将子构造函数的原型等于一个父对象的实例。通过这种方式,子对象成功地继承了父对象的属性和方法。
以上三种方式均可以实现原型继承,但它们各自有优缺点,我们应根据需要采用合适的方式来实现继承。

实现原型继承的技巧

时刻注意原型链上的引用值

在 JavaScript 中,原型继承是通过共享原型属性和方法来实现的。因此,在原型链上的一个对象上更改引用类型的属性会影响所有对象。例如,以下代码演示了这个问题。

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

Person.prototype.sayName = function() {
  console.log(this.name);
}

function Student(name, grade) {
  this.grade = grade;
  Person.call(this, name);
}

Student.prototype = new Person();

let student1 = new Student("John", 3);
let student2 = new Student("Jane", 4);

student1.sayName(); // John
student2.sayName(); // Jane

Student.prototype.sayGrade = function() {
  console.log(`Grade: ${this.grade}`);
}

student1.sayGrade(); // Grade: 3
student2.sayGrade(); // Grade: 4

Student.prototype.grade = 3;

student1.sayGrade(); // Grade: 3
student2.sayGrade(); // Grade: 3

在这个例子中,我们定义了一个父构造函数 Person 和一个子构造函数 Student。通过在子构造函数的原型属性上赋值一个父对象的实例来实现继承。然后,我们在 Student.prototype 上定义了一个 sayGrade() 方法,并将 grade 设置为 3。当我们使用对象 student1student2 调用 sayGrade()时,它们都输出了 3。因此,我们需要时刻注意原型链上的引用值并避免在原型链上更改引用类型的属性。

避免重写原型

在原型继承中,使用的是对象引用,因此,在重写原型时要非常小心。如果不小心重写原型,可能会导致之前所有继承了原型链的对象上的方法和属性全部失效,这是一个非常危险的操作。因此,在重写原型时,我们应该将其设置为新对象的实例,而不是直接赋值。

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

Person.prototype.sayName = function() {
  console.log(this.name);
}

function Student(name, grade) {
  this.grade = grade;
  Person.call(this, name);
}

// Bad practice
 Student.prototype = { 
    sayGrade: function() { 
        console.log(`Grade: ${this.grade}`);
    }
};

// Good practice
Student.prototype = Object.create(Person.prototype);
Student.prototype.constructor = Student;
Student.prototype.sayGrade = function() {
  console.log(`Grade: ${this.grade}`);
}

在上面的代码中,我们定义了一个父构造函数 Person 和一个子构造函数 Student。然后我们定义 Student.prototype,使用了 Object.create() 来设置新对象的原型为 Person.prototype。这样,我们成功地继承了父对象的属性和方法,而没有重写原型。

面向对象编程进阶

提到面向对象,我们提的更多的就是对象,我们很多时候都是在处理对象内容,比如在 react 中,我们的状态是对象类型,我们需要将该数据渲染到页面中。

对象相关方法

JavaScript 中 Object 类型内置有很多有用的方法,这里列举一些常用的方法:

  1. Object.keys(obj):返回一个包含所有可枚举属性名称数组(不包括从原型继承而来的属性)。
  2. Object.values(obj):返回一个包含所有可枚举属性值数组
  3. Object.entries(obj):返回一个包含所有可枚举属性名称和属性值二维数组
  4. Object.hasOwnProperty(prop):检查一个对象是否含有指定名称的属性(不包括从原型继承而来的属性)。
  5. object.defineProperty(object,key,options):为对象定义属性
  6. Object.freeze(obj)冻结一个对象,使其不能添加、删除、修改属性,也不能修改属性的特性
  7. Object.seal(obj)密封一个对象,使其不能添加、删除属性,但可以修改属性的值和特性
  8. Object.assign(target, s1, s2, …):将一个或多个源对象的所有可枚举属性复制到目标对象

其中,Object.keys()Object.values()Object.entries() 都是 ES6 新增的方法,在旧版本浏览器中不一定支持。

代理对象(Proxy)

另外,JavaScript 中还有 Object 类型的一个非常有用的特性——代理对象(Proxy)。代理对象允许你拦截对目标对象的操作,并定义自己的行为。它的语法为: new Proxy(target, handler)

其中,target 是要代理的目标对象,handler 是一个处理器对象,包含用于拦截各种操作的方法。 下面是一个例子:

let obj = {
  name: 'John',
  age: 30
};

let proxy = new Proxy(obj, {
  get(target, prop, receiver) {
    console.log('get ' + prop);
    return target[prop];
  },
  set(target, prop, value, receiver) {
    console.log('set ' + prop + ' to ' + value);
    target[prop] = value;
    return true;
  }
});

console.log(proxy.name); // 输出 "get name" 和 "John"
proxy.age = 31; // 输出 "set age to 31"
console.log(proxy.age); // 输出 "get age" 和 "31"

上面的代码创建了一个代理对象 proxy,然后通过 get 和 set 方法拦截了对目标对象 obj 的读写操作,并在控制台输出了相关信息。
在实际使用中,代理对象的用途非常广泛,比如可以用来实现数据绑定数据验证和缓存等功能。需要注意的是,代理对象不能拦截一些原生的操作,比如 Object.defineProperty() 方法

代理对象(Proxy)实战

非常流行的库——immer,便是运用了 proxy 这一特性,实现了 immutable 处理,官方原话:

Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way
Immer是一个小巧的工具包,可以更方便地使用不可变状态。

Immer 基本概念

immer 是一款非常流行的 JavaScript ,用于管理 JavaScript 对象的不可变状态。immer 使用 ES6 的 Proxy 对象来捕获对对象的修改,并返回一个可变的代理对象。在 immer 库内部,这个代理对象被用于追踪每次对对象的修改,最终生成一个全新的不可变对象。

手写 Immer 核心源码

下面是 immer 库使用 Proxy 对象实现的核心源码:

// 定义一个生成递归代理对象的函数
function createNextLevelProxy(base, proxy) {
  return new Proxy(base, {
    get(target, prop) {
      // 如果读取的不是函数,且 prop 不是immer.proxies或immer.originals,则返回代理对象的相应属性
      if (prop !== "immer_proxies" && prop !== "immer_original") return proxy[prop];
      // 否则返回目标对象的相应属性
      return target[prop];
    },
    set(target, prop, value) {
      // 将之前代理的值存入 original 对象中,用于恢复状态
      const original = target[prop];
      // 在代理对象上运行修改操作,改变对象的状态
      proxy[IMMER_PROXY_MARKER].mutations.push({
        prop,
        value,
        original,
      });
      // 将新的值赋值到目标对象中
      target[prop] = value;
      // 返回新的代理对象
      return true;
    },
  });
}

// 定义 createProxy 函数,用于创建递归代理对象
function createProxy(base, parent) {
  //浅拷贝一个对象或数组
  const target = Array.isArray(base) ? base.slice() : { ...base };
  //简单定义
  const proxy = Object.defineProperty(
    {
      [IMMER_PROXY_MARKER]: {
        parent,
        target,
        drafted: false,
        finalizing: false,
        finalized: false,
        mutations: [],
      },
    },
    "immer_original",
    {
      value: base,
      writable: true,
      configurable: true,
    }
  );
  for (let prop in target) {
    if (isObject(target[prop])) {
      // 递归代理子对象
      target[prop] = createNextLevelProxy(target[prop], proxy);
    }
  }
  return proxy;
}

在这个实现中,我们首先通过数组的 slice 方法或对象的扩展运算符创建了一个浅拷贝的对象,并且定义了一个新的代理对象 proxy。然后,我们在代理对象上调用 Object.defineProperty 方法,为代理对象添加一个 immer_proxies 属性,这个属性包含了代理对象的 details 对象和一个默认值为 false 的 drafted 属性,用于记录这个对象是否已经被代理。 然后,我们遍历了 target 对象上的所有属性,如果这些属性是一个对象的话,我们递归地为它们创建代理对象(通过 createNextLevelProxy 函数实现)。最后,我们返回新创建的代理对象

这样,我们就实现了 immer 库对象的代理功能。在使用 immer 时,我们可以通过 immer.produce 方法或者 immer 函数来创建一个对象的代理。在对象被代理后,我们可以随意地对对象进行修改操作,这些修改操作将会被记录在代理对象的 details 对象的 mutations 数组中。当需要创建新的不可变对象时,我们可以将代理对象中的修改操作应用于原始对象(即 immer_original 对象)。这样,我们就可以根据原始对象和修改操作的历史记录生成一个新的不可变对象。

在 set 函数中,我们还记录了修改前的值,并将其存储到代理对象的 details 对象中的 mutations 数组中,这样就可以在撤销修改时使用原始值。

createProxy 函数中,我们将 immer_proxies 属性添加到代理对象上(并不是作为 getter),并且将这个属性的默认值设置为一个空对象。在创建完毕代理对象后,我们遍历对象的所有属性,并递归调用 createNextLevelProxy 函数来为所有对象属性创建代理对象。最后,我们返回新创建的代理对象。

new 创建对象原理详解

创建对象的过程

使用关键字 new 可以在 JavaScript 中创建对象。当我们使用 new 操作符时,它会执行以下步骤:

  1. 创建一个新对象。
  2. 将新对象的原型设置为构造函数的 prototype 属性。
  3. 将新对象作为 this 对象调用构造函数。
  4. 如果构造函数返回一个对象,则返回该对象;否则返回这个新对象。

另外,构造函数内部使用 this 关键字来指代新创建的对象。

下面是一个示例代码:

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

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name + ', and I am ' + this.age + ' years old.');
};

let person = new Person('John', 30);
person.sayHello(); // 输出 "Hello, my name is John, and I am 30 years old."

在创建 person 对象的过程中,new 操作符执行了以上四个步骤:

  1. 创建了一个新对象 person。
  2. 将 person 的原型设置为 Person.prototype。
  3. 将构造函数 Person 的 this 对象设置为 person,并执行构造函数内部的代码。
  4. 因为构造函数 Person 没有返回值,所以返回的是新创建的对象 person。

需要注意的是,使用 new 进行对象创建时,若构造函数内部的 this 指向有问题,其可能会对全局作用域造成影响。 例如:

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

let circle = Circle(5); // 错误:没有创建一个新对象
console.log(circle); // 输出 undefined
console.log(radius); // 输出 5

在上面的代码中,Circle 函数内部缺少 new 关键字,导致 this 无法指向新创建的对象,而是指向了全局作用域。 因此,所有的属性和方法都被定义在了全局作用域下,circle 变量没有被赋值为新对象,而是为 undefined。此时,radius 变量的值被覆盖为 5。

手写一个 new

  1. 创建一个空对象,并将其原型设置为构造函数的 prototype 属性。
  2. 将该空对象作为 this 关键字调用构造函数。
  3. 如果构造函数返回一个对象,则返回该对象,否则返回 this 对象。

下面是一个示例代码:

function myNew(constructor, ...args) {
  let obj = Object.create(constructor.prototype);
  let res = constructor.apply(obj, args);
  return res instanceof Object ? res : obj;
}

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

Person.prototype.sayHello = function() {
  console.log('Hello, my name is ' + this.name + ', and I am ' + this.age + ' years old.');
};

let person = myNew(Person, 'john', 30);
person.sayHello(); // 输出 "Hello, my name is john, and I am 30 years old."

在 myNew 函数中,我们首先通过 Object.create() 方法创建了一个空对象 obj,并将其原型设置为构造函数的 prototype 属性。然后使用apply() 方法将构造函数的 this 指向该对象,并传递了参数 args。最后判断构造函数的返回值是否为一个对象,如果是,则返回该对象,否则返回创建的新对象 obj。这样就实现了一个简单的 new 函数。

需要注意的是,由于 new 操作符在使用时还会进行一些额外的处理,如设置该对象的 constructor 属性等,因此以上实现方式并不完整。但是,以上的实现方式可以帮助我们更好地理解 new 操作符的原理和实现过程。

面试常问

1.  简单说说 JavaScript 原型继承的概念
2.  怎么理解 prototype 和 __proto__