一、楔子
类构造函数在执行时会创建一个新对象,构造函数中的this
会指向这个新对象。
如果构造函数返回非空对象,则返回该对象,否则返回新创建的对象。
上面是类构造函数的一个知识点,我在学习JS的过程中曾多次看到。初读时觉得十分拗口,后面经过细细品读也逐渐理解了它的含义,不过彼时还是感到不求甚解。直到最近在研究一个问题过程中使用到这个知识点,一时颇有种拨云见日的感觉,于是便写下这篇文章,一来是想记录理解的过程,二来也是想谈一谈我对于这类基础知识点的看法。
二、一个不完美的方案
1.如何实现私有成员?
在最近的工作中我遇到这样一个问题:我开发了如下的一个Scheme
类,我希望能将其中诸如_id、 _beginTime、 _getEndTiem
这些属性和方法设置为类的私有成员(即只能在类的内部被访问,无法在外部被调用)
JavaScript
class Scheme {
constructor({ name, id }) {
this.name = name
//私有属性
this._id = id
this._beginTime = new Date().toLocaleString()
}
read() {
console.log(`
方案名:${this.name}
方案编码:${this._id}
创建时间:${this._beginTime}
`)
}
// 私有方法
_getEndTime() {
return new Date(
new Date(this._beginTime).getTime() + 7 * 24 * 60 * 60 * 1000
).toLocaleString()
}
}
2.Proxy
方案
遗憾的时目前在JS当中还并没有相关的语法,我必需要自己去实现它。在查阅了一些资料之后,我选择使用代理的方式去实现它(即Proxy
)。
基本思路是,使用Proxy
对Scheme
类的实例进行代理,当实例对属性/方法执行读、写、迭代的操作时,进行判断。如果属性名是以下划线开头,则判断其为私有成员,然后会阻止次当前操作。
JavaScript
const handler = {
get(target, prop) {
if (prop.startsWith('_') && prop !== '__proto__') {
return console.log(`${prop}无法在外部调用`)
}
if (typeof target[prop] === 'function') {
return target[prop].bind(target)
}
return target[prop]
},
set(target, prop, value) {
if (prop.startsWith('_')) {
return console.log(`${prop}无法在外部调用`)
}
target[prop] = value
},
ownKeys(target, prop) {
return Object.keys(target).filter((key) => !key.startsWith('_'))
},
}
const scheme1 = new Scheme({
name: '清江水库调度',
id: 'a472707b539aa9214d1dc951aa80968a186b7462',
})
const scheme1_proxy = new Proxy(scheme1,handler)
此时尝试使用代理过后的实例scheme1_proxy
, 通过读取或迭代的方法获取私有属性_id
,均没有效果。
JavaScript
console.log(scheme1_proxy._id);
console.log(Reflect.ownKeys(scheme1_proxy));
写到这里我就发现我上面的代码是有明显的问题的。首先,如果我每实例化一个对象都要写一遍这个代理,显然太过麻烦。另外,如果是其他同事使用我封装的类,显然是不能指望它们老老实实的进行代理的。
本着"'偷懒'是编程的第一生产力"这一原则,我必须要对上面的代码进行优化。主要是要将代理的过程封装到类当中,并保证每次实例化所抛出的是代理后的对象。
3.使用实例工厂优化方案
静态类方法非常适合作为实例工厂
此时上面这个知识点就立刻浮现在了我的脑海当中,我的思路是这样:
给构造函数设计一个密码参数,只有正确传递了密码才能够实例化成功,反之则会报错。同时使用静态方法创建一个实例工厂,在实例工厂中我会传递正确的密码参数实现实例化,之后对实例进行代理,最后返回代理后的实例。也就是说只要密码参数不泄露,所用人都只能使用实例工厂进行类的实例化,而实例工厂返回的是代理过后的实例,这样就能够保证私有成员功能的实现。
JavaScript
class Scheme {
//此处省略一千行代码
.......
//实例工厂
static create(params) {
const instance = new Scheme(params, true)//第二个参数为密码参数
return new Proxy(instance, handler)
}
}
4.一些遗憾
虽然我使用了实例工厂的方式解决之前的问题,但是优化后的代码我依旧还是不甚满意。主要原因有以下两点:
- 目前我封装的这个
Scheme
必须要使用我所设定的实例工厂去进行实例化,这其实是反直觉的,会让使用者感觉不太舒服,有一种被控制的感觉。这让我想起了Vue3 中ref
的.vue
, 衷心希望$ref
早日登堂入室。 - 我所设计这个密码参数实际上也并不安全,任何人只要浏览一下类的相关代码就能够知道密码是什么。哎!所谓"密码"也不过是我的一厢情愿罢了。我已经可以想见那个窥探到我的"密码"的人沾沾自喜的样子了😡。
虽然不甚满意,但是彼时我也是实在想不出什么更好的方法了。直到。。。
三、单例模式的启示
之后的某一天,我偶然看到了一篇文章,文章的内容是介绍如何在JS中实现单例模式(即类只能够有一个实例)
文章中的实现思路是这样的:
在第一次实例化的时候,在构造函数中将this
保存到一个静态属性中。之后再次实例化的时候就将之前保存的this
抛出。这样就能够保证每次实例化获取的都是同一个实例对象。
JavaScript
class Person {
constructor() {
if (!Person.instance) {
Person.instance = this
}
return Person.instance
}
}
const person1 = new Person
const person2 = new Person
console.log(person1 == person2);//true
上面的代码起初我是无法理解的,因为我之前从来没有深入思考过类构造函数中的this
究竟是什么,也没有尝试过在类构造函数中主动return
一个值。当然这一切的答案其实就在文章开头所讲的那个知识点当中,于是当我重新翻书查看之后,便恍然大悟。
其一,在类构造函数中,this
指向新创建的对象,我们可就以将其看做即将创建出来的实例。因此也就可以通过保存this
从而保存当前的实例。
其二,this
所指向的对象实际上又不一定是最后的实例对象,因为我们可以通过在类构造函数中return
一个非空对象来"篡改"实例化的结果。
基于上述的两点,在这里实现了单例模式。此时我就敏锐的发现,这两点也同样可以帮助我解决我在之前所遇到的封装Proxy
的问题。
最终我给出了这样的一个方案:
JavaScript
class Scheme {
constructor() {
return new Proxy(this, handler)
}
}
四、后记
故事结束了,我得到了一个更好的方案,并且深入理解了类构造函数。不过这时我开始思考一些问题,我想如果我一开始就理解类构造函数的话,那么或许我早就能够想到现在的这套方案。但实际上在学习的过程中这样的知识点就像路边的野草一样太多太多,即使我想近距离的打量一番也会发现自己根本瞧不出什么名堂来。但是这样的"野草"你又不能说它没有用,在我上面这一番求索的过程中,我想"野草"的价值也已经体现的淋漓尽致了。这里就可以得出结论了,"野草"也能治病救人,但是你若想发挥它的价值又不得不费上好一番周折。所谓:"不经一番寒彻骨,怎得梅花扑鼻香"。
看来在今后的学习过程,对于这些"野草"也是要重视起来了。不过这个问题也要辩证的来看,不久之前我就看到过有这样一个观点,有人说"新手程序员不应该过多的去看基础性的东西"。对于这种观点我是不太认同的,但是不得不承认的是它也是有一定的道理的, 学习编程的目的就是为了工作挣钱,但是工作是要讲究效率和成果的,恰恰基础性知识是很难体现出成果来的。它往往是一个厚积薄发的过程,起的也是一个潜移默化的影响。所以这里就有一个矛盾了,究竟是要长远的利益还是眼前的利益?