javascript 中的 delete

原文:Understanding delete

译文:javascript 中的 delete

译者:@justjavac

在这篇文章中作者从《JavaScript面向对象编程指南》一书中关于 delete 的毛病讲起,细致报告了关于 delete 操纵的完成, 范围以及在差别浏览器和插件(这里指 firebug)中的表现。

下面翻译个中的主要部份。

…书中宣称

“函数就像一个平常的变量那样——可以拷贝到差别变量,以至被删除”

并附上了下面的代码片断作为申明:

>>> var sum = function(a, b) {return a+b;};
>>> var add = sum;
>>> delete sum;
true
>>> typeof sum;
"undefined"

你能发明片断中的题目吗? 这个题目就是——删除 sum 变量的操纵不应当胜利; delete 的声明不应当返回 true 而 typeof sum 也不应当返回为 undefined。 因为,javascript 中不可以删除变量,最少不能以这个体式格局声明删除。

那末这个例子发作了什么? 是打印毛病或许打趣? 应当不是。 这个片断是 firebug 掌握台中的一个现实输出,而 Stoyan(上面所评话的作者)应当恰是用它做的疾速测试。 这好像申清晰明了 firebug 有一些差别的删除划定规矩。 恰是 firebug 误导了 Stoyan! 那末这内里究竟是怎么回事呢?

为了回复这个题目,我们须要相识 delete 运算符在 Javascript 中是怎样事情的: 哪些可以被删除,哪些不能删除以及为何。 下面我试着诠释一下这方面的细节。 我们将经由历程视察 firebug 的“新鲜”的表现而认识到它现实上完全不“新鲜”; 我们将深切相识那些,当我们声明变量、函数,赋值属性和删除它们时的,隐藏在背地的细节; 我们将看一下浏览器对此的完成和一些著名的 bug; 我们还会议论到 ECMAScript 版本 5 中的严厉形式(strict mode)以及它怎样转变 delete 运算符的行动。

我鄙人面交替运用的 Javascript 和 ECMPScript 平常都指 ECMAScript(除非当明白谈到 Mozilla 的 JavaScript™ 完成时)。

意料之中的,收集上如今关于 delete 的诠释非常少(笔者按:这篇文章写于 2010 年 1 月)。 MDC(MDN]) 的资本大概是这个中最细致的了,但不幸的是它遗漏了一些风趣的细节,这些细节中就包含了上述 firebug 的新鲜表现。 MSDN 文档险些没什么用途。

一、理论 | Theory

那末,为何我们能删除一个对象的属性:

var x = { a: 1 };
delete x.a; // true
x.a; // undefined

但却不能删除一个变量:

var x = 1;
delete x; // false;
x; // 1

也不能删除一个函数:

function x() {};
delete x; // false;
typeof x; // "function"

注重:delete 只要当一个属性没法被删除时才返回 false。

为了明白这一点,我们须要起首把握一些观点: 变量实例化(variable instantiation)和属性的内部属性(property attributes) (译者按:关于 property 和 attributes 的辨别见参考文章,依据下面涉及到的内容,拟译成内部属性) ——这些很少在 javascript 书中被提到。 鄙人面几段中我将试着简短地回忆这些内容,要明白它们并不难。 假如你并不关注它们表现背地的缘由,可以跳过这一章。

1.1、代码的范例 | Type of code

ECMAScript 中有三类可实行代码:

  1. 全局代码 Global code
  2. 函数代码 Function code
  3. Eval code

这几类的寄义大抵就像它们定名的那样,但照样疾速地回忆一下:

  1. 当一个源文件被看作是一个递次,它在全局作用域(scope)内实行,而这就被以为是一段全局代码 Global code。 在浏览器环境下,SCRIPT 元素的内容一般都被剖析为一个递次,因而作为全局代码来实行。

  2. 固然,任安在一段函数中直接实行的代码就被以为是一段函数代码 Function code, 在浏览器环境下,事宜属性的内容(e.g. <a onclick="...")一般都作为函数代码来剖析和实行。

  3. 末了,放入内建函数 eval 中的代码就作为 Eval code 来剖析。我们将很快看到为何这一范例是特别的。

1.2、代码实行的高低文 | Execution Context

当 ECMAScript 代码实行时,它老是发作在一个肯定的实行高低文(context)中。 实行作用域是一个笼统实体,它有助于明白作用域和变量实例化的事情道理。 上面三类可实行代码都有各自的实行高低文。 当函数代码实行时,我们说掌握端进入了函数代码的实行高低文; 当全局代码实行时,我们说掌握端进入了全局代码的实行高低文,以此类推。

正如你所见,实行高低文在逻辑上是一个栈(stack)。 起首可以有一段全局代码,它具有属于本身的实行高低文; 在这段代码中可以挪用一个函数,这个函数一样具有属于本身的实行高低文; 这个函数可以挪用另一个函数,等等。 纵然当函数递归挪用本身时,在每一步挪用中依然进入了差别的实行高低文。

1.3、活化对象和变量对象 | Activation object / Variable object

每个实行高低文都有一个与之相干联的变量对象(Variable object)。 和它相似的,变量对象也是一个笼统实体,一种用来形貌变量实例化的机制。 而风趣的是,在一段源代码中声明的变量和函数事实上被作为变量对象(Variable object)的属性(properties)而添加到变量对象中

当掌握进入了全局代码的实行高低文时,一个全局对象被用作变量对象。 这恰恰是为何全局声明的变量和函数变成一个全局对象的属性的缘由:

var GLOBAL_OBJECT = this;
var foo = 1;
GLOBAL_OBJECT.foo; // 1
function bar() {};
typeof GLOBAL_OBJECT.bar; // "function"
GLOBAL_OBJECT.bar === bar; // true

Ok, 所以全局变量成了全局函数的属性,那末局部变量——那些在函数代码(Function code)中声明的变量呢? 事实上那很简朴:他们也成了变量对象的属性。 唯一的辨别是,在函数代码中,变量对象不是一个全局对象, 而是一个我们称之为活化对象(Activation object)。 每次进入函数代码的实行高低文时都邑建立一个活化对象。

并不是只要在函数代码中声明的变量和函数才成为活化对象的属性: 函数的每个实参(arguments,以各自相对应的形参的名字为属性名), 以及一个特别的Arguments对象(以arguments为属性名)一样成为了活化对象的属性。 须要注重的是,活化对象作为一个内部的机制事实上不能被递次代码所接见。

(function(foo) {
    var bar = 2;
    function baz() {};
    /*
        在笼统的历程当中,
        特别的'arguments'对象变成了地点函数的活化对象的属性:
        ACTIVATION_OBJECT.arguments = arguments;
        ...参数'foo‘也是一样:
        ACTIVATION_OBJECT.foo; // 1
        ...变量'bar'也是一样:
        ACTIVATION_OBJECT.bar; // 2
        ...函数'baz'也是一样:
        typeof ACTIVATION_OBJECT.baz; // "function"
      */
}) (1);

末了,Eval code 中声明的变量成为了高低文的变量对象(context’s Variable object)的属性。 Eval code 简朴地运用在它挪用中的实行高低文的变量对象。

var GLOBAL_OBJECT = this;
eval('var foo = 1');
GLOBAL_OBJECT.foo; // 1;

(function() {
    eval('var bar = 2');

    /*
        在笼统历程当中
        ACTIVATION_OBJECT.bar; // 2
    */
}) ();

1.4、属性的内部属性 | Property attributes

就要靠近主题了。 如今我们明白了变量发作了什么(它们成了属性),剩下的须要明白的观点就是属性的内部属性(property attributes)。 每个属性具有零至多个如内部属性——*ReadOnly,DontEnum,DontDelete和Internal**。 你可以把它们设想为标签——一个属性可以具有也可以没有某个特别的内部属性。 在本日的议论中,我们所感兴趣的是 DontDelete。

当声明变量和函数时,它们成为了变量对象(Variable object)——要么是活化对象(在函数代码中), 要么是全局对象(在全局代码中)——的属性,这些属性陪伴天生了内部属性 DontDelete。 然则,任何显式/隐式赋值的属性不天生 DontDelete。 而这就是本质上为何我们能删除一些属性而不能删除其他的缘由。

var GLOBAL_OBJECT = this;

/* 'foo'是全局对象的一个属性,
    它经由历程变量声明而天生,因而具有内部属性DontDelete
    这就是为何它不能被删除*/
var foo = 1;
delete foo; // false
typeof foo; // "number"

/* 'bar'是全局对象的一个属性,
    它经由历程变量声明而天生,因而具有DontDelete子
    这就是为何它一样不能被删除*/
function bar() {};
delete bar; // false
typeof bar; // "function"

/* 'baz'也是全局对象的一个属性,
    然则,它经由历程属性赋值而天生,因而没有DontDelete
    这就是为何它可以被删除*/
GLOBAL_OBJECT.baz = "baz";
delete GLOBAL_OBJECT.baz; // true
typeof GLOBAL_OBJECT.baz; // "undefined"

1.5、内建和DontDelete | Build-ins and DontDelete

所以这就是一切这一切发作的缘由:属性的一个特别的内部属性掌握着该属性是不是可以被删除。 注重:内建对象的一些属性具有内部属性 DontDelete,因而不能被删除; 特别的 arguments 变量(如我们所知的,活化对象的属性)具有 DontDelete; 任何函数实例的 length (返回形参长度)属性也具有 DontDelete:

(function() {
    //不能删除'arguments',因为有DontDelete
    delete arguments; // false;
    typeof arguments; // "object"

    //也不能删除函数的length,因为有DontDelete
    function f() {};
    delete f.length; // false;
    typeof f.length; // "number"
}) ();

与函数 arguments 相干联的属性也具有 DontDelete,一样不能被删除

(function(foo,bar) {
    delete foo; // false
    foo; // 1

    delete bar; // false
    bar; // "bah"
}) (1,"bah");

1.6、未声明的变量赋值 | Undeclared assignments

你可以记得,未声明的变量赋值会成为全局对象的属性,除非这一属性在作用域链内的其他地方被找到。 而如今我们相识了属性赋值和变量声明的辨别——后者天生 DontDelete 而前者不天生——这也就是为何未声明的变量赋值可以被删除的缘由了。

var GLOBAL_OBJECT = this;

/* 经由历程变量声明天生全局对象的属性,具有DontDelete */
var foo = 1;

/* 经由历程未声明的变量赋值天生全局对象的属性,没有DontDelete */
bar = 2;

delete foo; // false
delete bar; // true

注重:内部属性是在属性天生时肯定的,以后的赋值历程不会转变已有的属性的内部属性。 明白这一辨别是主要的。

/* 'foo'建立的同时天生DontDelete */
function foo() {};

/* 以后的赋值历程不转变已有属性的内部属性,DontDelete依然存在 */
foo = 1;
delete foo; // false;
typeof foo; // "number"

/* 但赋值一个不存在的属性时,建立了一个没有内部属性的属性,因而没有DontDelete */
this.bar = 1;
delete bar; // true;
typeof bar; // "undefined"

二、Firebug 的杂沓 | Firebug confusion

那末, firebug 中发作了什么? 为安在掌握台中声明的变量可以被删除,而不是想我们之前议论的那样? 我之前说过,Eval code 在它处置惩罚变量声明时有一个特别的行动: 在 Eval code 中声明的变量事实上天生一个没有 DontDelete 的属性

eval('var foo = 1;');
foo; // 1
delete foo; // true
typeof foo; // "undefined"

在函数代码中也是一样:

(function() {
    eval('var foo = 1;');
    foo; // 1
    delete foo; // true
    typeof foo; // "undefined"
}) ();

而这就是 Firebug 中非常行动的缘由了。 一切在掌握台中的调试文本好像是以 Eval code 来编译和实行的,而不是在全局或函数代码中实行。 明显,个中的变量声明终究都天生了不带 DontDelete 的属性,所以可以被删除。 所以要警惕平常的全局代码和 Firebug 掌握台中代码的辨别。

2.1、经由历程eval删除变量 | Delete variables via eval

这个风趣的 eval 行动,连系 ECMAScript 的另一个方面可以在手艺上许可我们删除那些底本不能删除的属性。 这个方面是关于函数声明——在雷同的实行高低文中它们能掩盖同名的变量:

function x() { };
var x;
typeof x; // “function”

那末为何函数声明具有优先权而能掩盖同名变量(或许换句话说,变量对象(Variable object)的雷同属性)呢? 这是因为函数声明的实例化历程在变量声明以后,因而可以掩盖它们。

(译者按:函数声明只能掩盖声明而未赋值的同名变量,假如在声明时赋值了值(e.g. var x = 1)则赋值值的历程在函数初始化以后,函数声明反而被变量赋值所掩盖,以下:)

var x = 1;
function x() { };
typeof x; // "number"

函数声明不止替代了属性的值,同时也替代了它的内部属性。 假如我们经由历程 eval 来声明函数,这一函数也将用它本身的内部属性来替代之前的。 而因为在 eval 中声明的变量天生的属性没有 DontDelete, 实例化这个函数将在“理论上”移除原属性已有的 DontDelete 内部属性, 而使得这一属性可以删除(固然,同时也将值指向了新天生的函数)。

var x = 1;
/*不能删除,‘x’具有DontDelete*/
delete x; // false
typeof x; // "number"

eval('function x() { }');
/* 属性'x'如今指向函数,而且应当没有DontDelete */
typeof x; // "function"
delete x; // 应当是‘true’;
typeof x; // 应当是"undefined"

不幸的是,这类诳骗手艺在我尝试的各个浏览器中都没有胜利。 这里我可以错过了什么,或许这个行动太隐藏而以至于各个浏览器没有注重到它。

(译者按:这里的题目可以在于:函数声明和变量声明之间的掩盖只是值指向的转变, 而内部属性 DontDelete 则在最初声明处肯定而不再转变,而 eval 中声明的变量和函数,也只是在其外部高低文中未声明过的那部份才被删除。 关于实行递次,因为 eval 作为函数,它的挪用永久在其外部高低文中其他变量和函数声明以后, 因而相干的内部属性也已肯定,掩盖的只是值的指向。以下:)

/*  第一个 alert 返回 “undefined”,因为赋值历程在声明历程和eval实行历程以后;
    第二个alert返回 “false”, 因为只管x声明的位置在eval以后,
    然则eval的实行却在变量声明以后,因而已没法删除 */
eval(' alert( x ); alert(delete x) ');
var x = 1;

三、浏览器的恪守状况 | Browsers compliance

相识事物的事情道理是主要的,但现实的完成状况更主要。 浏览器在建立和删除变量/属性时都恪守这些范例吗? 关于大部份来讲,是的。

我写了一个简朴的测试单位来搜检全局代码、函数代码和Eval代码的恪守状况。 测试单位同时检测了 delete 操纵的返回值和属性是不是像预期那样被删除。 delete 的返回值并不像它的现实效果那样主要,delete 操纵返回 true 或 false 并不主要, 主要的是具有/没有 DontDelete 的属性是不是被删除。

当代浏览器总的来讲照样恪守删除划定规矩的,以下浏览器悉数经由历程测试: Opera 7.54+, Firefox 1.0+, Safari 3.1.2+, Chrome 4+。

Safari 2.x 和 3.0.4 在删除函数 arguments 时存在题目,好像这些属性在建立时不带 DontDelete,因而可以被删除。 Safari 2.x 另有其他题目——删除无援用时(比方delete 1)抛出毛病(译者按:IE 一样有); 函数声明天生了可删除的属性(新鲜的是变量声明则一般); eval 中的变量声明变成不可删除(而 eval 中的函数声明则一般)。

与 Safari 相似,Konqueror(3.5,而非4.3)在 delete 无援用和删除 arguments 是也存在一样题目。

3.1、Gecko DontDelete bug

Gecko 1.8.x 浏览器—— Firefox 2.x, Camino 1.x, Seamonkey 1.x, etc. ——存在一个风趣的 bug:显式赋值值给一个属机能移除它的 DontDelete,纵然该属性经由历程变量或函数声明而天生。

function foo() { };
delete foo; // false;
typeof foo; // "function"

this.foo = 1;
delete foo; // true
typeof foo; // "undefined"

使人惊奇的是,IE5.5-8 也经由历程了绝大部份测试,除了删除非援用抛出毛病(e.g. delete 1,就像旧的 Safari)。 然则,虽然不能立时发明,事实上 IE 存在更严峻的 bug,这些 bug 是关于全局对象。

四、IE bugs

在 IE 中(最少在 IE6-8 中),下面的表达式抛出非常(在全局代码中):

this.x = 1;
delete x; // TypeError: Object doesn't support this action

而下面则是另一个:

var x =1;
delete this.x; // TypeError: Cannot delete 'this.x'
// 译者按:在IE8下抛出此非常,在IE6,7下抛出的是和上面一样的非常

这好像申明,在 IE 中在全局代码中的变量声明并没有天生全局对象的同名属性。 经由历程赋值建立的属性(this.x = 1)然后经由历程 delete x 删除时抛出非常; 经由历程变量声明(var x = 1)建立的属性然后经由历程 delete this.x 删除时抛出另一个(译者按:在 IE6,7 下毛病信息与上面的雷同)。

但不只是如许,事实上经由历程显式赋值建立的属性在删除时老是抛出非常。 这不只是一个毛病,而是建立的属性看上去具有了 DontDelete 内部属性,而按划定规矩应当是没有的:

this.x = 1;
delete this.x; // TypeError: Object doesn't support this action
delete x; // TypeError: Object doesn't support this action

另一方面,未声明的变量赋值(那些一样天生全局对象的属性)又确着实IE下可以一般删除:

x = 1;
delete x; // true

但假如你试图经由历程 this 关键字来举行删除(delete this.x),那末上面的非常又将抛出:

x = 1;
delete this.x; //TypeError: Cannot delete 'this.x'

假如归结一下,我们将发明在全局代码中‘delete this.x’永久不会胜利。 当经由历程显式赋值来天生属性(this.x = 1)时抛出一个非常; 当经由历程声明/非声明变量的体式格局(var x = 1 or x = 1)天生属性时抛出另一个非常。 而另一方面,delete x 只要在显现赋值天生属性(this.x = 1)时才抛出非常。

9 月我议论了这个题目,个中 Garrett Smith 以为在 IE 中全局变量对象(Global variable object)完成为一个 JScript 对象,而全局对象则由宿主对象完成。

我们能经由历程几个测试来在某种程度上确认这一理论。 注重,this 和 window 好像援用同一个对象(假如 ‘===’运算符可以信托的话), 而变量对象 Variable object (函数声明的基本)则与 this 援用的差别。

function getBase() { return this; };

getBase() === this.getBase(); // false
this.getBase() === this.getBase(); // true
window.getBase() === this.getBase(); // true
window.getBase() === getBase(); // false

五、误会 | Misconceptions

我们不能低估明白事物事情道理的主要性。 我看过收集上一些关于 delete 操纵的误会。 比方,Stackoverflow 上的一个答案(而且品级还很高),内里诠释说“delete is supposed to be no-op when target isn’t an object property”。 如今我们相识了 delete 操纵的中心,也就清晰了这个答案是不正确的。 delete 不辨别变量和属性(事实上在 delete 操纵中这些都是援用),而只体贴 DontDelete(以及属性是不是已存在)。

六、’delete’和宿主对象 | ’delete‘ and host object

一个 delete 的算法大抵像如许:

1. 假如运算元(operand)不是援用,返回 true
2. 假如对象没有同名的**直接属性**,返回 true (如我们所知,对象可所以全局对象也可所以活化对象)
3. 假如属性已存在但有 DontDelete,返回 false
4. 不然,删除移除属性并返回 true

然则,关于宿主对象(host object)的 delete 操纵的行动却多是不可预感的。 而事实上这并没有错:宿主对象(经由历程肯定划定规矩)许可完成任何操纵, 比方读(内部[[Get]]要领)、写(内部[[Write]]要领)、删除(内部[[Delete]]要领),等等。 这类许可自定义[[Delete]]行动致使了宿主对象的杂沓。

我们已看到了在IE中的一些题目:当删除某些对象(那些完成为了宿主对象)属性时抛出非常。 一些版本的 firefox 当试图删除 window.location 时抛出非常(译者按:IE 一样抛出)。 一样,在一些宿主对象中你也不能置信 delete 的返回值, 例以下面发作在 firefox 中的(译者按:chrome 中一样效果;IE 中抛出非常;opera 和 safari 许可删除,而且删除后没法挪用,权且算’一般‘,只管,从下面的议论来看好像倒是不一般的,它们事实上删除了不能删除的属性,而前面的浏览器没有):

/* 'alert'是’window‘的一个直接属性(假如我们可以置信'hasOwnProperty') */
window.hasOwnProperty('alert'); // true

delete window.alert; // true
typeof window.alert; // "function"

delete window.alert 返回 true,只管这个属性没有任何条件可以发生这个效果(根据上面的算法): 它剖析为一个援用,因而不能在第一步返回 true; 它是 window 对象的直接属性,因而不能在第二步返回 true; 唯一能返回 true 的是当算法到达末了一步同时确切删除这个属性,而事实上它并没有被删除。 (译者按:不,在 opera 和 safari 中确切被删除了…)。

所以这个故事通知我们永久不要置信宿主对象

七、ES5 严厉形式 | ES5 strict mode

那末 ECMAScript 第 5 版中的严厉形式将带来什么? 如今引见了个中的一些限定。 当删除操纵指向一个变量/函数参数/函数声明的直接援用时抛出 SyntaxError。 另外,假如属性具有内部属性[[Configurable]] == false,将抛出 TypeError:

(function(foo) {
    "use strict"; //在函数中开启严厉形式

    var bar;
    function baz;
    delete foo; // SyntaxError,当删除函数参数时
    delete bar; // SyntaxError,当删除变量时
    delete baz; // SyntaxError,当删除由函数声明建立的变量时

    /* function实例的length具有[[Configurable]] : false */
    delete (function() {}).length; // TypeError
}) ();

而且,在严厉形式下,删除未声明的变量(换句话说,未剖析的援用),一样抛出 SyntaxError; 与它相似的,雷同形式下未声明的赋值也将抛出非常(ReferenceError)

"use strict";
delete i_dont_exist; // SyntaxError

i_dont_exist_either = 1; // ReferenceError

看了之前给出的变量、函数声明和参数的例子,置信如今你也明白了,一切这些限定都是有其意义的。 严厉形式采取了更主动的和形貌性的步伐,而不只是疏忽这些题目。

八、总结 | Summary

因为这篇文章已很长了,因而我就不再议论另一些内容(e.g.经由历程 delete 删除数组项及其影响)。 你可以翻阅 MDC/MDN 上的文章或浏览范例然后本身测试。

下面是关于 Javascript 中 delete 怎样事情的一个简朴的总结:

  • 变量和函数声明都是活化(Activation)全局(Global)对象的属性。
  • 属性具有内部属性,个中一个—— DontDelete 担任肯定一个属性是不是可以被删除。
  • 全局代码或函数代码中的变量、函数声明都天生具有 DontDelete 的属性。
  • 函数参数一样是活化对象的属性,也具有 DontDelete。
  • Eval代码中的变量和函数声明都天生没有 DontDelete 的属性。
  • 新的未声明的属性在天生时带空的内部属性,因而也没有 DontDelete。
  • 宿主对象许可以任何它们希冀的体式格局来相应删除历程。
    原文作者:justjavac
    原文地址: https://segmentfault.com/a/1190000000305211
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞