前言
面向对象设计的编程语言都会有三个基本特征:封装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
运算符的作用如此解释:
不完善之一 — 封装
如何在 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
的原型链如图所示:
graph LR
A[对象b] -- __proto__ --> B[实例化后的 A] -- __proto__ --> C[A.prototype] -- __proto__ --> D[Object.prototype] -- __proto__ --> F[null]
对于 b.print()
会沿着原型链直到在 A.prototype
找到才停止,然后运行该方法。此时将代码改成这样:
A.prototype.printIn = function() {
console.log(this.constructor.name)
}
直接导致子类 B
的实例运行出错。原型继承是最简单也是最好理解的一种继承方式,可以点击此处查看前人总结别的继承方式以及优缺点。虽然继承方案五花八门,但是终究离不开原型链的范畴,只要涉及到原型链就会有乱改父类的担忧。
不完善之三 — 多态
基于前面的原型继承事实,我们知道当寻找自身不存在的属性时,引擎会沿着原型链继续查找。那如果查找的属性既出现在自身,原型链上又存在,就会出现原型屏蔽这样的结果:
function Person() {
this.age = 20
}
function PersonA() {
this.age = 21
}
PersonA.prototype = new Person
const persona = new PersonA
persona.age // 21
对象 persona
中的属性 age
会屏蔽掉原型链上的所有 age
属性,因为 persona.age
总是会选择原型链中最底层的 age
属性,而这恰恰是实现多态的一种方式:覆盖。
除了覆盖,还有另一种 JavaScript 目前还不支持的可以实现多态的方式:重载,对象自身的方法与继承而来的方法重名时,可以通过入参个数的不同以示区分。
function B() {}
B.prototype = new A
B.prototype.print = function(param) {
console.log(param)
}
const b = new B
b.print() // A
b.print('b') // b
重载技术可用的情况下,上面的代码就是可行的。但同一属性名或者方法名都会产生原型遮蔽的效果,所以重载只能通过别的手段实现,比如通过判断入参数调用不同的方法。
最新技术 class
ES6 的到来给 JavaScript 泵入了新的血液,大量的新特性让开发者欢呼雀跃,同时也给不少的开发者增加了理解成本。与此同时,构造函数也迎来了属于自己的春天——有了一个名正言顺的关键字 class
,以及一些相关的特性。
更明确的「类」
class A{}
const a = new A
与构造函数相比,新的声明方式从语义上更为直白易懂,虽然本质上只是构造函数的语法糖。需要注意的是,通过 class
声明的「类」就不能直接以函数的方式调用了,这与之前稍有不同:
A() // Uncaught TypeError: Class constructor A cannot be invoked without 'new'
控制台显示的错误提示我们必须使用 new
关键字实例化「类」,这在之前是没有的,但是可以模拟实现,需要用到 new
相关的特性:
function A() {
if (new.target !== A) {
throw new TypeError(`Class constructor A cannot be invoked without 'new'`)
}
}
new.target
指向当前的构造函数本身,可以通过该特性模拟无法函数调用 class
行为。顺带一提,使用 class
关键字会默认启用严格模式,严格模式下声明不会提前,类内部的写法也需要额外注意。
更完备的封装
除了类声明,类的属性和方法权限也有了更新,新增的静态、私有属性和方法大大方便了构建一个合理的工具类。
class A {
static value = 1
#private_value = 2
}
静态标识以 static
关键字开头,私有标识以 #
开头,这两种关键字都可以在属性和方法上使用。
总结
ES6 虽然为构造函数注入了新的活力,依然改变不了语法糖的事实。在封装方面一定程度上提高了很多,但要是想对标 Java 这一类编程语言还有很多路要走;但是反过来想,为什么一定要对标?JavaScript 就走自己的路才能发挥动态语言的优势。
随写
总算是憋出第二篇了,破玩意不写感觉不对劲,动手写发现就跟自己过不去,为何不把时间用在打两把游戏上?
还是那句话,如果我还能写,那就会出第三篇了!
各位安好!
- 将数据和操作数据的行为打包在一个单元内。↩
- 单元 A 获取到单元 B 的数据和行为。↩
- 基于继承特征,针对同样的行为有不同的结果。↩
- 构造函数的首字母大写只是社区规范,既然是规范就可以选择遵守或者不遵守,编译器不会以这样的规范调整自己的编译。↩
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!