firefox下delete有bug?

Doyle

看书时,发现firefox 35.0下的控制台或者firebug里,delete表现跟理论上不太一样,chrome表现则很正常,难道firefox有问题?

搜了篇文章,看完才知道问题出在哪里,分享一下。Understanding delete

Theory

那么, 为什么我们能够删除对象的属性:

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

却不能删除这样声明的对象:

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

或者函数呢:

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

注意: 当一个属性无法被删除时,delete操作符只会返回false

要理解这个, 我们首先需要掌握这些有关变量实例和属性特性的概念——这些概念很不幸地, 很少在JavaScript书中被提及. 我将试着在接下来的几个段落中简单地复习一下这些概念. 这些概念是很难理解的!如果你不在乎"为什么这些东西会以这种方式工作"的话,尽情跳过这一章节好了.
代码的类型:

在ECMAScript中, 有3种不同类型的可执行代码: 全局代码(Global code), 函数代码(Function code)和 Eval代码(Eval code). 这些类型从名称上来说或多或少是有自解释性的, 这里有一个简短的概述:

当一段源代码被看成程序(Program)时, 它将会在全局环境下被执行, 并且被认为是全局代码(Global code). 在一个浏览器环境中, 脚本元素的内容通常被解释为程序, 因此被作为全局代码来执行.

任何直接在一个函数中执行的代码显然被认为是函数代码(Function code). 在浏览器中, 事件属性的内容(如 <p onclick="....">)通常被解释成函数代码.

最后, 被应用到内置函数eval的代码文本被解释成Eval代码(Eval code). 很快我们会发现为什么这种类型是特殊的.

执行上下文(Execution context):

当ECMAScript代码执行时, 它通常会发生在特定的执行上下文中.执行上下文是一个有些抽象的实体概念, 它能帮助理解范围(Scope)和变量实例(Variable instantiation)是如何工作的. 对三种可执行代码的每一种, 都有一个执行上下文相对应. 当一个函数被执行的时候, 我们说"程序控制进入了函数代码的执行上下文"; 当一段全局代码被执行时, 程序控制进入了全局代码的执行上下文, 等等.

正如你所见, 执行上下文可以在逻辑上构成一个堆栈. 首先, 可能有一段全局代码和其自己的执行上下文, 然后这段代码可能会调用一个函数, 并带着它(函数)的执行上下文. 这段函数可以调用另外一个函数, 等等等等. 即使函数是递归调用的, 每次调用时被也会进入一个新的执行上下文.
活动对象(Activation object) / 变量对象(Variable Object):

每一个执行上下文都有一个跟其所关联的所谓变量对象(Variable Object). 类似于执行上下文, 变量对象是一个抽象实体, 一种用来描述变量实例的机制. 有趣之处在于, 在源代码中声明的变量和函数通常会被当做属性(properties)增加到这个变量对象上.

当程序控制进入全局代码的执行上下文时, 一个全局对象(Global object)被用来作为一个变量对象. 这正是为什么声明为全局的函数变量会变成全局对象属性的原因.

/* remember that `this` refers to global object when in global scope */
var GLOBAL_OBJECT = this;

var foo = 1;
GLOBAL_OBJECT.foo; // 1
foo === GLOBAL_OBJECT.foo; // true

function bar(){}
typeof GLOBAL_OBJECT.bar; // "function"
GLOBAL_OBJECT.bar === bar; // true

好, 所以全局变量会变成全局对象的属性, 但是局部变量(那些在函数代码中定义的变量)会发生什么呢? 其实它们的行为也非常类似: 它们会变成变量对象(Variable object)的属性. 唯一的不同在于, 当在函数代码中时, 一个变量对象并不是全局对象, 而是所谓的活动对象(Activation object). 活动对象在会每次进入函数代码的执行上下文时被创建.

并不是只有在函数代码中声明的变量和函数会变成活动对象的属性; 这也会在每个函数参数(对应相应的形式参数的名称)和一个特殊的Arguments对象(以arguments为名称)上发生. 注意, 活动对象是一个内部描述机制, 在程序代码中并不能被访问.

(function(foo){

  var bar = 2;
  function baz(){}

  /*
  In abstract terms,

  Special `arguments` object becomes a property of containing function's Activation object:
    ACTIVATION_OBJECT.arguments; // Arguments object

  ...as well as argument `foo`:
    ACTIVATION_OBJECT.foo; // 1

  ...as well as variable `bar`:
    ACTIVATION_OBJECT.bar; // 2

  ...as well as function declared locally:
    typeof ACTIVATION_OBJECT.baz; // "function"
  */

})(1);

最后, 在Eval代码中声明的变量会成为调用者上下文(calling context)的变量对象的属性. Eval代码只是简单地使用调用它的代码的执行上下文的变量对象.

var GLOBAL_OBJECT = this;

/* `foo` is created as a property of calling context Variable object,
  which in this case is a Global object */

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

(function(){

  /* `bar` is created as a property of calling context Variable object,
  which in this case is an Activation object of containing function */

  eval('var bar = 1;');

  /*
  In abstract terms,
  ACTIVATION_OBJECT.bar; // 1
  */

})();

属性的特性(property attributes)

我们几乎是已经在这了. 既然我们已经很清楚在变量上发生了什么(它们变成了属性), 唯一剩下的需要理解的概念就是属性的特性(property attributes)了. 每一个属性可以拥有0个或多个特性, 它们从以下集合中选取: ReadOnly, DontEnum, DontDelete和 Internal. 你可以把它们认为是flags —— 一种特性可以在属性中存在, 也可以不存在. 对于我们今天的讨论来说, 我们只对DontDelete感兴趣.

当被声明的变量和函数成为变量对象(或者函数代码的活动对象, 或全局代码的全局对象)的属性时, 这些属性在创建时就带上了DontDelete的特性. 然而, 任何显式(或隐式)的属性赋值所建立的属性将不会被带上DontDelete特性. 这就是为什么我们能够删除一些属性, 但删除不了其它的.

var GLOBAL_OBJECT = this;

/*  `foo` is a property of a Global object.
    It is created via variable declaration and so has DontDelete attribute.
    This is why it can not be deleted. */

var foo = 1;
delete foo; // false
typeof foo; // "number"

/* `bar` is a property of a Global object.
   It is created via function declaration and so has DontDelete attribute.
   This is why it can not be deleted either. */

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

/*  `baz` is also a property of a Global object.
    However, it is created via property assignment and so has no DontDelete attribute.
    This is why it can be deleted. */

GLOBAL_OBJECT.baz = 'blah';
delete GLOBAL_OBJECT.baz; // true
typeof GLOBAL_OBJECT.baz; // "undefined"

内置对象和DontDelete

所以, 这就是有关它(DontDelete)的所有: 属性的一个特殊特性, 用来控制这个属性是否能够被删除. 注意, 有些内置对象的属性是指定含有DontDelete的, 所以无法被删除. 如特殊的arguments变量(或者, 正如我们现在所知道的, 一个活动对象的属性)拥有DontDelete. 函数实例的length属性也具有DontDelete属性.

(function(){

  /* can't delete `arguments`, since it has DontDelete */

  delete arguments; // false
  typeof arguments; // "object"

  /* can't delete function's `length`; it also has DontDelete */

  function f(){}
  delete f.length; // false
  typeof f.length; // "number"

})();

函数参数所对应的属性也是从建立开始就拥有DontDelete特性的, 所以我们也无法删除它.

(function(foo, bar){

  delete foo; // false
  foo; // 1

  delete bar; // false
  bar; // 'blah'

})(1, 'blah');

未声明的赋值:

你可能还记着, 未声明的赋值会在全局对象上建立一个属性, 除非这个属性已经在这个作用域链中全局对象之前的其它地方被找到. 并且, 现在我们知道属性赋值和变量声明的不同之处——后者会设置DontDelete属性, 但前者不会. 我们必须清楚, 为什么未声明的赋值会建立一个可删除的属性.

var GLOBAL_OBJECT = this;

/* create global property via variable declaration; property has DontDelete */
var foo = 1;

/* create global property via undeclared assignment; property has no DontDelete */
bar = 2;

delete foo; // false
typeof foo; // "number"

delete bar; // true
typeof bar; // "undefined"

请注意: 特性是在属性被创建时被决定的, 之后的赋值不会修改已存在属性的特性. 理解这一点区别非常重要.

/* `foo` is created as a property with DontDelete */
function foo(){}

/* Later assignments do not modify attributes. DontDelete is still there! */
foo = 1;
delete foo; // false
typeof foo; // "number"

/* But assigning to a property that doesn't exist,
   creates that property with empty attributes (and so without DontDelete) */

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

Firebug的困惑:

在Firebug中发生了什么? 为什么在console中声明的变量可以被删除, 这不是违背了我们之前所学到的知识么? 嗯, 就像我之前所说的那样, Eval代码在面对变量声明时会有特殊的表现. 在Eval中声明的变量实际上是作为不带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反常行为的依据. 在console中的所有文本都会被当做Eval代码来解析和执行, 而不是全局或函数代码. 显然, 这里声明的所有变量最后都会成为不带DontDelete特性的属性, 所以它们都能被轻松删除. 我们需要了解这个在全局代码和Firebug控制台之间的差异.
通过Eval来删除变量:

这个有趣的eval行为, 再加上ECMAScript的另一个方面, 可以在技术上允许我们删除"non-deletable"的属性. 有关函数声明的一点是, 它们能够覆盖相同执行上下文中同名的变量.

function x(){ }
var x;
typeof x; // "function"

注意函数声明是如何获得优先权并且覆盖同名变量(或者, 换句话说, 在变量对象中的相同属性)的. 这是因为函数声明是在变量声明之后被实例化的, 并且被允许覆盖它们(变量声明). 函数声明不仅会替换掉一个属性的值, 它还会替换掉那个属性的特性. 如果我们通过eval来声明一个函数, 那个函数就应该会用它自己的特性来替换掉原有的(被替换的)属性的特性. 并且, 由于通过eval声明的变量会创建不带DontDelete特性的属性, 实例化这个新函数将会实际上从属性中删除已存在的DontDelete特性, 从而使得一个属性能够被删除(并且, 显然会将其值指向新创建的函数).

var x = 1;

/* Can't delete, `x` has DontDelete */

delete x; // false
typeof x; // "number"

eval('function x(){}');

/* `x` property now references function, and should have no DontDelete */

typeof x; // "function"
delete x; // should be `true`
typeof x; // should be "undefined"

不幸的是, 这种"欺骗"在目前的任何实现中都不起作用. 也许我在这漏掉了什么, 或者是这种行为只是太晦涩了以至于实现者都没有注意到它.

阅读 2.4k

Doyle
linuxer vimer

前端, angular, vue

844 声望
16 粉丝
0 条评论

前端, angular, vue

844 声望
16 粉丝
文章目录
宣传栏