JavaScript私有方法

546

要创建一个有公共方法的JavaScript类,我会这样做:

function Restaurant() {}

Restaurant.prototype.buy_food = function(){
   // something here
}

Restaurant.prototype.use_restroom = function(){
   // something here
}

这样我的类的用户可以:

var restaurant = new Restaurant();
restaurant.buy_food();
restaurant.use_restroom();

如何创建一个私有方法,该方法可以被 buy_fooduse_restroom 方法调用,但不能被类的用户从外部调用?

换句话说,我希望我的方法实现能够做到:

Restaurant.prototype.use_restroom = function() {
   this.private_stuff();
}

但这不应该起作用:

var r = new Restaurant();
r.private_stuff();

如何将private_stuff定义为私有方法,以便这两个条件都成立?

我已经阅读了道格·克罗福德的文章几次,但似乎“私有”方法不能被公共方法调用,“特权”方法可以在外部调用。


请参考此答案,其中提供了一个干净简洁的“类”解决方案,具有私有和公共接口以及组合支持。 - kofifus
对于现代读者的注意事项,这个问题在JavaScript的现代版本(称为ECMAScript或ES)出现之前就已经提出了(并得到了回答)。如果你刚开始学习JavaScript,那么语法上的差异/缺少class是因为这个问题旧了。现在存在一种实验性的功能,可以使用#作为前缀来允许私有方法,但截至2020-08-19,并不是所有浏览器都支持该功能。请参见: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_class_fields - Josh Desmond
34个回答

452

你可以这样做,但缺点是它不能成为原型的一部分:

function Restaurant() {
    var myPrivateVar;

    var private_stuff = function() {  // Only visible inside Restaurant()
        myPrivateVar = "I can set this here!";
    }

    this.use_restroom = function() {  // use_restroom is visible to all
        private_stuff();
    }

    this.buy_food = function() {   // buy_food is visible to all
        private_stuff();
    }
}

9
将东西隐藏在闭包中并不能保证在所有解释器上的隐私。请参阅http://code.google.com/p/google-caja/wiki/EvalBreaksClosureEncapsulation。 - Mike Samuel
55
@mikesamuel - 是的,但只有在这些解释器存在漏洞时才会出现这种情况 :) - Jerod Venema
145
这是一个私有方法,但会比常规原型方法消耗更多内存,特别是如果您创建了大量这些对象。对于每个对象实例,它都会创建一个绑定到对象而不是类的单独函数。此外,只有在对象本身被销毁时,该函数才会被垃圾回收。 - Arindam
77
私有方法不应该被覆盖 - 它们是私有的。 - 17 of 26
4
如果你想要私有方法(而不是私有数据),那么只需要在原型之外简单地声明它们,并像这样调用:myFoo.apply(this, args)。这样你就可以保持公共接口简洁,同时仍然拥有可在本地对象上运行的私有方法。 - oligofren
显示剩余13条评论

217

使用自执行函数和call方法

JavaScript使用原型,没有像面向对象语言那样的类(或者说方法)。JavaScript开发人员需要以JavaScript的思维方式进行思考。

维基百科引用:

与许多面向对象的语言不同,函数定义和方法定义之间没有区别。相反,在函数调用时出现了区别;当一个函数作为对象的方法被调用时,该函数的本地this关键字会绑定到该对象上。

使用自执行函数call方法来调用私有“方法”的解决方案:

var MyObject = (function () {
    
  // Constructor
  function MyObject(foo) {
    this._foo = foo;
  }

  function privateFun(prefix) {
    return prefix + this._foo;
  }
    
  MyObject.prototype.publicFun = function () {
    return privateFun.call(this, ">>");
  }
    
  return MyObject;

}());

var myObject = new MyObject("bar");
myObject.publicFun();      // Returns ">>bar"
myObject.privateFun(">>"); // ReferenceError: private is not defined

call function允许我们使用适当的上下文(this)调用私有函数。

使用Node.js更简单

如果你正在使用Node.js,你不需要IIFE,因为你可以利用模块加载系统:

function MyObject(foo) {
  this._foo = foo;
}
    
function privateFun(prefix) {
  return prefix + this._foo;
}

MyObject.prototype.publicFun = function () {
  return privateFun.call(this, ">>");
}
    
module.exports= MyObject;

加载文件:

var MyObject = require("./MyObject");
    
var myObject = new MyObject("bar");
myObject.publicFun();      // Returns ">>bar"
myObject.privateFun(">>"); // ReferenceError: private is not defined

(新!)未来JavaScript版本中的本地私有方法

TC39 JavaScript类的私有方法和getter/setter 提案处于第3阶段。这意味着很快,JavaScript将原生支持私有方法!

请注意,JavaScript私有类字段 已经存在于现代JavaScript版本中。

以下是一个示例,展示了如何使用它:

class MyObject {

  // Private field
  #foo;
    
  constructor(foo) {
    this.#foo = foo;
  }

  #privateFun(prefix) {
   return prefix + this.#foo;
  }
    
  publicFun() {
    return this.#privateFun(">>");
  }

}

你可能需要一个JavaScript转译器/编译器来在旧的JavaScript引擎上运行此代码。

PS:如果你想知道为什么要使用#前缀,请阅读此处

(已弃用) ES7与绑定操作符

警告:绑定操作符TC39提案已经接近死亡https://github.com/tc39/proposal-bind-operator/issues/53#issuecomment-374271822

绑定操作符::是ECMAScript提案,并且已经在Babel中实现(stage 0)。

export default class MyObject {
  constructor (foo) {
    this._foo = foo;
  }

  publicFun () {
    return this::privateFun(">>");
  }
}

function privateFun (prefix) {
  return prefix + this._foo;
}

38
这是最佳答案。私有方法的好处,没有无用内容。 - TaylorMac
1
@TaylorMac 除了 .call 部分。 - pishpish
1
@janje 嗯?这就是问题的重点,目前 JavaScript 中没有私有 f() - Yves M.
2
@YvesM。这个问题的关键是选择最好的模式来模拟它。 - pishpish
1
私有字段提案已与 https://github.com/tc39/proposal-class-fields 合并,现已进入第4阶段。 - wheredidthatnamecomefrom
显示剩余5条评论

163

您可以像这样模拟私有方法:

function Restaurant() {
}

Restaurant.prototype = (function() {
    var private_stuff = function() {
        // Private code here
    };

    return {

        constructor:Restaurant,

        use_restroom:function() {
            private_stuff();
        }

    };
})();

var r = new Restaurant();

// This will work:
r.use_restroom();

// This will cause an error:
r.private_stuff();

关于这种技术的更多信息可以在这里查看:http://webreflection.blogspot.com/2008/04/natural-javascript-private-methods.html


7
我建议你去查看道格拉斯·克罗克福德的网站,关于私有/公有方法和成员方面的资源 http://javascript.crockford.com/private.html - Jared
10
他在问题中提到了那个链接。 - Gulzar Nazim
9
这种方法的缺点是你不能让private_stuff()接触到餐厅中的其他私有数据,而其他餐厅方法也无法调用private_stuff()。 优点是,如果你不需要我刚刚提到的条件之一,你可以在原型中保留use_restroom()。 - 17 of 26
6
这应该是解决方案并被采纳的答案,因为作者显然在使用原型属性。 - Gabriel Llamas
26
采用@georgebrock提出的方案,所有私有数据将在所有餐厅对象之间共享。这类似于基于类的面向对象编程中的静态私有变量和函数。我认为原始帖子并不希望这样。为了说明我的意思,我创建了jsFiddle - feklee
显示剩余10条评论

38
在具有公共API且需要私有和公共方法/属性的情况下,我总是使用模块模式。这种模式在YUI库中很流行,详细信息可以在此处找到:

http://yuiblog.com/blog/2007/06/12/module-pattern/

这非常直观,其他开发人员也很容易理解。以下是一个简单的例子:

var MYLIB = function() {  
    var aPrivateProperty = true;
    var aPrivateMethod = function() {
        // some code here...
    };
    return {
        aPublicMethod : function() {
            aPrivateMethod(); // okay
            // some code here...
        },
        aPublicProperty : true
    };  
}();

MYLIB.aPrivateMethod() // not okay
MYLIB.aPublicMethod() // okay

这种事情不会被IDE的自动完成检测到 :( - Ali
21
但这不是一个类,所以你不能有两个具有不同状态的“实例”。 - DevAntoine
去掉()部分,你就有了一个“类”。至少在这里,您可以用不同的状态实例化不同的对象。模块模式虽然非常消耗内存... - oligofren
@DevAntoine 请查看17 of 26的答案的评论。在JavaScript中,可扩展类和私有方法不容易同时实现。在这种情况下,我的建议是采用组合而非继承。创建一个可扩展的原型,其中包含与封闭的具体对象相同的方法。然后在原型内部,您可以决定何时调用具体对象上的方法。 - user4903
从私有函数中调用公共变量是否存在任何缺点,例如:aPrivateMethod = function() { MYLIB.aPublicProperty} - Hanna
这不允许私有方法访问公共方法。 - Stas Bichenko

25

ES12 私有方法

现在您可以使用ES12私有方法 来实现这个了。您只需要在方法名称前面添加一个 #

class ClassWithPrivateMethod {
  #privateMethod() {
    return 'hello world';
  }

  getPrivateMessage() {
    return #privateMethod();
  }
}

4
除此之外,这是第三阶段,尚未正式成为该语言的一部分。 - misterhtmlcss
2
现在,它被92.24%的浏览器支持(截至本评论时)。 - D-Marc
2
截至2022年,这应该是选择的答案,至少对于node.js LTS来说。 - Kyeno

21

这是我创建的类,用于理解Douglas Crockford在他的网站上建议的JavaScript中的私有成员

function Employee(id, name) { //Constructor
    //Public member variables
    this.id = id;
    this.name = name;
    //Private member variables
    var fName;
    var lName;
    var that = this;
    //By convention, we create a private variable 'that'. This is used to     
    //make the object available to the private methods. 

    //Private function
    function setFName(pfname) {
        fName = pfname;
        alert('setFName called');
    }
    //Privileged function
    this.setLName = function (plName, pfname) {
        lName = plName;  //Has access to private variables
        setFName(pfname); //Has access to private function
        alert('setLName called ' + this.id); //Has access to member variables
    }
    //Another privileged member has access to both member variables and private variables
    //Note access of this.dataOfBirth created by public member setDateOfBirth
    this.toString = function () {
        return 'toString called ' + this.id + ' ' + this.name + ' ' + fName + ' ' + lName + ' ' + this.dataOfBirth; 
    }
}
//Public function has access to member variable and can create on too but does not have access to private variable
Employee.prototype.setDateOfBirth = function (dob) {
    alert('setDateOfBirth called ' + this.id);
    this.dataOfBirth = dob;   //Creates new public member note this is accessed by toString
    //alert(fName); //Does not have access to private member
}
$(document).ready()
{
    var employee = new Employee(5, 'Shyam'); //Create a new object and initialize it with constructor
    employee.setLName('Bhaskar', 'Ram');  //Call privileged function
    employee.setDateOfBirth('1/1/2000');  //Call public function
    employee.id = 9;                     //Set up member value
    //employee.setFName('Ram');  //can not call Private Privileged method
    alert(employee.toString());  //See the changed object

}

5
这个“that=this”的模式非常常见,由前面提到的 Crockford 在他的书《JavaScript语言精粹》中推广。 - oligofren
8
为避免作用域问题,当函数绑定到不同的对象时,使用“that”代替“this”。在这里,你将“this”存储在“that”中,却从未再次使用它,这与根本没有使用是一样的。你应该在“Constructor”内部函数(非方法声明)中全部使用“that”来替换“this”。如果以某种方式applycall了“employee”,这些方法可能会抛出异常,因为“this”将被错误地绑定。 - Maroshii
此外,每个实例都将拥有私有函数的完整副本,效率低下。再加上公共方法无法访问私有类变量这一事实,让我想切换到Dart。不幸的是,Angulardart还处于超级测试版。 - Ray Foss

15

ES2021 / ES12 - 私有方法

私有方法名称以井号 # 前缀开头,只能在定义它的类内部访问。

class Restaurant {

  // private method
  #private_stuff() {
    console.log("private stuff");
  }

  // public method
  buy_food() {
    this.#private_stuff();
  }

};

const restaurant = new Restaurant();
restaurant.buy_food(); // "private stuff";
restaurant.private_stuff(); // Uncaught TypeError: restaurant.private_stuff is not a function

1
在调用站点上是否需要 #?不得不在所有调用站点上添加 # 看起来相当繁琐。 - wilx
1
@wilx 你可以直接按F2键(例如在VSCode中),更改字段名称,以#为前缀,然后名称将在代码中的所有位置更新。 - Parziphal
这已经发布了,但遗憾的是,_私有类方法_目前在Safari和IE上不起作用。浏览器兼容性列表 - Mahsa2
添加 # 将允许调用私有方法。restaurant.#private_stuff(); // "private stuff"; - MIA

13
我想到了这个:编辑:实际上,有人链接到了一个完全相同的解决方案。傻瓜!
var Car = function() {
}

Car.prototype = (function() {
    var hotWire = function() {
        // Private code *with* access to public properties through 'this'
        alert( this.drive() ); // Alerts 'Vroom!'
    }

    return {
        steal: function() {
            hotWire.call( this ); // Call a private method
        },
        drive: function() {
            return 'Vroom!';
        }
    };
})();

var getAwayVechile = new Car();

hotWire(); // Not allowed
getAwayVechile.hotWire(); // Not allowed
getAwayVechile.steal(); // Alerts 'Vroom!'

1
这是一种不错的技巧,但是如何在构造函数中传递参数呢?例如 var getAwayVehicle = new Car(100); 其中 100 是速度并且你想要弹出速度。谢谢! - Jason
1
弄清楚了,可以有 var Car = function(speed) { this.speed = speed; }return { constructor: Car, ...} - Jason

11

我认为这些问题一遍又一遍地出现是因为对闭包的理解不够。闭包是 JavaScript 中最重要的东西,每个 JavaScript 程序员都必须感受它的本质。

1. 首先,我们需要创建一个独立的作用域(闭包)。

function () {

}

2. 在这个区域,我们可以做任何想做的事情。而且没有人会知道。

function () {
    var name,
        secretSkills = {
            pizza: function () { return new Pizza() },
            sushi: function () { return new Sushi() }
        }

    function Restaurant(_name) {
        name = _name
    }
    Restaurant.prototype.getFood = function (name) {
        return name in secretSkills ? secretSkills[name]() : null
    }
}

3. 要让世界知道我们餐厅的水平,我们必须让它从关闭中回归。

var Restaurant = (function () {
    // Restaurant definition
    return Restaurant
})()

4. 最后,我们得到:

var Restaurant = (function () {
    var name,
        secretSkills = {
            pizza: function () { return new Pizza() },
            sushi: function () { return new Sushi() }
        }

    function Restaurant(_name) {
        name = _name
    }
    Restaurant.prototype.getFood = function (name) {
        return name in secretSkills ? secretSkills[name]() : null
    }
    return Restaurant
})()

5. 同时,这种方法具有继承和模板化的潜力

// Abstract class
function AbstractRestaurant(skills) {
    var name
    function Restaurant(_name) {
        name = _name
    }
    Restaurant.prototype.getFood = function (name) {
        return skills && name in skills ? skills[name]() : null
    }
    return Restaurant
}

// Concrete classes
SushiRestaurant = AbstractRestaurant({ 
    sushi: function() { return new Sushi() } 
})

PizzaRestaurant = AbstractRestaurant({ 
    pizza: function() { return new Pizza() } 
})

var r1 = new SushiRestaurant('Yo! Sushi'),
    r2 = new PizzaRestaurant('Dominos Pizza')

r1.getFood('sushi')
r2.getFood('pizza')

我希望这可以帮助某人更好地理解这个主题。


2
你在第四点中提到的真是太棒了!我认为这是所有答案中唯一一个既可以获得使用原型上的方法带来的性能/内存增益,又可以完全访问私有成员的公共方法。+1 - Hudon
7
这段话的意思是:“它无效。这里的名称变量像静态变量一样运作,并被所有Restaurant实例共享。这是jsbin链接:http://jsbin.com/oqewUWa/2/edit?js,output。” - Shital Shah
这是一个不错的尝试,但正如Shital指出的那样,name变量有漏洞。 - oligofren
2
在这里加入我的两分钱,以重申这不起作用,看起来很好,但正如上面指出的那样,“name”将作为静态变量,即在所有实例之间共享。 - Paul Carroll

10

就我个人而言,在JavaScript中创建类的首选模式如下:

var myClass = (function() {
    // Private class properties go here

    var blueprint = function() {
        // Private instance properties go here
        ...
    };

    blueprint.prototype = { 
        // Public class properties go here
        ...
    };

    return  {
         // Public class properties go here
        create : function() { return new blueprint(); }
        ...
    };
})();

正如您所看到的,它允许您定义类属性和实例属性,每个属性都可以是公开或私有。


演示

var Restaurant = function() {
    var totalfoodcount = 0;        // Private class property
    var totalrestroomcount  = 0;   // Private class property
    
    var Restaurant = function(name){
        var foodcount = 0;         // Private instance property
        var restroomcount  = 0;    // Private instance property
        
        this.name = name
        
        this.incrementFoodCount = function() {
            foodcount++;
            totalfoodcount++;
            this.printStatus();
        };
        this.incrementRestroomCount = function() {
            restroomcount++;
            totalrestroomcount++;
            this.printStatus();
        };
        this.getRestroomCount = function() {
            return restroomcount;
        },
        this.getFoodCount = function() {
            return foodcount;
        }
    };
   
    Restaurant.prototype = {
        name : '',
        buy_food : function(){
           this.incrementFoodCount();
        },
        use_restroom : function(){
           this.incrementRestroomCount();
        },
        getTotalRestroomCount : function() {
            return totalrestroomcount;
        },
        getTotalFoodCount : function() {
            return totalfoodcount;
        },
        printStatus : function() {
           document.body.innerHTML
               += '<h3>Buying food at '+this.name+'</h3>'
               + '<ul>' 
               + '<li>Restroom count at ' + this.name + ' : '+ this.getRestroomCount() + '</li>'
               + '<li>Food count at ' + this.name + ' : ' + this.getFoodCount() + '</li>'
               + '<li>Total restroom count : '+ this.getTotalRestroomCount() + '</li>'
               + '<li>Total food count : '+ this.getTotalFoodCount() + '</li>'
               + '</ul>';
        }
    };

    return  { // Singleton public properties
        create : function(name) {
            return new Restaurant(name);
        },
        printStatus : function() {
          document.body.innerHTML
              += '<hr />'
              + '<h3>Overview</h3>'
              + '<ul>' 
              + '<li>Total restroom count : '+ Restaurant.prototype.getTotalRestroomCount() + '</li>'
              + '<li>Total food count : '+ Restaurant.prototype.getTotalFoodCount() + '</li>'
              + '</ul>'
              + '<hr />';
        }
    };
}();

var Wendys = Restaurant.create("Wendy's");
var McDonalds = Restaurant.create("McDonald's");
var KFC = Restaurant.create("KFC");
var BurgerKing = Restaurant.create("Burger King");

Restaurant.printStatus();

Wendys.buy_food();
Wendys.use_restroom();
KFC.use_restroom();
KFC.use_restroom();
Wendys.use_restroom();
McDonalds.buy_food();
BurgerKing.buy_food();

Restaurant.printStatus();

BurgerKing.buy_food();
Wendys.use_restroom();
McDonalds.buy_food();
KFC.buy_food();
Wendys.buy_food();
BurgerKing.buy_food();
McDonalds.buy_food();

Restaurant.printStatus();

参见this Fiddle


这让我想使用ES6类并查看它的转译结果。 - sheriffderek

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接