如何使用JavaScript的Object.defineProperty

190
我寻找如何使用Object.defineProperty方法的相关信息,但是没有找到什么有用的东西。
有人给我提供了这段代码片段:
Object.defineProperty(player, "health", {
    get: function () {
        return 10 + ( player.level * 15 );
    }
})

但我不理解它。主要是,我无法获得 get (双关语意)。它是如何工作的?


1
这是一个非常好的教程。https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty - MartianMartian
10个回答

537

既然你提了一个类似的问题,我们就一步一步来解决吧。这会有点长,但比我写这个答案花费的时间要多得多:

属性是面向对象编程的一个特性,旨在清晰地分离客户端代码。举个例子,在某个电子商务网站中,你可能会有像这样的对象:

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%”。你需要再次在多个地方进行更改,而且你需要记住在策略改变时随时修改这些行。这是一个糟糕的设计。这就是为什么封装是面向对象编程的基本原则。如果构造函数是这样的:
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; } 
}

然后你只需修改 `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

请注意倒数第二行:正确的折扣值的责任已从客户端代码(电子商店定义)转移到产品定义中。产品负责保持其数据成员的一致性。好的设计是(粗略地说)如果代码与我们的思维方式相同的话。
另外,ES6允许使用class语法,这是一个“特殊函数”,可以为代码提供相同的功能。
class Product {
  constructor(name, price) {
    this.name = name;
    this.price = price;
  }
  #discount = 0; // # = private member in classes
  get discount() { return this.#discount; }
  set discount(v) { this.#discount = v; if(this.#discount>80) this.#discount = 80; }
}

关于属性的内容就这么多。但是JavaScript与纯面向对象语言(如C#)不同,对特性的编码方式也不同:
在C#中,将字段转换为属性是一种破坏性变更,因此如果你的代码可能在单独编译的客户端中使用,公共字段应该被编码为自动实现属性
而在JavaScript中,标准属性(上述具有getter和setter的数据成员)由访问器描述符定义(在你问题中提供的链接中有详细说明)。此外,你还可以使用数据描述符(因此不能在同一个属性上同时使用valueset等):
  • 访问器描述符 = get + set(参见上面的示例)
    • get 必须是一个函数;其返回值用于读取属性;如果未指定,默认为 undefined,行为类似于返回 undefined 的函数
    • set 必须是一个函数;其参数被赋值给属性时填充 RHS;如果未指定,默认为 undefined,行为类似于空函数
  • 数据描述符 = value + writable(参见下面的示例)
    • value 默认为 undefined;如果 writableconfigurableenumerable(见下文)都为 true,则该属性行为类似于普通数据字段
    • writable - 默认为 false;如果不是 true,则该属性为只读;尝试写入将被忽略且不会报错*!

这两种描述符都可以具有以下成员:

  • configurable - 默认值为false;如果不为true,则该属性无法被删除;尝试删除将被忽略而不会报错*!
  • enumerable - 默认值为false;如果为true,则在for(var i in theObject)中进行迭代;如果为false,则不会进行迭代,但仍然可以作为公共属性访问

* 除非在严格模式下 - 在这种情况下,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(yourObject) 防止新属性被添加到 yourObject。使用 Object.isExtensible(<yourObject>) 来检查该方法是否在对象上使用过。这种防止是浅层的(请参阅下文)。
  • Object.seal(yourObject) 与上述相同,属性不能被删除(实际上将所有属性的configurable: false)。使用 Object.isSealed(<yourObject>) 来检测对象上的此特性。这种封闭是浅层的(请参阅下文)。
  • Object.freeze(yourObject) 与上述相同,属性不能被更改(实际上将所有具有数据描述符的属性的writable: false)。Setter 的 writable 属性不受影响(因为它没有该属性)。冻结是浅层的:这意味着如果属性是对象,则其属性不会被冻结(如果您希望冻结,请执行类似于深拷贝 - 克隆的“深度冻结”操作)。使用 Object.isFrozen(<yourObject>) 来检测它。
如果你只是写几行有趣的代码,那么你不需要太过烦恼。但是,如果你想编写一个游戏(正如你在链接的问题中提到的),你应该关注良好的设计。试着搜索一些关于“反模式”和“代码异味”的内容。这将帮助你避免像“哦,我需要完全重写我的代码!”这样的情况,如果你想大量编写代码,它可以节省你数月的绝望。祝你好运。

这部分很清楚。function Product(name,price) { this.name = name; this.price = price; var _discount; // 私有成员 Object.defineProperty(this,"discount",{ get: function() { return _discount; }, set: function(value) { _discount = value; if(_discount>80) _discount = 80; } }); } var sneakers = new Product("Sneakers",20); sneakers.discount = 50; // 调用setter,值为50 sneakers.discount+= 20; // 调用setter,值为70 sneakers.discount+= 20; // 值为80,而不是90! alert(sneakers.discount); // 调用getter - abu abu

27

get是一个函数,当你尝试读取值player.health时会调用它,例如:

console.log(player.health);

它实际上与以下内容没有太大区别:

player.getHealth = function(){
  return 10 + this.level*15;
}
console.log(player.getHealth());

get 的反义词是 set,当你需要给某个值赋值时会使用 set。由于没有 setter,因此似乎不打算将玩家的健康值赋值:

player.health = 5; // Doesn't do anything, since there is no set function defined

一个非常简单的例子:

var player = {
  level: 5
};

Object.defineProperty(player, "health", {
  get: function() {
    return 10 + (player.level * 15);
  }
});

console.log(player.health); // 85
player.level++;
console.log(player.health); // 100

player.health = 5; // Does nothing
console.log(player.health); // 100


这就像一个函数,你不需要实际使用 () 来调用它...我不明白他们发明这个东西的想法是什么。函数完全一样:https://jsbin.com/bugipi/edit?js,console,output - vsync

20

defineProperty 是 Object 上的一个方法,可以让你配置属性以满足某些标准。 以下是一个简单的示例,使用一个名为 employee 的对象,其中有两个属性 firstName 和 lastName,并通过覆盖该对象上的 toString 方法来附加这两个属性。

var employee = {
    firstName: "Jameel",
    lastName: "Moideen"
};
employee.toString=function () {
    return this.firstName + " " + this.lastName;
};
console.log(employee.toString());

输出结果为:Jameel Moideen

我将使用defineProperty在对象上更改相同的代码。

var employee = {
    firstName: "Jameel",
    lastName: "Moideen"
};
Object.defineProperty(employee, 'toString', {
    value: function () {
        return this.firstName + " " + this.lastName;
    },
    writable: true,
    enumerable: true,
    configurable: true
});
console.log(employee.toString());

第一个参数是对象的名称,第二个参数是我们正在添加的属性的名称,对于我们来说,它是toString,最后一个参数是JSON对象,其值将成为一个具有三个参数(writable、enumerable和configurable)的函数。现在,我将所有内容都声明为true。

如果您运行示例,您将得到输出:Jameel Moideen

让我们了解为什么我们需要三个属性,例如writable,enumerable和configurable。

writable

Javascript中非常讨厌的部分之一是,如果您将toString属性更改为其他内容,例如:

enter image description here

如果再次运行此操作,则会出现错误。让我们将writable更改为false。如果再次运行相同的操作,您将获得正确的输出“Jameel Moideen”。该属性将防止后面覆盖此属性。

enumerable

如果您打印对象内的所有键,则可以看到所有属性,包括toString。

console.log(Object.keys(employee));

输入图像描述

如果把enumerable设为false,就可以隐藏toString属性,让其他人看不到。如果再次运行,将会输出firstName和lastName。

configurable

如果稍后有人重新定义对象,例如将enumerable设置为true并运行它,你就会看到toString属性再次出现。

var employee = {
    firstName: "Jameel",
    lastName: "Moideen"
};
Object.defineProperty(employee, 'toString', {
    value: function () {
        return this.firstName + " " + this.lastName;
    },
    writable: false,
    enumerable: false,
    configurable: true
});

//change enumerable to false
Object.defineProperty(employee, 'toString', {

    enumerable: true
});
employee.toString="changed";
console.log(Object.keys(employee));

图片描述

您可以将configurable设置为false来限制此行为。

此信息的原始参考来源是我的个人博客


1
我知道这篇文章可能是你在博客上写好后粘贴到这里来的,但至少要记住一点:在SO网站上,截图并不受欢迎。因为无法复制粘贴代码进行测试,搜索引擎和辅助技术也无法识别代码。 - Domino
@JacqueGoupil 你是对的。我会通过添加代码来更新,而不是截图。 - Jameel Moideen
1
说实话,这个答案比被采纳的答案更简单,也更有道理。 - wenn32

2

基本上,defineProperty是一个方法,它需要三个参数 - 一个对象,一个属性和一个描述符。在这个特定的调用中发生的是,player对象的"health"属性被分配为10加上该玩家对象级别的15倍。


0

摘要:

Object.defineProperty(player, "health", {
    get: function () {
        return 10 + ( player.level * 15 );
    }
});

Object.defineProperty 用于在 player 对象上创建一个新属性。 Object.defineProperty 是 JS 运行时环境中本地存在的函数,它接受以下参数:

Object.defineProperty(obj, prop, descriptor)

  1. 我们想要定义新属性的对象
  2. 我们想要定义的新属性的名称
  3. 描述符对象

描述符对象是有趣的部分。在这里,我们可以定义以下内容:

  1. configurable <boolean>: 如果为true,则属性描述符可以被更改并且该属性可以从对象中删除。如果configurable为false,则在Object.defineProperty中传递的描述符属性不能被更改。
  2. writable <boolean>: 如果为true,则可以使用赋值运算符覆盖该属性。
  3. enumerable <boolean>: 如果为true,则可以在for...in循环中迭代该属性。同时,在使用Object.keys函数时,该键将存在。如果该属性为false,则不会在for..in循环中迭代,并且在使用Object.keys时不会显示。
  4. get <function> : 每当需要该属性时调用的函数。而不是直接给出值,调用此函数并返回的值作为属性的值。
  5. set <function> : 每当分配该属性时调用的函数。而不是设置直接值,调用此函数并使用返回的值来设置属性的值。

例子:

const player = {
  level: 10
};

Object.defineProperty(player, "health", {
  configurable: true,
  enumerable: false,
  get: function() {
    console.log('Inside the get function');
    return 10 + (player.level * 15);
  }
});

console.log(player.health);
// the get function is called and the return value is returned as a value

for (let prop in player) {
  console.log(prop);
  // only prop is logged here, health is not logged because is not an iterable property.
  // This is because we set the enumerable to false when defining the property
}


0

是的,不再为设置setter和getter函数进行更多的功能扩展。 这是我的例子:Object.defineProperty(obj,name,func)

var obj = {};
['data', 'name'].forEach(function(name) {
    Object.defineProperty(obj, name, {
        get : function() {
            return 'setter & getter';
        }
    });
});


console.log(obj.data);
console.log(obj.name);

0

import { CSSProperties } from 'react'
import { BLACK, BLUE, GREY_DARK, WHITE } from '../colours'

export const COLOR_ACCENT = BLUE
export const COLOR_DEFAULT = BLACK
export const FAMILY = "'Segoe UI', sans-serif"
export const SIZE_LARGE = '26px'
export const SIZE_MEDIUM = '20px'
export const WEIGHT = 400

type Font = {
  color: string,
  size: string,
  accent: Font,
  default: Font,
  light: Font,
  neutral: Font,
  xsmall: Font,
  small: Font,
  medium: Font,
  large: Font,
  xlarge: Font,
  xxlarge: Font
} & (() => CSSProperties)

function font (this: Font): CSSProperties {
  const css = {
    color: this.color,
    fontFamily: FAMILY,
    fontSize: this.size,
    fontWeight: WEIGHT
  }
  delete this.color
  delete this.size
  return css
}

const dp = (type: 'color' | 'size', name: string, value: string) => {
  Object.defineProperty(font, name, { get () {
    this[type] = value
    return this
  }})
}

dp('color', 'accent', COLOR_ACCENT)
dp('color', 'default', COLOR_DEFAULT)
dp('color', 'light', COLOR_LIGHT)
dp('color', 'neutral', COLOR_NEUTRAL)
dp('size', 'xsmall', SIZE_XSMALL)
dp('size', 'small', SIZE_SMALL)
dp('size', 'medium', SIZE_MEDIUM)

export default font as Font


0

Object.defineProperty() 是一个全局函数。它不能在声明对象的函数内部使用。否则,您必须静态地使用它...


0

Object.defineProperty(Array.prototype, "last", {
  get: function() {
    if (this[this.length -1] == undefined) { return [] }
    else { return this[this.length -1] }
  }
});

console.log([1,2,3,4].last) //returns 4


0

在对象上直接定义一个新属性,或者修改对象上的现有属性,并返回该对象。

注意:您需要直接在 Object 构造函数上调用此方法,而不是在 Object 类型的实例上调用。

   const object1 = {};
   Object.defineProperty(object1, 'property1', {
      value: 42,
      writable: false, //If its false can't modify value using equal symbol
      enumerable: false, // If its false can't able to get value in Object.keys and for in loop
      configurable: false //if its false, can't able to modify value using defineproperty while writable in false
   });

enter image description here

关于定义属性的简单解释。

示例代码:https://jsfiddle.net/manoj_antony32/pu5n61fs/


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