谈谈Javascript中的delete操作符

瘢痂派生
• 阅读 3906

你觉得下列代码中,哪些delete操作能成功?人肉判断一下,不要放进浏览器里执行。

// #1
a = "hello world";
delete a;

// #2
var b = "hello world";
delete b;

// #3
x = {};
Object.defineProperties(x, {
  "p1": {
    value: 'hello',
    configurable: true
  },
  "p2": {
    value: "world",
    configurable: false
  }
});

console.log(delete x.p1);
console.log(delete x.p2);

// #4
function f() {
    console.log("hello f");
}
delete f;

// #5
with({t:'try'}) {
    console.log(t);
    delete t;
    console.log(t); // print what?
}

// #6
try {
  throw "hello";
} catch (e) {
  console.log(e);
  delete e;
  console.log(e);// print what?
}

// #7
function test(a, b, c) {
  delete a;
  console.log(a);

  delete arguments;
  console.log(arguments);
}
test(1,2,3);

// #8
eval('var v = "ttt"');
delete v;
console.log(v);

// #9
y = {a:'bye'};
function f() {
    return y.a;
}
delete f();

如果上述代码都在strict模式下执行呢,又有哪些操作是成功的呢?如果不清楚的话,往下看。

PS:本文所表述的内容均由规范演译而来,而非通过实验进行推理,代码示例仅用来证明文中阐述内容的正确性,如有疑惑欢迎留言讨论。

delete究竟在做啥?

参见EMCA 262-5 第11.4.1小节:

The production UnaryExpression : delete UnaryExpression is evaluated as follows:

  1. Let ref be the result of evaluating UnaryExpression.
  2. If Type(ref) is not Reference, return true.
  3. If IsUnresolvableReference(ref) then,

    • If IsStrictReference(ref) is true, throw a SyntaxError exception.
    • Else, return true.
  4. If IsPropertyReference(ref) is true, then

    • Return the result of calling the [[Delete]] internal method on
      ToObject(GetBase(ref)) providing GetReferencedName(ref) and
      IsStrictReference(ref) as the arguments.
  5. Else, ref is a Reference to an Environment Record binding, so

    • If IsStrictReference(ref) is true, throw a SyntaxError exception.
    • Let bindings be GetBase(ref).
    • Return the result of calling the DeleteBinding concrete method of bindings, providing GetReferencedName(ref) as the argument.

要读懂上面这一堆术语诸如Type(ref), Reference, GetBase(ref),DeleteBinding似乎还是需要花费点力气的,没关系,慢慢来。

什么是Reference?

在ECMA规范中,Reference是一个抽象的概念,由三个部分组成,可以理解为:

{
  base: undefined | Object | Boolean | String | Number | environment record, //这个表示Reference的基
  refName: string,       //在ECMA中,常以Reference Name表示
  isStrict: boolean       //是否是一个strict的reference,如果在strict mode下执行的话,对所有的Reference这个值都是true。而在普通mode下,则需要分情况讨论了
}

什么时候会创建一个Reference呢?有两种情况:
- 解析变量(GetIdentifierReference
- 访问对象属性(Property Accessors

对于如下代码(在全局作用域下):

var jake= 'string';
delete jake;

在delete表达式中,对jake变量的解析便可得到这样的一个Reference:

{
  base: GLOBAL, //base是全局对象,在浏览器环境下就是window对象
  refName: 'jake', //Reference Name就是字符串jake
  isStrict: false
}

而对于如下代码:

var man = {
  name: 'delta',
  age: 24
};

console.log(man.age);

console.log(man.age)语句中,对man.age的解析便可得到如下的Reference

{
 base: man,
 refName: 'age',
 isStrict: false
}

So Easy,那什么情况下会有IsUnresolvableReference(ref)true的情况呢?当且仅当一个Reference的Base值为undefined时,才会有IsUnresolvableReference(ref)为true。

delete abcd; 

在解析abcd变量时,会查找当前环境记录(Environment Record)是否有一个叫abcd这样的绑定(Binding),如果有,则当前环境记录则为Base值,否则再从当前词法环境(Lexical Environment)的父环境(parent Lexical Environment)的环境记录中查找,直到undefined。故对于解析abcd而言,得到的*Reference`为:

{
    base: undefined,
    refName: 'abcd',
    isStrict: false
}

上述所有Reference的isStrict属性在strict mode下均为true

回到delete的定义,可以看到:

If Type(ref) is not Reference, return true.
If IsUnresolvableReference(ref) then,

  • If IsStrictReference(ref) is true, throw a SyntaxError exception.
  • Else, return true.

这就很好理解了,可以得出如下结论(在普通mode下):

delete abcdefg; //不会报错,而且还返回true
delete "abcde"; //"abcde"是一个值,不是Reference,返回true

Property Reference

什么时候会有IsPropertyReference(ref)为true呢?这很好理解,仅当一个Reference的Base值为一个Object或一个JS原生类型如string, boolean, number时,它才会为true.

回到delete的定义:

If IsPropertyReference(ref) is true, then

+ Return the result of calling the [[Delete]] internal method on ToObject(GetBase(ref)) providing GetReferencedName(ref) and IsStrictReference(ref) as the arguments.

因此有:

a = {};
delete a.p; //结果是true
delete "hello".p //结果也是true

y = {a:'bye'};
function f() {
    return y.a;
}
delete f(); //结果是true,因为f()的结果是一个值,不是Reference

重点在于[[Delete]]这个内部方法,如果一个属性的Configurable为false,那么:

  • 在普通mode下,属性不会被删除,返回true
  • 在strict mode下,抛出Type Error异常

如果一个属性的Configurable为true的话,那么delete操作就能成功去除相应的属性。

继续

回到delete的定义,最后一段:

Else, ref is a Reference to an Environment Record binding, so

  • If IsStrictReference(ref) is true, throw a SyntaxError exception.

    • Let bindings be GetBase(ref).
    • Return the result of calling the DeleteBinding concrete method of
      bindings, providing GetReferencedName(ref) as the argument.

如果一个reference是一个Environment Record binding的话,但Environment Record是什么?而Environment Record binding又是什么?

这要从执行上下文(Execution Context)说起。

Execution Context

对于一个特定的执行上下文,它有如下构成:

{
  LexicalEnvironment: {},
  VariableEnvironment: {},
  ThisBinding: {} 
}

ThisBinding很好理解,就是一个特定执行上下文的this值。而LexicalEnvironment和VariableEnvironment又是什么?这两个都是Lexical Environment,(摔,术语越来越多了)。

一个Lexical Environment由两部分组成:

{
  EnvironmentRecord: {}, //一个Environment Record
  OuterLexicalEnvironment: outer //指向它外层的词法环境
}

那环境记录(Environment Record)是什么呢?

Environment Record

Environment Record分为两种,一种是Object Environment Record,另一种是Declarative Environment Record。 从概念上来讲,这两者区别不大,它们都实现了相同的接口。唯一区别就是Object Environment Record是一个用户可访问到的Javascript Object。而Declarative Environment Record无法在JS代码中访问到。一个Environment Record上会有一系列的绑定(binding),如果把Environment Record当做一个对象的话,那么它上面的绑定(binding)就可以认为是它的属性了。

//对于一个函数
function hello(b, c) {
    var a = 10;
}
hello();//执行它会进入一个新的Execution Context
//它有一个Environment Record
er =  {
  a: undefined,
  b: undefined,
  c: undefined,
  arguments: `List of args`
}

//它有一个Lexical Environment
le = {
  EnvironmentRecord: er,
  OuterLexicalEnvironment: GLOBAL
}

//而它的Execution Context为:
EC = {
  LexicalEnvironment: le,
  VariableEnvironment: le, //VariableEnvironment和LexicalEnvironment指向同一个Lexical Environment
  ThisBinding: GLOBAL
}

其实对于任意Execution Context(简称EC),99%的情况你都可以认为它的LexicalEnvironment和VariableEnvironment都指向同一个Lexical Environment。但为什么还区分出这两个呢?

  • 对于一个EC的VariableEnvironment,一量创建它的指向不会改变,永远是指向同一个Lexical Environment
  • 对于一个LexicalEnvironment,可能会根据代码的控制流改变,如进入了with代码块里或是catch代码块里,进入withcatch后,会创建新的LexicalEnvironment(简称LE),然后将当前的LE当做新的LE的parent,最后将EC.LexicalEnvironment指向新的LE

一旦了解了Execution Context, Lexical Environment, Environment Record这些概念,回到delete定义:

  • Let bindings be GetBase(ref).
  • Return the result of calling the DeleteBinding concrete method of
    bindings, providing GetReferencedName(ref) as the argument.

通过GetBase(ref)取得它的Environment Record,然后调用相应的DeleteBinding的内部方法来删除binding。那么DeleteBinding又有什么玄机呢?

DeleteBinding

DeleteBinding的操作可理解为:

  • 对于Object Environment Record,调用其内部的[[Delete]]方法。
  • 对于Declarative Environment Record,当且仅且在创建这个Binding时指定了它是可删除的,才可以从当前Record中删掉这个binding

首先看简单的Object Environment Record情况:

a = "ttt";
delete a;
console.log(a); //报错,因为GLOBAL是一个Object Environment Record(简称OER),而a属性是可删除的

var t = {a:'ccc'}
with(t) {
  delete a;
  console.log(a); //报错,因为当前的Environment Record是一个指向t的OER,而其a属性是可删除的
}

对于其它情况,我们就需要充分理解Create Binding细节了,我总结了一下。

  1. 几乎所有的binding都是不可删除的。函数的参数,变量声明,函数声明,catch变量,arguments均不可删除
  2. 例外是eval环境下的变量声明和函数声明是可删除的

详细的可参见:

故有:

var a = "cccc";
delete a; //没用的

function s(){
}
delete s; //没用

function f(a,b){
  //均没用
  delete a;
  delete b;
  delete f;
  delete arguments;
}

try {
  throw "hello";
} catch(e) {
  delete e; //没用
}

eval("var a = 'ccc'; delete a; console.log(a)");//能删掉,最后的console.log会报错

总结

  • 对于对象属性而言,delete a.b,取决于configurable属性。

    • Object Environment Record 上的binding也取决于其configurable属性,然而一个OER的binding的创建方式有两种,一种是用户代码自己赋上去,如a = 123,另一种是引擎采用CreateBinding来创建,如在全局作用域下的var x = 123,就会在GLOBAL对象上创建一个configurable为false的binding
  • 对于Environment Record而言,取决于CreateBinding时是否指定了这个Binding是一个可删除了,除了eval中的变量声明和函数声明是可删除的外,其它所有binding均不可删除

- 完 -

点赞
收藏
评论区
推荐文章
blmius blmius
4年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
美凌格栋栋酱 美凌格栋栋酱
7个月前
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Karen110 Karen110
4年前
一篇文章带你了解JavaScript日期
日期对象允许您使用日期(年、月、日、小时、分钟、秒和毫秒)。一、JavaScript的日期格式一个JavaScript日期可以写为一个字符串:ThuFeb02201909:59:51GMT0800(中国标准时间)或者是一个数字:1486000791164写数字的日期,指定的毫秒数自1970年1月1日00:00:00到现在。1\.显示日期使用
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1年前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
4年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Android So动态加载 优雅实现与原理分析
背景:漫品Android客户端集成适配转换功能(基于目标识别(So库35M)和人脸识别库(5M)),导致apk体积50M左右,为优化客户端体验,决定实现So文件动态加载.!(https://oscimg.oschina.net/oscnet/00d1ff90e4b34869664fef59e3ec3fdd20b.png)点击上方“蓝字”关注我
Wesley13 Wesley13
3年前
MySQL 清空表(truncate)与删除表中数据(delete) 详解
删除表信息的方式有两种:truncatetabletable\_name;delete\fromtable\_name;注:truncate操作中的table可以省略,delete操作中的\可以省略truncate、delete清空表数据的区别:1truncate是整体删除(速度较快),delete是逐条删
Stella981 Stella981
3年前
Forrester机器学习报告发布,腾讯云跃居第一阵营
  !(https://nimg.ws.126.net/?urlhttp%3A%2F%2Fdingyue.ws.126.net%2F2020%2F1016%2Fecdc1f59j00qi98j7000od200u000fpg00it009u.jpg&thumbnail650x2147483647&quality80&typejpg)  A
为什么mysql不推荐使用雪花ID作为主键
作者:毛辰飞背景在mysql中设计表的时候,mysql官方推荐不要使用uuid或者不连续不重复的雪花id(long形且唯一),而是推荐连续自增的主键id,官方的推荐是auto_increment,那么为什么不建议采用uuid,使用uuid究
Python进阶者 Python进阶者
1年前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这