你知道引用这个东西吗?

eval('x = 100');
(0, eval)('x = 100');
首先先抛出一个问题,这两者有什么区别?

delete这个运算符

var obj = { x: 1 };
delete obj.x;

相信大家使用delete的时候基本上是上面代码片段的场景,删除一个对象的一个属性。
那么以下这些表达式,他们又在删些什么?

delete 0;
delete x;
delete undefined;
delete null;

上面几个语句,都可以执行,并且不会报错。
那么这个delete究竟是什么神仙,在隐秘的角落里耍些什么花里胡哨的法术呢?

delete在Javascript1.2的时候就有了,那时候还没有try catch呢。
所以不管删除是否有问题,都不会报错,删除成功或者没有作删除动作都会返回true,删除失败返回false

delete x;

上面这个语句,单纯在语法分析是不知道它在删除什么。
实际上,delete运算符后面跟着一个芋圆表达式,不管后面是0, null, x,都视为一个表达式,并尝试删除它的结果。

一元表达式的结果,可能是,可能是引用

如果它是值,则按照约定直接返回true,并不做任何实际行动;
如果它是引用,则删除它(除非它不能被删除)。

这里说的引用不是我们日常说的指针,这里说的引用是ECMAScript中的规范。

在ECMAScript规范里,有这些类型:
引用Reference 、 列表List 、 完结Completion 、 属性描述式 Property Descriptor、 属性标示Property Identifier 、 词法环境(Lexical Environment)、 环境纪录(Environment Record)

x = x;

实际上是

x = GetValue(x);

当一个表达式作为左手端的时候,它是一个引用,当一个表达式作为右手端的时候,它实际上会被GetValue变成一个值。

回到delete x;
delete是为数不多能直接操作引用的操作符,那么现在你们知道为什么delete/typeof后面跟一个不存在的变量不会报错了吗?
因为它们都是直接操作引用,并没有作GetValue的操作,所以不会取值,就不会报错。

现在来考考大家

// case 1
var x = 1;
delete x;

// case 2
window.x = 1;
delete x;

// case 3
with ({ x: 1 }) { delete x; }

这以上3个case,它们能删除掉x吗?

-----------下面是答案----------
当使用var声明的变量,会放在window上,大家都知道,但是它实际是

Object.defineProperty(window, 'x', {
    configurable: false,
    value: 1
});

并不能删除,所以case 1会返回false,表示删除失败。

case 2就不多解释了,删除成功返回true。

case 3中,在with语句当中,会产生一个{ x: 1 }的词法环境,所以删除成功并返回true。

引用长什么样

讲了这么久,引用长什么样子都不知道。
它长这个样子

{
    [[BaseValue]]: Env Record或者携带这个引用的对象
    [[ReferencedName]]: 名字
    [[StrictReference]]: 严格模式?
}

函数的this是怎么确认的?

  1. 假设ref是一个方法
  2. 如果 Type(ref) 为 Reference,那么 如果 IsPropertyReference(ref) (如果ref是Env Record则返回false,如果是被其他引用携带,则返回true)为 true,那么 令 thisValue 为 GetBase(ref)(返回[[BaseValue]]).
  3. 否则 , ref 的基值是一个环境记录项(Env Record),令 thisValue 为调用 GetBase(ref) 的 ImplicitThisValue 具体方法的结果(ImplicitthisValue一直返回undefined)
  4. 否则 , 假如 Type(ref) 不是 Reference. 令 thisValue 为 undefined.
  5. 在非严格模式下,如果thisValue是undefined,则会变成全局对象。
var value = 1;
var foo = {
    value: 2,
    bar: function() {
        return this.value;
    }
};
// 1
foo.bar();
// 2
(foo.bar)();
// 3
(foo.bar = foo.bar)();
// 4
(false || foo.bar)();
// 5
(foo.bar, foo.bar)();

套用上面的步骤,大家自己回答一下答案是什么,如果能正确回答出来,那么基本上就弄懂了。

最后讲回eval这个方法
eval分为直接调用和间接调用
直接调用:作为Reference并且[[BaseValue]]是全局对象
间接调用:总是执行在全局环境中,总是执行在非严格模式下

eval环境是唯一一个将变量环境指向了与它自身的词法环境不同位置的环境。
啥意思?

eval('let x = 1;');
console.log(x); // 报错,eval有自己的词法环境
eval('var x = 1;');
console.log(x); // 1

上面两个代码片段,相信大家已经看出来了,使用let声明的变量,是在eval内部的词法环境中,但是使用var声明的变量,是声明在了外部的变量环境。

现在大家能回答出

eval(‘x = 100’);
(0, eval)(‘x = 100’);
eval(‘this.x = 100’);
(0, eval)(‘this.x = 100’);

他们的区别了吗?^_^

参考
极客时间《JavaScript核心原理分析》


一画先生
83 声望12 粉丝

我司长期招聘前端开发工程师,有意的小伙伴+vx: Mr_yihua