JS学习笔记-原型链&类&构造函数

最近有个项目要用到 uniapp 去开发跨端的小程序,在github上找了一个项目想看看人家源码,结果看到下面这行的时候就有点发懵。

Vue.prototype.StatusBar = e.statusBarHeight;

Vue 我还算明白,prototype 是咋回事?应该是和原型相关的,之前也尝试理解过 JS 的原型链,不过每次总结的都比较零散,这次尝试系统的总结一下。

# 基础知识

复习一下 JS 的引用数据类型 (opens new window)(对象类型):对象(Object)、数组(Array)、函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。我们这篇文章主要关注的是:引用数据类型中的「对象(Object)」和「函数(Function)」。 尽管函数和对象都算是对象类型,但是只有函数对象有 prototype 属性,而 _proto_ 属性是大家都有的。 可以不用管 prototype 和 proto 到底是干嘛的,后面会有介绍到。

可以先来个例子来直观感受一下:

//函数对象
function printHello() {
  console.log(`hello world`)
}
console.log("普通函数对象原型", printHello.prototype)
console.log("普通函数对象原型", printHello.__proto__)

Untitled

# 构造函数

在传统的 JS 里是没有类这个概念的,生成实例对象的方法是通过构造函数的方式,如下

function Person(name, age) {
  this.name = name;
  this.age = age;
}
Person.prototype.newPro = 'all';  
//传统添加对象方法的方式
Person.prototype.sayHello = function() {
  console.log("Hello, I am " + this.name);
}
let p1 = new Person('Dora',6)
console.log('person is ', p1);
console.log('person new property ', p1.newPro);
p1.sayHello()

我们在基础知识中说过,函数对象是有 prototype 属性的,构造函数当然也算函数对象,所以构造函数也有 prototype 属性,我们来打印一下 Person.prototype 看看,结果如图

Untitled

发现 Person.prototype 里面有后加入的属性和方法,同时它还指向了一个原型对象,这里先不管它。

但传统的构造方法创造实例这种写法很繁琐,而且对于其他面向对象语言的编程者来说,这样写很困惑,于是在 ES6 的时候,和其他面向对象编程语言看齐,引入了类(Class)的概念。

#

ES6 引入的类作为对象的模板,你可以使用 class (opens new window) 关键字声明一个类。

定义类并通过类来创建对象的语法如下:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
  sayHello() { console.log("Hello, I am " + this.name); }
}
let person = new Person("Dora", 6);
person.sayHello();

代码中 Person 类中的 constructor() 就是 Person 类的构造方法。

ES6 的类,完全可以看作构造函数的另一种写法:

class Person {
  // ...
}
typeof Person // "function"
Person === Person.prototype.constructor // true

上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

而传统构造函数的 prototype 属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的 prototype 属性上面。我们可以打印一下类的 prototype,即 Person.prototype

Untitled

# 类和构造函数

对比类和传统的构造函数的话,其实我自己理解类就是传统构造函数的一种新的语法表现形式,而并不是一种新的东西,它们创建对象的背后依赖的都还是原型。

在传统构造函数里,原型是通过构造函数来创建对象,对象也可以通过 Prototype 属性来指向原型。而在类的实现里,其实就是还是原型和类的构造函数一起完成了类的创建。

Untitled

# 原型

上面说了半天原型,那原型到底是什么?MDN 里对原型的定义 (opens new window)是「原型是 JavaScript 对象相互继承特性的机制,JavaScript 中所有的对象都有一个内置属性,称为它的 prototype(原型)。」

注意!指向区分对象原型的属性并不是 prototype,而是 __proto__。访问对象原型的标准方法是 Object.getPrototypeOf()

所有对象都有原型,每个原型都有与之对应的构造函数,要么是传统的构造函数,要么是类的构造函数。以后为了方便我们就说是类的构造函数了。

前面我们说到的类的原型是通过访问类的 prototype 属性来获取到的,那如何获取实例对象的原型属性呢?这里引出一个隐式原型和显式原型的概念。

# 隐式原型和显式原型

官方似乎并没有这俩概念的区分,这里为了让自己看明白一些国内的文档,所以提一下这俩概念。

  • 实例对象的属性 __proto__ 称为隐式原型,
  • 类(或者说传统构造函数)的属性 prototype 称为显式原型。

还是拿上面的 Person 类来举例子:

class Person {
  constructor(name, age) {
    this.name = name;
    this.age = age;
  }
}
let person = new Person("Dora", 6);
console.log(Person.prototype);
console.log(person.__proto__);
console.log(Person.prototype === person.__proto__)

执行结果如图

Untitled

可以看到不管是隐式原型和显式原型都指向同样的原型地址。

# 原型链

什么是原型链?官方定义如下,JavaScript 中所有的对象都有一个内置属性(__proto__),称为它的原型。它本身是一个对象,故原型对象也会有它自己的原型,逐渐构成了原型链。原型链终止于拥有 null 作为其原型的对象上。

原型链有点类似类继承的感觉,举个例子来测试一下,还是使用 Person 类,这次我们新建一个 Person 的子类 Student

class Student extends Person { 
  constructor(name, age, score) {
    super(name, age);
    this.score = score;
  }
}
let student = new Student("fanthus", 18, 99)
console.log(student);

执行结果如下

Untitled

执行结果中 [[prototype]] 就相当于 __proto__,根据 ES 标准,访问 someObject 实例对象原型,是用 someObject.[[Prototype]] 这种方式。

其实我并不明白 student.__proto__ 的 Prototype 这儿为啥打印的是 Person,难道不应该 是 Student 的原型吗,下面对应的 constructorclass student,倒是对的上。

用图来说明原型链的话,如图:

Untitled

# 原型链作用

最重要的是帮助实现了继承相关的功能。当你试图访问一个对象的属性时:如果在对象本身中找不到该属性,就会在原型中搜索该属性。如果仍然找不到该属性,那么就搜索原型的原型,以此类推,直到找到该属性,或者到达链的末端,在这种情况下,返回 undefined。

# 总结

以上就是构造函数,类,原型的一些知识,基本上梳理了我的疑惑。回到文章最开始的问题 Vue.prototype.StatusBar = e.statusBarHeight; 这句话本质就是给 Vue 原型增加了一个 StatusBar 的属性,后续就能在 Vue 实例里通过 this.StatusBar 来访问这个变量了。

参考地址:

  1. B站-js原型链、构造函数和类 (opens new window) #这个1分钟视频介绍原型链、构造函数和类的关系,很清晰。
  2. 轻松理解JS 原型原型链 (opens new window)
  3. Vue中 Vue.prototype 详解及使用 (opens new window)
  4. Vue2-添加实例 property (opens new window)
  5. MDN-对象原型 (opens new window)
  6. MDN-JavaScript 中的类 (opens new window)
  7. MDN-构造方法 (opens new window)
  8. ES6教程-Class 的基本语法 (opens new window)
  9. MDN-继承与原型链 (opens new window)
  10. 运行时学习笔记-基于运行时的继承体系

关注我的微信公众号,我在上面会分享我的日常所思所想。