這種現象的原理是什麼?setTimeout的規範在哪裡,W3C?WHATWG?

在setTimeout里不能獲得一些變數

換句話說

這是setTimeout的問題還是chrome dev的問題

這類變數還有哪些

謝謝


我在推上問了一下 Paul Irish(Chrome DevTool 的老大)。簡而言之,$0 這套 Command Line API 完全是 Native 實現的,所以你並不能用 JavaScript 的邏輯來理解它。

WTF

Chrome 的源碼我是追不下去的,但大概來說:

InstallCommandLineAPI() 會去調用 V8Console.cc 的 createCommandLineAPI(), 包括 $(), $$(), $0-4 在內的 CL API 都是在這裡通過 createBoundFunctionProperty() 直接綁到當前 Session 的 Context 上的,$0 實際調用的 Callback 也是 V8 的 InspectedObject()。

但是你問我具體為什麼 V8 從 Event Loop 任務隊列里取出 timer callback 然後調用的時候就 lookup 不到剛才綁的屬性了?抱歉啊我也答不上來。


@紫雲飛 老師的答案解釋了我的疑惑:

I think that it works as intended. We inject CommandLineAPI during console evaluation only. All asynchronous callbacks doesn"t have injected CommandLineAPI

他說是非同步回調就不能用那些 API,但為啥同樣是非同步的,Promise.resolve().then 里的 callback 執行時 $$ 還掛在 window 上,setTimeout 里的回調執行時就不在了,應該是因為前者的回調是用 microtask 執行的,執行的時候還在當前 loop 里,下個 loop 里 $$ 才會被消除。

(這裡應該是,下一個 Event Loop 就沒有再注入了)


這些屬性是專門提供給開發者工具使用的 Command Line API。我們常用的 $(), $$(), $0-4, copy() 等都是這類 API。

具體有哪些可以參考https://developers.google.com/web/tools/chrome-devtools/console/command-line-reference。也可以通過https://cs.chromium.org/chromium/src/v8/src/inspector/v8-console.cc?type=csq=dirxmlsq=package:chromiuml=670這裡來看到具體的定義。

這些 API 並不能在網頁中的腳本中使用。

之前你的代碼可以正常執行是因為在控制台里,全局環境是被注入了 Command Line API 的。

當你使用 setTimeout 執行的時候,因為沒有直接在控制台執行,相當於是網頁中的腳本執行了。然後因為那些屬性是控制台專用的,在正常的作用域鏈中查詢的時候並無法查到,然後就報錯了。

// 分割線

突然發現你還問了 setTimeout 的規範在哪。鏈接如下,自行查閱吧。

HTML Standard

7. Web application APIs


糾正兩個 typo:是 W3C,不是 W3(雖然網址是 http://w3.org);是 WHATWG,不是 WHATWT。

16 年之前,Command Line API 在 Chrome DevTools 里是使用 with 語句來實現的,下面這句代碼還有印象吧:

with (typeof __commandLineAPI !== "undefined" ? __commandLineAPI : { __proto__: null }) {

}

這種實現方式下,即便在非同步回調里,也可以正常使用那些 API,不會有樓主提到的這個問題。在 ES6 之前,這裡用 with 語句帶來的副作用幾乎察覺不到,但 ES6 里有了塊級作用域,就有麻煩了:Sina Visitor System。

為了解決全局作用域下塊級作用域變數變成 with 語句內的局部變數這一問題,inject 這些 API 的實現方式從 with 語句換成了其它更底層(C++ 層面實現的)、更 magic 的實現方式(作為普通的前端,我是無法理解是怎麼 work 的)。

當時我就發現了樓主提的這個問題了,並且報了 bug(609409 - Command Line APIs are not defined in callbacks of promise.then() which returned by fetch() - chromium - Monorail),但我發現的 bug 比你發現的更加奇怪點,下面是我當時反饋時傳的圖,兩句代碼其實都是非同步的,但表現不一:

當時負責 DevTools JavaScript 調試這塊的開發告訴我:

I think that it works as intended. We inject CommandLineAPI during console evaluation only. All asynchronous callbacks doesn"t have injected CommandLineAPI

他說是非同步回調就不能用那些 API,但為啥同樣是非同步的,Promise.resolve().then 里的 callback 執行時 $$ 還掛在 window 上,setTimeout 里的回調執行時就不在了,應該是因為前者的回調是用 microtask 執行的,執行的時候還在當前 loop 里,下個 loop 里 $$ 才會被消除。


這裡有很多大大已經給出解釋了,和setTimeout無關,和devtools實現相關,這個不在任何規範里的。

不過好像都沒有具體說出這種場景(chrome devtools)出現這種現象(setTimeout不能用$0)是因為什麼。

原因非常簡單,就是在chrome devtools的REPL模式下,每次執行的時候都會創建一個InjectedScript::ContextScope,並在這個scope下執行installCommandLineAPI來讓v8::Context可以訪問到這些native變數,所有的這些Command Line API都是放在一個unique_ptr上的。

這個scope是個棧變數,也就是RAII的做法,在語句執行結束後就會釋放,而scope的析構函數會調用cleanup方法,那個指向Command Line API的unique_ptr就會reset,此時就無法訪問到了。

所以當你語句執行完之後,v8::Context還在,事件循環後的回調還是能訪問到以前的變數,但此時已經無法訪問到那個Command Line API了,畢竟已經被RAII清理掉了。


這個不是 HTML 的規範,$_、$0 - $4、$$、$x等等這些 API 都是 Chrome Devtools 提供的 Command Line API,是由瀏覽器實現的,方便開發者進行調試用的。

其它瀏覽器也提供了類似(或者一樣)的變數,但是行為可能不同。比如在 Firefox 中,setTimeout 是可以使用這些變數的。

其實在 Chrome Devtools 也是可以通過黑科技方法來使用的。後面慢慢分析。


Chrome 不能使用,可能和 event loop 有關。

setTimeout 的執行使用了 event loop,所以從 Event loops 章節找找。

To coordinate events, user interaction, scripts, rendering, networking, and so forth, user agents must use event loops as described in this section. There are two kinds of event loops: those for browsing contexts, and those for workers.

Event loop 有 2 種:瀏覽器 Context 和 web worker。

不僅僅是 setTimeout,只要實在事件循環中的 handler,都無法訪問這些變數。

比如我們在 addEventListener 的處理方法中使用這些變數也會拋出同樣的異常。


[NoInterfaceObject, Exposed=(Window, Worker)]
interface WindowTimers {
long setTimeout((Function or DOMString) handler, optional long timeout = 0, any... arguments);
void clearTimeout(optional long handle = 0);
long setInterval((Function or DOMString) handler, optional long timeout = 0, any... arguments);
void clearInterval(optional long handle = 0);
};
Window implements WindowTimers;
WorkerGlobalScope implements WindowTimers;

可以看到 setTimeout 的方法簽名中還有第三個參數。對第三個參數的解釋是:

Any arguments are passed straight through to the handler.

從第二個參數之後的任何參數都會傳遞到第一個參數中去。所以,我們可以通過第三個參數傳遞這些變數:


謝邀。$0 這個變數根本就沒出現,出錯是正常的,和 setTimeout 有什麼關係?你直接執行 f 函數難道就沒發生錯誤嗎?


經過進一步了解(主要是通過認為女朋友最好最可愛的 @馮天然 大大的回答和親手測試),大抵是這麼一回事:

  1. $、$$、$0 等確實不是我之前知道的JS語言規範或瀏覽器宿主實現規範的一部分,我傾向於認為不是,而只是某些瀏覽器自行為了開發者方便提供的 API。然而經過測試,各大瀏覽器居然都支持這些功能,這讓我比較難以確定,這些到底是某個瀏覽器(Chrome)自行實現了,別的瀏覽器學去了(就像火狐的 firbug 插件後來變成大一統的調試控制台),還是這就是正統規範,我只是從未聽說過。
  2. 我傾向於認為這些不是規範要求的一部分,而是私有實現(哪怕是被普及後的私有實現,依然屬於私有實現),因為 $ 只是規範中 document.querySelector 的單純重複,而從 jQuery 庫在更早之前就放心大膽地採用了 $ 這個變數名來導出,以及實現了十分類似的選擇器功能,比較容易猜到,是 jQuery 先行這麼做了,才被收入規範。其次,從 $.toString() 可以發現,它的內容是 [Command Line API],而非 [native code]。
  3. 從語法原理上講,變數只可能與定義位置有關,不可能與運行時執行位置有關,因此這一定是一個特例,因此一定與 setTimeout 無關
  4. 把這種特例當做一個知識記住當然可以,不過如果是這樣,也不必問這個問題了,特例如果沒有某種規律,也依然是令人焦慮的。當然,通過第二點,我們至少限定了它與 setTimeout 的 bug 或特性無關,而與 $ 這些 API 有關。這是最基本的安全確認。
  5. 經過進一步測試,別的瀏覽器中,像題主這種寫法,是不會報錯的,所以這非但是一個語法的特例,而且只是 Chrome 瀏覽器中才存在的特例。
  6. 我們可以相對妥善地揣測,Chrome 瀏覽器之所以這樣做,可能是因為兩種原因,一種是因為這不是語言規範的一部分,因此開發者不知道它而造成程序 bug 的情況是不合理的,所以在開發者沒有明確使用控制台的情況下(經測試確認,包括直接寫在 & 標籤中,和命令行中綁定到 onclick、setTimeout 等延遲獨立觸發(獨立觸發的定義是如果拋出錯誤,只會終止自身內部後續的代碼,不會終止此後的獨立觸發隊列的執行)的回調函數中),讓這些輔助性的 API 處於相當於不存在的情況;另一種可能的原因,是因為這種 API 可能會造成性能損耗,在開發者沒有明確進行調試行為時,默認不啟用類似 $0 這種智能跟蹤。
  7. 多年的前端開發工作中我從未產生過對這些 API 功能的需求,也沒有遇到過因這些 API 的存在而造成的 bug,更沒有在查別的資料的過程中碰巧看到過這些 API,所以確實不知道。這裡其實存在一個問題,就是題主是怎麼知道這些 API 的?在看到這些 API 的地方,沒有同時看到與這些 API 的使用注意事項相關的線索嗎?(這是一個問題,不是一個質疑)


寫錯了吧,是不是應該是

setTimeout(f(),0);

求啪啪打臉


這個 提示很明顯 是 不認識這個變數!

很明顯是 變數的問題吧


推薦閱讀:

chrome,firefox的主頁同時被國外流氓軟體篡改,怎麼辦?
Chrome瀏覽器如何給一個書籤增加快捷鍵?
為什麼在中國區的谷歌瀏覽器賬戶同步功能不好用的情況下(被限制),谷歌瀏覽器仍被這麼多人使用?
用chrome瀏覽器在b站看視頻出現error #2048應該如何解決?
為什麼 chrome 瀏覽器打開下載的 PDF,默認方式是瀏覽器而不是軟體?

TAG:GoogleChrome | 前端開發 | JavaScript | V8 |