春眠不觉晓,JS 私有属性知多少

106 阅读4分钟

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

欢迎一起代码交流。

前言: JavaScript编程语言并不是像Java等面向对象式语言,其中原因有很多种,比如JavaScript没有直接的类啊,现在的class都是语法糖,不能提供传统的类式继承啊;还有比如JavaScript不能实现信息的隐藏,即不能实现私有属性的功能。

所以呢,在此只是简单介绍几种JavaScript实现私有属性的方式,以及各自的优劣,请各位看官且,请我听细细道来~

1. 基于代码规范实现

很多编码规范把以下划线_开头的变量约定为私有成员,便于同团队开发人员的协同工作,代码如下:

function Test(word) {
    this._word = word;
}

const t = new Test("nice")

但是呢,这样的约束方式比较容易被打破,其实并没有实际实现私有属性的效果,并且可以直接获取私有属性,可靠性并不强:

console.log(t._word) // "nice"

2. 基于闭包原理实现

为了达到私有效果,大家平时使用比较多的方式就是通过Javascript闭包特性。通过高阶函数或者闭包达到私有属性效果。代码如下:

function Test (name) {
    var _name = name;
    this.getName = () => {
        return _name;
    }
}
const t = new Test("tiger");

这种方式隐藏了私有属性,不能直接访问私有属性,只能通过getName方法获取:

console.log(t._name); // undefined
console.log(t.getName()); // "tiger"

闭包方式有很多开发者采用,但是这个方式实现有一个缺陷:

  • 其实,用闭包创建私有变量是不行的,因为实例之间会共享变量,虽然几个变量都实例化了,但是操作的还是同一个属性,这显然是不可取的,这会导致属性的泄露。

3. 基于强引用散列表的实现方式

JavaScript是不支持Map数据结构的,所谓强引用散列表方式其实是Map模式的一种变体。简单来讲,就是给每个实例新增一个唯一的标识符,以此标识符为key,对应的value便是这个实例的私有属性,这对key-value保存在一个Object内。实现方式如下:

var Person = (function () {
    var privateData = {},
        privateId = 0;

    function Person(name) {
        Object.defineProperty(this, "_id", { value: privateId++ });

        privateData[this._id] = {
            name
        };
    }

    Person.prototype.getName = function () {
        return privateData[this._id].name;
    };

    return Person;
}());

当然,你也可以通过Symbol数据类型进行键的取值,利用 Symbol 变量可以作为对象 key 的特点,我们可以模拟实现更真实的私有属性。 具体代码如下:

var Person = (function () {
    const d = Symbol('d'); // 私有属性symbol
    const m = Symbol('m'); // 私有方法symbol
    
    class Person {
        constructor(first_name, last_name) {
        this[d] = {
            // 所有的私有属性
            // 通过`this[d].xxxx`来访问私有属性
            first_name: first_name,
            last_name: last_name
        }
    }
    
      get full_name() {
          return this[m].get_full_name.call(this)
      }
    }

      Person.prototype[m] = {
          // 私有方法
          // 通过`this[m].get_full_name.call(this)`可以访问私有方法
          get_full_name() {
              return `${this[d].first_name} ${this[d].last_name}`;
          },
      };
          return Person;
      })();

虽然Symbol 唯一,不会暴露,外界拿不到,但是这个也不是毫无破绽,ES6 的 Object.getOwnPropertySymbols 可以获取symbol属性的,而且散列表d对每个实例都是强引用,导致实例不能被垃圾回收处理。如果存在大量实例必然会导致内存溢出(memory leak)。所以为了解决上述问题,我们可以使用接下来的方法:WeakMap

4. 基于WeakMap的实现方式

WeakMap有以下特点:

  1. 支持使用对象类型作为key值;
  2. 弱引用。 如果对WeakMap不了解的话,请点这里哦

根据WeakMap的特点,便不必为每个实例都创建一个唯一标识符,因为实例本身便可以作为WeakMapkey:

var Person = (function () {
    var privateData = new WeakMap();
    function Person(name, age) {
        privateData.set(this, {
            name,
            age
        });
    }
    Person.prototype.getName = function () {
        return privateData.get(this).name;
    };
    Person.prototype.getAge = function () {
        return privateData.get(this).age;
    };
    return Person;
}());

改进后的代码不仅仅干净整洁了不少,而且WeakMap是一种弱引用散列表, 这意味着,如果没有其他引用和该键引用同一个对象,这个对象将会被当作垃圾回收掉,解决了内存泄露的问题。

但是呢,目前部分浏览器不支持WeakMap的一些方法。

5. 新式做法 - TypeScript 中的处理方式

TypeScript 是 JavaScript 的一个超集,它会编译为原生 JavaScript 用在生产环境。允许指定私有的、公共的或受保护的属性是 TypeScript 的特性之一。

class Person {
    private name;
    private age;
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
    get userInfo() {
        return '姓名:' + this.name + ',年龄:' + this.age;
    }
}
const person = new Person('JeSenFu', 200);
console.log(person.userInfo); // 姓名:JeSenFu,年龄:200

使用 TypeScript 需要注意的重要一点是,它只有在 编译 时才获知这些类型,而私有、公共修饰符在编译时才有效果。如果你尝试访问 person.name,你会发现,居然是可以的。只不过 TypeScript 会在编译时给你报出一个错误,但不会停止它的编译。

TypeScript 是不会自作聪明的,并不会做任何的事情来尝试阻止代码在运行时访问私有属性,它只是对代码进行一个约束。大家要知道它并不能直接解决问题。

其实啊,TypeScript 的 class 私有变量最终编译也是通过 WeakMap 来实现的~。

好啦,本文的内容就结束啦~,欢迎大家的点赞加收藏~,如果有什么疑问可以一起讨论哦~❤