前言
面向对象设计的编程语言都会有三个基本特征:封装1、继承2和多态3,有了这三种特征的配合,才能将面向对象思想的优势尽可能的展现出来。 JavaScript 虽然是 OOP(Object-oriented programming)语言,但是由于它的动态语言特征,对「类」支持的不是很完善。
JavaScript 中的类 — 构造函数
单从面向对象来说,JavaScript 做的的确不错,因为它可以不通过类直接创建对象。这样带来的最大优点就是能让初入门的开发者无需理解面向对象思想涉及到的概念就能直接上手开发,所谓依葫芦画瓢,别人咋写我咋写,就能运行了;缺点也是对应的,因为无法理解面向对象的核心思想,所以写出来的代码可能无法兼顾效率和性能。
JavaScript 为了照顾到更多的开发者,依然拿出了自己的”类”,它就是构造函数,例子如下:
function Article(title, content) {
this.title = title
this.content = content
}
Article.prototype.Author = "wolfberry"
Article.prototype.getAuthor = function() {
return this.Author
}
// 实例化
const article = new Article("构造函数", "AAAA")
复制代码
需要注意的是,JavaScript 编译器并不会区分函数是构造函数还是普通函数4,这就意味这任何一个 JavaScript 函数既是普通函数又是构造函数:
console.log(Article("构造函数", "AAAA"), window.title, window.content) // undefined "构造函数" "AAAA"
复制代码
因为构造函数的词法作用域,直接调用的时候自身的 this
指向全局对象 window
,所以入参全部赋值到全局对象的属性上了。那么区别构造函数和普通函数的方式只能是 new
构造化调用了。
MDN 对 new
运算符的作用如此解释:
new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
不完善之一 — 封装
如何在 JavaScript 中有效的封装是许多 JavaScript 工具库开发者遇到的第一个难题。因为 JavaScript 的构造函数太过简陋,不支持静态、私有属性或者方法,而这些在工具库里面恰恰是不可或缺的。举个例子来说,我想在 Article
里面新增一个 createTime
属性,但是该属性不能是自己设定的,而是实例化的时候自动生成的:
function Article(title, content) {
this.title = title
this.content = content
this.createTime = new Date()
}
const article = new Article("构造函数", "AAAA")
复制代码
它的数据格式截图如下:
熟悉 JavaScript 的开发者都知道,一般情况下,对象是可以随意更改的,这就意味着 creatTime
属性是不安全的。为此社区提出的规范是这样的:私有属性通过前缀下划线以示区分:
this._createTime = new Date()
复制代码
但这样也仅仅属于“掩耳盗铃”,因为规范不是语法,从代码层面依然可以通过实例化出来的对象随意修改该属性。那么,有没有更好的方式呢?有的,但是稍微麻烦点,而且也增加了理解难度:
function Article(title, content) {
// 私有变量
const createTime = new Date()
this.title = title
this.content = content
// 特权函数
this.getCreateTime = function() {
return createTime
}
}
复制代码
将私有属性直接声明为变量,然后在构造函数里面创建特权函数获取该变量并返回。一定程度上隐藏了该属性,但是带来的问题就是该变量和特权函数存在于每个实例上,增加了资源的占用,显然不是一个很好的解决方案。
不完善之二 — 继承
JavaScript 的继承会让子类和父类产生紧密的联系,父类的任何改动都有可能影响到子类,这与别的 OOP 语言大有不同。原因在于 JavaScript 的对象和构造函数都有“原型”这一概念,子类和父类通过原型产生继承关系,而原型又有行为委托的特性,父类的任何改动都有可能影响到子类。
行为委托通俗点来说,就是在对象获取自身不存在的属性或者方法时,会去原型上继续寻找该属性和方法。
可以从原型继承上可以了解到这一点:
function A() {}
A.prototype.print = function() {
console.log(this.constructor.name)
}
function B() {}
B.prototype = new A
const b = new B
b.print() // A
复制代码
将 B.prototype
链接到实例化后的 A
,最终对象 b
的原型链如图所示: