全面梳理JS對象的訪問控制及代理反射

在 Javascript 中,讀取、賦值、調用方法等等,幾乎一切操做都是圍繞「對象」展開的;長久以來,如何更好的瞭解和控制這些操做,就成了該語言發展中的重要問題。javascript

I. JS對象的訪問控制

[1.1] 熟悉的 getter/setter

所謂 getter/setter,其定義通常爲:php

  • 一個 getter 方法不接受任何參數,且老是返回一個值
  • 一個 setter 老是接受一個參數,且並不會返回值

一些 getter/setter 的常識:html

  • 也被稱爲存取方法,是訪問方法(access methods)中最經常使用的兩個
  • 用來封裝私有成員方法,以隔離外界對其的直接訪問
  • 也能夠在存取過程當中添加其餘的邏輯,保證了外部調用的簡潔性
  • 實現了對象或類內部邏輯的靈活性,保留了改變的可能
  • 在不少 IDE 中能夠自動生成

首先看看其餘語言中通常的實現方式:java

一種是傳統的顯式 getXXX()/setXXX(v) 方法調用es6

//JAVA
public class People {
    private Integer _age;

    public Integer getAge() {
        return this._age;
    }
    public void setAge(Integer age) {
        this._age = age;
    }

    public static void main(String[] args) {

        People p = new People();
        p.setAge(18);
        System.out.println(p.getAge().toString()); //18

    }
}複製代碼

毫無疑問,顯式調用命名實際上是隨意的,並且各類語言都能實現express

另外一種是隱式(implicit)的 getter/settersegmentfault

//AS2
class Login2 {
    private var _username:String;

    public function get userName():String {
        return this._username;
    }
    public function set userName(value:String):Void {
        this._username = value;
    }
}

var lg = new Login2;
lg.userName = "tom";
trace(lg.userName); //"tom"複製代碼
//C#
class People {  
    private string _name;  

    public string name
    {  
        get {
            return _name;
        }  
        set {
            _name = value;
        }  
    }  
} 

People p = new People();
p.name = "tom";
Console.WriteLine(p.name)複製代碼
//PHP
class MyClass {
  private $firstField;
  private $secondField;

  public function __get($property) {
    if (property_exists($this, $property)) {
      return $this->$property;
    }
  }
  public function __set($property, $value) {
    if (property_exists($this, $property)) {
      $this->$property = $value." world";
    }
  }
}

$mc = new MyClass;
$mc->firstField = "hello";
echo $mc->firstField; //"hello world"複製代碼

隱式存取方法須要特定語言的支持,使用起來感受就是讀取屬性(var x = obj.x)或給屬性賦值(obj.x = "foo"設計模式

[1.2] ES5 中的 getter 和 setter

從 2011 年的 ECMAScript 5.1 (ECMA-262) 規範開始,JavaScript 也開始支持 getter/setter;形式上,天然是和同爲 ECMAScript 實現的 AS2/AS3 相同api

getter 的語法:瀏覽器

// prop 指的是要綁定到給定函數的屬性名
{get prop() { ... } }

// 還可使用一個計算屬性名的 expression 綁定到給定的函數, 注意瀏覽器兼容性
{get [expression]() { ... } }複製代碼

🌰 例子:

var obj = {
  log: ['example','test'],
  get latest() {
    if (this.log.length == 0) return undefined;
    return this.log[this.log.length - 1];
  }
}
console.log(obj.latest); // "test"

var expr = 'foo';
var obj2 = {
  get [expr]() { return 'bar'; }
};
console.log(obj2.foo); // "bar"複製代碼

使用 get 語法時應注意如下問題:

  • 可使用數值或字符串做爲標識
  • 必須不帶參數
  • 不能與另外一個get或具備相同屬性的數據條目的對象字面量中出現

經過 delete 操做符刪除 getter:

delete obj.latest;複製代碼

如下展現了一種進階的用法,即首次調用時才取值(lazy getter),而且將 getter 轉爲普通數據屬性:

get notifier() {
  delete this.notifier;
  return this.notifier = document.getElementById('myId');
},複製代碼

setter 的語法:

//prop 指的是要綁定到給定函數的屬性名
//val 指的是分配給prop的值
{set prop(val) { . . . }}

// 還可使用一個計算屬性名的 expression 綁定到給定的函數, 注意瀏覽器兼容性
{set [expression](val) { . . . }}複製代碼

使用 set 語法時應注意如下問題:

  • 標識符能夠是數字或字符串
  • 必須有一個明確的參數
  • 在同一個對象中,不能爲一個已有真實值的變量使用 set ,也不能爲一個屬性設置多個 set

🌰 例子:

var language = {
  set current(name) {
    this.log.push(name);
  },
  log: []
}
language.current = 'EN';
console.log(language.log); // ['EN']
language.current = 'FA';
console.log(language.log); // ['EN', 'FA']

var expr = "foo";
var obj = {
  baz: "bar",
  set [expr](v) { this.baz = v; }
};
console.log(obj.baz); // "bar"
obj.foo = "baz";      // run the setter
console.log(obj.baz); // "baz"複製代碼

setter 能夠用delete操做來移除:

delete o.current;複製代碼

[1.4] 用 Object.defineProperty() 精肯定義對象成員

回顧前面提到過的,對象裏存在的屬性描述符有兩種主要形式:數據屬性和存取方法。描述符必須是兩種形式之一,不能同時是二者。

而且在通常狀況下,經過賦值來爲對象添加的屬性,能夠由 for...in 或 Object.keys 方法遍歷枚舉出來;且經過這種方式添加的屬性值能夠被改變,也能夠被刪除。

var obj = {
    _c: 99,
    get c() {
        return this._c;
    }
};
obj.a = 'foo';
obj.b = function() {
    alert("hello world!");
};

console.log( Object.keys(obj) ); //["_c", "c", "a", "b"]

for (var k in obj) console.log(k); //"_c", "c", "a", "b"

delete obj.b;
delete obj.c;
console.log(obj.b, obj.c); //undefined, undefined複製代碼

對於這樣定義的數據屬性或存取方法,沒法控制其是否可被 delete,也沒法限制其是否能被枚舉

而使用 Object.defineProperty() 則容許改變這些默認設置

一樣從 ECMAScript 5.1 規範開始,定義了 Object.defineProperty() 方法。用於直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象

其語法爲:

//obj 須要被操做的目標對象
//prop 目標對象須要定義或修改的屬性的名稱
//descriptor 將被定義或修改的屬性的描述符
Object.defineProperty(obj, prop, descriptor)複製代碼

其中 descriptor 能夠設置的屬性爲:

屬性 描述 應用於
configurable 是否能被修改及刪除 數據屬性、存取方法
enumerable 是否可被枚舉 數據屬性、存取方法
value 屬性值 數據屬性
writable 是否能被賦值運算符改變 數據屬性
get getter 方法 存取方法
set setter 方法 存取方法

須要瞭解的是,從 IE8 開始有限支持這個方法(非 DOM 對象不可用)

🌰 例子:

var o = {};

o.a = 1;
// 等同於 :
Object.defineProperty(o, "a", {
  value : 1,
  writable : true,
  configurable : true,
  enumerable : true
});複製代碼
var o = {};

var bValue;
Object.defineProperty(o, "b", {
  get : function(){ //添加存取方法
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});複製代碼
var o = {};

Object.defineProperty(o, "a", {
    value : 37,
    writable : false //定義了一個「只讀」的屬性
});

console.log(o.a); // 37
o.a = 25; // 在嚴格模式下會拋出錯誤,非嚴格模式只是不起做用
console.log(o.a); // 37複製代碼
var o = {};
Object.defineProperty(o, "a", {
    get : function(){return 1;}, 
    configurable : false //不可編輯、不可刪除
});

// throws a TypeError
Object.defineProperty(o, "a", {configurable : true}); 
// throws a TypeError
Object.defineProperty(o, "a", {enumerable : true}); 
// throws a TypeError
Object.defineProperty(o, "a", {set : function(){}}); 
// throws a TypeError
Object.defineProperty(o, "a", {get : function(){return 1;}});
// throws a TypeError
Object.defineProperty(o, "a", {value : 12});

console.log(o.a); //1
delete o.a; // 在嚴格模式下會拋出TypeError,非嚴格模式只是不起做用
console.log(o.a); //1複製代碼
Object.defineProperty(o, "conflict", {
  value: 0x9f91102, 
  get: function() { 
    return 0xdeadbeef; 
  } 
}); //拋出 TypeError,數據屬性和存取方法不能混合設置複製代碼

相關方法:Object.getOwnPropertyDescriptor()

返回指定對象上一個自有屬性對應的屬性描述符。(自有屬性指的是直接賦予該對象的屬性,而非從原型鏈上進行查找的屬性)

語法:

//其中 prop 對應於 Object.defineProperty() 中第三個參數 descriptor
Object.getOwnPropertyDescriptor(obj, prop)複製代碼

🌰 例子:

var o = {
    get foo() {
        return 17;
    }
};

Object.getOwnPropertyDescriptor(o, "foo");
// {
//   configurable: true,
//   enumerable: true,
//   get: /*the getter function*/,
//   set: undefined
// }複製代碼

相關方法:Object.defineProperties()

直接在一個對象上定義多個新的屬性或修改現有屬性

語法:

//prop 和 descriptor 的定義對應於 Object.defineProperty()
Object.defineProperties(obj, {
    prop1: descriptor1,
    prop2: descriptor2,
    ...
})複製代碼

🌰 例子:

var obj = {};
Object.defineProperties(obj, {
  'property1': {
    value: true,
    writable: true
  },
  'property2': {
    value: 'Hello',
    writable: false
  }
});複製代碼

相關方法:Object.create()

使用指定的原型對象及其屬性去建立一個新的對象

語法:

//proto 爲新建立對象的原型對象
//props 對應於 Object.defineProperties() 中的第二個參數
Object.create(proto[, props])複製代碼

🌰 例子:

// 建立一個原型爲null的空對象
var o = Object.create(null);

var o2 = {};
// 以字面量方式建立的空對象就至關於:
var o2 = Object.create(Object.prototype);複製代碼
var foo = {a:1, b:2};
var o = Object.create(foo, {
  // foo會成爲所建立對象的數據屬性
  foo: { 
    writable:true,
    configurable:true,
    value: "hello" 
  },
  // bar會成爲所建立對象的訪問器屬性
  bar: {
    configurable: false,
    get: function() { return 10 },
    set: function(value) {
      console.log("Setting `o.bar` to", value);
    }
  }
});複製代碼

[1.5] __define[G,S]etter__()

做爲非標準已廢棄的方法,defineGetter() 和 defineSetter() 有時會出如今一些歷史代碼中,並仍能運行在 Firefox/Safari/Chrome 等瀏覽器中

🌰 直接看例子:

var o = {
    word: null
};

o.__defineGetter__('gimmeFive', function() { 
    return 5;
});
console.log(o.gimmeFive); // 5

o.__defineSetter__('say', function(vlu) { 
    this.word = vlu;
});
o.say = "hello";
console.log(o.word); //"hello"複製代碼

[1.6] __lookup[G,S]etter__()

一樣,還有 lookupGetter() 和 lookupSetter() 兩個非標準已廢棄的方法

  • lookupGetter() 會返回對象上某個屬性的 getter 函數

🌰 例子:

var obj = {
    get foo() {
        return Math.random() > 0.5 ? "foo" : "bar";
    }
};

obj.__lookupGetter__("foo") 
// (function (){return Math.random() > 0.5 ? "foo" : "bar"})複製代碼

若是換成標準的方法,則是:

Object.getOwnPropertyDescriptor(obj, "foo").get
// (function (){return Math.random() > 0.5 ? "foo" : "bar"})複製代碼

而若是那個訪問器屬性是繼承來的:

Object.getOwnPropertyDescriptor(Object.getPrototypeOf(obj), "foo").get 
// function __proto__() {[native code]}複製代碼
  • lookupSetter() 會返回對象的某個屬性的 setter 函數

🌰 例子:

var obj = {
  set foo(value) {
    this.bar = value;
  }
};

obj.__lookupSetter__('foo')
// (function(value) { this.bar = value; })

// 標準且推薦使用的方式。
Object.getOwnPropertyDescriptor(obj, 'foo').set;
// (function(value) { this.bar = value; })複製代碼

[1.7] 用 onpropertychange 兼容古早瀏覽器

在某些要求兼容 IE6/IE7 等瀏覽器的極端狀況下,利用 IE 支持的 onpropertychange 事件,也是能夠模擬 getter/setter 的

要注意這種方法僅限於已加載到文檔中的 DOM 對象

function addProperty(obj, name, onGet, onSet) {
    var
        oldValue = obj[name],
        getter = function () {
            return onGet.apply(obj, [oldValue]);
        },
        setter = function (newValue) {
            return oldValue = onSet.apply(obj, [newValue]);
        },
        onPropertyChange = function (event) {
            if (event.propertyName == name) {
                // 暫時移除事件監聽以避免循環調用
                obj.detachEvent("onpropertychange", onPropertyChange);
                // 把改變後的值傳遞給 setter
                var newValue = setter(obj[name]);
                // 設置 getter
                obj[name] = getter;
                obj[name].toString = getter;
                // 恢復事件監聽
                obj.attachEvent("onpropertychange", onPropertyChange);
            }
        };
    // 設置 getter
    obj[name] = getter;
    obj[name].toString = getter;

    obj.attachEvent("onpropertychange", onPropertyChange);
}複製代碼

II. JS中的代理和反射

在對象自己上,一個個屬性的定義訪問控制,有時會帶來代碼臃腫,甚至難以維護;瞭解代理和反射的概念和用法,能夠有效改善這些情況。

[2.1] 傳統的代理模式

在經典的設計模式(Design Pattern)中,代理模式(Proxy Pattern)被普遍應用;其定義爲:

在代理模式中,一個代理對象(Proxy)充當着另外一個目標對象(Real Subject)的接口。代理對象居於目標對象的用戶(Client)和目標對象自己的中間,並負責保護對目標對象的訪問。

典型的應用場景爲:

  • 對目標對象的訪問控制和緩存
  • 延遲目標對象的初始化
  • 訪問遠端對象

🌰 舉個例子:

function Book(id, name) {
    this.id = id;
    this.name = name;
}

function BookShop() {
    this.books = {};
}
BookShop.prototype = {
    addBook: function(book) {
        this.books[book.id] = book;
    },
    findBook: function(id) {
        return this.books[id];
    }
}

function BookShopProxy() {
}
BookShopProxy.prototype = {
    _init: function() {
        if (this.bookshop)
            return;
        else
            this.bookshop = new BookShop;
    },
    addBook: function(book) {
        this._init();
        if (book.id in this.bookshop.books) {
            console.log('existed book!', book.id);
            return;
        } else {
            this.bookshop.addBook(book);
        }
    },
    findBook: function(id) {
        this._init();
        if (id in this.bookshop.books)
            return this.bookshop.findBook(id);
        else 
            return null;
    }
}

var proxy = new BookShopProxy;
proxy.addBook({id:1, name:"head first design pattern"});
proxy.addBook({id:2, name:"thinking in java"});
proxy.addBook({id:3, name:"lua programming"});
proxy.addBook({id:2, name:"thinking in java"}); //existed book! 2

console.log(proxy.findBook(1)); //{ id: 1, name: 'head first design pattern' }
console.log(proxy.findBook(3)); //{ id: 3, name: 'lua programming' }複製代碼

顯然,以上示例代碼中展現了使用代理來實現延遲初始化訪問控制

值得一提的是,代理模式與設計模式中另外一種裝飾者模式(Decorator Pattern)容易被混淆,二者的相同之處在於都是對原始的目標對象的包裝;不一樣之處在於,前者着眼於提供與原始對象相同的API,並將對其的訪問控制保護起來,然後者則側重於在原有API的基礎上添加新的功能。

[2.2] ES6 中的 Proxy

在 ECMAScript 2015 (6th Edition, ECMA-262) 標準中,提出了原生的 Proxy 對象。用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)

語法:

let p = new Proxy(target, handler);複製代碼

proxy 對象的目標對象 target,能夠是任何類型的對象,如 Object、Array、Function,甚至另外一個 Proxy 對象;在進行let proxy=new Proxy(target,handle)的操做後,proxy、target兩個對象會相互影響。即:

let target = {
    _prop: 'foo',
    prop: 'foo'
};
let proxy = new Proxy(target, handler);
proxy._prop = 'bar';
target._attr = 'new'
console.log(target._prop) //'bar'
console.log(proxy._attr) //'new'複製代碼

handler 也是一個對象,其若干規定好的屬性是定義好一個個函數,表示了當執行目標對象的對應訪問時所執行的操做;最多見的操做是定義 getter/setter 的 get 和 set 屬性:

let handler = {
    get (target, key){
        return key in target
            ? target[key]
            : -1; //默認值
    },
    set (target, key, value) {
        if (key === 'age') { //校驗
            target[key] = value > 0 && value < 100 ? value : 0
        }
        return true;
    }
};

let target = {};
let proxy = new Proxy(target, handler);
proxy.age = 22 //22複製代碼

能夠注意到,和 ES5 中對象自己的 setter 不一樣的是, proxy 中的 setter 必須有返回值;

而且應該也很容易理解,不光是名字相同,Proxy 對象也的確符合經典的代理模式 -- 由代理對象對目標對象的 API 進行封裝和保護,隱藏目標對象,控制對其的訪問行爲。

除了能夠定義 getter/setter,較完整的 handler 屬性以下:

  • "get": function (oTarget, sKey)
  • "set": function (oTarget, sKey, vValue)
  • "enumerate": function (oTarget, sKey)
  • "ownKeys": function (oTarget, sKey)
  • "has": function (oTarget, sKey)
  • "defineProperty": function (oTarget, sKey, oDesc)
  • "deleteProperty": function (oTarget, sKey)
  • "getOwnPropertyDescriptor": function (oTarget, sKey)
  • "getPrototypeOf(oTarget)"
  • "setPrototypeOf(oTarget, oPrototype)"
  • "apply(oTarget, thisArg, argumentsList)":
  • "construct(oTarget, argumentsList, newTarget)"
  • "isExtensible(oTarget)"
  • "preventExtensions(oTarget)"

[2.3] 反射

對象的反射(reflection)是一種在運行時(runtime)探查和操做對象屬性的語言能力。

在 JAVA/AS3 等語言中,反射通常被用於在運行時獲取某個對象的類名、屬性列表,而後再動態構造等;好比經過 XML 配置文件中的值動態建立對象,或者根據名稱提取 swf 文件中的 MovieClip 等。

JS 原本也具備相關的反射API,好比 Object.getOwnPropertyDescriptor()Function.prototype.apply()indelete等,但這些 API 分佈在不一樣的命名空間甚至全局保留字中,而且執行失敗時是以拋出異常的方式進行的。這些因素使得涉及到對象反射的代碼難以書寫和維護。

[2.4] ES6 中的 Reflect

和 Proxy 同時,在 ECMAScript 2015 (6th Edition, ECMA-262) 中,引入了 Reflect 對象,用來囊括對象反射的若干方法。

反射方法 類似操做
Reflect.apply() Function.prototype.apply()
Reflect.construct() new target(...args)
Reflect.defineProperty() Object.defineProperty()
Reflect.deleteProperty() delete target[name]
Reflect.enumerate() 供 for...in 操做遍歷到的屬性
Reflect.get() 相似於 target[name]
Reflect.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor()
Reflect.getPrototypeOf() Object.getPrototypeOf()
Reflect.has() in 運算符
Reflect.isExtensible() Object.isExtensible()
Reflect.ownKeys() Object.getOwnPropertyNames(target).concat(Object.getOwnPropertySymbols(target))
Reflect.preventExtensions() Object.preventExtensions()
Reflect.set() target[name] = val
Reflect.setPrototypeOf() Object.setPrototypeOf()
  • Reflect 與 ES5 的 Object 有點相似,包含了對象語言內部的方法,Reflect 也有和 Proxy 互相一一對應的若干種方法。
  • Proxy 至關於去修改設置對象的屬性行爲,而Reflect則是獲取對象的這些行爲(的原始版本)。二者常常搭配使用。
  • Reflect 沒有構造函數,可被調用的都是其靜態方法。
var target = {
    a: 1
};
var proxy = new Proxy(target, {
    get: function(tgt, key) {
        console.log("Get %s", key);
        return tgt[key] + 100;
    },
    set: function(tgt, key, val) {
        console.log("Set %s = %s", key, val);
        return tgt[key] = "VAL_" + val;
    }
});

proxy.a = 2;
//Set a = 2

console.log(proxy.a);
//Get a
//VAL_2100

console.log(Reflect.get(target, "a"));
//VAL_2

Reflect.set(target, "a", 3);
console.log(Reflect.get(target, "a"));
//3複製代碼

能夠看到,若是直接在 Proxy 中存取目標對象的值,極可能調用多餘的 getter/setter;而搭配 Reflect 中對應的方法使用則可有效避免此狀況

同時應注意到,在執行失敗時,這些方法並不拋出錯誤,而是返回 false;這極大的簡化了處理:

//In ES5
var o = {};
Object.defineProperty(o, 'a', {
  get: function() { return 1; },
  configurable: false
});
try {
    Object.defineProperty(o, 'a', { configurable: true });
} catch(e) {
    console.log("Exception");
}

//In ES2015
var o = {};
Reflect.defineProperty(o, 'a', {
  get: function() { return 1; },
  configurable: false
});
if( !Reflect.defineProperty(o, 'a', { configurable: true }) ) {
    console.log("Operation Failed");
}複製代碼

[2.5] 配合使用 Proxy/Reflect

🌰 例子1:爲對象的每一個屬性設置 getter/setter

//in ES5
var obj = {
    x: 1,
    y: 2,
    z: 3
};

function trace1() {
    var cache = {};
    Object.keys(obj).forEach(function(key) {
        cache[key] = obj[key]; //避免循環 setter
        Object.defineProperty(obj, key, {
            get: function() {
                console.log('Get ', key);
                return cache[key];
            },
            set: function(vlu) {
                console.log('Set ', key, vlu);
                cache[key] = vlu;
            }
        })
    });
}
trace1();

obj.x = 5;
console.log(obj.z);
// Set x 5
// Get z
// 3複製代碼
//in ES6
var obj2 = {
    x: 6,
    y: 7,
    z: 8
};

function trace2() {
    return new Proxy(obj2, {
        get(target, key) {
            if (Reflect.has(target, key)) {
                console.log('Get ', key);
            }
            return Reflect.get(target, key);
        },
        set(target, key, vlu) {
            if (Reflect.has(target, key)) {
                console.log('Set ', key, vlu);
            }
            return Reflect.set(target, key, vlu);
        }
    });
}

const proxy2 = trace2();
proxy2.x = 99;
console.log(proxy2.z);
// Set x 99
// Get z
// 8複製代碼

🌰 例子2:跟蹤方法調用

var obj = {
    x: 1,
    y: 2,
    say: function(word) {
        console.log("hello ", word)
    }
};

var proxy = new Proxy(obj, {
    get(target, key) {
        const targetValue = Reflect.get(target, key);
        if (typeof targetValue === 'function') {
            return function (...args) {
                console.log('CALL', key, args);
                return targetValue.apply(this, args);
            }
        } else {
            console.log('Get ', key);
            return targetValue;
        }
    }
});

proxy.x;
proxy.y;
proxy.say('excel!');
// Get x
// Get y
// CALL say [ 'excel!' ]
// hello excel!複製代碼

總結

  • getter/setter 也被稱爲存取方法,是訪問方法中最經常使用的兩個
  • 能夠用訪問方法封裝保護原對象,並保留邏輯的靈活性
  • ES5 中開始支持了隱式的 get 和 set 訪問方法,能夠經過 delete 刪除
  • 使用 使用 Object.defineProperty() 也能夠設置 getter/setter 等
  • 歷史上利用 Object.prototype.define[G,S]etter() 和 onpropertychange 實現存取方法的兼容
  • 能夠利用代理和反射改善傳統的訪問控制
  • 代理對象居於目標對象的用戶和目標對象自己的中間,並負責保護對目標對象的訪問
  • ES6 原生的 Proxy 對象。用於定義基本操做的自定義行爲(如屬性查找,賦值,枚舉,函數調用等)
  • 對象的反射是一種在運行時探查和操做對象屬性的語言能力
  • ES6 引入了 Reflect 對象,用來囊括對象反射的若干方法
  • Reflect 有和 Proxy 一一對應的若干種方法,常常搭配使用

參考資料:


(end)


長按二維碼或搜索 fewelife 關注咱們哦

相關文章
相關標籤/搜索