Có thể triển khai getters / setters động trong JavaScript không?


131

Tôi nhận thức được cách tạo getters và setters cho các thuộc tính mà tên người ta đã biết, bằng cách thực hiện một cái gì đó như thế này:

// A trivial example:
function MyObject(val){
    this.count = 0;
    this.value = val;
}
MyObject.prototype = {
    get value(){
        return this.count < 2 ? "Go away" : this._value;
    },
    set value(val){
        this._value = val + (++this.count);
    }
};
var a = new MyObject('foo');

alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"

Bây giờ, câu hỏi của tôi là, có thể định nghĩa loại getters và setters như thế này không? Tức là, tạo getters và setters cho bất kỳ tên thuộc tính nào chưa được xác định.

Khái niệm này có thể có trong PHP bằng cách sử dụng các phương thức __get()__set()phép thuật (xem tài liệu PHP để biết thông tin về các phương thức này), vì vậy tôi thực sự hỏi là có JavaScript tương đương với các phương thức này không?

Không cần phải nói, tôi lý tưởng như một giải pháp tương thích với nhiều trình duyệt.


Tôi quản lý để làm điều đó, xem câu trả lời của tôi ở đây để làm thế nào.

Câu trả lời:


215

Cập nhật 2013 và 2015 (xem bên dưới để biết câu trả lời gốc từ năm 2011) :

Điều này đã thay đổi kể từ thông số ES2015 (còn gọi là "ES6"): JavaScript hiện có proxy . Proxy cho phép bạn tạo các đối tượng là proxy thực sự cho (các mặt trên) các đối tượng khác. Đây là một ví dụ đơn giản biến mọi giá trị thuộc tính là chuỗi thành tất cả các giới hạn khi truy xuất:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let original = {
    "foo": "bar"
};
let proxy = new Proxy(original, {
    get(target, name, receiver) {
        let rv = Reflect.get(target, name, receiver);
        if (typeof rv === "string") {
            rv = rv.toUpperCase();
        }
        return rv;
      }
});
console.log(`original.foo = ${original.foo}`); // "original.foo = bar"
console.log(`proxy.foo = ${proxy.foo}`);       // "proxy.foo = BAR"

Các hoạt động bạn không ghi đè có hành vi mặc định của họ. Ở trên, tất cả chúng ta ghi đè là get, nhưng có toàn bộ danh sách các thao tác bạn có thể nối vào.

Trong getdanh sách đối số của hàm xử lý:

  • targetlà đối tượng được ủy nhiệm ( original, trong trường hợp của chúng tôi).
  • name là (tất nhiên) tên của tài sản đang được lấy, thường là một chuỗi nhưng cũng có thể là một Biểu tượng.
  • receiverlà đối tượng nên được sử dụng như thistrong hàm getter nếu thuộc tính là một trình truy cập chứ không phải là một thuộc tính dữ liệu. Trong trường hợp bình thường, đây là proxy hoặc thứ gì đó thừa hưởng từ nó, nhưng nó có thể là bất cứ thứ gì vì bẫy có thể được kích hoạt bởi Reflect.get.

Điều này cho phép bạn tạo một đối tượng với tính năng getter và setter bắt tất cả bạn muốn:

"use strict";
if (typeof Proxy == "undefined") {
    throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
    get(target, name, receiver) {
        if (!Reflect.has(target, name)) {
            console.log("Getting non-existent property '" + name + "'");
            return undefined;
        }
        return Reflect.get(target, name, receiver);
    },
    set(target, name, value, receiver) {
        if (!Reflect.has(target, name)) {
            console.log(`Setting non-existent property '${name}', initial value: ${value}`);
        }
        return Reflect.set(target, name, value, receiver);
    }
});

console.log(`[before] obj.foo = ${obj.foo}`);
obj.foo = "bar";
console.log(`[after] obj.foo = ${obj.foo}`);

Đầu ra của ở trên là:

Nhận tài sản không tồn tại 'foo'
[trước] obj.foo = không xác định
Đặt thuộc tính không tồn tại 'foo', giá trị ban đầu: bar
[sau] obj.foo = thanh

Lưu ý cách chúng tôi nhận được thông báo "không tồn tại" khi chúng tôi cố gắng truy xuất fookhi nó chưa tồn tại và một lần nữa khi chúng tôi tạo nó, nhưng không phải sau đó.


Trả lời từ năm 2011 (xem phần cập nhật 2013 và 2015) :

Không, JavaScript không có tính năng thuộc tính bắt tất cả. Cú pháp của trình truy cập mà bạn đang sử dụng được trình bày trong Phần 11.1.5 của thông số kỹ thuật và không cung cấp bất kỳ ký tự đại diện nào hoặc đại loại như thế.

Tất nhiên, bạn có thể thực hiện một chức năng để làm điều đó, nhưng tôi đoán có lẽ bạn không muốn sử dụng f = obj.prop("foo");hơn là f = obj.foo;obj.prop("foo", value);hơn là obj.foo = value;(điều này cần thiết cho chức năng để xử lý các thuộc tính không xác định).

FWIW, hàm getter (tôi không bận tâm với logic setter) sẽ trông giống như thế này:

MyObject.prototype.prop = function(propName) {
    if (propName in this) {
        // This object or its prototype already has this property,
        // return the existing value.
        return this[propName];
    }

    // ...Catch-all, deal with undefined property here...
};

Nhưng một lần nữa, tôi không thể tưởng tượng bạn thực sự muốn làm điều đó, vì cách nó thay đổi cách bạn sử dụng đối tượng.


1
Có một sự thay thế cho Proxy: Object.defineProperty(). Tôi đặt các chi tiết trong câu trả lời mới của tôi .
Andrew

@Andrew - Tôi sợ bạn đã đọc sai câu hỏi, hãy xem nhận xét của tôi về câu trả lời của bạn.
TJ Crowder

4

Sau đây có thể là một cách tiếp cận ban đầu cho vấn đề này:

var obj = {
  emptyValue: null,
  get: function(prop){
    if(typeof this[prop] == "undefined")
        return this.emptyValue;
    else
        return this[prop];
  },
  set: function(prop,value){
    this[prop] = value;
  }
}

Để sử dụng nó, các thuộc tính phải được truyền dưới dạng chuỗi. Vì vậy, đây là một ví dụ về cách thức hoạt động:

//To set a property
obj.set('myProperty','myValue');

//To get a property
var myVar = obj.get('myProperty');

Chỉnh sửa: Một cách tiếp cận hướng đối tượng được cải thiện hơn dựa trên những gì tôi đề xuất là như sau:

function MyObject() {
    var emptyValue = null;
    var obj = {};
    this.get = function(prop){
        return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
    };
    this.set = function(prop,value){
        obj[prop] = value;
    };
}

var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));

Bạn có thể thấy nó làm việc ở đây .


Điều này không hoạt động. Bạn không thể xác định một getter mà không chỉ định tên của tài sản.
John Kurlak 1/5/2015

@JohnKurlak Kiểm tra jsFiddle này: jsfiddle.net/oga7ne4x Nó hoạt động. Bạn chỉ phải chuyển tên thuộc tính dưới dạng chuỗi.
clami219

3
Ah, cảm ơn đã làm rõ. Tôi nghĩ rằng bạn đang cố gắng sử dụng tính năng ngôn ngữ get () / set (), chứ không phải viết get () / set () của riêng bạn. Tôi vẫn không thích giải pháp này vì nó không thực sự giải quyết được vấn đề ban đầu.
John Kurlak

@JohnKurlak Vâng, tôi đã viết nó là một cách tiếp cận ban đầu. Nó cung cấp một cách khác để giải quyết vấn đề, mặc dù nó không giải quyết được vấn đề khi bạn có một mã hiện có sử dụng cách tiếp cận truyền thống hơn. Nhưng thật tốt nếu bạn bắt đầu lại từ đầu. Chắc chắn không xứng đáng với một downvote ...
clami219

@JohnKurlak Xem nếu bây giờ nó có vẻ tốt hơn! :)
clami219

0

Lời nói đầu:

Câu trả lời của TJ Crowder đề cập đến a Proxy, sẽ cần thiết cho một getter / setter bắt tất cả cho các thuộc tính không tồn tại, như OP đã yêu cầu. Tùy thuộc vào hành vi nào thực sự muốn với getters / setters động, Proxymặc dù có thể không thực sự cần thiết; hoặc, có khả năng, bạn có thể muốn sử dụng kết hợp a Proxyvới những gì tôi sẽ chỉ cho bạn bên dưới.

(PS tôi đã thử nghiệm Proxykỹ lưỡng về Firefox trên Linux gần đây và nhận thấy nó rất có khả năng, nhưng cũng hơi khó hiểu / khó làm việc và làm đúng. liên quan đến cách tối ưu hóa JavaScript có xu hướng ngày nay) - Tôi đang nói về lĩnh vực của nhiều deca chậm hơn.)


Để thực hiện cụ thể các getters và setters được tạo động, bạn có thể sử dụng Object.defineProperty()hoặc Object.defineProperties(). Điều này cũng khá nhanh.

Ý chính là bạn có thể định nghĩa một getter và / hoặc setter trên một đối tượng như vậy:

let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
  //Alternatively, use: `get() {}`
  get: function() {
    return val;
  },
  //Alternatively, use: `set(newValue) {}`
  set: function(newValue) {
    val = newValue;
  }
});

//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.

//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.

Một số điều cần lưu ý ở đây:

  • Bạn không thể sử dụng valuetài sản trong bộ mô tả thuộc tính ( không được hiển thị ở trên) đồng thời với getvà / hoặc set; từ các tài liệu:

    Các mô tả thuộc tính có trong các đối tượng có hai loại chính: mô tả dữ liệu và mô tả truy cập. Một mô tả dữ liệu là một tài sản có giá trị, mà có thể hoặc không thể được ghi. Một bộ mô tả accessor là một thuộc tính được mô tả bởi một cặp hàm getter-setter. Một mô tả phải là một trong hai hương vị này; nó không thể là cả hai

  • Như vậy, bạn sẽ lưu ý rằng tôi đã tạo ra một valtài sản bên ngoài của Object.defineProperty()bộ mô tả cuộc gọi / tài sản. Đây là hành vi tiêu chuẩn.
  • Theo lỗi ở đây , không được đặt writablethành truetrong mô tả thuộc tính nếu bạn sử dụng gethoặc set.
  • Bạn có thể muốn xem xét cài đặt configurableenumerable, tuy nhiên, tùy thuộc vào những gì bạn đang theo đuổi; từ các tài liệu:

    cấu hình

    • true nếu và chỉ khi loại mô tả thuộc tính này có thể được thay đổi và nếu thuộc tính có thể bị xóa khỏi đối tượng tương ứng.

    • Mặc định là sai.


    vô số

    • true nếu và chỉ khi thuộc tính này xuất hiện trong quá trình liệt kê các thuộc tính trên đối tượng tương ứng.

    • Mặc định là sai.


Trên lưu ý này, đây cũng có thể được quan tâm:

  • Object.getOwnPropertyNames(obj): lấy tất cả các thuộc tính của một đối tượng, ngay cả những thứ không thể đếm được (AFAIK đây là cách duy nhất để làm như vậy!).
  • Object.getOwnPropertyDescriptor(obj, prop): lấy mô tả thuộc tính của một đối tượng, đối tượng được truyền lên Object.defineProperty()trên.
  • obj.propertyIsEnumerable(prop);: đối với một thuộc tính riêng lẻ trên một đối tượng cụ thể, hãy gọi hàm này trên thể hiện đối tượng để xác định xem thuộc tính cụ thể có đếm được hay không.

2
Tôi sợ bạn đã đọc sai câu hỏi. OP đặc biệt yêu cầu bắt tất cả như PHP __get__set . definePropertykhông xử lý trường hợp đó. Từ câu hỏi: "Tức là, tạo getters và setters cho bất kỳ tên thuộc tính nào chưa được xác định." (nhấn mạnh của họ). definePropertyđịnh nghĩa các thuộc tính trước. Cách duy nhất để làm những gì OP yêu cầu là một proxy.
TJ Crowder

@TJCrowder tôi thấy. Bạn đúng về mặt kỹ thuật, mặc dù câu hỏi không rõ ràng lắm. Tôi đã điều chỉnh câu trả lời của mình cho phù hợp. Ngoài ra, một số thực sự có thể muốn kết hợp các câu trả lời của chúng tôi (cá nhân tôi làm).
Andrew

@Andrew khi tôi hỏi câu hỏi này vào năm 2011, trường hợp sử dụng mà tôi có trong đầu là một thư viện có thể trả về một đối tượng mà người dùng có thể gọi obj.whateverPropertyđể thư viện có thể chặn nó với một getter chung và được đặt tên thuộc tính người dùng đã cố gắng truy cập. Do đó, yêu cầu về 'bắt tất cả các getters và setters'.
daiscog

-6
var x={}
var propName = 'value' 
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)

cái này hiệu quả với tôi


13
Sử dụng Function()như thế cũng giống như sử dụng eval. Chỉ cần đặt trực tiếp các chức năng như tham số của defineProperty. Hoặc, nếu vì lý do nào đó, bạn khăng khăng tạo động getsetsau đó sử dụng hàm bậc cao tạo hàm và trả về, nhưvar get = (function(propName) { return function() { return this[propName]; };})('value');
chris-l
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.