每天一个小技巧:Javascript中定义私有属性(Private Properties) IIFE 实现构造函数实现Class实现原生实现

2020-07-10 11:16:04 浏览数 (1)

和很多高级语言不同,JavaScript 中没有 publicprivateprotected 这些访问修饰符(access modifiers),而且长期以来也没有私有属性这个概念,对象的属性/方法默认都是public的。虽然目前 class 的私有属性特性已经进入了 Stage3 实验阶段(Spec),通过 Babel 已经可以使用,并且 Node v12 中也增加了对私有属性的支持,但这并不妨碍我们用 JS 的现有功能实现一个私有属性特性,以加深对这一概念的理解。

私有属性(方法)的意义在于将模块的内部实现隐藏起来,而对外接口只通过public成员进行暴露,以减少其他模块对该模块内部实现的依赖或修改,降低模块的维护成本。

IIFE 实现

IIFE(立即执行函数) 大家应该耳熟能详了,IIFE 经常被用来:

  1. 定义一个自执行的匿名函数
  2. 创建一个局部作用域,避免对全局产生污染

基于以上特性,用 IIFE 可以给一个对象实现简单的私有属性:

代码语言:javascript复制
let person = (function () {
  // 私有属性
  let _name = "bruce"; 

  return {
    age: 30,
    // getter
    get name() {
      return _name;
    },
    // setter
    set name(val) {
      _name = val;
    },
    greet: function () {
      console.log(`hi, i'm ${_name} and i'm ${this.age} years old`);
    }
  };
})();

测试一下:

代码语言:javascript复制
console.log(person.name); // 'bruce'
console.log(person._name); // undefined

person.name = "frank";

console.log(person.name); // 'frank'

console.log(Object.keys(person)); // ['age', 'name']

person.greet(); // hi, i'm frank and i'm 30 years old

IIFE 的实现简单易懂,但是只能作用于单个对象,而不能给 Class 或者构造函数定义私有属性。

构造函数实现

利用在构造函数中创建的局部变量可以作为 “私有属性” 使用:

代码语言:javascript复制
function Person(name, age) {
  // 私有属性
  let _name = name; 
  
  this.age = age;
  this.setName = function (name) {
    _name = name;
  };
  this.getName = function () {
    return _name;
  };
}

Person.prototype.greet = function (){
  console.log(`hi, i'm ${this.getName()} and i'm ${this.age} years old`);
}

测试一下:

代码语言:javascript复制
const person = new Person("bruce", 30);

console.log(person.getName()); // bruce

person.setName('frank');

console.log(person.getName()); // frank

person.greet(); // hi, i'm frank and i'm 30 years old

看起来还行,但是该实现方式需要在构造函数中定义 gettersetter 方法,这两个方法是绑定在实例上而不是原型上的,如果私有属性增加会导致实例方法暴增,对内存不太友好。

Class实现

Class中实现和构造函数类似,因为JavaScript中的class本质上是构造函数和原型的语法糖,实现如下:

代码语言:javascript复制
class Person {
  constructor(name, age) {
    // 私有属性
    let _name = name; 
    
    this.age = age;
    this.setName = function (name) {
      _name = name;
    };
    this.getName = function () {
      return _name;
    };
  }

  greet() {
    console.log(`hi, i'm ${this.getName()} and i'm ${this.age} years old`);
  }
}

Class中的实现也会存在和构造函数中一样的问题,而且在 greet() 方法中无法访问 _name,需要通过调用 getter 方法。这和一般意义上的私有属性还是有差别的,真正的私有属性在class内部应该是可以正常访问的,而不仅仅是在构造函数内部可以访问。

原生实现

以上三种实现或多或少都有一些问题,还好在ES2019中已经增加了对 class 私有属性的原生支持,只需要在属性/方法名前面加上 '#' 就可以将其定义为私有,并且支持定义私有的 static 属性/方法。例如:

代码语言:javascript复制
class Person {
  // 私有属性
  #name; 

  constructor(name, age) {
    this.#name = name;
    this.age = age;
  }

  greet() {
    console.log(`hi, i'm ${this.#name} and i'm ${this.age} years old`);
  }
}

测试一下:

代码语言:javascript复制
const person = new Person("bruce", 30);

console.log(person.name); // undefine

person.greet(); // hi, i'm bruce and i'm 30 years old

更多语法可以参考 MDN: Private class field。

我们可以去babel里面将原生的代码转换一下,看看babel的polyfill是怎么实现的:

发现主要思路居然使用 WeakMap。。好吧,还是太年轻。格式化后的polyfill代码贴在下面,有兴趣的同学可以研究一下:

代码语言:javascript复制
"use strict";

function _classPrivateFieldGet(receiver, privateMap) {
  var descriptor = privateMap.get(receiver);
  if (!descriptor) {
    throw new TypeError("attempted to get private field on non-instance");
  }
  if (descriptor.get) {
    return descriptor.get.call(receiver);
  }
  return descriptor.value;
}

function _classPrivateFieldSet(receiver, privateMap, value) {
  var descriptor = privateMap.get(receiver);
  if (!descriptor) {
    throw new TypeError("attempted to set private field on non-instance");
  }
  if (descriptor.set) {
    descriptor.set.call(receiver, value);
  } else {
    if (!descriptor.writable) {
      throw new TypeError("attempted to set read only private field");
    }
    descriptor.value = value;
  }
  return value;
}

var _name = new WeakMap();

class Person {
  constructor(name, age) {
    _name.set(this, {
      writable: true,
      value: void 0,
    });

    _classPrivateFieldSet(this, _name, name);

    this.age = age;
  }

  greet() {
    console.log(
      "hi, i'm "
        .concat(_classPrivateFieldGet(this, _name), " and i'm ")
        .concat(this.age, " years old")
    );
  }
}

每天一个小技巧(Tricks by Day),量变引起质变,希望你和我一起每天多学一点,让技术有趣一点。所有示例将会汇总到我的 tricks-by-day github 项目中,欢迎大家莅临指导 ?

0 人点赞