javascript(ecmascript 6(
我为Array
项目写了一个代理。我这样做是为了解决问题...我在这样的示例的基础上显示了这个问题:
function Node(value){
this.parent = null; // parent Node item
this.value = value;
this.children = []; // child Node items
}
我希望children
数组的每个项目都会在将该项目添加到该数组中或从中删除时更新自己的parent
属性。
下面是我的单位测试决定。 i Appologize供俄罗斯评论(您可以用Google翻译那些感兴趣的人(。
也将代码放在此处的JSFiddle上(此资源显示代码行号(。
您可以看到我的一个测试失败了...当我直接降低数组的 length
属性删除项目时,这就是发生。我看到deleteProperty
处理程序不会捕获此事件。
即。我的问题存在于这样删除的这种变体:
myarray.length -= 1;
在这种情况下,最后一项将从数组中删除,但其parent
属性不会更新为null
值。
如何通过代理捕获这种情况?
谢谢。
/* This is the module which I test. My QUnit tests start with code row 118.
The failed test is on the code row 351.*/
(function(exports){
/* Массив, автоматически управляющий ссылками на родителя для всех своих
* дочерних элементов при их добавлении или удалении.
*
* propName - имя свойства, которое автоматически должно появляться и
* обновляться у каждого элемента массива.
*
* parent - ссылка на объект, который должен быть указан в качестве значения
* свойства, имя которого указано в propName у каждого элемента массива.
*
* ВНИМАНИЕ!
* Если удалять элементы из хвоста прокси-массива посредством назначения
* свойству length меньшего значения, то у удалённых таким образом элементов
* свойство, подлежащее мониторингу, НЕ обновит своё значение! Поэтому не
* следует пользоваться таким способом удаления элементов из прокси-массива,
* полученного при помощи ArrayWithParentLink. Вместо этого используйте
* методы pop(), shift() и splice().
*/
exports.ArrayWithParentLink = function(propName, parent){
/** Выяснение того, является ли ключ корректным индексом массива.
* Вычисление выполняется в соответствии со спецификацией ECMAScript 6.
* См. книгу "ECMAScript 6 для разработчиков" Николаса Вирта, стр. 307.
* */
function isArrayIndex(key){
const toUint32 = function(value){
return Math.floor(Math.abs(Number(value))) % Math.pow(2,32);
};
let numericKey = toUint32(key);
return String(numericKey) == key && numericKey < (Math.pow(2,32)-1);
};
/* Редактирование состава вложенного массива будем выполнять через
* прокси, дабы автоматизировать синхронизацию значения свойства,
* имя которого было ранее указано в propName при создании экземпляра
* прокси-массива. */
var arrayChangeHandler = {
set: function(target, key, value, receiver) {
/* Если имя свойства является допустимым индексом массива,
* значит выполняется операция над элементом массива... */
if(isArrayIndex(key)){
value[propName] = parent;
/* Очистить значение свойства (имя которого указано в
* propName) у заменяемого элемента, если в массиве на него
* имеется только одна ссылка. В некоторые моменты в массиве
* может присутствовать одновременно несколько ссылок на
* один и тот же элемент. Пример такого случая приведён в
* комментарии ниже.*/
if((key in target) && target[key] &&
(propName in target[key])){
/* Если выполняется ВСТАВКА новых элементов методом
* Array.prototype.splice(), то происходит добавление к
* массиву необходимого количества дополнительных ячеек,
* в которые записываются ссылки на уже существующие
* элементы массива, а их прежние ячейки переписываются
* ссылками на вставляемые элементы. Т.е. до того, как
* произойдёт перезапись, в составе массива временно
* присутствует ДВЕ ссылки на смещаемый элемент. Если не
* учесть этот момент, то свойству parent перемещаемого
* дочернего элемента будет назначено значение null, что
* будет являться неправильным действием. Чтобы избежать
* этой ошибки, проверяем количество ссылок на удаляемый
* объект. Если их более одной, то считаем, что
* происходит не удаление, но перемещение элемента в
* массиве: */
let linksCount = 0;
for (let i = 0; i < target.length; i++){
if(target[i] == target[key])
linksCount++;
}
if(1 == linksCount)
target[key][propName] = null;
}
}
return Reflect.set(target, key, value, receiver);
},
deleteProperty: function(target, key) {
/* Если имя свойства является допустимым индексом массива,
* значит выполняется операция над элементом массива... */
if(isArrayIndex(Number(key))){
/* ВНИМАНИЕ! Прочти комментарий перед аналогичной строкой
* кода для сеттера set, объявленого выше, дабы понимать,
* для чего выполняется проверка количества ссылок. */
let linksCount = 0;
for (let i = 0; i < target.length; i++){
if(target[i] == target[key])
linksCount++;
}
if(1 == linksCount)
target[key][propName] = null;
}
return Reflect.deleteProperty(target, key);
}
};
return new Proxy([], arrayChangeHandler);
};
})(window.Services = Object.create(null));
QUnit.module('Services.ArrayWithParentLink');
/** Проверка всех элементов прокси-массива.
*
* Это дополнительная проверка, которая используется в тестах, дабы убедиться в
* том, что помимо ожидаемых изменений не произошли изменения неожиданные,
* затрагивающие другие элементы, имеющиеся в прокси-массиве.
*
* array - прокси-массив, подлежащий проверке.
* propName - имя свойства, проверяемого у элементов прокси-массива.
* value - ожидаемое значение свойства, имя которого указано в propName.
* assert - ссылка на объект assert фреймворка QUnit.
* */
function checkAllItems(array, propName, value, assert) {
// Проверим, что все элементы массива имеют правильное значение подлежащего
// мониторингу свойства.
let rightValues = true;
for(let i = 0; i < array.length; i++){
if(array[i][propName] != value){
rightValues = false;
break;
}
}
assert.ok(rightValues, "Каждый элемент в прокси-массиве содержат правильное"
+ " значение в свойстве, подлежащем мониторингу.");
}
QUnit.test( "Новый прокси-массив является обёрткой над экземпляром Array.",
function( assert ) {
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
assert.ok(root.items instanceof Array);
});
QUnit.test( "Длина нового прокси-массива равна нулю.", function( assert ) {
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
assert.equal(root.items.length, 0);
});
QUnit.test( "В новом элементе, добавленном в прокси-массив при помощи метода" +
"'push()', автоматически появляется подлежащее мониторингу свойство, " +
"инициализированное правильным значением. ",
function( assert ) {
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
root.items.push({});
assert.equal(root.items[0][name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test( "В новом элементе, добавленном в прокси-массив при помощи индекса" +
" [index], автоматически появляется подлежащее мониторингу свойство, " +
"инициализированное правильным значением. ",
function( assert ) {
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
root.items[0] = {};
assert.equal(root.items[0][name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Метод 'pop()' прокси-массива успешно удаляет последний элемент.",
function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 5;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
const item = root.items[count - 1];
const item2 = root.items.pop();
assert.equal(item2, item);
assert.equal(root.items.length, count - 1);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("У удаляемого методом 'pop()' объекта, значение подлежащего "
+ " мониторингу свойства автоматически становится равным null, если в " +
"массиве имелась только одна ссылка на удаляемый объект.",
function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 5;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
let item = root.items.pop();
assert.equal(item[name], null);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("У удаляемого методом 'pop()' объекта, значение подлежащего "
+ " мониторингу свойства не изменяется на null, если в " +
"массиве имелось более одной ссылки на удаляемый объект.",
function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 2;
const obj = {};
for(let i = 0; i < count; i++){
root.items[i] = obj;
}
let item = root.items.pop();
assert.equal(item[name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test( "В новом элементе, добавленном в прокси-массив при помощи метода" +
" 'unshift()', автоматически появляется подлежащее мониторингу свойство, " +
"инициализированное правильным значением. ",
function( assert ) {
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
root.items.unshift({});
assert.equal(root.items[0][name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Метод 'shift()' успешно удаляет первый элемент прокси-массива.",
function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 5;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
const item = root.items[0];
const item2 = root.items.shift();
assert.equal(item2, item);
assert.equal(root.items.length, count - 1);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("У удаляемого методом 'shift()' объекта, значение подлежащего "
+ " мониторингу свойства автоматически становится равным null, если в " +
"массиве имелась только одна ссылка на удаляемый объект.",
function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 5;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
let item = root.items.shift();
assert.equal(item[name], null);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("У удаляемого методом 'shift()' объекта, значение подлежащего "
+ " мониторингу свойства не становится равным null, если в " +
"массиве имелось более одной ссылки на удаляемый объект.",
function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 2;
const obj = {};
for(let i = 0; i < count; i++){
root.items[i] = obj;
}
let item = root.items.shift();
assert.equal(item[name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Значение подлежащего мониторингу свойства у элемента, удаляемого " +
"посредством уменьшения значения свойства length массива, становится "
+ "равным null, если в массиве имелось не более одной ссылки на удаляемый "
+ "элемент", function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 2;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
let item = root.items[count - 1];
root.items.length = 1;
assert.equal(item[name], null);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Значение подлежащего мониторингу свойства у элемента, удаляемого " +
"посредством уменьшения значения свойства length массива, не становится "
+ "равным null, если в массиве имелось более одной ссылки на удаляемый "
+ "элемент", function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 2;
const obj = {};
for(let i = 0; i < count; i++){
root.items[i] = obj;
}
let item = root.items[count - 1];
root.items.length = 1;
assert.equal(item[name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Значение подлежащего мониторингу свойства у элемента, удаляемого " +
"посредством метода 'splice()', становится равным null, если в массиве " +
"имелось не более одной ссылки на удаляемый элемент", function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 3;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
const index = 1;
let item = root.items[index];
root.items.splice(index,1);
assert.equal(item[name], null);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Значение подлежащего мониторингу свойства у элемента, удаляемого " +
"посредством метода 'splice()', не становится равным null, если в массиве "
+ "имелось более одной ссылки на удаляемый элемент", function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 3;
const obj = {};
for(let i = 0; i < count; i++){
root.items[i] = obj;
}
const index = 1;
let item = root.items[index];
root.items.splice(index,1);
assert.equal(item[name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
QUnit.test("Значение подлежащего мониторингу свойства у элемента, добавляемого "
+ "в прокси-массив посредством метода 'splice()', инициализируется " +
"правильным значением.", function(assert){
const root = Object.create(null);
const name = 'parent';
root.items = new window.Services.ArrayWithParentLink(name, root);
const count = 3;
for(let i = 0; i < count; i++){
root.items[i] = {};
}
const index = 1;
const obj = {};
root.items.splice(index,0,obj);
assert.equal(obj[name], root);
// Дополнительная проверка
checkAllItems(root.items, name, root, assert);
});
/*!
* QUnit 2.3.3
* https://qunitjs.com/
*
* Copyright jQuery Foundation and other contributors
* Released under the MIT license
* https://jquery.org/license
*
* Date: 2017-06-02T14:07Z
*/
/** Font Family and Sizes */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult {
font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif;
}
#qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; }
#qunit-tests { font-size: smaller; }
/** Resets */
#qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter {
margin: 0;
padding: 0;
}
/** Header (excluding toolbar) */
#qunit-header {
padding: 0.5em 0 0.5em 1em;
color: #8699A4;
background-color: #0D3349;
font-size: 1.5em;
line-height: 1em;
font-weight: 400;
border-radius: 5px 5px 0 0;
}
#qunit-header a {
text-decoration: none;
color: #C2CCD1;
}
#qunit-header a:hover,
#qunit-header a:focus {
color: #FFF;
}
#qunit-banner {
height: 5px;
}
#qunit-filteredTest {
padding: 0.5em 1em 0.5em 1em;
color: #366097;
background-color: #F4FF77;
}
#qunit-userAgent {
padding: 0.5em 1em 0.5em 1em;
color: #FFF;
background-color: #2B81AF;
text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px;
}
/** Toolbar */
#qunit-testrunner-toolbar {
padding: 0.5em 1em 0.5em 1em;
color: #5E740B;
background-color: #EEE;
}
#qunit-testrunner-toolbar .clearfix {
height: 0;
clear: both;
}
#qunit-testrunner-toolbar label {
display: inline-block;
}
#qunit-testrunner-toolbar input[type=checkbox],
#qunit-testrunner-toolbar input[type=radio] {
margin: 3px;
vertical-align: -2px;
}
#qunit-testrunner-toolbar input[type=text] {
box-sizing: border-box;
height: 1.6em;
}
.qunit-url-config,
.qunit-filter,
#qunit-modulefilter {
display: inline-block;
line-height: 2.1em;
}
.qunit-filter,
#qunit-modulefilter {
float: right;
position: relative;
margin-left: 1em;
}
.qunit-url-config label {
margin-right: 0.5em;
}
#qunit-modulefilter-search {
box-sizing: border-box;
width: 400px;
}
#qunit-modulefilter-search-container:after {
position: absolute;
right: 0.3em;
content: "25bc";
color: black;
}
#qunit-modulefilter-dropdown {
/* align with #qunit-modulefilter-search */
box-sizing: border-box;
width: 400px;
position: absolute;
right: 0;
top: 50%;
margin-top: 0.8em;
border: 1px solid #D3D3D3;
border-top: none;
border-radius: 0 0 .25em .25em;
color: #000;
background-color: #F5F5F5;
z-index: 99;
}
#qunit-modulefilter-dropdown a {
color: inherit;
text-decoration: none;
}
#qunit-modulefilter-dropdown .clickable.checked {
font-weight: bold;
color: #000;
background-color: #D2E0E6;
}
#qunit-modulefilter-dropdown .clickable:hover {
color: #FFF;
background-color: #0D3349;
}
#qunit-modulefilter-actions {
display: block;
overflow: auto;
/* align with #qunit-modulefilter-dropdown-list */
font: smaller/1.5em sans-serif;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > * {
box-sizing: border-box;
max-height: 2.8em;
display: block;
padding: 0.4em;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > button {
float: right;
font: inherit;
}
#qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child {
/* insert padding to align with checkbox margins */
padding-left: 3px;
}
#qunit-modulefilter-dropdown-list {
max-height: 200px;
overflow-y: auto;
margin: 0;
border-top: 2px groove threedhighlight;
padding: 0.4em 0 0;
font: smaller/1.5em sans-serif;
}
#qunit-modulefilter-dropdown-list li {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#qunit-modulefilter-dropdown-list .clickable {
display: block;
padding-left: 0.15em;
}
/** Tests: Pass/Fail */
#qunit-tests {
list-style-position: inside;
}
#qunit-tests li {
padding: 0.4em 1em 0.4em 1em;
border-bottom: 1px solid #FFF;
list-style-position: inside;
}
#qunit-tests > li {
display: none;
}
#qunit-tests li.running,
#qunit-tests li.pass,
#qunit-tests li.fail,
#qunit-tests li.skipped,
#qunit-tests li.aborted {
display: list-item;
}
#qunit-tests.hidepass {
position: relative;
}
#qunit-tests.hidepass li.running,
#qunit-tests.hidepass li.pass:not(.todo) {
visibility: hidden;
position: absolute;
width: 0;
height: 0;
padding: 0;
border: 0;
margin: 0;
}
#qunit-tests li strong {
cursor: pointer;
}
#qunit-tests li.skipped strong {
cursor: default;
}
#qunit-tests li a {
padding: 0.5em;
color: #C2CCD1;
text-decoration: none;
}
#qunit-tests li p a {
padding: 0.25em;
color: #6B6464;
}
#qunit-tests li a:hover,
#qunit-tests li a:focus {
color: #000;
}
#qunit-tests li .runtime {
float: right;
font-size: smaller;
}
.qunit-assert-list {
margin-top: 0.5em;
padding: 0.5em;
background-color: #FFF;
border-radius: 5px;
}
.qunit-source {
margin: 0.6em 0 0.3em;
}
.qunit-collapsed {
display: none;
}
#qunit-tests table {
border-collapse: collapse;
margin-top: 0.2em;
}
#qunit-tests th {
text-align: right;
vertical-align: top;
padding: 0 0.5em 0 0;
}
#qunit-tests td {
vertical-align: top;
}
#qunit-tests pre {
margin: 0;
white-space: pre-wrap;
word-wrap: break-word;
}
#qunit-tests del {
color: #374E0C;
background-color: #E0F2BE;
text-decoration: none;
}
#qunit-tests ins {
color: #500;
background-color: #FFCACA;
text-decoration: none;
}
/*** Test Counts */
#qunit-tests b.counts { color: #000; }
#qunit-tests b.passed { color: #5E740B; }
#qunit-tests b.failed { color: #710909; }
#qunit-tests li li {
padding: 5px;
background-color: #FFF;
border-bottom: none;
list-style-position: inside;
}
/*** Passing Styles */
#qunit-tests li li.pass {
color: #3C510C;
background-color: #FFF;
border-left: 10px solid #C6E746;
}
#qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; }
#qunit-tests .pass .test-name { color: #366097; }
#qunit-tests .pass .test-actual,
#qunit-tests .pass .test-expected { color: #999; }
#qunit-banner.qunit-pass { background-color: #C6E746; }
/*** Failing Styles */
#qunit-tests li li.fail {
color: #710909;
background-color: #FFF;
border-left: 10px solid #EE5757;
white-space: pre;
}
#qunit-tests > li:last-child {
border-radius: 0 0 5px 5px;
}
#qunit-tests .fail { color: #000; background-color: #EE5757; }
#qunit-tests .fail .test-name,
#qunit-tests .fail .module-name { color: #000; }
#qunit-tests .fail .test-actual { color: #EE5757; }
#qunit-tests .fail .test-expected { color: #008000; }
#qunit-banner.qunit-fail { background-color: #EE5757; }
/*** Aborted tests */
#qunit-tests .aborted { color: #000; background-color: orange; }
/*** Skipped tests */
#qunit-tests .skipped {
background-color: #EBECE9;
}
#qunit-tests .qunit-todo-label,
#qunit-tests .qunit-skipped-label {
background-color: #F4FF77;
display: inline-block;
font-style: normal;
color: #366097;
line-height: 1.8em;
padding: 0 0.5em;
margin: -0.4em 0.4em -0.4em 0;
}
#qunit-tests .qunit-todo-label {
background-color: #EEE;
}
/** Result */
#qunit-testresult {
color: #2B81AF;
background-color: #D2E0E6;
border-bottom: 1px solid #FFF;
}
#qunit-testresult .clearfix {
height: 0;
clear: both;
}
#qunit-testresult .module-name {
font-weight: 700;
}
#qunit-testresult-display {
padding: 0.5em 1em 0.5em 1em;
width: 85%;
float:left;
}
#qunit-testresult-controls {
padding: 0.5em 1em 0.5em 1em;
width: 10%;
float:left;
}
/** Fixture */
#qunit-fixture {
position: absolute;
top: -10000px;
left: -10000px;
width: 1000px;
height: 1000px;
}
<div id="qunit"></div>
<div id="qunit-fixture"></div>
<script src="https://code.jquery.com/qunit/qunit-2.3.3.js"></script>
upd
这是我的固定变体。它有效。
您可以收听长度属性的更改,然后手动删除元素:
set(key,value,target){
if(key==="length"){
for(var i=0;i<value-target[key];i++){
var removed=target.pop();
///change removed...
}
return;
}
//change target[key]
}