如题,初学者看视频,看到with语句的特殊作用域问题,然后跟着老师视频里的代码敲了一次,因为刚巧是用的chrome canary(chrome 49),结果输出跟老师的不一样,然后我又换了好几个浏览器,惊讶发现只有chrome canary和firefox是不一样的。昨天升级了chrome canary到50了再次截图。
代码如下:
var foo = 'abc';
with({
foo: 'bar'
}) {
function f() {
console.log(foo);
}
(function() {
console.log(foo);
})();
f();
}
截图如下:
google找了下,不知道应该找什么关键字了。(衰)
下面是找到的chrome的ES版本变更信息查询,可是没找出什么猫腻,然后,嗯,就跑这里来当“伸手党”了。
https://www.chromestatus.com/feature/4633745457938432
https://www.chromestatus.com/feature/4633745457938432
PS:都要翻墙。(摔)
所以说想请教一下,with语句中的作用域到底怎么理解的?
顺便吐槽一下markdown的序号分多了,后面有些的内容不能继续跟这缩进了(如上面的链接)。
就你的问题而言
按道理来说你例子中应该输出两次 bar。之前的浏览器表现可能从 ES 定义上来说是错误的。各大浏览器中 Firefox 对标准的支持是最严格的。Chrome Canary 可能是最近修复了这个问题,也许这是 Chrome Canary 和 Firefox 表现一致的原因。ES7 的草案中对 with
的定义有所修改,你想深挖的话最好看 ECMAScript 规范 。说实话我没仔细研究,也不建议你花精力去看这个。
就 with 本身而言
这是一个早就不推荐使用的关键字,它最开始被发明只是为了减少一点敲代码的时间。比如:
var foo = {bar: "bar"};
with(foo) {
bar; // 等同于 foo.bar
bar = "baz"; // 等同于 foo.bar = "baz"
}
这种表面的简单带来的是更多的疑惑和维护问题。比如在复杂的代码中很难搞懂一个东西到底是上层 object 的属性还是一个变量。因为这个原因 with
是不推荐使用的,在 strict mode 中更是被禁掉了。所以这个关键字是没必要花很大精力去研究细节的。
关于搜索的问题
想搜索得有效率,怎么去定位核心问题和怎么去描述很重要。这是我在 Google 中以 "javascript with keyword" 为关键字找到的结果前三位。BTW 三篇文章都值得一看。
with - JavaScript | MDN
MDN 的文章,没什么好说的。学习 JavaScript 规范最好的地方。ECMAScript 规范涉及到太多设计细节了,不适合每个人去读。
JavaScript's with statement and why it's deprecated
一个知名的博客,作者对 JavaScript 有很深的的理解。很多文章都值得一看。貌似他还是 ES 规范的参与者。
Are there legitimate uses for JavaScript's "with" statement?
StackOverflow 的问题。我想很多人看到这个的感觉是 “有救了”。
首先,谈到with
大家的反应都是一致的——不要用!也不要去深究它!这一点上我的观点是一致的。但是题主的这个问题本身却是比较有意思的,它其实跟with
的关系不大。我们不妨来思考一下,就当做做脑保健操了。ok,开始分析。
我觉得之所以出现这种现象,是因为ES5不支持块级作用域,而ES6支持。什么意思呢?
在ES5或之前的浏览器中,with
的工作方式类似于定义了一个块级作用域(注意是类似于
),当其中的代码查找某个变量但找不到时,会先在with
定义的默认对象里找,再找不到才会去更上一层作用域里去找。
这些是我们所知道的,但是我想很多人并没有真正理解这一过程。微妙的事情其实在代码解析阶段就已经悄然发生了
。
我们知道,浏览器在执行JS代码前会先解析代码,而解析一个函数时,不同的写法其解析时机并不一样。
函数定义
function f() {}
,这是一个函数定义,它会被提升到当前作用域的顶部来解析,而无论这个函数定义是写在当前作用域的什么位置。函数表达式
var f = function(){};
和(function(){})();
这两种写法都是函数表达式的写法。对于第一种,变量f
会被进行提升,但函数表达式则不会;后面一种也是一样。
然后来看看你给出的代码:
var foo = 'abc';
with({
foo: 'bar'
}) {
function f() {
console.log(foo);
}
(function() {
console.log(foo);
})();
f();
}
在ES5
中,with
并没有定义一个新的作用域,整段代码处于一个当前作用域
之下(例如全局作用域或某个函数作用域),所以这段代码相当于:
var foo = 'abc';
// 函数f被提升到这里了!!
function f() {
console.log(foo);
}
with({
foo: 'bar'
}) {
(function() {
console.log(foo);
})();
f();
}
经过这样一次等效转换,应该能够一眼看出结果了吧,最后必然是bar abc
。
再来看看ES6
的情况。在ES6
中,with
确实定义了一个新的作用域,所以变量foo
和with
中的代码处于不同的作用域下面。此时f()
函数同样会进行提升,但它已经处于它所在作用域的最前面了,所以提不提升都是一样的。因此最后输出的必然是bar bar
。
至此,问题和原因都已经很明确了。
那么有没有办法让这段代码在ES5
中也能输出bar bar
呢?有的,只需要将f()
函数定义成函数表达式
或跟它后面的代码一样写成立即执行函数
即可。例如:
var foo = 'abc';
with({
foo: 'bar'
}) {
// 改成函数表达式的方式,阻止函数的提升
var f = function() {
console.log(foo);
}; // 注意这里需要加上分号
// 否则会与后面的立即执行函数冲突导致运行时错误
(function() {
console.log(foo);
})();
f();
}
这段代码会输出bar bar
。实际上它相当于:
var foo = 'abc';
var f; // 变量f被提升到了这里
with({
foo: 'bar'
}) {
// 但真正的赋值是在这里,所以这里的函数仍然处于with的“监管”之下
f = function() {
console.log(foo);
};
(function() {
console.log(foo);
})();
f();
}