既然你问了类似的问题,让我们一步一步来。它有点长,但它可能比我花在写这篇文章上节省的时间要多得多:
属性是一项 OOP 功能,旨在彻底分离客户端代码。例如,在某些电子商店中,您可能有如下对象:
function Product(name,price) {
this.name = name;
this.price = price;
this.discount = 0;
}
var sneakers = new Product("Sneakers",20); // {name:"Sneakers",price:20,discount:0}
var tshirt = new Product("T-shirt",10); // {name:"T-shirt",price:10,discount:0}
然后,在您的客户代码(电子商店)中,您可以为产品添加折扣:
function badProduct(obj) { obj.discount+= 20; ... }
function generalDiscount(obj) { obj.discount+= 10; ... }
function distributorDiscount(obj) { obj.discount+= 15; ... }
后来,电子商店老板可能会意识到折扣不能大于80%。现在,您需要在客户端代码中找到每次出现的折扣修改,并添加一行
if(obj.discount>80) obj.discount = 80;
然后,电子商店老板可能会进一步改变他的策略,比如“如果客户是经销商,最大折扣可以是90%”。而且您需要再次在多个地方进行更改,并且需要记住在策略更改时更改这些行。这是一个糟糕的设计。这就是为什么封装是OOP的基本原则。如果构造函数是这样的:
function Product(name,price) {
var _name=name, _price=price, _discount=0;
this.getName = function() { return _name; }
this.setName = function(value) { _name = value; }
this.getPrice = function() { return _price; }
this.setPrice = function(value) { _price = value; }
this.getDiscount = function() { return _discount; }
this.setDiscount = function(value) { _discount = value; }
}
然后,您可以更改(访问器)和(赋值器)方法。问题是大多数成员的行为都像公共变量一样,只是折扣在这里需要特别注意。但是,良好的设计需要封装每个数据成员,以保持代码的可扩展性。因此,您需要添加大量不执行任何操作的代码。这也是一个糟糕的设计,一个样板反模式。有时你不能只是将字段重构为方法(eshop代码可能会变得很大,或者一些第三方代码可能依赖于旧版本),所以样板在这里是较小的邪恶。但是,它仍然是邪恶的。这就是属性被引入多种语言的原因。您可以保留原始代码,只需将折扣成员转换为具有和块的属性:getDiscount
setDiscount
get
set
function Product(name,price) {
this.name = name;
this.price = price;
//this.discount = 0; // <- remove this line and refactor with the code below
var _discount; // private member
Object.defineProperty(this,"discount",{
get: function() { return _discount; },
set: function(value) { _discount = value; if(_discount>80) _discount = 80; }
});
}
// the client code
var sneakers = new Product("Sneakers",20);
sneakers.discount = 50; // 50, setter is called
sneakers.discount+= 20; // 70, setter is called
sneakers.discount+= 20; // 80, not 90!
alert(sneakers.discount); // getter is called
请注意最后一行:正确折扣值的责任已从客户代码(电子商店定义)移至产品定义。产品负责保持其数据成员的一致性。好的设计是(粗略地说)如果代码的工作方式与我们的想法相同。
关于物业的很多。但是javascript不同于像C#这样的纯面向对象语言,并且以不同的方式编码功能:
在 C# 中,将字段转换为属性是一项重大更改,因此,如果代码可能在单独编译的客户端中使用,则应将公共字段编码为自动实现的属性。
在Javascript中,标准属性(具有上述getter和setter的数据成员)由访问器描述符定义(在问题中的链接中)。以独占方式,您可以使用数据描述符(因此您不能使用即值并设置在同一属性上):
-
访问器描述符 = get + set(请参阅上面的示例)
-
get 必须是一个函数;其返回值用于读取属性;如果未指定,则默认值为未定义,其行为类似于返回未定义的函数
-
set 必须是函数;其参数在为属性赋值时用 RHS 填充;如果未指定,则默认值为未定义,其行为类似于空函数
-
数据描述符 = 值 + 可写(请参阅下面的示例)
-
值默认值未定义;如果可写、可配置和可枚举(见下文)为真,则该属性的行为类似于普通数据字段
-
可写 - 默认为假;如果不是 true,则该属性为只读;尝试写入将被忽略,而不会出现错误*!
两个描述符都可以具有以下成员:
-
可配置 - 默认为假;如果不是 true,则无法删除该属性;尝试删除将被忽略,而不会出现错误*!
-
可枚举 - 默认为假;如果为 true,它将在 ; 中迭代。如果为假,则不会对其进行迭代,但仍可作为公共访问
for(var i in theObject)
* 除非在严格模式下 - 在这种情况下,JS 停止使用 TypeError 执行,除非它被捕获在 try-catch 块中
要读取这些设置,请使用 。Object.getOwnPropertyDescriptor()
通过示例学习:
var o = {};
Object.defineProperty(o,"test",{
value: "a",
configurable: true
});
console.log(Object.getOwnPropertyDescriptor(o,"test")); // check the settings
for(var i in o) console.log(o[i]); // nothing, o.test is not enumerable
console.log(o.test); // "a"
o.test = "b"; // o.test is still "a", (is not writable, no error)
delete(o.test); // bye bye, o.test (was configurable)
o.test = "b"; // o.test is "b"
for(var i in o) console.log(o[i]); // "b", default fields are enumerable
如果您不希望允许客户端代码进行此类作弊,则可以通过三个级别的限制来限制对象:
-
Object.preventExtensions(您的对象)可防止将新属性添加到您的对象中。用于检查是否在对象上使用了该方法。预防是浅层的(阅读下文)。
Object.isExtensible(<yourObject>)
-
Object.seal(yourObject)与上述相同,并且无法删除属性(有效地设置为所有属性)。用于检测对象上的此功能。密封很浅(请阅读以下内容)。
configurable: false
Object.isSealed(<yourObject>)
-
Object.freeze(yourObject)与上述相同,并且属性无法更改(有效地使用数据描述符设置为所有属性)。Setter 的可写属性不受影响(因为它没有)。冻结是浅层的:这意味着如果属性是Object,则其属性不会冻结(如果您愿意,则应执行类似“深度冻结”之类的操作,类似于深度复制 - 克隆)。用于检测它。
writable: false
Object.isFrozen(<yourObject>)
如果你只写了几行有趣的话,你就不需要为此烦恼。但是,如果你想编写一个游戏(正如你在链接的问题中提到的),你应该关心好的设计。尝试在谷歌上搜索一些关于反模式和代码气味的东西。它将帮助您避免诸如“哦,我需要再次完全重写我的代码!”之类的情况,如果您想编写大量代码,它可以为您节省数月的绝望。祝你好运。