Getters和Setters的简明教程

142

12
就个人而言,我不知道你怎么能够得到比约翰所提供的更清晰的解释。 - Jason Bunting
基本上我这样看:你正在定义获取和设置属性的重载,而这些重载是函数;但是,你不必调用它们。这样,你可以用 a = 5; 替换 a = setValue(5);,并且 setValue() 将在幕后被调用以执行任何你想要的操作。 - Andrew
14个回答

106

除了@millimoose的回答之外,setter也可以用于更新其他值。

function Name(first, last) {
    this.first = first;
    this.last = last;
}

Name.prototype = {
    get fullName() {
        return this.first + " " + this.last;
    },

    set fullName(name) {
        var names = name.split(" ");
        this.first = names[0];
        this.last = names[1];
    }
};

现在,您可以设置fullName,然后firstlast将被更新,反之亦然。

n = new Name('Claude', 'Monet')
n.first # "Claude"
n.last # "Monet"
n.fullName # "Claude Monet"
n.fullName = "Gustav Klimt"
n.first # "Gustav"
n.last # "Klimt"

2
@Akash:不,尽管Internet Explorer 9支持更新的Object.defineProperty函数来定义getter和setter。 - Matthew Crumley
9
微软不正确地支持JS并且他们没有使Silverlight在所有平台上都能运行,这真的很痛苦,所以我不得不为SL和其他的世界分别编写两次程序 :) - Akash Kava
2
@Martin:你可以使用与John的答案相同的技术将它们设置为私有。如果你想要使用真正的getter/setter,你需要使用this.__defineGetter__或者较新的Object.defineProperty函数。 - Matthew Crumley
1
以上列出的方法只有一个问题,如果您想为已经存在的类添加getter和setter,它将覆盖原型,原始方法将无法访问。 - xchg.ca
1
这种方法不会覆盖 Name.prototype.constructor 吗?看起来这是 millimoose 的回答 的一个糟糕的替代方案。 - r0estir0bbe
显示剩余3条评论

64

JavaScript中的Getter和Setter

概述

JavaScript中的Getter和Setter用于定义计算属性访问器。计算属性是使用函数来获取或设置对象值的属性。基本原理可描述为:

var user = { /* ... object with getters and setters ... */ };
user.phone = '+1 (123) 456-7890'; // updates a database
console.log( user.areaCode ); // displays '123'
console.log( user.area ); // displays 'Anytown, USA'

当访问属性时,自动执行某些操作非常有用,比如保持数字在范围内,重新格式化字符串,触发值已更改事件,更新关联数据,提供对私有属性的访问等等。

以下示例展示了基本语法,尽管它们只是获取和设置内部对象的值,而没有做任何特殊处理。在实际情况中,您需要修改输入和/或输出值,以满足您的需求,如上所述。

get/set关键字

ECMAScript 5 支持 getset 关键字来定义计算属性。它们适用于除IE 8及以下之外的所有现代浏览器。

var foo = {
    bar : 123,
    get bar(){ return bar; },
    set bar( value ){ this.bar = value; }
};
foo.bar = 456;
var gaz = foo.bar;

自定义 Getter 和 Setter

getset不是保留字,因此可以重载它们,创建自定义的跨浏览器计算属性函数。这将在任何浏览器中运行。

var foo = {
    _bar : 123,
    get : function( name ){ return this[ '_' + name ]; },
    set : function( name, value ){ this[ '_' + name ] = value; }
};
foo.set( 'bar', 456 );
var gaz = foo.get( 'bar' );

或者采用更紧凑的方法,可以使用单个函数。

var foo = {
    _bar : 123,
    value : function( name /*, value */ ){
        if( arguments.length < 2 ){ return this[ '_' + name ]; }
        this[ '_' + name ] = value;
    }
};
foo.value( 'bar', 456 );
var gaz = foo.value( 'bar' );

避免像这样做,因为它可能导致代码膨胀。

var foo = {
    _a : 123, _b : 456, _c : 789,
    getA : function(){ return this._a; },
    getB : ..., getC : ..., setA : ..., setB : ..., setC : ...
};

为了防止用户简单地使用foo.bar而获取一个“未经处理”的值,上述示例中的内部属性名称是用下划线抽象的。您可以使用条件代码根据所访问的属性名称(通过name参数)来执行不同的操作。

Object.defineProperty()

使用Object.defineProperty()是另一种添加getter和setter的方法,可以在对象定义后使用。它还可用于设置可配置和可枚举的行为。这种语法在IE 8上也适用,但不幸的是只适用于DOM对象。

var foo = { _bar : 123 };
Object.defineProperty( foo, 'bar', {
    get : function(){ return this._bar; },
    set : function( value ){ this._bar = value; }
} );
foo.bar = 456;
var gaz = foo.bar;

__defineGetter__()

__defineGetter__()是另外一种选择。虽然它已经被弃用,但仍然广泛用于网络,因此不太可能很快消失。它适用于除IE 10及以下版本的所有浏览器。虽然其他选项在非IE浏览器上也很有效,因此这个选项并不是那么有用。

var foo = { _bar : 123; }
foo.__defineGetter__( 'bar', function(){ return this._bar; } );
foo.__defineSetter__( 'bar', function( value ){ this._bar = value; } );
值得注意的是,在后面的示例中,内部名称必须与访问器名称不同,以避免递归调用(即foo.bar 调用 foo.get(bar) ,进而调用 foo.bar ,再调用 foo.get(bar) ...)。

另请参阅

MDN getsetObject.defineProperty()__defineGetter__()__defineSetter__()
MSDN IE8 Getter Support


1
更紧凑的方法中,this[ '_' + name ] = value; 可以改为 this[ '_' + name ] = arguments[1];,这样就不需要指定 value 参数了。 - dystopiandev
1
示例: bar : 123, get bar(){ return bar; }, set bar( value ){ this.bar = value; } }; foo.bar = 456;引发异常: Uncaught RangeError: Maximum call stack size exceeded at Object.set bar [as bar] (<anonymous>:4:32) at Object.set bar [as bar] (<anonymous>:4:32) at Object.set bar [as bar] (<anonymous>:4:32) at Object.set bar [as bar] (<anonymous>:4:32) at Object.set bar [as bar] (<anonymous>:4:32) at Object.set bar [as bar] (<anonymous>:4:32) - nevf
3
get和set方法的名称必须与属性名称不同。因此,将bar: 123this.bar=value更改为例如_bar。详情请见:https://www.hongkiat.com/blog/getters-setters-javascript/ - nevf
@nevf 感谢您的纠正!通常使用计算属性时,“真实”的内部属性会被命名为 _foomFoo。如果与 getter/setter 相同,它将由于递归而导致无限循环,然后出现 Stack Overflow™;-) 因为当您说 a = b 时,它调用 a.get(b),它本身又调用 a = b,这又调用 a.get(b),... - Beejor

59

你可以使用它们来实现计算属性。

例如:

function Circle(radius) {
    this.radius = radius;
}

Object.defineProperty(Circle.prototype, 'circumference', {
    get: function() { return 2*Math.PI*this.radius; }
});

Object.defineProperty(Circle.prototype, 'area', {
    get: function() { return Math.PI*this.radius*this.radius; }
});

c = new Circle(10);
console.log(c.area); // Should output 314.159
console.log(c.circumference); // Should output 62.832

(CodePen)


好的,我想我开始明白了。我正在尝试为数组对象的长度属性分配一个getter,但是出现了错误:“变量length的重新声明”代码看起来像这样:obj = []; obj.__defineGetter__('length',function(){ return this.length; }); - oksf
1
那是因为数组对象已经具有内置的长度属性。如果允许重新声明,调用新的长度会无限递归。尝试命名为“my_length”或类似名称的属性。 - millimoose
为了在一个语句中定义两个getter,可以使用Object.defineProperties - r0estir0bbe
你能否只是将 { "area": function () {return ...} } 分配为对象属性吗? - RegarBoy
@开发者,那不是Javascript语言定义的getter,那只是一个函数。你必须调用它才能获取值,它不能重载对属性的访问。此外,为了构建JS已有的对象系统,而不是发明自己的破碎对象系统的人,有一个特殊的地狱圈子。 - millimoose

16

抱歉搬起老问题,但我想提供一些非常基本的例子和白痴式的解释。到目前为止,没有其他答案展示了像MDN指南第一个例子那样的语法,这是最基本的。

Getter:

var settings = {
    firstname: 'John',
    lastname: 'Smith',
    get fullname() { return this.firstname + ' ' + this.lastname; }
};

console.log(settings.fullname);

当然会记录 John Smith。一个getter类似于变量对象属性,但提供了像函数一样实时计算其返回值的灵活性。它基本上是创建一个在调用时不需要()的函数的花哨方式。

Setter:

var address = {
    set raw(what) {
        var loc = what.split(/\s*;\s*/),
        area = loc[1].split(/,?\s+(\w{2})\s+(?=\d{5})/);

        this.street = loc[0];
        this.city = area[0];
        this.state = area[1];
        this.zip = area[2];
    }
};

address.raw = '123 Lexington Ave; New York NY  10001';
console.log(address.city);

...将会在控制台记录New York。与getter一样,setters的语法与设置对象属性值的语法相同,但是这是另一种不需要使用()调用函数的花式方法。

请参见此jsfiddle,以获取更详细、更实用的示例。将值传递到对象的setter中会触发其他对象项的创建或填充。具体来说,在jsfiddle示例中,传递一个数字数组会提示setter计算平均数、中位数、众数和范围;然后为每个结果设置对象属性。


我仍然不理解使用get和set与创建getMethod或setMethod的好处。唯一的好处是可以在没有()的情况下调用它吗?肯定还有其他原因将其添加到JavaScript中。 - Andreas
@Andreas 获取器和设置器在被调用时的行为类似于属性,这有助于表达它们的预期目的。它们并不会解锁其他缺失的功能,但是它们的使用可以帮助您组织思路。作为一个实际的例子,我曾经使用getter来扩展Google Maps对象。我需要计算相机滚动角度,以便将地图瓦片旋转到地平线上。现在Google在后端自动完成了这个操作;但当时,对我来说检索maps.roll作为属性而不是maps.roll()的返回值是有帮助的。这只是一种偏好。 - rojo
所以这只是一种语法糖,使代码看起来更干净,没有()。我不明白为什么你不能用maps.roll()来举例。 - Andreas
1
@Andreas 谁说我不能这么做?就像我说的,这只是一种帮助我保持思维组织的方式。编程是一门艺术。你不会问 Bob Ross 为什么他要用烧赭色而不是橙色。也许你现在看不到需要,但有一天当你决定你的画需要一点烧赭色时,它就会出现在你的调色板上。 - rojo
我看到 get 和 set 语法所做的一件事情是,如果它们被用作属性的属性,则会自动运行。 - Andreas

12

只有在类的私有属性存在时,getter和setter才真正有意义。由于Javascript实际上没有像面向对象语言中通常想象的那样的私有类属性,因此很难理解。以下是一个私有计数器对象的示例。这个对象的好处在于,内部变量"count"无法从对象外部访问。

var counter = function() {
    var count = 0;

    this.inc = function() {
        count++;
    };

    this.getCount = function() {
        return count;
    };
};

var i = new Counter();
i.inc();
i.inc();
// writes "2" to the document
document.write( i.getCount());

如果您仍然感到困惑,可以查看Crockford关于JavaScript中的私有成员的文章。

40
我不同意。获取器和设置器对于封装信息非常有用,这些信息的定义可能不仅仅是简单的变量。如果您需要更改先前使用简单属性的系统的行为,并且其他内容可能依赖于该行为,则可以使用它。此外,您的示例只演示了“伪获取器”,即函数。真正的JavaScript获取器看起来像简单的值(并且可以在没有函数符号的情况下访问),因此拥有真正的强大功能。 - devios1
1
不确定我是否会称其为强大。某些看似X但实际上是Y的东西并不一定清晰明了。我绝对不会期望var baz = foo.bar背后有一整套隐藏的行为。然而,我会期望foo.getBar()有这样的行为。 - AgmLauncher

8
我认为你提供的第一篇文章已经很清楚地阐述了这个问题:
明显的好处是,以这种方式编写JavaScript代码可以使用模糊的值,以避免用户直接访问它们。此处的目标是通过仅允许通过`get()`或`set()`方法访问字段来封装和抽象化这些字段。这样,你可以在内部以任何你想要的方式存储字段/数据,但外部组件只知道你的公开接口。这使得你可以在不改变外部接口的情况下进行内部更改,在`set()`方法中执行某些验证或错误检查等操作。

6

虽然我们经常看到具有公共属性且没有任何访问控制的对象,但JavaScript允许我们准确地描述属性。实际上,我们可以使用描述符来控制属性的访问方式以及可以应用哪些逻辑。考虑以下示例:

var employee = {
    first: "Boris",
    last: "Sergeev",
    get fullName() {
        return this.first + " " + this.last;
    },
    set fullName(value) {
        var parts = value.toString().split(" ");
        this.first = parts[0] || "";
        this.last = parts[1] || "";
    },
    email: "boris.sergeev@example.com"
};

最终结果:
console.log(employee.fullName); //Boris Sergeev
employee.fullName = "Alex Makarenko";

console.log(employee.first);//Alex
console.log(employee.last);//Makarenko
console.log(employee.fullName);//Alex Makarenko

3
你可以通过构造函数的原型定义JS类的实例方法。
以下是示例代码:
// BaseClass

var BaseClass = function(name) {
    // instance property
    this.name = name;
};

// instance method
BaseClass.prototype.getName = function() {
    return this.name;
};
BaseClass.prototype.setName = function(name) {
    return this.name = name;
};


// test - start
function test() {
    var b1 = new BaseClass("b1");
    var b2 = new BaseClass("b2");
    console.log(b1.getName());
    console.log(b2.getName());

    b1.setName("b1_new");
    console.log(b1.getName());
    console.log(b2.getName());
}

test();
// test - end

而且,这适用于任何浏览器,您还可以使用Node.js来运行此代码。


1
这只是创建新的getName和setName方法。它们与创建属性无关! - uzay95

2

这有什么难懂的... getters 是在获取属性时调用的函数,setters 是在设置属性时调用的函数。 例如,如果你执行以下代码:

obj.prop = "abc";

如果你正在使用getter/setter,那么你正在设置属性prop,setter函数将被调用,并传入"abc"作为参数。对象内的setter函数定义应该像这样:

set prop(var) {
   // do stuff with var...
}

我不确定这在不同浏览器上的实现效果如何。似乎Firefox也有一种替代语法,使用双下划线特殊(“魔术”)方法。像往常一样,Internet Explorer不支持这些。


2
如果您指的是访问器的概念,那么它的简单目标是隐藏底层存储,以防止任意操作。这方面最极端的机制是使用属性(accessor)。
function Foo(someValue) {
    this.getValue = function() { return someValue; }
    return this;
}

var myFoo = new Foo(5);
/* We can read someValue through getValue(), but there is no mechanism
 * to modify it -- hurrah, we have achieved encapsulation!
 */
myFoo.getValue();

如果您指的是JS的getter/setter功能,例如defineGetter/defineSetter{ get Foo() { /* code */ } },那么值得注意的是,在大多数现代引擎中,这些属性的后续使用速度会比其他方式要慢得多。例如,比较以下性能:
var a = { getValue: function(){ return 5; }; }
for (var i = 0; i < 100000; i++)
    a.getValue();

对比。

var a = { get value(){ return 5; }; }
for (var i = 0; i < 100000; i++)
    a.value;

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