JavaScript 中的类与原型

159 阅读4分钟

本文主要内容:

  1. 简单介绍一下什么是原型(链)
  2. 通过例子探究一下 JS 的类,以及类实例化的过程和函数的声明过程

应该还能了解到:

  1. [[prototype]]__proto__prototype的区别

原型(链)

JavaScript中的所有对象都有一个内置的属性,称为原型。这个内置属性在ES6之前是__proto____proto__是一个非标准的属性,实际上是由各个浏览器厂商添加的。在ES6中,官方引入了标准的属性 [[prototype]],但是为了向下兼容,__proto__ 仍然保留。

同时,原型也是一个对象,也有一个原型属性,这样就会形成原型链,原型链终止于拥有 null 作为其原型的对象上。当我们访问对象的某个属性时,如果对象本身找不到这个属性,则会在原型中搜索,如果还是没有,就会继续搜索原型的原型,依次类推,直到找到该属性,或者到达原型链末端,这时则返回 undefined。

打印一下如下的字面量对象:

console.log({
  name: 'Nio',
})

我们可以看到:

原型链-对象打印.jpg

这个对象(这里叫它A对象)的原型链是这样的:

原型链.png

JS 中的类

在面向对象编程中,类是一种抽象数据类型,是用来描述具有相同特征和行为的一组对象的模板或蓝图。它是由一组属性和方法组成,可以用来创建新的对象。

对于使用过基于类的语言 (如 Java 或 C++) 的开发者们来说,JavaScript 实在是有些令人困惑 —— JavaScript 是动态的,本身不提供一个 class 的实现。即便是在 ES2015/ES6 中引入了 class 关键字,但那也只是语法糖,JavaScript 仍然是基于原型的。

Java 中,一旦定义了一个类,编译后会生成一个 class 文件。并且,这个类是静态的,可以在程序的任何地方实例化它,而且一旦实例化,对象的属性和方法就不能再改变(正常情况下)。

JavaScript 中并没有这样的类,它是基于原型的,每个对象都有一个指向它的原型对象的引用,在原型对象中定义的属性和方法可以被所有该对象的实例共享。并且,JavaScript 的类是动态的(类也是一个对象),可以在程序运行时修改类的属性和方法,甚至可以动态修改对象的原型,因为它的原型仅是一个属性(/对象引用)。

ES6 中的 class 定义的类其实就是一个方法(或者说函数,本文不明确区分两者概念),方法在 JS 是一个 Function 对象。

可以在代码中试一下:

class Person {
  constructor(name) {
    this.name = name
  }
  sayHello() {
    console.log(`${this.name} say hello~`)
  }
}

const zs = new Person('zs')
console.log(typeof Person) // function
console.log(zs)
console.log(typeof zs) // object
console.log(Person.prototype === zs.__proto__) // true
console.log(Person.prototype.constructor === Person) // true

打印的结果:

class打印输出.png

所以,可以确定 JS 中的类就是一个方法。

下面一个例子是我在 webpack 中使用 ts 写的:

export class Person {
  private name: string
  constructor(name: string) {
    this.name = name
  }
  sayHello() {
    console.log(`${this.name} say hello~`)
  }
}

Chrome 中查看:

查看webpack转换ts源码.png

同时打印一下 Person 对象和一个 Person 类:

Person对象和类.png

所以,可以确定 JS 中的类就是一个方法。

并且可以画一个示意图理清其中的关系:

class的指向.png

  1. Person 类其实是一个 Function 对象,这个对象包含一个 prototype 属性
  2. prototype 对象包含定义在 Person 类中的 sayHello 方法
  3. 和一个 constructor 属性,指向的是这个 Person
  4. 当我们通过new实例化(详情见下面)一个Person对象(例如:张三)的时候,这个对象的原型属性(__proto__)指向Personprototype对象。

new 运算符

new(详情) 运算符用于创建一个新的对象实例。当我们使用 new 操作符创建一个对象时,它会进行以下几个步骤:

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

函数声明过程

我们知道函数其实也是对象,那么函数声明时引擎都做了什么?

创建函数时,JavaScript 引擎会创建一个函数对象,该对象具有以下属性:

  1. [[Prototype]] 指向 Function.prototype
  2. length 属性表示函数的形参个数
  3. name 属性表示函数的名称
  4. prototype 属性指向该函数的原型对象
  5. [[Call]] 内部方法用于调用该函数

函数声明或者说函数对象实例化(不特指new),这个过程和类(即函数对象)实例化对象时是类似的,都会有原型属性指向类对象的prototypejs 的类是基于原型的,继承是通过原型链)。

总结

  1. JS对象都存在一个原型属性,这个属性也是一个对象,也有一个原型属性,这些指向形成了原型链。
  2. 这个原型属性在ES6中是[[prototype]],但是为了向下兼容,__proto__ 仍然保留。
  3. JS的类是函数,JS的函数是一个Function对象,包含prototype属性(当然,还有其他属性),这个prototype有一个constructor属性指向类(函数)。
  4. 类实例化对象时,这个对象的原型属性指向类的prototype属性。