关于ES5中构造函数的问题

我们用组合构造函数和原型模式来创建构造器,构造函数模式用于定义实例属性,而原型模式用于定义方法和共享属性。那么。如何让构造函数模式中的方法不被实例所创建呢?比如下面这个例子:

function Person(name, family) {
    this.name = name;
    this.family = family;
    var records = [{type: "in", amount: 0}];
    this.addTransaction = function(trans) {
        if(trans.hasOwnProperty("type") && trans.hasOwnProperty("amount")) {
           records.push(trans);
        }
    }

    this.balance = function() {
       var total = 0;

       records.forEach(function(record) {
           if(record.type === "in") {
             total += record.amount;
           }
           else {
             total -= record.amount;
           }
       });

        return total;
    };
};
Person.prototype.getFull = function() {
    return this.name + " " + this.family;
};
Person.prototype.getProfile = function() {
     return this.getFull() + ", total balance: " + this.balance();
};
var me = new Person('sun','yang');
me.balance//0
me.getProfile()//"sun yang, total balance: 0"

在这个Person构造函数内部有一个records 的私有变量,这个变量我们是不希望通过函数内部以外的方法去操作这个变量, 但是又想通过构造函数new出来的对象能获取构造函数内部的私有变量,所以在构造函数内定义了两个方法addTransaction和balance(有点像闭包的概念),这两个方法是处理变量的,并把处理的结果赋给Person.prototype上共享给实例。那么问题来了
图片描述
在构造函数内部的addTransaction和balance两个方法只是为了处理变量,并不想被实例创建一个副本,这只是一个简单的例子,如果在一个复杂的构造器,里面有很多这样的方法,那岂不是会造成巨大的内存浪费吗?
另外我在ES6中看到有Class的静态方法,似乎可以解决这样的问题,将上面的例子改写成es6的形式:

class Person{
  constructor(name, family) {
    this.name = name;
    this.family = family;
  }
  static addTransaction = function(trans) {
        if(trans.hasOwnProperty("type") && trans.hasOwnProperty("amount")) {
           records.push(trans);
        }
    }
  static balance = function() {
       let records = [{type: "in", amount: 0}];
       let total = 0;
       records.forEach(function(record) {
           if(record.type === "in") {
             total += record.amount;
           }
           else {
             total -= record.amount;
           }
       });
        return total;
    }

   getFull = function() {
    return this.name + " " + this.family;
 }
   getProfile = function() {
     return this.getFull() + ", total balance: " + Person.balance();
 }
}

let me = new Person('sun','yang');
console.log(me)

如上,我这样写没有报错,另外这两个只是用作处理构造函数内部的方balanceaddTransaction也没被实例所创建。,那么我想知道,虽然我这样写是没有报错,但是自己能力水平很差,可能是自己理解错了,也可能这就不是个问题。如果我没理解错的话,ES6可以这么写,那ES5这个问题是怎么解决的呢?

阅读 6k
6 个回答

由于私有变量这个概念在JS里可能指的是函数内部局部变量,或者是文件级的未export变量,看了一下题目中的标签上有OOP,大概明白楼主问的是类似java和php那种类内部私有变量。

首先要纠正的是,楼主提到的这两个OOP名词:OOP下的私有变量和静态私有变量,在当前的JS下是没有的,虽然可以用原型链包装成类似的概念。

并且JS里的类,实际上和java、php这些OOP语言的类很不相同,它是一个原型链继承的实现。ES2015所加的class关键字也只是一个语法糖。

私有变量目前不在ES规范里,目前有一个提案ECMAScript Private Fields 还在stage1的阶段,不过可以参考一下。

静态变量也不在ES规范里,目前有一个提案Public Class Fields
已经处于stage2的阶段,有很大希望进入ES2017规范里,不过要注意它是public class fields的提案。

以上是JS的现状,再回答一下楼主,区别很简单,就是私有变量是每个实例都独立的,而静态私有变量是共用的,比如我们可以设计这么一个类(用的是草案上的语法)

class Test{
    #a;  // 私有变量
    static #count;  // 静态私有变量

    constructor(a = 0) {
        this.#a = +a;
        #count++;
    }

    get a() { return this.#a }
    set a(value) { this.#a = +value }
    
    static get count(){
       return #count;
    }
}

可以看到这两个变量都已经被封装,外部无法访问,只能通过另外一个方法来代理访问,但是count变量是所有类实例共用的,这样我们可以统计出这个类到底创建了多少个实例。

这和其它OOP语言的概念都是一致的。

须知ES6的class只是语法糖,他的底层还是原型链继承这套东西。可以使用babel编译一下有助于理解。
通过构造函数this定义的属性,都在prototype里面,如果子类发现自身没有该属性都会顺着原型链去寻找,所以不希望子类继承的属性不要用this定义在构造函数里面,子类独有的,让子类自己拥有。构造函数就拿来构造对象你还期待她做什么?
那么又有些方法想用构造函数来处理又不希望被继承,怎么办?
那就不用要this,举个例子:

var Person = function(val){
this.age = val;
console.log('may the force be with you')
}

在new Person()的时候会打印出这句话,但是不会被继承

为什么要在构造函数中添加 局部变量呢?感觉这样做没有意义啊。
records变成这个对象的属性, 就像这样 this.records,然后把方法放到 原型中去,这样所有实例共享这两个方法。

题主为了创建一个私有变量,在构造函数Person里用了一个闭包。为了访问这个闭包中的records变量,题主不得不在构造函数里用 this 给对象添加两个方法,并且不能把这两个方法放到原型中。因为如果放到原型中就无法访问闭包中的records了。

所以我总结一下题主的需求是想要保持records私有的同时,又不想让实例化出来的对象具有addTransactionbalance的属性,而是想把这两个属性放在原型里。

那么答案其实很简单。在 JavaScript 中,要创造私有变量,就必须使用闭包,问题就是闭包用在哪里。既然题主已经使用了闭包,那为什么不把整个构造函数和records变量放在一个闭包里呢?不过按照这个方法,Person本身就不能用new调用了,而是要像普通函数一样调用了。而且实例化出来的各个对象的原型也都不是同一个原型了,题主能接受吗。。。

另外,我怀疑要在对象拥有私有属性的前提下,让原型中的方法能够访问私有属性,而且还要保证实例化出来的对象拥有的是同一个原型,这件事是不可能做到的。因为所有实例化出来的对象的原型是同一个对象。JavaScript 中的作用域不会随着调用的上下文而动态改变,原型对象的作用域始终是定义它的时候的作用域。而一个作用域中只能有一个 records,所以所有实例化的对象所访问的都是这同一个 records,不可能有自己的records。除非你把records变成一个对象,然后把各个实例化对象的私有属性用键值对保存在records里。

如果题主找到了完美的办法,请告诉我。。。

代码如下:

var Person = function (name, family) {
  var records = [{type: "in", amount: 0}];

  function PersonConstructor(name, family){
    this.name = name;
    this.family = family;
  }

  PersonConstructor.prototype.addTransaction = function(trans) {
      if(trans.hasOwnProperty("type") && trans.hasOwnProperty("amount")) {
         records.push(trans);
      }
  }

  PersonConstructor.prototype.balance = function() {
     var total = 0;

     records.forEach(function(record) {
         if(record.type === "in") {
           total += record.amount;
         }
         else {
           total -= record.amount;
         }
     });

      return total;
  };

  PersonConstructor.prototype.getFull = function() {
      return this.name + " " + this.family;
  };

  PersonConstructor.prototype.getProfile = function() {
       return this.getFull() + ", total balance: " + this.balance();
  };

  return new PersonConstructor(name, family);
}
var me = Person('sun','yang');
me.balance()//0
me.getProfile()//"sun yang, total balance: 0"

你可以把处理函数放在构造函数外面,比如function handle(){},然后在构造函数里面去调用这个handle()函数就好了,

本来看到这么多人回答都挺好的,我就忽略邀请了。不过既然再次邀请,我也说说我的想法吧

静态方法与实例方法

首先,如 @maser_yoda 所说 es6 的 class 就是一个语法糖。不过用 Babel 翻译出来,附加代码有点多,所以我用 TypeScript 来翻译下面这段代码(没用到特殊语法,所以翻译结果不会有问题)

class Person {
    constructor(name) {
        this.name = name;
    }
    
    static setName(name) {
      this.name = name;
    }
    
    setName(name) {
      this.name = name;
    }
}
var Person = (function () {
    function Person(name) {
        this.name = name;
    }
    // static setName()
    Person.setName = function (name) {
        this.name = name;
    };
    // setName()
    Person.prototype.setName = function (name) {
        this.name = name;
    };
    return Person;
}());

这个代码就很容易搞明白,由于 JavaScript 的 function 也是对象,所以 static setName() 其实是定义在构造函数这个对象上的,这种方法在其它静态语言中也称为类方法。而非静态的 setName() 则是定义在原型中,即就是实例方法(或对象方法)。

由于类(静态)方法不能访问实例属性(因为类永远不会明白你想把方法作用于哪个实例),所以静态方法里你是不可能去访问实例 records 属性的。你的代码里没加 this 限定,所以访问的是一个全局的 records

成员私有化

JavaScript 并没有从语言的层面提供私有化解决方案,所以要在 JavaScript 里实现严格意义上的私有化是不能实现的。

在很早以前就存在一种解决方法——不使用原型方法,而是使用闭包+实例方法,也就是你第一段代码里用的方法,records 那个闭包变量。缺点你也发现了,就是每个实例都会有相同的方法存在,浪费内存资源。

我在 ES5 中模拟 ES6 的 Symbol 实现私有成员 中讨论了一种使用原型方法的解决方案。这种方案不能从语法上阻止别人去找到真实的成员变量,只是加大了找到它们的难度而已。

对于模拟 Symbol 的问题,现在有 Babel,所以一般不需要模拟 Symbol,Babel 会做替代处理(不过替换的情况下名称非常有规律,很容易查出来)。

提一下 TypeScript 有 private 关键字,但是仅限用于静态语法检查,生成的 JS 中所有 private 的东西都会对外可用(变成 public)。社区里在讨论让 TypeScript 用 Symbol 等方法实现真正私有化的问题,不过目前貌似还没有结果。

推荐问题
宣传栏