最近有个项目要用到 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__)
# 构造函数
在传统的 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
看看,结果如图
发现 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
。
# 类和构造函数
对比类和传统的构造函数的话,其实我自己理解类就是传统构造函数的一种新的语法表现形式,而并不是一种新的东西,它们创建对象的背后依赖的都还是原型。
在传统构造函数里,原型是通过构造函数来创建对象,对象也可以通过 Prototype 属性来指向原型。而在类的实现里,其实就是还是原型和类的构造函数一起完成了类的创建。
# 原型
上面说了半天原型,那原型到底是什么?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__)
执行结果如图
可以看到不管是隐式原型和显式原型都指向同样的原型地址。
# 原型链
什么是原型链?官方定义如下,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);
执行结果如下
执行结果中 [[prototype]]
就相当于 __proto__
,根据 ES 标准,访问 someObject
实例对象原型,是用 someObject.[[Prototype]]
这种方式。
其实我并不明白
student.__proto__
的 Prototype 这儿为啥打印的是Person
,难道不应该 是 Student 的原型吗,下面对应的constructor
是class student
,倒是对的上。
用图来说明原型链的话,如图:
# 原型链作用
最重要的是帮助实现了继承相关的功能。当你试图访问一个对象的属性时:如果在对象本身中找不到该属性,就会在原型中搜索该属性。如果仍然找不到该属性,那么就搜索原型的原型,以此类推,直到找到该属性,或者到达链的末端,在这种情况下,返回 undefined。
# 总结
以上就是构造函数,类,原型的一些知识,基本上梳理了我的疑惑。回到文章最开始的问题 Vue.prototype.StatusBar = e.statusBarHeight;
这句话本质就是给 Vue
原型增加了一个 StatusBar
的属性,后续就能在 Vue 实例里通过 this.StatusBar
来访问这个变量了。
参考地址:
- B站-js原型链、构造函数和类 (opens new window) #这个1分钟视频介绍原型链、构造函数和类的关系,很清晰。
- 轻松理解JS 原型原型链 (opens new window)
- Vue中 Vue.prototype 详解及使用 (opens new window)
- Vue2-添加实例 property (opens new window)
- MDN-对象原型 (opens new window)
- MDN-JavaScript 中的类 (opens new window)
- MDN-构造方法 (opens new window)
- ES6教程-Class 的基本语法 (opens new window)
- MDN-继承与原型链 (opens new window)
- 运行时学习笔记-基于运行时的继承体系
关注我的微信公众号,我在上面会分享我的日常所思所想。