在web開發中咱們會花大量的時間用於dom的操做上,通常狀況下咱們會選擇第三方庫如jQuery來替代原生的方法,由於原生的方法在操做上會使代碼大量重複冗餘而不易操做。d3一樣提供了一套本身的方法來很方便的對dom進行操做,像修改樣式、註冊事件等等均可以經過它來完成。html
d3的selection主要用於直接對DOM進行操做,如設置屬性、修改樣式等等,同時它能夠和data join(數據鏈接)這一強大的功能結合起來對元素和元素上綁定的數據進行操做。 selection中的方法計算後返回當前selection,這樣能夠進行方法的鏈式調用。因爲經過這種方式調用方法會使得每行的代碼很長,所以按照約定:若該方法返回的是當前的selection,則使用四個空格進行縮進;若方法返回的是新的selection,則使用兩個空格進行縮進。node
元素的選擇經過兩種方法select
和selectAll
來實現,前者只返回第一個匹配的元素,然後者返回全部匹配元素。 因爲以後全部的操做都是在selection上進行,而該對象則是經過Selection構造函數獲得的,源碼以下:web
function Selection(groups, parents) {
this._groups = groups;
this._parents = parents;
}
複製代碼
能夠看出,selection對象包含兩個基本屬性_groups
和_parents
,前者用於存儲結點組,然後者則存儲結點的父節點信息。數組
var p = d3.selectAll('div')
.select('p');
複製代碼
該方法對selection中的每一個元素進行查找,選擇其中第一個匹配selector的子元素,源碼以下:bash
/*
* Selection的select方法
* 經過select方法選擇時,若不存在元素,則會在數組中將該位置留出(賦值爲null)用於以後插入時使用。
*/
function selection_select(select) {
if (typeof select !== "function") select = selector(select);
//當select是函數時,直接調用該函數,並依次對該函數傳入data信息、當前的索引和當前的結點group,同時將函數的this設置爲當前dom對象
for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, subgroup = subgroups[j] = new Array(n), node, subnode, i = 0; i < n; ++i) {
if ((node = group[i]) && (subnode = select.call(node, node.__data__, i, group))) {
//當node中有data信息時,node的子元素也添加該data信息
if ("__data__" in node) subnode.__data__ = node.__data__;
subgroup[i] = subnode;
}
}
}
//將父級的_parents屬性做爲子元素的_parents屬性
return new Selection(subgroups, this._parents);
}
複製代碼
若selector爲選擇器字符串時,則會先調用selector方法將其轉化爲函數,源碼以下:app
function selector(selector) {
return selector == null ? none$2 : function() {
//只返回第一個選中元素
return this.querySelector(selector);
};
}
複製代碼
能夠看出,其內部調用的是js的原生方法querySelector
。 上述代碼對select參數進行處理後,使得其轉化爲函數,在後來的循環中調用時,經過select.call
進行調用,傳入的參數依次爲結點的__data__屬性值,結點在該結點組中的索引,該結點組。 有如下幾點值得注意:dom
__data__
屬性則會對匹配的子元素也設置該屬性。_parents
值並不會改變。var p = d3.selectAll('div')
.selectAll('p');
複製代碼
該方法對selection中的每一個元素進行查找,選擇其中匹配selector的子元素,返回的selection中的元素根據其父結點進行對應的分組,源碼以下:svg
/*
* 對selection進行selectAll計算會改變selection結構,parents也會改變
*/
function selection_selectAll(select) {
if (typeof select !== "function") select = selectorAll(select);
for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
if (node = group[i]) {
subgroups.push(select.call(node, node.__data__, i, group));
parents.push(node);
}
}
}
return new Selection(subgroups, parents);
}
複製代碼
上述代碼能夠看出,對node調用select方法後,查找到的結果存入subgroups中,同時將node做爲父結點存入parents數組中,使得結點與父結點一一對應,最終返回新的selection。函數
var red = d3.selectAll('p')
.filter('.red')
複製代碼
將使得filter爲true的元素構形成新的selection並返回,源碼以下:ui
//filter方法對當前selection進行過濾,保留知足條件的元素
function selection_filter(match) {
if (typeof match !== "function") match = matcher$1(match);
for (var groups = this._groups, m = groups.length, subgroups = new Array(m), j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, subgroup = subgroups[j] = [], node, i = 0; i < n; ++i) {
if ((node = group[i]) && match.call(node, node.__data__, i, group)) {
subgroup.push(node);
}
}
}
return new Selection(subgroups, this._parents);
}
複製代碼
若match
不爲函數,則經過matcher$1
函數對其進行處理,其源碼以下:
var matcher = function(selector) {
return function() {
//Element.matches(s),若是元素能經過s選擇器選擇到則返回true;不然返回false
return this.matches(selector);
};
};
複製代碼
可看出matcher
函數內部是調用原生的Element.matches
方法實現。
var circle = svg.selectAll("circle").data(data) // UPDATE
.style("fill", "blue");
circle.exit().remove(); // EXIT
circle.enter().append("circle") // ENTER
.style("fill", "green")
.merge(circle) // ENTER + UPDATE
.style("stroke", "black");
複製代碼
該方法將兩個selection進行合併成一個新的selection並返回,源碼以下:
function selection_merge(selection) {
//新的selection的_.groups長度和groups0相同,合併時只在m範圍內計算
for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
//groups0數組大小不變,只有在group0[i]不存在,group1[i]存在時才選擇group1[i]
if (node = group0[i] || group1[i]) {
merge[i] = node;
}
}
}
// 若m1 < m0,則將groups0剩餘的複製過來
for (; j < m0; ++j) {
merges[j] = groups0[j];
}
return new Selection(merges, this._parents);
}
複製代碼
該方法實際上至關於對this selection中的空元素進行填充。
在選擇元素以後可使用selection的方法來修改元素,如樣式、屬性等。
var p = d3.selectAll('p')
.attr('class', 'red');
複製代碼
對selection中的元素以指定的name和value設置屬性,並返回當前selection,源碼以下:
function selection_attr(name, value) {
var fullname = namespace(name);
if (arguments.length < 2) {
//獲得selection中第一個存在的元素
var node = this.node();
return fullname.local
? node.getAttributeNS(fullname.space, fullname.local)
: node.getAttribute(fullname);
}
return this.each((value == null
? (fullname.local ? attrRemoveNS : attrRemove) : (typeof value === "function"
? (fullname.local ? attrFunctionNS : attrFunction)
: (fullname.local ? attrConstantNS : attrConstant)))(fullname, value));
}
複製代碼
若只有name參數時,則返回第一個存在的元素的name屬性值,調用的是原生的Element.getAttribute
方法。 當有兩個參數時,調用selection.each
方法對selection中的每一個元素進行操做,其源碼以下:
function selection_each(callback) {
for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
if (node = group[i]) callback.call(node, node.__data__, i, group);
}
}
return this;
}
複製代碼
若value值爲函數,則將name和value傳入attrFunction函數中進行處理。
function attrFunction(name, value) {
return function() {
var v = value.apply(this, arguments);
if (v == null) this.removeAttribute(name);
else this.setAttribute(name, v);
};
}
複製代碼
在selection.each
方法中,將參數傳入value函數中,根據value返回結果選擇設置和刪除屬性操做。
var p = d3.selectAll('p')
.classed('red warn', true);
複製代碼
對selection中的元素設置類名,源碼以下:
// 當value爲真值時,在全部元素類名中添加name;不然刪除name
function selection_classed(name, value) {
var names = classArray(name + "");
// 當只有name參數時,判斷該selection對象的_groups裏第一個存在的結點是否包含全部的name的類名,若是是則返回true;不然,返回false
if (arguments.length < 2) {
var list = classList(this.node()), i = -1, n = names.length;
while (++i < n) if (!list.contains(names[i])) return false;
return true;
}
return this.each((typeof value === "function"
? classedFunction : value
? classedTrue
: classedFalse)(names, value));
}
複製代碼
其中classArray
方法是將類名字符串拆分紅數組:
// 將類名拆分紅數組,如'button button-warn' => ['button', 'button-warn']
function classArray(string) {
return string.trim().split(/^|\s+/);
}
複製代碼
var p = d3.selectAll('p')
.style('color', 'red');
複製代碼
該方法對selection中的元素設置樣式,源碼以下:
// 設置selection的樣式,注意樣式的單位問題
function selection_style(name, value, priority) {
var node;
return arguments.length > 1
? this.each((value == null
? styleRemove : typeof value === "function"
? styleFunction
: styleConstant)(name, value, priority == null ? "" : priority))
: window(node = this.node())
.getComputedStyle(node, null)
.getPropertyValue(name);
}
複製代碼
從上述代碼能夠看出,獲取樣式是經過window.getComputedStyle(element).getPropertyValue(name)
來獲得(該方法獲得的值是隻讀的),而刪除樣式則是經過element.style.removeProperty(name)
來實現,設置屬性經過element.style.setProperty(name, value)
來實現。
var checkbox = d3.selectAll('input[type=checkbox]')
.property('checked', 'checked');
複製代碼
該方法設置一些特殊的屬性。
function selection_property(name, value) {
return arguments.length > 1
? this.each((value == null
? propertyRemove : typeof value === "function"
? propertyFunction
: propertyConstant)(name, value))
: this.node()[name];
}
function propertyRemove(name) {
return function() {
delete this[name];
};
}
function propertyConstant(name, value) {
return function() {
this[name] = value;
};
}
function propertyFunction(name, value) {
return function() {
var v = value.apply(this, arguments);
if (v == null) delete this[name];
else this[name] = v;
};
}
複製代碼
內部經過直接修改元素的屬性來實現。
該方法對selection中的全部元素設置文本內容,同時會替換掉元素中的子元素,源碼以下:
function textRemove() {
this.textContent = "";
}
function textConstant(value) {
return function() {
this.textContent = value;
};
}
function textFunction(value) {
return function() {
var v = value.apply(this, arguments);
this.textContent = v == null ? "" : v;
};
}
// 設置元素的textContent屬性,該屬性返回的是元素內的純文本內容,不包含結點標籤(但包含標籤內的文本)
function selection_text(value) {
return arguments.length
? this.each(value == null
? textRemove : (typeof value === "function"
? textFunction
: textConstant)(value))
: this.node().textContent;
}
複製代碼
上述代碼是經過element.textContent
方法來獲取和修改文本內容。
對selection中的全部元素設置innerHTML。方法同上述selection.text
相似,只是經過element.innerHTML
來修改元素內的全部內容。
對selection中的元素添加新的元素。
/*
* selection的append方法
* 該方法返回新的Selection對象,
*/
function selection_append(name) {
var create = typeof name === "function" ? name : creator(name);
return this.select(function() {
//arguments是傳入當前匿名函數的參數
return this.appendChild(create.apply(this, arguments));
});
}
function creatorInherit(name) {
return function() {
//ownerDocument返回當前document對象
var document = this.ownerDocument,
uri = this.namespaceURI;
return uri === xhtml && document.documentElement.namespaceURI === xhtml
? document.createElement(name)//建立dom對象
: document.createElementNS(uri, name);
};
}
複製代碼
該方法中調用到了selection.select
方法,因爲element.appendChild
方法返回的是該子結點,所以返回的新的selection包含的是全部添加的子結點。
對selection中的元素插入新的元素,同上述selection.append
方法相似,只是內部使用element.insertBefore
方法實現。
根據compare
函數對selection中的元素進行排序,排好序後按照排序結果對dom進行排序,返回排序後新建立的selection對象。
function selection_sort(compare) {
if (!compare) compare = ascending$2;
function compareNode(a, b) {
// 比較結點的data大小
return a && b ? compare(a.__data__, b.__data__) : !a - !b;
}
// copy一份selection中的_groups包含的結點
for (var groups = this._groups, m = groups.length, sortgroups = new Array(m), j = 0; j < m; ++j) {
for (var group = groups[j], n = group.length, sortgroup = sortgroups[j] = new Array(n), node, i = 0; i < n; ++i) {
if (node = group[i]) {
sortgroup[i] = node;
}
}
// 調用array的sort方法
sortgroup.sort(compareNode);
}
return new Selection(sortgroups, this._parents).order();
}
// 遞增
function ascending$2(a, b) {
return a < b ? -1 : a > b ? 1 : a >= b ? 0 : NaN;
}
複製代碼
若沒有compare參數,則默認以遞增的方式排序。同時排序是首先比較元素中的__data__
屬性值的大小,對selection排好序後調用order方法。
按照selection中每組內元素的順序對dom進行排序
// 對dom結點進行排序
function selection_order() {
for (var groups = this._groups, j = -1, m = groups.length; ++j < m;) {
for (var group = groups[j], i = group.length - 1, next = group[i], node; --i >= 0;) {
if (node = group[i]) {
// 將node移至next的前面,並將node賦值給next
if (next && next !== node.nextSibling) next.parentNode.insertBefore(node, next);
next = node;
}
}
}
return this;
}
複製代碼
鏈接數據是將數據綁定到selection對象上,其實是將數據存儲到__data__
屬性中,這樣以後對selection的操做過程當中即可以直接使用綁定好的數據。主要要理解update
、enter
和exit
,可參考文章Thinking With Joins。
該方法將指定的data數組綁定到選中的元素上,返回的selection包含成功綁定數據的元素,也叫作updata selection
,源碼以下:
function selection_data(value, key) {
//當value爲假值時,將selection全部元素的__data__屬性以數組形式返回
if (!value) {
data = new Array(this.size()), j = -1;
this.each(function(d) { data[++j] = d; });
return data;
}
var bind = key ? bindKey : bindIndex,
parents = this._parents,
groups = this._groups;
if (typeof value !== "function") value = constant$4(value);
for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
var parent = parents[j],
group = groups[j],
groupLength = group.length,
data = value.call(parent, parent && parent.__data__, j, parents),
dataLength = data.length,
enterGroup = enter[j] = new Array(dataLength),
updateGroup = update[j] = new Array(dataLength),
exitGroup = exit[j] = new Array(groupLength);
bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);
// 對enter結點設置_next屬性,存儲其索引以後的第一個update結點
for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
if (previous = enterGroup[i0]) {
if (i0 >= i1) i1 = i0 + 1;
while (!(next = updateGroup[i1]) && ++i1 < dataLength);
previous._next = next || null;
}
}
}
// 將enter和exit存入update selection的屬性中
update = new Selection(update, parents);
update._enter = enter;
update._exit = exit;
return update;
}
複製代碼
經過上述代碼能夠看到,對每組group綁定的是相同的data數據。 當沒有key參數時,綁定數據使用的是bindIndex
方法,按照索引一次綁定。
function bindIndex(parent, group, enter, update, exit, data) {
var i = 0,
node,
groupLength = group.length,
dataLength = data.length;
/*
* 將data數據綁定到node,並將該node存入update數組中
* 將剩餘的data數據存入enter數組中
*/
for (; i < dataLength; ++i) {
if (node = group[i]) {
node.__data__ = data[i];
update[i] = node;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
// 將剩餘的node存入exit數組中
for (; i < groupLength; ++i) {
if (node = group[i]) {
exit[i] = node;
}
}
}
複製代碼
若含有key參數,則調用bindKey
方法來綁定數據。
function bindKey(parent, group, enter, update, exit, data, key) {
var i,
node,
nodeByKeyValue = {},
groupLength = group.length,
dataLength = data.length,
keyValues = new Array(groupLength),
keyValue;
// 對group中每一個結點計算keyValue,若是以後的結點含有與前面結點相同的keyValue則將該結點存入exit數組中
for (i = 0; i < groupLength; ++i) {
if (node = group[i]) {
keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
if (keyValue in nodeByKeyValue) {
exit[i] = node;
} else {
nodeByKeyValue[keyValue] = node;
}
}
}
// 對每個data計算keyValue,若是該keyValue已存在nodeByKeyValue數組中,則將其對應的node存入update數組且綁定data數據;不然將data存入enter中
for (i = 0; i < dataLength; ++i) {
keyValue = keyPrefix + key.call(parent, data[i], i, data);
if (node = nodeByKeyValue[keyValue]) {
update[i] = node;
node.__data__ = data[i];
nodeByKeyValue[keyValue] = null;
} else {
enter[i] = new EnterNode(parent, data[i]);
}
}
// 將剩餘的沒有綁定數據的結點存入exit數組
for (i = 0; i < groupLength; ++i) {
if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
exit[i] = node;
}
}
}
複製代碼
返回selections中的enter selection,即selection._enter
的結果。
function sparse(update) {
return new Array(update.length);
}
// selection的enter方法
function selection_enter() {
return new Selection(this._enter || this._groups.map(sparse), this._parents);
}
複製代碼
若selection沒有_enter
屬性,即沒有進行過data
操做,則建立空的數組。
返回selection中的exit selection,即selection._exit
的結果。
function selection_exit() {
return new Selection(this._exit || this._groups.map(sparse), this._parents);
}
複製代碼
對selection中的每一個元素設置綁定數據,該方法並不會影響到enter
和exit
的值。
function selection_datum(value) {
return arguments.length
? this.property("__data__", value)
: this.node().__data__;
}
複製代碼
可見其調用selection.property
方法來設置__data__
屬性。 常常會使用該方法來進行HTML5 data屬性的訪問,如
selection.datum(function() {return this.dataset});
複製代碼
element.dataset
是原生方法,返回的是元素綁定的全部data屬性。
該方法用於對selection中的元素添加或者移除事件。
function selection_on(typename, value, capture) {
var typenames = parseTypenames$1(typename + ""), i, n = typenames.length, t;
// 若是隻有typename參數,根據type和name值來找到selection中第一個存在的元素的__on屬性中對應的value值。
if (arguments.length < 2) {
var on = this.node().__on;
if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
for (i = 0, o = on[j]; i < n; ++i) {
if ((t = typenames[i]).type === o.type && t.name === o.name) {
return o.value;
}
}
}
return;
}
// 若是value爲真值,則添加事件;不然移除事件。
on = value ? onAdd : onRemove;
if (capture == null) capture = false;
for (i = 0; i < n; ++i) this.each(on(typenames[i], value, capture));
return this;
}
複製代碼
首先是對typename參數進行處理,使用的是parseTypenames
函數。
// 對typenames根據空格來劃分紅數組,並根據分離後的字符串中的'.'來將該字符串分割爲type和name部分,如:'click.foo click.bar' => [{type: 'click', name: 'foo'}, {type: 'click', name: 'bar'}]
function parseTypenames$1(typenames) {
return typenames.trim().split(/^|\s+/).map(function(t) {
var name = "", i = t.indexOf(".");
if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
return {type: t, name: name};
});
}
複製代碼
根據value是否爲真值來選擇添加或者移除事件。
// 添加事件函數
function onAdd(typename, value, capture) {
var wrap = filterEvents.hasOwnProperty(typename.type) ? filterContextListener : contextListener;
return function(d, i, group) {
var on = this.__on, o, listener = wrap(value, i, group);
if (on) for (var j = 0, m = on.length; j < m; ++j) {
// 若是新事件的type和name和以前已綁定的事件相同,則移除以前的事件並綁定新的事件
if ((o = on[j]).type === typename.type && o.name === typename.name) {
this.removeEventListener(o.type, o.listener, o.capture);
this.addEventListener(o.type, o.listener = listener, o.capture = capture);
o.value = value;
return;
}
}
// 添加事件,並將事件信息存入selection.__on屬性中
this.addEventListener(typename.type, listener, capture);
o = {type: typename.type, name: typename.name, value: value, listener: listener, capture: capture};
if (!on) this.__on = [o];
else on.push(o);
};
}
複製代碼
// 移除事件函數
function onRemove(typename) {
return function() {
var on = this.__on;
if (!on) return;
for (var j = 0, i = -1, m = on.length, o; j < m; ++j) {
if (o = on[j], (!typename.type || o.type === typename.type) && o.name === typename.name) {
//對結點移除事件
this.removeEventListener(o.type, o.listener, o.capture);
} else {
//修改結點的__on屬性值
on[++i] = o;
}
}
if (++i) on.length = i;
//on爲空則刪除__on屬性
else delete this.__on;
};
}
複製代碼
對selection中的元素分派指定類型的自定義事件,其中parameters可能包含如下內容:
//分派事件
function selection_dispatch(type, params) {
return this.each((typeof params === "function"
? dispatchFunction
: dispatchConstant)(type, params));
}
function dispatchConstant(type, params) {
return function() {
return dispatchEvent(this, type, params);
};
}
function dispatchFunction(type, params) {
return function() {
return dispatchEvent(this, type, params.apply(this, arguments));
};
}
複製代碼
dispatchEvent
函數以下:
//建立自定義事件並分派給指定元素
function dispatchEvent(node, type, params) {
var window$$ = window(node),
event = window$$.CustomEvent;
if (event) {
event = new event(type, params);
} else {
//該方法已被廢棄
event = window$$.document.createEvent("Event");
if (params) event.initEvent(type, params.bubbles, params.cancelable), event.detail = params.detail;
else event.initEvent(type, false, false);
}
node.dispatchEvent(event);
}
複製代碼
存儲當前事件,在調用事件監聽器的時候設置,處理函數執行完畢後重置,能夠獲取其中包含的事件信息如:event.pageX
。
該方法調用指定的監聽器。
//調用指定的監聽器
function customEvent(event1, listener, that, args) {
//記錄當前事件
var event0 = exports.event;
event1.sourceEvent = exports.event;
exports.event = event1;
try {
return listener.apply(that, args);
} finally {
//監聽器執行完後恢復事件
exports.event = event0;
}
}
複製代碼
返回當前當前事件相對於指定容器的x和y座標。
function mouse(node) {
var event = sourceEvent();
//若是是觸摸事件,返回Touch對象
if (event.changedTouches) event = event.changedTouches[0];
return point$5(node, event);
}
複製代碼
point$5
方法用於計算座標。
function point$5(node, event) {
//若node是svg元素,則獲取svg容器
var svg = node.ownerSVGElement || node;
if (svg.createSVGPoint) {
//建立SVGPoint對象
var point = svg.createSVGPoint();
//將事件相對客戶端的x和y座標賦值給point對象
point.x = event.clientX, point.y = event.clientY;
//進行座標轉換
point = point.matrixTransform(node.getScreenCTM().inverse());
return [point.x, point.y];
}
var rect = node.getBoundingClientRect();
//返回event事件相對於容器的座標
return [event.clientX - rect.left - node.clientLeft, event.clientY - rect.top - node.clientTop];
}
複製代碼
用於selection的一些高級操做。
爲每個選中的元素調用指定的函數。
//selection的each方法
function selection_each(callback) {
for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
//經過call方式調用回調函數
if (node = group[i]) callback.call(node, node.__data__, i, group);
}
}
return this;
}
複製代碼
將selection和其餘參數傳入指定函數中執行,並返回當前selection,這和直接鏈式調用function(selection)
相同。
function selection_call() {
var callback = arguments[0];
arguments[0] = this;
callback.apply(null, arguments);
return this;
}
複製代碼
d3的局部變量能夠定義與data獨立開來的局部狀態,它的做用域是dom元素。 其構造函數和原型方法以下:
function Local() {
this._ = "@" + (++nextId).toString(36);
}
Local.prototype = local.prototype = {
constructor: Local,
get: function(node) {
var id = this._;
while (!(id in node)) if (!(node = node.parentNode)) return;
return node[id];
},
set: function(node, value) {
return node[this._] = value;
},
remove: function(node) {
return this._ in node && delete node[this._];
},
toString: function() {
return this._;
}
};
複製代碼