はしがき
オブジェクト指向プログラミング パラダイムでは、カプセル化は不可欠な概念であり、Java や C などの従来のオブジェクト指向言語では、プライベート メンバーがカプセル化を実現する重要な方法です。ただし、JavaScript では文法的な機能の点でプライベート メンバーがサポートされていないため、開発者は JS でプライベート メンバーを実装するためにさまざまなテクニックを使用する必要があります。以下では、JS でのプライベート メンバー機能の現在の実装について説明します。それらの長所と短所の比較。
既存の実装ソリューションの一部
一般的な命名スキーム
アンダースコア '_' で始まるメンバー名はプライベート メンバーとみなされ、アクセスおよび呼び出しが許可されるのはクラス メンバー メソッドのみであり、プライベート メンバーは外部からアクセスできないことが合意されています。簡単なコードは次のとおりです:
JavaScript
var MyClass = function () { this._privateProp = ‘privateProp'; }; MyClass.prototype.getPrivateProp = function () { return this._privateProp; }; var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my._privateProp); // 并未真正隐藏,依然弹出 ‘privateProp'
メリット
命名規則が、コードレベルの作業を行わずにプライベートメンバーを実装するための最も簡単な解決策であることは疑いの余地がありません。
デバッグは便利で、コンソール上のオブジェクトのプライベート メンバーを直接確認できるため、問題のトラブルシューティングが簡単になります。
優れた互換性、ie6 でサポート
不十分
外部からのアクセスやプライベート メンバーへの変更を防ぐ方法はありません。契約を知らない開発者、または同意しない開発者がプライベート属性を変更した場合、何もすることができません。
もちろん、コード標準を備えたチームでは、これは大きな問題ではありません。
es6 シンボル ソリューション
es6 では、プライベート メンバーを実装するためにシンボル機能が導入されました。
主なアイデアは、各プライベート メンバーの名前に対してランダムで一意の文字列キーを生成することです。このキーは、js のクロージャー変数によって内部可視化されます。
JavaScript
(function() { var privateProp = Symbol(); // 每次调用会产生一个唯一的key function MyClass() { this[privateProp] = ‘privateProp'; // 闭包内引用到这个 key } MyClass.prototype.getPrivateProp = function () { return this[privateProp]; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 弹出 undefined,因为成员的key其实是随机字符串
メリット
これは命名規則スキームの欠点を補っており、外部関係者は通常のチャネルを通じてプライベート メンバーにアクセスできません。
デバッグの利便性は許容できますが、文字列パラメーターはシンボルのコンストラクターに渡され、コンソール上の対応するプライベート プロパティ名は Symbol(key)
として表示されます。
互換性は良好で、Symbol をサポートしていないブラウザでも簡単に shim アウトできます。
不十分
書き方は少し面倒です。内部メソッドがアクセスできるように、プライベート メンバーごとにクロージャ変数を作成する必要があります。
外部的には、Object.getOwnPropertySymbols を通じてインスタンスのシンボル プロパティ名を取得し、この名前を通じてプライベート メンバーへのアクセス権を取得できます。このシナリオが発生することは比較的まれであり、このアプローチを知っている開発者のレベルは、自分のアクションの影響を認識するのに十分な能力があると考えられているため、この欠点は実際の欠点ではありません。
es6 WeakMap ソリューション
es6で導入されたMapコンテナとWeakMapコンテナ、最大の特徴はコンテナのキー名を任意のデータ型にできることですが、本来はプライベートメンバーの導入が目的ではありませんが、意外と使えるようになっています。プライベート会員機能。
主なアイデアは、クラス レベルで WeakMap コンテナを作成し、各インスタンスのプライベート メンバーを格納することです。このコンテナは外部からは見えず、内部メソッドによってインスタンスをキー名として取得します。コンテナ上の対応するインスタンスのプライベート メンバー。サンプル コードは次のとおりです。
JavaScript
(function() { var privateStore = new WeakMap(); // 私有成员存储容器 function MyClass() { privateStore.set(this, {privateProp: ‘privateProp'}); // 闭包内引用到privateStore, 用当前实例做 key,设置私有成员 } MyClass.prototype.getPrivateProp = function () { return privateStore.get(this).privateProp; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 弹出 undefined,实例上并没有 privateProp 属性
优点
弥补了命名约定方案的缺陷,外部无法通过正常途径获得私有成员的访问权。
对 WeakMap 做一些封装,抽出一个私有特性的实现模块,可以在写法上相对 Symbol 方案更加简洁干净,其中一种封装的实现可以查看参考文章3。
最后一个是个人认为最大的优势:基于 WeakMap 方案,可以方便的实现保护成员特性(这个话题会在其他文章说到:))
不足
不好调试,因为是私有成员都在闭包容器内,无法在控制台打印实例查看对应的私有成员
待确认的性能问题,根据 es6的相关邮件列表,weakmap 内部似乎是通过顺序一一对比的方式去定位 key 的,时间复杂度为 O(n),和 hash 算法的 O(1)相比会慢不少
最大的缺陷则是兼容性带来的内存膨胀问题,在不支持 WeakMap 的浏览器中是无法实现 WeakMap 的弱引用特性,因此实例无法被垃圾回收。 比如示例代码中 privateProp 是一个很大的数据项,无弱引用的情况下,实例无法回收,从而造成内存泄露。
现有实现方案小结
从上面的对比来看,Symbol方案最大优势在于很容易模拟实现;而WeakMap的优势则是能够实现保护成员, 现阶段无法忍受的不足是无法模拟实现弱引用特性而导致的内存问题。于是我的思路又转向了将两者优势结合起来的方向。
Symbol + 类WeakMap 的整合方案
在 WeakMap 的方案中最大的问题是无法 shim 弱引用,较次要的问题是不大方便调试。
shim 出来的 WeakMap 主要是无法追溯实例的生命周期,而实例上的私有成员的生命周期又是依赖实例, 因此将实例级别的私有成员部分放在实例上不就好了? 实例没了,自然其属性也随之摧毁。而私有存储区域的隐藏则可以使用 Symol 来做。
该方案的提供一个 createPrivate 函数,该函数会返回一个私有的 token 函数,对外不可见,对内通过闭包函数获得, 传入当前实例会返回当前实例的私有存储区域。使用方式如下:
JavaScript
(function() { var $private = createPrivate(); // 私有成员 token 函数,可以传入对象参数,会作为原型链上的私有成员 function MyClass() { $private(this).privateProp = ‘privateProp' ; // 闭包内引用到privateStore, 用当前实例做 key,设置私有成员 } MyClass.prototype.getPrivateProp = function () { return $private(this).privateProp; }; })(); var my = new MyClass(); alert(my.getPrivateProp()); // ‘privateProp'; alert(my.privateProp); // 弹出 undefined,实例上并没有 privateProp 属性
代码中主要就是实现 createPrivate 函数,大概的实现如下:
JavaScript
// createPrivate.js function createPrivate(prototype) { var privateStore = Symbol('privateStore'); var classToken = Symbol(‘classToken'); return function getPrivate(instance) { if (!instance.hasOwnProperty(privateStore)) { instance[privateStore] = {}; } var store = instance[classToken]; store[token] = store[token] || Object.create(prototype || {}); return store[token]; }; }
上述实现做了两层存储,privateStore 这层是实例上统一的私有成员存储区域,而 classToken 对应的则是继承层次之间不同类的私有成员定义,基类有基类的私有成员区域,子类和基类的私有成员区域是不同的。
当然,只做一层的存储也可以实现,两层存储仅仅是为了调试方便,可以直接在控制台通过Symbol(‘privateStore')这个属性来查看实例各个层次的私有部分。
奇葩的 es5 property getter 拦截方案
该方案纯粹是闲得无聊玩了玩,主要是利用了 es5 提供的 getter,根据 argument.callee.caller 去判断调用场景,如果是外部的则抛异常或返回 undefined, 如果是内部调用则返回真正的私有成员,实现起来比较复杂,且不支持 strict 模式,不推荐使用。 有兴趣的同学可以看看实现。
总结
以上几个方案对比下来,我个人是倾向 Symbol+WeakMap 的整合方案,结合了两者的优点,又弥补了 WeakMap 的不足和 Symbol 书写的冗余。 当然了,我相信随着 JS 的发展,私有成员和保护成员也迟早会在语法层面上进行支持,正如 es6 对 class 关键字和 super 语法糖的支持一样, 只是现阶段需要开发者使用一些技巧去填补语言特性上的空白。
Javascript私有成员的实现方式
总体来讲这本书还是可以的,但看完这本书还留了几个问题一直困扰着我,如js中私有变量的实现,prototype等,经过自己一系列测试,现在终于弄明白了。
很多书上都是说,Javascript是不能真正实现Javascript私有成员的,因此在开发的时候,统一约定 __ 两个下划线开头为私有变量。
后来,发现Javascript中闭包的特性,从而彻底解决了Javascript私有成员的问题。
function testFn(){ var _Name;//定义Javascript私有成员 this.setName = function(name){ _Name = name; //从当前执行环境中获取_Name } this.getName = function(){ return _Name; } }// End testFn var test = testFn(); alert(typeof test._Name === "undefined")//true test.setName("KenChen");
test._Name 根本访问不到,但是用对象方法能访问到,因为闭包能从当前的执行环境中获取信息。
接下来我们看看,共有成员是怎样实现的
function testFn(name){ this.Name = name; this.getName = function(){ return this.Name; } } var test = new testFn("KenChen"); test.getName(); //KenChen test.Name = "CC"; est.getName();//CC
接下来在看看类静态变量是怎样实现的
function testFn(){ } testFn.Name = "KenChen"; alert(testFn.Name);//KenChen testFn.Name = "CC"; alert(testFn.Name);//CC