From 1612c10b5443bc263ec02e88292f75f821de896e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 02:17:10 +0000 Subject: [PATCH] Deploy to GitHub pages --- CNAME | 1 + about/index.html | 305 + archives/2019/08/index.html | 279 + archives/2019/09/index.html | 279 + archives/2019/10/index.html | 279 + archives/2019/10/page/2/index.html | 279 + archives/2019/10/page/3/index.html | 279 + archives/2019/10/page/4/index.html | 279 + archives/2019/10/page/5/index.html | 279 + archives/2019/11/index.html | 279 + archives/2019/12/index.html | 279 + archives/2019/index.html | 279 + archives/2019/page/2/index.html | 279 + archives/2019/page/3/index.html | 279 + archives/2019/page/4/index.html | 279 + archives/2019/page/5/index.html | 279 + archives/2019/page/6/index.html | 279 + archives/2020/01/index.html | 279 + archives/2020/index.html | 279 + archives/2021/03/index.html | 279 + archives/2021/index.html | 279 + archives/2022/09/index.html | 279 + archives/2022/10/index.html | 279 + archives/2022/index.html | 279 + archives/2023/03/index.html | 279 + archives/2023/05/index.html | 279 + archives/2023/08/index.html | 279 + archives/2023/index.html | 279 + archives/2024/04/index.html | 279 + archives/2024/09/index.html | 279 + archives/2024/10/index.html | 279 + archives/2024/index.html | 279 + archives/index.html | 279 + archives/page/2/index.html | 279 + archives/page/3/index.html | 279 + archives/page/4/index.html | 279 + archives/page/5/index.html | 279 + archives/page/6/index.html | 279 + archives/page/7/index.html | 279 + archives/page/8/index.html | 279 + assets/algolia/algoliasearch.js | 7190 +++++++++++++++++ assets/algolia/algoliasearch.min.js | 4 + assets/algolia/algoliasearchLite.js | 4453 ++++++++++ assets/algolia/algoliasearchLite.min.js | 3 + atom.xml | 461 ++ books/index.html | 283 + categories/JavaScript/ES6/index.html | 279 + categories/JavaScript/ES6/page/2/index.html | 279 + categories/JavaScript/ES6/page/3/index.html | 279 + categories/JavaScript/ES6/page/4/index.html | 279 + categories/JavaScript/JQuery/index.html | 279 + categories/JavaScript/Node-js/index.html | 279 + categories/JavaScript/React-js/index.html | 279 + categories/JavaScript/Vue-js/index.html | 279 + categories/JavaScript/index.html | 279 + categories/JavaScript/page/2/index.html | 279 + categories/JavaScript/page/3/index.html | 279 + categories/JavaScript/page/4/index.html | 279 + categories/JavaScript/page/5/index.html | 279 + categories/TypeScript/index.html | 279 + categories/Web-Office/index.html | 279 + categories/index.html | 281 + .../Element-Plus/index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + css/index.css | 6302 +++++++++++++++ css/var.css | 0 games/index.html | 283 + img/404.jpg | Bin 0 -> 16393 bytes img/butterfly-icon.png | Bin 0 -> 275383 bytes img/error-page.png | Bin 0 -> 35850 bytes img/favicon.ico | Bin 0 -> 15406 bytes img/friend_404.gif | Bin 0 -> 65097 bytes index.html | 279 + js/main.js | 923 +++ js/search/algolia.js | 174 + js/search/local-search.js | 360 + js/tw_cn.js | 117 + js/utils.js | 313 + link/index.html | 407 + manifest.json | 1 + movies/index.html | 283 + page/2/index.html | 279 + page/3/index.html | 279 + page/4/index.html | 279 + page/5/index.html | 279 + page/6/index.html | 279 + page/7/index.html | 279 + page/8/index.html | 279 + posts/20220903nr/index.html | 214 + posts/20220903sg/index.html | 217 + posts/20220905ap/index.html | 214 + posts/20220909px/index.html | 222 + posts/20220915up/index.html | 218 + posts/20221010bg/index.html | 232 + posts/20221021ov/index.html | 217 + posts/20230329gt/index.html | 263 + posts/20230330gt/index.html | 265 + posts/20230505tr/index.html | 236 + posts/20230806pa/index.html | 228 + posts/20240417re/index.html | 295 + posts/20240924wg/index.html | 215 + posts/20241017dv/index.html | 274 + .../index.html" | 279 + posts/DOC-ESLint/index.html | 210 + .../index.html" | 269 + .../index.html" | 478 ++ .../index.html" | 303 + .../index.html" | 4767 +++++++++++ posts/ES6-ArrayBuffer/index.html | 732 ++ .../index.html" | 432 + .../index.html" | 378 + .../index.html" | 391 + .../index.html" | 415 + .../index.html" | 520 ++ .../index.html" | 417 + posts/ES6-Mixin/index.html | 238 + .../index.html" | 461 ++ .../index.html" | 474 ++ .../index.html" | 459 ++ posts/ES6-Proxy/index.html | 477 ++ posts/ES6-Reflect/index.html | 338 + posts/ES6-SIMD/index.html | 440 + .../index.html" | 505 ++ posts/ES6-Symbol/index.html | 430 + .../index.html" | 451 ++ .../index.html" | 393 + .../index.html" | 385 + .../index.html" | 270 + .../index.html" | 562 ++ .../index.html" | 444 + .../index.html" | 408 + .../index.html" | 476 ++ .../index.html" | 609 ++ .../index.html" | 431 + .../index.html" | 500 ++ .../index.html" | 465 ++ .../index.html" | 419 + .../index.html" | 323 + .../index.html" | 420 + .../index.html" | 540 ++ .../JS-JS\345\237\272\347\241\200/index.html" | 335 + .../JS-JS\351\253\230\347\272\247/index.html" | 210 + posts/JS-Web Apis/index.html | 440 + .../index.html" | 273 + .../index.html" | 223 + .../index.html" | 238 + .../index.html" | 336 + .../index.html" | 409 + posts/Node-Promise/index.html | 238 + .../index.html" | 244 + .../index.html" | 235 + .../index.html" | 381 + .../index.html" | 434 + posts/Vue-Vuex/index.html | 306 + .../index.html" | 859 ++ .../index.html" | 215 + .../index.html" | 254 + .../index.html" | 1199 +++ posts/navs/index.html | 225 + robots.txt | 5 + service-worker.js | 2 + service-worker.js.map | 1 + sitemap.txt | 117 + sitemap.xml | 979 +++ songs/index.html | 283 + tags/ES6/index.html | 279 + tags/ES6/page/2/index.html | 279 + tags/ES6/page/3/index.html | 279 + tags/ES6/page/4/index.html | 279 + tags/Element-Plus/index.html | 279 + tags/Fingerprintjs/index.html | 279 + tags/GeeTest/index.html | 279 + tags/Hexo/index.html | 279 + tags/JavaScript/index.html | 279 + tags/Node-js/index.html | 279 + tags/PageSpy/index.html | 279 + tags/React-jjs/index.html | 279 + tags/Tauri/index.html | 279 + tags/TypeScript/index.html | 279 + tags/VSCode/index.html | 279 + tags/Vue-js/index.html | 279 + tags/WPS-Office/index.html | 279 + tags/index.html | 281 + tags/kkFileViews/index.html | 279 + tags/reCAPTCHA/index.html | 279 + .../index.html" | 279 + "tags/\345\257\274\350\210\252/index.html" | 279 + .../index.html" | 279 + "tags/\346\213\226\346\213\275/index.html" | 279 + .../index.html" | 279 + "tags/\347\254\224\350\256\260/index.html" | 279 + .../index.html" | 279 + .../index.html" | 279 + workbox-1f84e78b.js | 2 + workbox-1f84e78b.js.map | 1 + 201 files changed, 81472 insertions(+) create mode 100644 CNAME create mode 100644 about/index.html create mode 100644 archives/2019/08/index.html create mode 100644 archives/2019/09/index.html create mode 100644 archives/2019/10/index.html create mode 100644 archives/2019/10/page/2/index.html create mode 100644 archives/2019/10/page/3/index.html create mode 100644 archives/2019/10/page/4/index.html create mode 100644 archives/2019/10/page/5/index.html create mode 100644 archives/2019/11/index.html create mode 100644 archives/2019/12/index.html create mode 100644 archives/2019/index.html create mode 100644 archives/2019/page/2/index.html create mode 100644 archives/2019/page/3/index.html create mode 100644 archives/2019/page/4/index.html create mode 100644 archives/2019/page/5/index.html create mode 100644 archives/2019/page/6/index.html create mode 100644 archives/2020/01/index.html create mode 100644 archives/2020/index.html create mode 100644 archives/2021/03/index.html create mode 100644 archives/2021/index.html create mode 100644 archives/2022/09/index.html create mode 100644 archives/2022/10/index.html create mode 100644 archives/2022/index.html create mode 100644 archives/2023/03/index.html create mode 100644 archives/2023/05/index.html create mode 100644 archives/2023/08/index.html create mode 100644 archives/2023/index.html create mode 100644 archives/2024/04/index.html create mode 100644 archives/2024/09/index.html create mode 100644 archives/2024/10/index.html create mode 100644 archives/2024/index.html create mode 100644 archives/index.html create mode 100644 archives/page/2/index.html create mode 100644 archives/page/3/index.html create mode 100644 archives/page/4/index.html create mode 100644 archives/page/5/index.html create mode 100644 archives/page/6/index.html create mode 100644 archives/page/7/index.html create mode 100644 archives/page/8/index.html create mode 100644 assets/algolia/algoliasearch.js create mode 100644 assets/algolia/algoliasearch.min.js create mode 100644 assets/algolia/algoliasearchLite.js create mode 100644 assets/algolia/algoliasearchLite.min.js create mode 100644 atom.xml create mode 100644 books/index.html create mode 100644 categories/JavaScript/ES6/index.html create mode 100644 categories/JavaScript/ES6/page/2/index.html create mode 100644 categories/JavaScript/ES6/page/3/index.html create mode 100644 categories/JavaScript/ES6/page/4/index.html create mode 100644 categories/JavaScript/JQuery/index.html create mode 100644 categories/JavaScript/Node-js/index.html create mode 100644 categories/JavaScript/React-js/index.html create mode 100644 categories/JavaScript/Vue-js/index.html create mode 100644 categories/JavaScript/index.html create mode 100644 categories/JavaScript/page/2/index.html create mode 100644 categories/JavaScript/page/3/index.html create mode 100644 categories/JavaScript/page/4/index.html create mode 100644 categories/JavaScript/page/5/index.html create mode 100644 categories/TypeScript/index.html create mode 100644 categories/Web-Office/index.html create mode 100644 categories/index.html create mode 100644 "categories/\344\275\277\347\224\250\346\226\207\346\241\243/Element-Plus/index.html" create mode 100644 "categories/\344\275\277\347\224\250\346\226\207\346\241\243/index.html" create mode 100644 "categories/\345\234\260\345\233\276\347\261\273/index.html" create mode 100644 "categories/\345\274\200\345\217\221\345\267\245\345\205\267/index.html" create mode 100644 "categories/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" create mode 100644 "categories/\346\212\200\346\234\257\346\226\207\346\241\243/index.html" create mode 100644 "categories/\350\276\205\345\212\251\347\261\273/index.html" create mode 100644 "categories/\351\252\214\350\257\201\347\261\273/index.html" create mode 100644 css/index.css create mode 100644 css/var.css create mode 100644 games/index.html create mode 100644 img/404.jpg create mode 100644 img/butterfly-icon.png create mode 100644 img/error-page.png create mode 100644 img/favicon.ico create mode 100644 img/friend_404.gif create mode 100644 index.html create mode 100644 js/main.js create mode 100644 js/search/algolia.js create mode 100644 js/search/local-search.js create mode 100644 js/tw_cn.js create mode 100644 js/utils.js create mode 100644 link/index.html create mode 100644 manifest.json create mode 100644 movies/index.html create mode 100644 page/2/index.html create mode 100644 page/3/index.html create mode 100644 page/4/index.html create mode 100644 page/5/index.html create mode 100644 page/6/index.html create mode 100644 page/7/index.html create mode 100644 page/8/index.html create mode 100644 posts/20220903nr/index.html create mode 100644 posts/20220903sg/index.html create mode 100644 posts/20220905ap/index.html create mode 100644 posts/20220909px/index.html create mode 100644 posts/20220915up/index.html create mode 100644 posts/20221010bg/index.html create mode 100644 posts/20221021ov/index.html create mode 100644 posts/20230329gt/index.html create mode 100644 posts/20230330gt/index.html create mode 100644 posts/20230505tr/index.html create mode 100644 posts/20230806pa/index.html create mode 100644 posts/20240417re/index.html create mode 100644 posts/20240924wg/index.html create mode 100644 posts/20241017dv/index.html create mode 100644 "posts/DOC-24\351\201\223JavaScript\347\256\227\346\263\225/index.html" create mode 100644 posts/DOC-ESLint/index.html create mode 100644 "posts/DOC-Hexo\345\215\232\345\256\242\346\220\255\345\273\272\346\265\201\347\250\213/index.html" create mode 100644 "posts/DOC-JavaScript\345\270\270\350\247\201\351\227\256\351\242\230/index.html" create mode 100644 "posts/DOC-Jquery\347\232\204\345\270\270\350\247\201\351\227\256\351\242\230/index.html" create mode 100644 "posts/DOC-Xmind\347\254\224\350\256\260\346\261\207\346\200\273/index.html" create mode 100644 posts/ES6-ArrayBuffer/index.html create mode 100644 "posts/ES6-Class \347\232\204\345\237\272\346\234\254\350\257\255\346\263\225/index.html" create mode 100644 "posts/ES6-Class \347\232\204\347\273\247\346\211\277/index.html" create mode 100644 "posts/ES6-ECMAScript 6 \347\256\200\344\273\213/index.html" create mode 100644 "posts/ES6-Generator \345\207\275\346\225\260\347\232\204\345\274\202\346\255\245\345\272\224\347\224\250/index.html" create mode 100644 "posts/ES6-Generator \345\207\275\346\225\260\347\232\204\350\257\255\346\263\225/index.html" create mode 100644 "posts/ES6-Iterator \345\222\214 for...of \345\276\252\347\216\257/index.html" create mode 100644 posts/ES6-Mixin/index.html create mode 100644 "posts/ES6-Module \347\232\204\345\212\240\350\275\275\345\256\236\347\216\260/index.html" create mode 100644 "posts/ES6-Module \347\232\204\350\257\255\346\263\225/index.html" create mode 100644 "posts/ES6-Promise \345\257\271\350\261\241/index.html" create mode 100644 posts/ES6-Proxy/index.html create mode 100644 posts/ES6-Reflect/index.html create mode 100644 posts/ES6-SIMD/index.html create mode 100644 "posts/ES6-Set \345\222\214 Map \346\225\260\346\215\256\347\273\223\346\236\204/index.html" create mode 100644 posts/ES6-Symbol/index.html create mode 100644 "posts/ES6-async \345\207\275\346\225\260/index.html" create mode 100644 "posts/ES6-let \345\222\214 const \345\221\275\344\273\244/index.html" create mode 100644 "posts/ES6-\344\277\256\351\245\260\345\231\250/index.html" create mode 100644 "posts/ES6-\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213/index.html" create mode 100644 "posts/ES6-\345\207\275\346\225\260\347\232\204\346\211\251\345\261\225/index.html" create mode 100644 "posts/ES6-\345\217\202\350\200\203\351\223\276\346\216\245/index.html" create mode 100644 "posts/ES6-\345\217\230\351\207\217\347\232\204\350\247\243\346\236\204\350\265\213\345\200\274/index.html" create mode 100644 "posts/ES6-\345\255\227\347\254\246\344\270\262\347\232\204\346\211\251\345\261\225/index.html" create mode 100644 "posts/ES6-\345\257\271\350\261\241\347\232\204\346\211\251\345\261\225/index.html" create mode 100644 "posts/ES6-\346\225\260\345\200\274\347\232\204\346\211\251\345\261\225/index.html" create mode 100644 "posts/ES6-\346\225\260\347\273\204\347\232\204\346\211\251\345\261\225/index.html" create mode 100644 "posts/ES6-\346\234\200\346\226\260\346\217\220\346\241\210/index.html" create mode 100644 "posts/ES6-\346\255\243\345\210\231\347\232\204\346\211\251\345\261\225/index.html" create mode 100644 "posts/ES6-\347\274\226\347\250\213\351\243\216\346\240\274/index.html" create mode 100644 "posts/ES6-\350\257\273\346\207\202 ECMAScript \350\247\204\346\240\274/index.html" create mode 100644 "posts/JS-JQuery\345\237\272\347\241\200/index.html" create mode 100644 "posts/JS-JS\345\237\272\347\241\200/index.html" create mode 100644 "posts/JS-JS\351\253\230\347\272\247/index.html" create mode 100644 posts/JS-Web Apis/index.html create mode 100644 "posts/Node-Express\346\241\206\346\236\266/index.html" create mode 100644 "posts/Node-MySQL\344\275\277\347\224\250/index.html" create mode 100644 "posts/Node-Node\344\270\255\347\232\204\344\274\232\350\257\235\346\212\200\346\234\257/index.html" create mode 100644 "posts/Node-Node\344\270\255\347\232\204\350\267\250\345\237\237/index.html" create mode 100644 "posts/Node-Node\345\237\272\347\241\200/index.html" create mode 100644 posts/Node-Promise/index.html create mode 100644 "posts/React-React\345\237\272\347\241\200/index.html" create mode 100644 "posts/TS-TS\345\237\272\347\241\200 /index.html" create mode 100644 "posts/Vue-MVVM\345\216\237\347\220\206/index.html" create mode 100644 "posts/Vue-Vue-Cli\347\232\204\344\275\277\347\224\250/index.html" create mode 100644 posts/Vue-Vuex/index.html create mode 100644 "posts/Vue-Vue\345\237\272\347\241\200/index.html" create mode 100644 "posts/Vue-vue\347\232\204\351\222\251\345\255\220\345\207\275\346\225\260/index.html" create mode 100644 "posts/Wechat-Uni-App\345\274\200\345\217\221\351\241\271\347\233\256/index.html" create mode 100644 "posts/Wechat-\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\237\272\347\241\200/index.html" create mode 100644 posts/navs/index.html create mode 100644 robots.txt create mode 100644 service-worker.js create mode 100644 service-worker.js.map create mode 100644 sitemap.txt create mode 100644 sitemap.xml create mode 100644 songs/index.html create mode 100644 tags/ES6/index.html create mode 100644 tags/ES6/page/2/index.html create mode 100644 tags/ES6/page/3/index.html create mode 100644 tags/ES6/page/4/index.html create mode 100644 tags/Element-Plus/index.html create mode 100644 tags/Fingerprintjs/index.html create mode 100644 tags/GeeTest/index.html create mode 100644 tags/Hexo/index.html create mode 100644 tags/JavaScript/index.html create mode 100644 tags/Node-js/index.html create mode 100644 tags/PageSpy/index.html create mode 100644 tags/React-jjs/index.html create mode 100644 tags/Tauri/index.html create mode 100644 tags/TypeScript/index.html create mode 100644 tags/VSCode/index.html create mode 100644 tags/Vue-js/index.html create mode 100644 tags/WPS-Office/index.html create mode 100644 tags/index.html create mode 100644 tags/kkFileViews/index.html create mode 100644 tags/reCAPTCHA/index.html create mode 100644 "tags/\345\234\250\347\272\277\351\242\204\350\247\210/index.html" create mode 100644 "tags/\345\257\274\350\210\252/index.html" create mode 100644 "tags/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" create mode 100644 "tags/\346\213\226\346\213\275/index.html" create mode 100644 "tags/\346\226\207\344\273\266\345\244\271\344\270\212\344\274\240/index.html" create mode 100644 "tags/\347\254\224\350\256\260/index.html" create mode 100644 "tags/\351\200\232\350\241\214\347\247\230\351\222\245/index.html" create mode 100644 "tags/\351\253\230\345\276\267\345\234\260\345\233\276/index.html" create mode 100644 workbox-1f84e78b.js create mode 100644 workbox-1f84e78b.js.map diff --git a/CNAME b/CNAME new file mode 100644 index 00000000..81e760d9 --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +blog.zhangsifan.com \ No newline at end of file diff --git a/about/index.html b/about/index.html new file mode 100644 index 00000000..fb4631d7 --- /dev/null +++ b/about/index.html @@ -0,0 +1,305 @@ +关于本站 | JCAlways + + + + + + + + + + + + + +
关于本站

2024

+

10-09

+

本站已运行五周年啦~~~

+
+

2023

+

10-09

+

本站已运行四周年啦~~~

+
+

2022

+

10-09

+

本站已运行三周年啦~~~

+
+

2021

+

10-09

+

本站已运行二周年啦~~~

+
+

2020

+

10-09

+

本站已运行一周年啦~~~

+
+

2019

+

10-09

+

本站正式上线。

+
+
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/08/index.html b/archives/2019/08/index.html new file mode 100644 index 00000000..ca3a5cb6 --- /dev/null +++ b/archives/2019/08/index.html @@ -0,0 +1,279 @@ +八月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 3
2019
JQuery基础
JQuery基础
Web Apis
Web Apis
JS基础
JS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/09/index.html b/archives/2019/09/index.html new file mode 100644 index 00000000..0a5917e8 --- /dev/null +++ b/archives/2019/09/index.html @@ -0,0 +1,279 @@ +九月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 3
2019
Node基础
Node基础
Hexo博客搭建流程
Hexo博客搭建流程
JS高级
JS高级
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/10/index.html b/archives/2019/10/index.html new file mode 100644 index 00000000..f0b56264 --- /dev/null +++ b/archives/2019/10/index.html @@ -0,0 +1,279 @@ +十月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 44
2019
Vue的钩子函数
Vue的钩子函数
Vue-Cli的使用
Vue-Cli的使用
Vue基础
Vue基础
MVVM原理
MVVM原理
Jquery的常见问题
Jquery的常见问题
Promise
Promise
Node操作MySQL
Node操作MySQL
Xmind笔记汇总
24道JavaScript算法题
24道JavaScript算法题
JavaScript常见问题
JavaScript常见问题
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/10/page/2/index.html b/archives/2019/10/page/2/index.html new file mode 100644 index 00000000..de984f35 --- /dev/null +++ b/archives/2019/10/page/2/index.html @@ -0,0 +1,279 @@ +十月 2019 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/10/page/3/index.html b/archives/2019/10/page/3/index.html new file mode 100644 index 00000000..941cd0de --- /dev/null +++ b/archives/2019/10/page/3/index.html @@ -0,0 +1,279 @@ +十月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 44
2019
Module 的加载实现
Module 的加载实现
Module 的语法
Module 的语法
Promise 对象
Promise 对象
Proxy
Proxy
Reflect
Reflect
SIMD
SIMD
Set 和 Map 数据结构
Set 和 Map 数据结构
Symbol
Symbol
async 函数
async 函数
let 和 const 命令
let 和 const 命令
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/10/page/4/index.html b/archives/2019/10/page/4/index.html new file mode 100644 index 00000000..82031c11 --- /dev/null +++ b/archives/2019/10/page/4/index.html @@ -0,0 +1,279 @@ +十月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 44
2019
修饰器
修饰器
函数式编程
函数式编程
函数的扩展
函数的扩展
变量的解构赋值
变量的解构赋值
参考链接
参考链接
字符串的扩展
字符串的扩展
对象的扩展
对象的扩展
数值的扩展
数值的扩展
数组的扩展
数组的扩展
最新提案
最新提案
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/10/page/5/index.html b/archives/2019/10/page/5/index.html new file mode 100644 index 00000000..eb4afcd1 --- /dev/null +++ b/archives/2019/10/page/5/index.html @@ -0,0 +1,279 @@ +十月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 44
2019
正则的扩展
正则的扩展
编程风格
编程风格
读懂 ECMAScript 规格
读懂 ECMAScript 规格
Express框架
Express框架
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/11/index.html b/archives/2019/11/index.html new file mode 100644 index 00000000..c26c3698 --- /dev/null +++ b/archives/2019/11/index.html @@ -0,0 +1,279 @@ +十一月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 3
2019
微信小程序基础
微信小程序基础
VueX
VueX
文章导航
文章导航
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/12/index.html b/archives/2019/12/index.html new file mode 100644 index 00000000..0de84a10 --- /dev/null +++ b/archives/2019/12/index.html @@ -0,0 +1,279 @@ +十二月 2019 | JCAlways + + + + + + + + + + + +
全部文章 - 2
2019
Uni-App开发项目
Uni-App开发项目
ESLint
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/index.html b/archives/2019/index.html new file mode 100644 index 00000000..31601fc3 --- /dev/null +++ b/archives/2019/index.html @@ -0,0 +1,279 @@ +2019 | JCAlways + + + + + + + + + + + +
全部文章 - 55
2019
Uni-App开发项目
Uni-App开发项目
ESLint
微信小程序基础
微信小程序基础
VueX
VueX
文章导航
文章导航
Vue的钩子函数
Vue的钩子函数
Vue-Cli的使用
Vue-Cli的使用
Vue基础
Vue基础
MVVM原理
MVVM原理
Jquery的常见问题
Jquery的常见问题
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/page/2/index.html b/archives/2019/page/2/index.html new file mode 100644 index 00000000..79b8edc0 --- /dev/null +++ b/archives/2019/page/2/index.html @@ -0,0 +1,279 @@ +2019 | JCAlways + + + + + + + + + + + +
全部文章 - 55
2019
Promise
Promise
Node操作MySQL
Node操作MySQL
Xmind笔记汇总
24道JavaScript算法题
24道JavaScript算法题
JavaScript常见问题
JavaScript常见问题
Node中的会话技术
Node中的会话技术
Node中的跨域
Node中的跨域
ArrayBuffer
ArrayBuffer
Class 的基本语法
Class 的基本语法
Class 的继承
Class 的继承
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/page/3/index.html b/archives/2019/page/3/index.html new file mode 100644 index 00000000..80f18f0e --- /dev/null +++ b/archives/2019/page/3/index.html @@ -0,0 +1,279 @@ +2019 | JCAlways + + + + + + + + + + + +
全部文章 - 55
2019
ECMAScript 6 简介
ECMAScript 6 简介
Generator 函数的异步应用
Generator 函数的异步应用
Generator 函数的语法
Generator 函数的语法
Mixin
Mixin
Iterator 和 for...of 循环
Iterator 和 for...of 循环
Module 的加载实现
Module 的加载实现
Module 的语法
Module 的语法
Promise 对象
Promise 对象
Proxy
Proxy
Reflect
Reflect
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/page/4/index.html b/archives/2019/page/4/index.html new file mode 100644 index 00000000..f93f3416 --- /dev/null +++ b/archives/2019/page/4/index.html @@ -0,0 +1,279 @@ +2019 | JCAlways + + + + + + + + + + + +
全部文章 - 55
2019
SIMD
SIMD
Set 和 Map 数据结构
Set 和 Map 数据结构
Symbol
Symbol
async 函数
async 函数
let 和 const 命令
let 和 const 命令
修饰器
修饰器
函数式编程
函数式编程
函数的扩展
函数的扩展
变量的解构赋值
变量的解构赋值
参考链接
参考链接
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/page/5/index.html b/archives/2019/page/5/index.html new file mode 100644 index 00000000..805a5b10 --- /dev/null +++ b/archives/2019/page/5/index.html @@ -0,0 +1,279 @@ +2019 | JCAlways + + + + + + + + + + + +
全部文章 - 55
2019
字符串的扩展
字符串的扩展
对象的扩展
对象的扩展
数值的扩展
数值的扩展
数组的扩展
数组的扩展
最新提案
最新提案
正则的扩展
正则的扩展
编程风格
编程风格
读懂 ECMAScript 规格
读懂 ECMAScript 规格
Express框架
Express框架
Node基础
Node基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2019/page/6/index.html b/archives/2019/page/6/index.html new file mode 100644 index 00000000..79d7c52a --- /dev/null +++ b/archives/2019/page/6/index.html @@ -0,0 +1,279 @@ +2019 | JCAlways + + + + + + + + + + + +
全部文章 - 55
2019
Hexo博客搭建流程
Hexo博客搭建流程
JS高级
JS高级
JQuery基础
JQuery基础
Web Apis
Web Apis
JS基础
JS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2020/01/index.html b/archives/2020/01/index.html new file mode 100644 index 00000000..ae511810 --- /dev/null +++ b/archives/2020/01/index.html @@ -0,0 +1,279 @@ +一月 2020 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2020
React基础
React基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2020/index.html b/archives/2020/index.html new file mode 100644 index 00000000..7bbc097d --- /dev/null +++ b/archives/2020/index.html @@ -0,0 +1,279 @@ +2020 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2020
React基础
React基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2021/03/index.html b/archives/2021/03/index.html new file mode 100644 index 00000000..a1267890 --- /dev/null +++ b/archives/2021/03/index.html @@ -0,0 +1,279 @@ +三月 2021 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2021
TS基础
TS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2021/index.html b/archives/2021/index.html new file mode 100644 index 00000000..ab8d48a5 --- /dev/null +++ b/archives/2021/index.html @@ -0,0 +1,279 @@ +2021 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2021
TS基础
TS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2022/09/index.html b/archives/2022/09/index.html new file mode 100644 index 00000000..c8f8eeee --- /dev/null +++ b/archives/2022/09/index.html @@ -0,0 +1,279 @@ +九月 2022 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2022/10/index.html b/archives/2022/10/index.html new file mode 100644 index 00000000..7000ca09 --- /dev/null +++ b/archives/2022/10/index.html @@ -0,0 +1,279 @@ +十月 2022 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2022/index.html b/archives/2022/index.html new file mode 100644 index 00000000..56920b88 --- /dev/null +++ b/archives/2022/index.html @@ -0,0 +1,279 @@ +2022 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2023/03/index.html b/archives/2023/03/index.html new file mode 100644 index 00000000..487be4ea --- /dev/null +++ b/archives/2023/03/index.html @@ -0,0 +1,279 @@ +三月 2023 | JCAlways + + + + + + + + + + + +
全部文章 - 2
2023
GeeTest3.0使用教程
GeeTest3.0使用教程
GeeTest4.0使用教程
GeeTest4.0使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2023/05/index.html b/archives/2023/05/index.html new file mode 100644 index 00000000..f0d617f1 --- /dev/null +++ b/archives/2023/05/index.html @@ -0,0 +1,279 @@ +五月 2023 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2023
Tauri使用教程
Tauri使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2023/08/index.html b/archives/2023/08/index.html new file mode 100644 index 00000000..c324bac5 --- /dev/null +++ b/archives/2023/08/index.html @@ -0,0 +1,279 @@ +八月 2023 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2023
通行密钥开发 Passkey
通行密钥开发 Passkey
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2023/index.html b/archives/2023/index.html new file mode 100644 index 00000000..14e70a2f --- /dev/null +++ b/archives/2023/index.html @@ -0,0 +1,279 @@ +2023 | JCAlways + + + + + + + + + + + +
全部文章 - 4
2023
通行密钥开发 Passkey
通行密钥开发 Passkey
Tauri使用教程
Tauri使用教程
GeeTest3.0使用教程
GeeTest3.0使用教程
GeeTest4.0使用教程
GeeTest4.0使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2024/04/index.html b/archives/2024/04/index.html new file mode 100644 index 00000000..6d17786f --- /dev/null +++ b/archives/2024/04/index.html @@ -0,0 +1,279 @@ +四月 2024 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2024
Google reCAPTCHA使用教程
Google reCAPTCHA使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2024/09/index.html b/archives/2024/09/index.html new file mode 100644 index 00000000..6a8bf585 --- /dev/null +++ b/archives/2024/09/index.html @@ -0,0 +1,279 @@ +九月 2024 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2024
风灵月影下载
风灵月影下载
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2024/10/index.html b/archives/2024/10/index.html new file mode 100644 index 00000000..e1e89ebf --- /dev/null +++ b/archives/2024/10/index.html @@ -0,0 +1,279 @@ +十月 2024 | JCAlways + + + + + + + + + + + +
全部文章 - 1
2024
PageSpy使用教程
PageSpy使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/2024/index.html b/archives/2024/index.html new file mode 100644 index 00000000..e2e1474f --- /dev/null +++ b/archives/2024/index.html @@ -0,0 +1,279 @@ +2024 | JCAlways + + + + + + + + + + + +
全部文章 - 3
2024
PageSpy使用教程
PageSpy使用教程
风灵月影下载
风灵月影下载
Google reCAPTCHA使用教程
Google reCAPTCHA使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/index.html b/archives/index.html new file mode 100644 index 00000000..427b1ac8 --- /dev/null +++ b/archives/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/2/index.html b/archives/page/2/index.html new file mode 100644 index 00000000..7bb6ff91 --- /dev/null +++ b/archives/page/2/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2022
vue.draggable vue3-拖拽排序组件
vue.draggable vue3-拖拽排序组件
Vue3使用高德地图
Vue3使用高德地图
Nprogress使用教程
Nprogress使用教程
Fingerprintjs使用教程
Fingerprintjs使用教程
2021
TS基础
TS基础
2020
React基础
React基础
2019
Uni-App开发项目
Uni-App开发项目
ESLint
微信小程序基础
微信小程序基础
VueX
VueX
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/3/index.html b/archives/page/3/index.html new file mode 100644 index 00000000..afe82134 --- /dev/null +++ b/archives/page/3/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2019
文章导航
文章导航
Vue的钩子函数
Vue的钩子函数
Vue-Cli的使用
Vue-Cli的使用
Vue基础
Vue基础
MVVM原理
MVVM原理
Jquery的常见问题
Jquery的常见问题
Promise
Promise
Node操作MySQL
Node操作MySQL
Xmind笔记汇总
24道JavaScript算法题
24道JavaScript算法题
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/4/index.html b/archives/page/4/index.html new file mode 100644 index 00000000..c8921d7e --- /dev/null +++ b/archives/page/4/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2019
JavaScript常见问题
JavaScript常见问题
Node中的会话技术
Node中的会话技术
Node中的跨域
Node中的跨域
ArrayBuffer
ArrayBuffer
Class 的基本语法
Class 的基本语法
Class 的继承
Class 的继承
ECMAScript 6 简介
ECMAScript 6 简介
Generator 函数的异步应用
Generator 函数的异步应用
Generator 函数的语法
Generator 函数的语法
Mixin
Mixin
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/5/index.html b/archives/page/5/index.html new file mode 100644 index 00000000..28cdd1db --- /dev/null +++ b/archives/page/5/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2019
Iterator 和 for...of 循环
Iterator 和 for...of 循环
Module 的加载实现
Module 的加载实现
Module 的语法
Module 的语法
Promise 对象
Promise 对象
Proxy
Proxy
Reflect
Reflect
SIMD
SIMD
Set 和 Map 数据结构
Set 和 Map 数据结构
Symbol
Symbol
async 函数
async 函数
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/6/index.html b/archives/page/6/index.html new file mode 100644 index 00000000..2211ddf2 --- /dev/null +++ b/archives/page/6/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2019
let 和 const 命令
let 和 const 命令
修饰器
修饰器
函数式编程
函数式编程
函数的扩展
函数的扩展
变量的解构赋值
变量的解构赋值
参考链接
参考链接
字符串的扩展
字符串的扩展
对象的扩展
对象的扩展
数值的扩展
数值的扩展
数组的扩展
数组的扩展
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/7/index.html b/archives/page/7/index.html new file mode 100644 index 00000000..60f5ac80 --- /dev/null +++ b/archives/page/7/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2019
最新提案
最新提案
正则的扩展
正则的扩展
编程风格
编程风格
读懂 ECMAScript 规格
读懂 ECMAScript 规格
Express框架
Express框架
Node基础
Node基础
Hexo博客搭建流程
Hexo博客搭建流程
JS高级
JS高级
JQuery基础
JQuery基础
Web Apis
Web Apis
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/archives/page/8/index.html b/archives/page/8/index.html new file mode 100644 index 00000000..d27b6ae8 --- /dev/null +++ b/archives/page/8/index.html @@ -0,0 +1,279 @@ +文章总览 | JCAlways + + + + + + + + + + + +
全部文章 - 71
2019
JS基础
JS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/assets/algolia/algoliasearch.js b/assets/algolia/algoliasearch.js new file mode 100644 index 00000000..7707138f --- /dev/null +++ b/assets/algolia/algoliasearch.js @@ -0,0 +1,7190 @@ +/*! algoliasearch 3.35.1 | © 2014, 2015 Algolia SAS | github.com/algolia/algoliasearch-client-js */ +(function(f){var g;if(typeof window!=='undefined'){g=window}else if(typeof self!=='undefined'){g=self}g.ALGOLIA_MIGRATION_LAYER=f()})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;owindow.ALGOLIA_SUPPORTS_DOCWRITE = true\x3C/script>'); + + if (window.ALGOLIA_SUPPORTS_DOCWRITE === true) { + document.write('\x3Cscript src="' + v2ScriptUrl + '">\x3C/script>'); + scriptLoaded('document.write')(); + } else { + loadScript(v2ScriptUrl, scriptLoaded('DOMElement')); + } + } catch (e) { + loadScript(v2ScriptUrl, scriptLoaded('DOMElement')); + } +} + +function scriptLoaded(method) { + return function log() { + var message = 'AlgoliaSearch: loaded V2 script using ' + method; + + if (window.console && window.console.log) { + window.console.log(message); + } + }; +} + +},{"1":1}],4:[function(require,module,exports){ +'use strict'; + +/* eslint no-unused-vars: [2, {"vars": "local"}] */ + +module.exports = oldGlobals; + +// put old window.AlgoliaSearch.. into window. again so that +// users upgrading to V3 without changing their code, will be warned +function oldGlobals() { + var message = '-- AlgoliaSearch V2 => V3 error --\n' + + 'You are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\n' + + 'Please read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n' + + '-- /AlgoliaSearch V2 => V3 error --'; + + window.AlgoliaSearch = function() { + throw new Error(message); + }; + + window.AlgoliaSearchHelper = function() { + throw new Error(message); + }; + + window.AlgoliaExplainResults = function() { + throw new Error(message); + }; +} + +},{}],5:[function(require,module,exports){ +'use strict'; + +// This script will be browserified and prepended to the normal build +// directly in window, not wrapped in any module definition +// To avoid cases where we are loaded with /latest/ along with +migrationLayer("algoliasearch"); + +// Now onto the V2 related code: +// If the client is using /latest/$BUILDNAME.min.js, load V2 of the library +// +// Otherwise, setup a migration layer that will throw on old constructors like +// new AlgoliaSearch(). +// So that users upgrading from v2 to v3 will have a clear information +// message on what to do if they did not read the migration guide +function migrationLayer(buildName) { + var isUsingLatest = require(2); + var loadV2 = require(3); + var oldGlobals = require(4); + + if (isUsingLatest(buildName)) { + loadV2(buildName); + } else { + oldGlobals(); + } +} + +},{"2":2,"3":3,"4":4}]},{},[5])(5) +});(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.algoliasearch = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o= v31, + * and the Firebug extension (any Firefox version) are known + * to support "%c" CSS customizations. + * + * TODO: add a `localStorage` variable to explicitly enable/disable colors + */ + +function useColors() { + // NB: In an Electron preload script, document will be defined but not fully + // initialized. Since we know we're in Chrome, we'll just detect this case + // explicitly + if (typeof window !== 'undefined' && window.process && window.process.type === 'renderer') { + return true; + } + + // is webkit? http://stackoverflow.com/a/16459606/376773 + // document is undefined in react-native: https://github.com/facebook/react-native/pull/1632 + return (typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance) || + // is firebug? http://stackoverflow.com/a/398120/376773 + (typeof window !== 'undefined' && window.console && (window.console.firebug || (window.console.exception && window.console.table))) || + // is firefox >= v31? + // https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31) || + // double check webkit in userAgent just in case we are in a worker + (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/)); +} + +/** + * Map %j to `JSON.stringify()`, since no Web Inspectors do that by default. + */ + +exports.formatters.j = function(v) { + try { + return JSON.stringify(v); + } catch (err) { + return '[UnexpectedJSONParseError]: ' + err.message; + } +}; + + +/** + * Colorize log arguments if enabled. + * + * @api public + */ + +function formatArgs(args) { + var useColors = this.useColors; + + args[0] = (useColors ? '%c' : '') + + this.namespace + + (useColors ? ' %c' : ' ') + + args[0] + + (useColors ? '%c ' : ' ') + + '+' + exports.humanize(this.diff); + + if (!useColors) return; + + var c = 'color: ' + this.color; + args.splice(1, 0, c, 'color: inherit') + + // the final "%c" is somewhat tricky, because there could be other + // arguments passed either before or after the %c, so we need to + // figure out the correct index to insert the CSS into + var index = 0; + var lastC = 0; + args[0].replace(/%[a-zA-Z%]/g, function(match) { + if ('%%' === match) return; + index++; + if ('%c' === match) { + // we only are interested in the *last* %c + // (the user may have provided their own) + lastC = index; + } + }); + + args.splice(lastC, 0, c); +} + +/** + * Invokes `console.log()` when available. + * No-op when `console.log` is not a "function". + * + * @api public + */ + +function log() { + // this hackery is required for IE8/9, where + // the `console.log` function doesn't have 'apply' + return 'object' === typeof console + && console.log + && Function.prototype.apply.call(console.log, console, arguments); +} + +/** + * Save `namespaces`. + * + * @param {String} namespaces + * @api private + */ + +function save(namespaces) { + try { + if (null == namespaces) { + exports.storage.removeItem('debug'); + } else { + exports.storage.debug = namespaces; + } + } catch(e) {} +} + +/** + * Load `namespaces`. + * + * @return {String} returns the previously persisted debug modes + * @api private + */ + +function load() { + var r; + try { + r = exports.storage.debug; + } catch(e) {} + + // If debug isn't set in LS, and we're in Electron, try to load $DEBUG + if (!r && typeof process !== 'undefined' && 'env' in process) { + r = process.env.DEBUG; + } + + return r; +} + +/** + * Enable namespaces listed in `localStorage.debug` initially. + */ + +exports.enable(load()); + +/** + * Localstorage attempts to return the localstorage. + * + * This is necessary because safari throws + * when a user disables cookies/localstorage + * and you attempt to access it. + * + * @return {LocalStorage} + * @api private + */ + +function localstorage() { + try { + return window.localStorage; + } catch (e) {} +} + +}).call(this,require(12)) +},{"12":12,"2":2}],2:[function(require,module,exports){ + +/** + * This is the common logic for both the Node.js and web browser + * implementations of `debug()`. + * + * Expose `debug()` as the module. + */ + +exports = module.exports = createDebug.debug = createDebug['default'] = createDebug; +exports.coerce = coerce; +exports.disable = disable; +exports.enable = enable; +exports.enabled = enabled; +exports.humanize = require(9); + +/** + * The currently active debug mode names, and names to skip. + */ + +exports.names = []; +exports.skips = []; + +/** + * Map of special "%n" handling functions, for the debug "format" argument. + * + * Valid key names are a single, lower or upper-case letter, i.e. "n" and "N". + */ + +exports.formatters = {}; + +/** + * Previous log timestamp. + */ + +var prevTime; + +/** + * Select a color. + * @param {String} namespace + * @return {Number} + * @api private + */ + +function selectColor(namespace) { + var hash = 0, i; + + for (i in namespace) { + hash = ((hash << 5) - hash) + namespace.charCodeAt(i); + hash |= 0; // Convert to 32bit integer + } + + return exports.colors[Math.abs(hash) % exports.colors.length]; +} + +/** + * Create a debugger with the given `namespace`. + * + * @param {String} namespace + * @return {Function} + * @api public + */ + +function createDebug(namespace) { + + function debug() { + // disabled? + if (!debug.enabled) return; + + var self = debug; + + // set `diff` timestamp + var curr = +new Date(); + var ms = curr - (prevTime || curr); + self.diff = ms; + self.prev = prevTime; + self.curr = curr; + prevTime = curr; + + // turn the `arguments` into a proper Array + var args = new Array(arguments.length); + for (var i = 0; i < args.length; i++) { + args[i] = arguments[i]; + } + + args[0] = exports.coerce(args[0]); + + if ('string' !== typeof args[0]) { + // anything else let's inspect with %O + args.unshift('%O'); + } + + // apply any `formatters` transformations + var index = 0; + args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) { + // if we encounter an escaped % then don't increase the array index + if (match === '%%') return match; + index++; + var formatter = exports.formatters[format]; + if ('function' === typeof formatter) { + var val = args[index]; + match = formatter.call(self, val); + + // now we need to remove `args[index]` since it's inlined in the `format` + args.splice(index, 1); + index--; + } + return match; + }); + + // apply env-specific formatting (colors, etc.) + exports.formatArgs.call(self, args); + + var logFn = debug.log || exports.log || console.log.bind(console); + logFn.apply(self, args); + } + + debug.namespace = namespace; + debug.enabled = exports.enabled(namespace); + debug.useColors = exports.useColors(); + debug.color = selectColor(namespace); + + // env-specific initialization logic for debug instances + if ('function' === typeof exports.init) { + exports.init(debug); + } + + return debug; +} + +/** + * Enables a debug mode by namespaces. This can include modes + * separated by a colon and wildcards. + * + * @param {String} namespaces + * @api public + */ + +function enable(namespaces) { + exports.save(namespaces); + + exports.names = []; + exports.skips = []; + + var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/); + var len = split.length; + + for (var i = 0; i < len; i++) { + if (!split[i]) continue; // ignore empty strings + namespaces = split[i].replace(/\*/g, '.*?'); + if (namespaces[0] === '-') { + exports.skips.push(new RegExp('^' + namespaces.substr(1) + '$')); + } else { + exports.names.push(new RegExp('^' + namespaces + '$')); + } + } +} + +/** + * Disable debug output. + * + * @api public + */ + +function disable() { + exports.enable(''); +} + +/** + * Returns true if the given mode name is enabled, false otherwise. + * + * @param {String} name + * @return {Boolean} + * @api public + */ + +function enabled(name) { + var i, len; + for (i = 0, len = exports.skips.length; i < len; i++) { + if (exports.skips[i].test(name)) { + return false; + } + } + for (i = 0, len = exports.names.length; i < len; i++) { + if (exports.names[i].test(name)) { + return true; + } + } + return false; +} + +/** + * Coerce `val`. + * + * @param {Mixed} val + * @return {Mixed} + * @api private + */ + +function coerce(val) { + if (val instanceof Error) return val.stack || val.message; + return val; +} + +},{"9":9}],3:[function(require,module,exports){ +(function (process,global){ +/*! + * @overview es6-promise - a tiny implementation of Promises/A+. + * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) + * @license Licensed under MIT license + * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE + * @version 4.1.1 + */ + +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global.ES6Promise = factory()); +}(this, (function () { 'use strict'; + +function objectOrFunction(x) { + var type = typeof x; + return x !== null && (type === 'object' || type === 'function'); +} + +function isFunction(x) { + return typeof x === 'function'; +} + +var _isArray = undefined; +if (Array.isArray) { + _isArray = Array.isArray; +} else { + _isArray = function (x) { + return Object.prototype.toString.call(x) === '[object Array]'; + }; +} + +var isArray = _isArray; + +var len = 0; +var vertxNext = undefined; +var customSchedulerFn = undefined; + +var asap = function asap(callback, arg) { + queue[len] = callback; + queue[len + 1] = arg; + len += 2; + if (len === 2) { + // If len is 2, that means that we need to schedule an async flush. + // If additional callbacks are queued before the queue is flushed, they + // will be processed by this flush that we are scheduling. + if (customSchedulerFn) { + customSchedulerFn(flush); + } else { + scheduleFlush(); + } + } +}; + +function setScheduler(scheduleFn) { + customSchedulerFn = scheduleFn; +} + +function setAsap(asapFn) { + asap = asapFn; +} + +var browserWindow = typeof window !== 'undefined' ? window : undefined; +var browserGlobal = browserWindow || {}; +var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver; +var isNode = typeof self === 'undefined' && typeof process !== 'undefined' && ({}).toString.call(process) === '[object process]'; + +// test for web worker but not in IE10 +var isWorker = typeof Uint8ClampedArray !== 'undefined' && typeof importScripts !== 'undefined' && typeof MessageChannel !== 'undefined'; + +// node +function useNextTick() { + // node version 0.10.x displays a deprecation warning when nextTick is used recursively + // see https://github.com/cujojs/when/issues/410 for details + return function () { + return process.nextTick(flush); + }; +} + +// vertx +function useVertxTimer() { + if (typeof vertxNext !== 'undefined') { + return function () { + vertxNext(flush); + }; + } + + return useSetTimeout(); +} + +function useMutationObserver() { + var iterations = 0; + var observer = new BrowserMutationObserver(flush); + var node = document.createTextNode(''); + observer.observe(node, { characterData: true }); + + return function () { + node.data = iterations = ++iterations % 2; + }; +} + +// web worker +function useMessageChannel() { + var channel = new MessageChannel(); + channel.port1.onmessage = flush; + return function () { + return channel.port2.postMessage(0); + }; +} + +function useSetTimeout() { + // Store setTimeout reference so es6-promise will be unaffected by + // other code modifying setTimeout (like sinon.useFakeTimers()) + var globalSetTimeout = setTimeout; + return function () { + return globalSetTimeout(flush, 1); + }; +} + +var queue = new Array(1000); +function flush() { + for (var i = 0; i < len; i += 2) { + var callback = queue[i]; + var arg = queue[i + 1]; + + callback(arg); + + queue[i] = undefined; + queue[i + 1] = undefined; + } + + len = 0; +} + +function attemptVertx() { + try { + var r = require; + var vertx = r('vertx'); + vertxNext = vertx.runOnLoop || vertx.runOnContext; + return useVertxTimer(); + } catch (e) { + return useSetTimeout(); + } +} + +var scheduleFlush = undefined; +// Decide what async method to use to triggering processing of queued callbacks: +if (isNode) { + scheduleFlush = useNextTick(); +} else if (BrowserMutationObserver) { + scheduleFlush = useMutationObserver(); +} else if (isWorker) { + scheduleFlush = useMessageChannel(); +} else if (browserWindow === undefined && typeof require === 'function') { + scheduleFlush = attemptVertx(); +} else { + scheduleFlush = useSetTimeout(); +} + +function then(onFulfillment, onRejection) { + var _arguments = arguments; + + var parent = this; + + var child = new this.constructor(noop); + + if (child[PROMISE_ID] === undefined) { + makePromise(child); + } + + var _state = parent._state; + + if (_state) { + (function () { + var callback = _arguments[_state - 1]; + asap(function () { + return invokeCallback(_state, child, callback, parent._result); + }); + })(); + } else { + subscribe(parent, child, onFulfillment, onRejection); + } + + return child; +} + +/** + `Promise.resolve` returns a promise that will become resolved with the + passed `value`. It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + resolve(1); + }); + + promise.then(function(value){ + // value === 1 + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.resolve(1); + + promise.then(function(value){ + // value === 1 + }); + ``` + + @method resolve + @static + @param {Any} value value that the returned promise will be resolved with + Useful for tooling. + @return {Promise} a promise that will become fulfilled with the given + `value` +*/ +function resolve$1(object) { + /*jshint validthis:true */ + var Constructor = this; + + if (object && typeof object === 'object' && object.constructor === Constructor) { + return object; + } + + var promise = new Constructor(noop); + resolve(promise, object); + return promise; +} + +var PROMISE_ID = Math.random().toString(36).substring(16); + +function noop() {} + +var PENDING = void 0; +var FULFILLED = 1; +var REJECTED = 2; + +var GET_THEN_ERROR = new ErrorObject(); + +function selfFulfillment() { + return new TypeError("You cannot resolve a promise with itself"); +} + +function cannotReturnOwn() { + return new TypeError('A promises callback cannot return that same promise.'); +} + +function getThen(promise) { + try { + return promise.then; + } catch (error) { + GET_THEN_ERROR.error = error; + return GET_THEN_ERROR; + } +} + +function tryThen(then$$1, value, fulfillmentHandler, rejectionHandler) { + try { + then$$1.call(value, fulfillmentHandler, rejectionHandler); + } catch (e) { + return e; + } +} + +function handleForeignThenable(promise, thenable, then$$1) { + asap(function (promise) { + var sealed = false; + var error = tryThen(then$$1, thenable, function (value) { + if (sealed) { + return; + } + sealed = true; + if (thenable !== value) { + resolve(promise, value); + } else { + fulfill(promise, value); + } + }, function (reason) { + if (sealed) { + return; + } + sealed = true; + + reject(promise, reason); + }, 'Settle: ' + (promise._label || ' unknown promise')); + + if (!sealed && error) { + sealed = true; + reject(promise, error); + } + }, promise); +} + +function handleOwnThenable(promise, thenable) { + if (thenable._state === FULFILLED) { + fulfill(promise, thenable._result); + } else if (thenable._state === REJECTED) { + reject(promise, thenable._result); + } else { + subscribe(thenable, undefined, function (value) { + return resolve(promise, value); + }, function (reason) { + return reject(promise, reason); + }); + } +} + +function handleMaybeThenable(promise, maybeThenable, then$$1) { + if (maybeThenable.constructor === promise.constructor && then$$1 === then && maybeThenable.constructor.resolve === resolve$1) { + handleOwnThenable(promise, maybeThenable); + } else { + if (then$$1 === GET_THEN_ERROR) { + reject(promise, GET_THEN_ERROR.error); + GET_THEN_ERROR.error = null; + } else if (then$$1 === undefined) { + fulfill(promise, maybeThenable); + } else if (isFunction(then$$1)) { + handleForeignThenable(promise, maybeThenable, then$$1); + } else { + fulfill(promise, maybeThenable); + } + } +} + +function resolve(promise, value) { + if (promise === value) { + reject(promise, selfFulfillment()); + } else if (objectOrFunction(value)) { + handleMaybeThenable(promise, value, getThen(value)); + } else { + fulfill(promise, value); + } +} + +function publishRejection(promise) { + if (promise._onerror) { + promise._onerror(promise._result); + } + + publish(promise); +} + +function fulfill(promise, value) { + if (promise._state !== PENDING) { + return; + } + + promise._result = value; + promise._state = FULFILLED; + + if (promise._subscribers.length !== 0) { + asap(publish, promise); + } +} + +function reject(promise, reason) { + if (promise._state !== PENDING) { + return; + } + promise._state = REJECTED; + promise._result = reason; + + asap(publishRejection, promise); +} + +function subscribe(parent, child, onFulfillment, onRejection) { + var _subscribers = parent._subscribers; + var length = _subscribers.length; + + parent._onerror = null; + + _subscribers[length] = child; + _subscribers[length + FULFILLED] = onFulfillment; + _subscribers[length + REJECTED] = onRejection; + + if (length === 0 && parent._state) { + asap(publish, parent); + } +} + +function publish(promise) { + var subscribers = promise._subscribers; + var settled = promise._state; + + if (subscribers.length === 0) { + return; + } + + var child = undefined, + callback = undefined, + detail = promise._result; + + for (var i = 0; i < subscribers.length; i += 3) { + child = subscribers[i]; + callback = subscribers[i + settled]; + + if (child) { + invokeCallback(settled, child, callback, detail); + } else { + callback(detail); + } + } + + promise._subscribers.length = 0; +} + +function ErrorObject() { + this.error = null; +} + +var TRY_CATCH_ERROR = new ErrorObject(); + +function tryCatch(callback, detail) { + try { + return callback(detail); + } catch (e) { + TRY_CATCH_ERROR.error = e; + return TRY_CATCH_ERROR; + } +} + +function invokeCallback(settled, promise, callback, detail) { + var hasCallback = isFunction(callback), + value = undefined, + error = undefined, + succeeded = undefined, + failed = undefined; + + if (hasCallback) { + value = tryCatch(callback, detail); + + if (value === TRY_CATCH_ERROR) { + failed = true; + error = value.error; + value.error = null; + } else { + succeeded = true; + } + + if (promise === value) { + reject(promise, cannotReturnOwn()); + return; + } + } else { + value = detail; + succeeded = true; + } + + if (promise._state !== PENDING) { + // noop + } else if (hasCallback && succeeded) { + resolve(promise, value); + } else if (failed) { + reject(promise, error); + } else if (settled === FULFILLED) { + fulfill(promise, value); + } else if (settled === REJECTED) { + reject(promise, value); + } +} + +function initializePromise(promise, resolver) { + try { + resolver(function resolvePromise(value) { + resolve(promise, value); + }, function rejectPromise(reason) { + reject(promise, reason); + }); + } catch (e) { + reject(promise, e); + } +} + +var id = 0; +function nextId() { + return id++; +} + +function makePromise(promise) { + promise[PROMISE_ID] = id++; + promise._state = undefined; + promise._result = undefined; + promise._subscribers = []; +} + +function Enumerator$1(Constructor, input) { + this._instanceConstructor = Constructor; + this.promise = new Constructor(noop); + + if (!this.promise[PROMISE_ID]) { + makePromise(this.promise); + } + + if (isArray(input)) { + this.length = input.length; + this._remaining = input.length; + + this._result = new Array(this.length); + + if (this.length === 0) { + fulfill(this.promise, this._result); + } else { + this.length = this.length || 0; + this._enumerate(input); + if (this._remaining === 0) { + fulfill(this.promise, this._result); + } + } + } else { + reject(this.promise, validationError()); + } +} + +function validationError() { + return new Error('Array Methods must be provided an Array'); +} + +Enumerator$1.prototype._enumerate = function (input) { + for (var i = 0; this._state === PENDING && i < input.length; i++) { + this._eachEntry(input[i], i); + } +}; + +Enumerator$1.prototype._eachEntry = function (entry, i) { + var c = this._instanceConstructor; + var resolve$$1 = c.resolve; + + if (resolve$$1 === resolve$1) { + var _then = getThen(entry); + + if (_then === then && entry._state !== PENDING) { + this._settledAt(entry._state, i, entry._result); + } else if (typeof _then !== 'function') { + this._remaining--; + this._result[i] = entry; + } else if (c === Promise$2) { + var promise = new c(noop); + handleMaybeThenable(promise, entry, _then); + this._willSettleAt(promise, i); + } else { + this._willSettleAt(new c(function (resolve$$1) { + return resolve$$1(entry); + }), i); + } + } else { + this._willSettleAt(resolve$$1(entry), i); + } +}; + +Enumerator$1.prototype._settledAt = function (state, i, value) { + var promise = this.promise; + + if (promise._state === PENDING) { + this._remaining--; + + if (state === REJECTED) { + reject(promise, value); + } else { + this._result[i] = value; + } + } + + if (this._remaining === 0) { + fulfill(promise, this._result); + } +}; + +Enumerator$1.prototype._willSettleAt = function (promise, i) { + var enumerator = this; + + subscribe(promise, undefined, function (value) { + return enumerator._settledAt(FULFILLED, i, value); + }, function (reason) { + return enumerator._settledAt(REJECTED, i, reason); + }); +}; + +/** + `Promise.all` accepts an array of promises, and returns a new promise which + is fulfilled with an array of fulfillment values for the passed promises, or + rejected with the reason of the first passed promise to be rejected. It casts all + elements of the passed iterable to promises as it runs this algorithm. + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = resolve(2); + let promise3 = resolve(3); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // The array here would be [ 1, 2, 3 ]; + }); + ``` + + If any of the `promises` given to `all` are rejected, the first promise + that is rejected will be given as an argument to the returned promises's + rejection handler. For example: + + Example: + + ```javascript + let promise1 = resolve(1); + let promise2 = reject(new Error("2")); + let promise3 = reject(new Error("3")); + let promises = [ promise1, promise2, promise3 ]; + + Promise.all(promises).then(function(array){ + // Code here never runs because there are rejected promises! + }, function(error) { + // error.message === "2" + }); + ``` + + @method all + @static + @param {Array} entries array of promises + @param {String} label optional string for labeling the promise. + Useful for tooling. + @return {Promise} promise that is fulfilled when all `promises` have been + fulfilled, or rejected if any of them become rejected. + @static +*/ +function all$1(entries) { + return new Enumerator$1(this, entries).promise; +} + +/** + `Promise.race` returns a new promise which is settled in the same way as the + first passed promise to settle. + + Example: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 2'); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // result === 'promise 2' because it was resolved before promise1 + // was resolved. + }); + ``` + + `Promise.race` is deterministic in that only the state of the first + settled promise matters. For example, even if other promises given to the + `promises` array argument are resolved, but the first settled promise has + become rejected before the other promises became fulfilled, the returned + promise will become rejected: + + ```javascript + let promise1 = new Promise(function(resolve, reject){ + setTimeout(function(){ + resolve('promise 1'); + }, 200); + }); + + let promise2 = new Promise(function(resolve, reject){ + setTimeout(function(){ + reject(new Error('promise 2')); + }, 100); + }); + + Promise.race([promise1, promise2]).then(function(result){ + // Code here never runs + }, function(reason){ + // reason.message === 'promise 2' because promise 2 became rejected before + // promise 1 became fulfilled + }); + ``` + + An example real-world use case is implementing timeouts: + + ```javascript + Promise.race([ajax('foo.json'), timeout(5000)]) + ``` + + @method race + @static + @param {Array} promises array of promises to observe + Useful for tooling. + @return {Promise} a promise which settles in the same way as the first passed + promise to settle. +*/ +function race$1(entries) { + /*jshint validthis:true */ + var Constructor = this; + + if (!isArray(entries)) { + return new Constructor(function (_, reject) { + return reject(new TypeError('You must pass an array to race.')); + }); + } else { + return new Constructor(function (resolve, reject) { + var length = entries.length; + for (var i = 0; i < length; i++) { + Constructor.resolve(entries[i]).then(resolve, reject); + } + }); + } +} + +/** + `Promise.reject` returns a promise rejected with the passed `reason`. + It is shorthand for the following: + + ```javascript + let promise = new Promise(function(resolve, reject){ + reject(new Error('WHOOPS')); + }); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + Instead of writing the above, your code now simply becomes the following: + + ```javascript + let promise = Promise.reject(new Error('WHOOPS')); + + promise.then(function(value){ + // Code here doesn't run because the promise is rejected! + }, function(reason){ + // reason.message === 'WHOOPS' + }); + ``` + + @method reject + @static + @param {Any} reason value that the returned promise will be rejected with. + Useful for tooling. + @return {Promise} a promise rejected with the given `reason`. +*/ +function reject$1(reason) { + /*jshint validthis:true */ + var Constructor = this; + var promise = new Constructor(noop); + reject(promise, reason); + return promise; +} + +function needsResolver() { + throw new TypeError('You must pass a resolver function as the first argument to the promise constructor'); +} + +function needsNew() { + throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function."); +} + +/** + Promise objects represent the eventual result of an asynchronous operation. The + primary way of interacting with a promise is through its `then` method, which + registers callbacks to receive either a promise's eventual value or the reason + why the promise cannot be fulfilled. + + Terminology + ----------- + + - `promise` is an object or function with a `then` method whose behavior conforms to this specification. + - `thenable` is an object or function that defines a `then` method. + - `value` is any legal JavaScript value (including undefined, a thenable, or a promise). + - `exception` is a value that is thrown using the throw statement. + - `reason` is a value that indicates why a promise was rejected. + - `settled` the final resting state of a promise, fulfilled or rejected. + + A promise can be in one of three states: pending, fulfilled, or rejected. + + Promises that are fulfilled have a fulfillment value and are in the fulfilled + state. Promises that are rejected have a rejection reason and are in the + rejected state. A fulfillment value is never a thenable. + + Promises can also be said to *resolve* a value. If this value is also a + promise, then the original promise's settled state will match the value's + settled state. So a promise that *resolves* a promise that rejects will + itself reject, and a promise that *resolves* a promise that fulfills will + itself fulfill. + + + Basic Usage: + ------------ + + ```js + let promise = new Promise(function(resolve, reject) { + // on success + resolve(value); + + // on failure + reject(reason); + }); + + promise.then(function(value) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Advanced Usage: + --------------- + + Promises shine when abstracting away asynchronous interactions such as + `XMLHttpRequest`s. + + ```js + function getJSON(url) { + return new Promise(function(resolve, reject){ + let xhr = new XMLHttpRequest(); + + xhr.open('GET', url); + xhr.onreadystatechange = handler; + xhr.responseType = 'json'; + xhr.setRequestHeader('Accept', 'application/json'); + xhr.send(); + + function handler() { + if (this.readyState === this.DONE) { + if (this.status === 200) { + resolve(this.response); + } else { + reject(new Error('getJSON: `' + url + '` failed with status: [' + this.status + ']')); + } + } + }; + }); + } + + getJSON('/posts.json').then(function(json) { + // on fulfillment + }, function(reason) { + // on rejection + }); + ``` + + Unlike callbacks, promises are great composable primitives. + + ```js + Promise.all([ + getJSON('/posts'), + getJSON('/comments') + ]).then(function(values){ + values[0] // => postsJSON + values[1] // => commentsJSON + + return values; + }); + ``` + + @class Promise + @param {function} resolver + Useful for tooling. + @constructor +*/ +function Promise$2(resolver) { + this[PROMISE_ID] = nextId(); + this._result = this._state = undefined; + this._subscribers = []; + + if (noop !== resolver) { + typeof resolver !== 'function' && needsResolver(); + this instanceof Promise$2 ? initializePromise(this, resolver) : needsNew(); + } +} + +Promise$2.all = all$1; +Promise$2.race = race$1; +Promise$2.resolve = resolve$1; +Promise$2.reject = reject$1; +Promise$2._setScheduler = setScheduler; +Promise$2._setAsap = setAsap; +Promise$2._asap = asap; + +Promise$2.prototype = { + constructor: Promise$2, + + /** + The primary way of interacting with a promise is through its `then` method, + which registers callbacks to receive either a promise's eventual value or the + reason why the promise cannot be fulfilled. + + ```js + findUser().then(function(user){ + // user is available + }, function(reason){ + // user is unavailable, and you are given the reason why + }); + ``` + + Chaining + -------- + + The return value of `then` is itself a promise. This second, 'downstream' + promise is resolved with the return value of the first promise's fulfillment + or rejection handler, or rejected if the handler throws an exception. + + ```js + findUser().then(function (user) { + return user.name; + }, function (reason) { + return 'default name'; + }).then(function (userName) { + // If `findUser` fulfilled, `userName` will be the user's name, otherwise it + // will be `'default name'` + }); + + findUser().then(function (user) { + throw new Error('Found user, but still unhappy'); + }, function (reason) { + throw new Error('`findUser` rejected and we're unhappy'); + }).then(function (value) { + // never reached + }, function (reason) { + // if `findUser` fulfilled, `reason` will be 'Found user, but still unhappy'. + // If `findUser` rejected, `reason` will be '`findUser` rejected and we're unhappy'. + }); + ``` + If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream. + + ```js + findUser().then(function (user) { + throw new PedagogicalException('Upstream error'); + }).then(function (value) { + // never reached + }).then(function (value) { + // never reached + }, function (reason) { + // The `PedgagocialException` is propagated all the way down to here + }); + ``` + + Assimilation + ------------ + + Sometimes the value you want to propagate to a downstream promise can only be + retrieved asynchronously. This can be achieved by returning a promise in the + fulfillment or rejection handler. The downstream promise will then be pending + until the returned promise is settled. This is called *assimilation*. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // The user's comments are now available + }); + ``` + + If the assimliated promise rejects, then the downstream promise will also reject. + + ```js + findUser().then(function (user) { + return findCommentsByAuthor(user); + }).then(function (comments) { + // If `findCommentsByAuthor` fulfills, we'll have the value here + }, function (reason) { + // If `findCommentsByAuthor` rejects, we'll have the reason here + }); + ``` + + Simple Example + -------------- + + Synchronous Example + + ```javascript + let result; + + try { + result = findResult(); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + findResult(function(result, err){ + if (err) { + // failure + } else { + // success + } + }); + ``` + + Promise Example; + + ```javascript + findResult().then(function(result){ + // success + }, function(reason){ + // failure + }); + ``` + + Advanced Example + -------------- + + Synchronous Example + + ```javascript + let author, books; + + try { + author = findAuthor(); + books = findBooksByAuthor(author); + // success + } catch(reason) { + // failure + } + ``` + + Errback Example + + ```js + + function foundBooks(books) { + + } + + function failure(reason) { + + } + + findAuthor(function(author, err){ + if (err) { + failure(err); + // failure + } else { + try { + findBoooksByAuthor(author, function(books, err) { + if (err) { + failure(err); + } else { + try { + foundBooks(books); + } catch(reason) { + failure(reason); + } + } + }); + } catch(error) { + failure(err); + } + // success + } + }); + ``` + + Promise Example; + + ```javascript + findAuthor(). + then(findBooksByAuthor). + then(function(books){ + // found books + }).catch(function(reason){ + // something went wrong + }); + ``` + + @method then + @param {Function} onFulfilled + @param {Function} onRejected + Useful for tooling. + @return {Promise} + */ + then: then, + + /** + `catch` is simply sugar for `then(undefined, onRejection)` which makes it the same + as the catch block of a try/catch statement. + + ```js + function findAuthor(){ + throw new Error('couldn't find that author'); + } + + // synchronous + try { + findAuthor(); + } catch(reason) { + // something went wrong + } + + // async with promises + findAuthor().catch(function(reason){ + // something went wrong + }); + ``` + + @method catch + @param {Function} onRejection + Useful for tooling. + @return {Promise} + */ + 'catch': function _catch(onRejection) { + return this.then(null, onRejection); + } +}; + +/*global self*/ +function polyfill$1() { + var local = undefined; + + if (typeof global !== 'undefined') { + local = global; + } else if (typeof self !== 'undefined') { + local = self; + } else { + try { + local = Function('return this')(); + } catch (e) { + throw new Error('polyfill failed because global object is unavailable in this environment'); + } + } + + var P = local.Promise; + + if (P) { + var promiseToString = null; + try { + promiseToString = Object.prototype.toString.call(P.resolve()); + } catch (e) { + // silently ignored + } + + if (promiseToString === '[object Promise]' && !P.cast) { + return; + } + } + + local.Promise = Promise$2; +} + +// Strange compat.. +Promise$2.polyfill = polyfill$1; +Promise$2.Promise = Promise$2; + +return Promise$2; + +}))); + + + +}).call(this,require(12),typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{"12":12}],4:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +function EventEmitter() { + this._events = this._events || {}; + this._maxListeners = this._maxListeners || undefined; +} +module.exports = EventEmitter; + +// Backwards-compat with node 0.10.x +EventEmitter.EventEmitter = EventEmitter; + +EventEmitter.prototype._events = undefined; +EventEmitter.prototype._maxListeners = undefined; + +// By default EventEmitters will print a warning if more than 10 listeners are +// added to it. This is a useful default which helps finding memory leaks. +EventEmitter.defaultMaxListeners = 10; + +// Obviously not all Emitters should be limited to 10. This function allows +// that to be increased. Set to zero for unlimited. +EventEmitter.prototype.setMaxListeners = function(n) { + if (!isNumber(n) || n < 0 || isNaN(n)) + throw TypeError('n must be a positive number'); + this._maxListeners = n; + return this; +}; + +EventEmitter.prototype.emit = function(type) { + var er, handler, len, args, i, listeners; + + if (!this._events) + this._events = {}; + + // If there is no 'error' event listener then throw. + if (type === 'error') { + if (!this._events.error || + (isObject(this._events.error) && !this._events.error.length)) { + er = arguments[1]; + if (er instanceof Error) { + throw er; // Unhandled 'error' event + } else { + // At least give some kind of context to the user + var err = new Error('Uncaught, unspecified "error" event. (' + er + ')'); + err.context = er; + throw err; + } + } + } + + handler = this._events[type]; + + if (isUndefined(handler)) + return false; + + if (isFunction(handler)) { + switch (arguments.length) { + // fast cases + case 1: + handler.call(this); + break; + case 2: + handler.call(this, arguments[1]); + break; + case 3: + handler.call(this, arguments[1], arguments[2]); + break; + // slower + default: + args = Array.prototype.slice.call(arguments, 1); + handler.apply(this, args); + } + } else if (isObject(handler)) { + args = Array.prototype.slice.call(arguments, 1); + listeners = handler.slice(); + len = listeners.length; + for (i = 0; i < len; i++) + listeners[i].apply(this, args); + } + + return true; +}; + +EventEmitter.prototype.addListener = function(type, listener) { + var m; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events) + this._events = {}; + + // To avoid recursion in the case that type === "newListener"! Before + // adding it to the listeners, first emit "newListener". + if (this._events.newListener) + this.emit('newListener', type, + isFunction(listener.listener) ? + listener.listener : listener); + + if (!this._events[type]) + // Optimize the case of one listener. Don't need the extra array object. + this._events[type] = listener; + else if (isObject(this._events[type])) + // If we've already got an array, just append. + this._events[type].push(listener); + else + // Adding the second element, need to change to array. + this._events[type] = [this._events[type], listener]; + + // Check for listener leak + if (isObject(this._events[type]) && !this._events[type].warned) { + if (!isUndefined(this._maxListeners)) { + m = this._maxListeners; + } else { + m = EventEmitter.defaultMaxListeners; + } + + if (m && m > 0 && this._events[type].length > m) { + this._events[type].warned = true; + console.error('(node) warning: possible EventEmitter memory ' + + 'leak detected. %d listeners added. ' + + 'Use emitter.setMaxListeners() to increase limit.', + this._events[type].length); + if (typeof console.trace === 'function') { + // not supported in IE 10 + console.trace(); + } + } + } + + return this; +}; + +EventEmitter.prototype.on = EventEmitter.prototype.addListener; + +EventEmitter.prototype.once = function(type, listener) { + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + var fired = false; + + function g() { + this.removeListener(type, g); + + if (!fired) { + fired = true; + listener.apply(this, arguments); + } + } + + g.listener = listener; + this.on(type, g); + + return this; +}; + +// emits a 'removeListener' event iff the listener was removed +EventEmitter.prototype.removeListener = function(type, listener) { + var list, position, length, i; + + if (!isFunction(listener)) + throw TypeError('listener must be a function'); + + if (!this._events || !this._events[type]) + return this; + + list = this._events[type]; + length = list.length; + position = -1; + + if (list === listener || + (isFunction(list.listener) && list.listener === listener)) { + delete this._events[type]; + if (this._events.removeListener) + this.emit('removeListener', type, listener); + + } else if (isObject(list)) { + for (i = length; i-- > 0;) { + if (list[i] === listener || + (list[i].listener && list[i].listener === listener)) { + position = i; + break; + } + } + + if (position < 0) + return this; + + if (list.length === 1) { + list.length = 0; + delete this._events[type]; + } else { + list.splice(position, 1); + } + + if (this._events.removeListener) + this.emit('removeListener', type, listener); + } + + return this; +}; + +EventEmitter.prototype.removeAllListeners = function(type) { + var key, listeners; + + if (!this._events) + return this; + + // not listening for removeListener, no need to emit + if (!this._events.removeListener) { + if (arguments.length === 0) + this._events = {}; + else if (this._events[type]) + delete this._events[type]; + return this; + } + + // emit removeListener for all listeners on all events + if (arguments.length === 0) { + for (key in this._events) { + if (key === 'removeListener') continue; + this.removeAllListeners(key); + } + this.removeAllListeners('removeListener'); + this._events = {}; + return this; + } + + listeners = this._events[type]; + + if (isFunction(listeners)) { + this.removeListener(type, listeners); + } else if (listeners) { + // LIFO order + while (listeners.length) + this.removeListener(type, listeners[listeners.length - 1]); + } + delete this._events[type]; + + return this; +}; + +EventEmitter.prototype.listeners = function(type) { + var ret; + if (!this._events || !this._events[type]) + ret = []; + else if (isFunction(this._events[type])) + ret = [this._events[type]]; + else + ret = this._events[type].slice(); + return ret; +}; + +EventEmitter.prototype.listenerCount = function(type) { + if (this._events) { + var evlistener = this._events[type]; + + if (isFunction(evlistener)) + return 1; + else if (evlistener) + return evlistener.length; + } + return 0; +}; + +EventEmitter.listenerCount = function(emitter, type) { + return emitter.listenerCount(type); +}; + +function isFunction(arg) { + return typeof arg === 'function'; +} + +function isNumber(arg) { + return typeof arg === 'number'; +} + +function isObject(arg) { + return typeof arg === 'object' && arg !== null; +} + +function isUndefined(arg) { + return arg === void 0; +} + +},{}],5:[function(require,module,exports){ + +var hasOwn = Object.prototype.hasOwnProperty; +var toString = Object.prototype.toString; + +module.exports = function forEach (obj, fn, ctx) { + if (toString.call(fn) !== '[object Function]') { + throw new TypeError('iterator must be a function'); + } + var l = obj.length; + if (l === +l) { + for (var i = 0; i < l; i++) { + fn.call(ctx, obj[i], i, obj); + } + } else { + for (var k in obj) { + if (hasOwn.call(obj, k)) { + fn.call(ctx, obj[k], k, obj); + } + } + } +}; + + +},{}],6:[function(require,module,exports){ +(function (global){ +var win; + +if (typeof window !== "undefined") { + win = window; +} else if (typeof global !== "undefined") { + win = global; +} else if (typeof self !== "undefined"){ + win = self; +} else { + win = {}; +} + +module.exports = win; + +}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) +},{}],7:[function(require,module,exports){ +if (typeof Object.create === 'function') { + // implementation from standard node.js 'util' module + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + ctor.prototype = Object.create(superCtor.prototype, { + constructor: { + value: ctor, + enumerable: false, + writable: true, + configurable: true + } + }); + }; +} else { + // old school shim for old browsers + module.exports = function inherits(ctor, superCtor) { + ctor.super_ = superCtor + var TempCtor = function () {} + TempCtor.prototype = superCtor.prototype + ctor.prototype = new TempCtor() + ctor.prototype.constructor = ctor + } +} + +},{}],8:[function(require,module,exports){ +var toString = {}.toString; + +module.exports = Array.isArray || function (arr) { + return toString.call(arr) == '[object Array]'; +}; + +},{}],9:[function(require,module,exports){ +/** + * Helpers. + */ + +var s = 1000; +var m = s * 60; +var h = m * 60; +var d = h * 24; +var y = d * 365.25; + +/** + * Parse or format the given `val`. + * + * Options: + * + * - `long` verbose formatting [false] + * + * @param {String|Number} val + * @param {Object} [options] + * @throws {Error} throw an error if val is not a non-empty string or a number + * @return {String|Number} + * @api public + */ + +module.exports = function(val, options) { + options = options || {}; + var type = typeof val; + if (type === 'string' && val.length > 0) { + return parse(val); + } else if (type === 'number' && isNaN(val) === false) { + return options.long ? fmtLong(val) : fmtShort(val); + } + throw new Error( + 'val is not a non-empty string or a valid number. val=' + + JSON.stringify(val) + ); +}; + +/** + * Parse the given `str` and return milliseconds. + * + * @param {String} str + * @return {Number} + * @api private + */ + +function parse(str) { + str = String(str); + if (str.length > 100) { + return; + } + var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec( + str + ); + if (!match) { + return; + } + var n = parseFloat(match[1]); + var type = (match[2] || 'ms').toLowerCase(); + switch (type) { + case 'years': + case 'year': + case 'yrs': + case 'yr': + case 'y': + return n * y; + case 'days': + case 'day': + case 'd': + return n * d; + case 'hours': + case 'hour': + case 'hrs': + case 'hr': + case 'h': + return n * h; + case 'minutes': + case 'minute': + case 'mins': + case 'min': + case 'm': + return n * m; + case 'seconds': + case 'second': + case 'secs': + case 'sec': + case 's': + return n * s; + case 'milliseconds': + case 'millisecond': + case 'msecs': + case 'msec': + case 'ms': + return n; + default: + return undefined; + } +} + +/** + * Short format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtShort(ms) { + if (ms >= d) { + return Math.round(ms / d) + 'd'; + } + if (ms >= h) { + return Math.round(ms / h) + 'h'; + } + if (ms >= m) { + return Math.round(ms / m) + 'm'; + } + if (ms >= s) { + return Math.round(ms / s) + 's'; + } + return ms + 'ms'; +} + +/** + * Long format for `ms`. + * + * @param {Number} ms + * @return {String} + * @api private + */ + +function fmtLong(ms) { + return plural(ms, d, 'day') || + plural(ms, h, 'hour') || + plural(ms, m, 'minute') || + plural(ms, s, 'second') || + ms + ' ms'; +} + +/** + * Pluralization helper. + */ + +function plural(ms, n, name) { + if (ms < n) { + return; + } + if (ms < n * 1.5) { + return Math.floor(ms / n) + ' ' + name; + } + return Math.ceil(ms / n) + ' ' + name + 's'; +} + +},{}],10:[function(require,module,exports){ +'use strict'; + +// modified from https://github.com/es-shims/es5-shim +var has = Object.prototype.hasOwnProperty; +var toStr = Object.prototype.toString; +var slice = Array.prototype.slice; +var isArgs = require(11); +var isEnumerable = Object.prototype.propertyIsEnumerable; +var hasDontEnumBug = !isEnumerable.call({ toString: null }, 'toString'); +var hasProtoEnumBug = isEnumerable.call(function () {}, 'prototype'); +var dontEnums = [ + 'toString', + 'toLocaleString', + 'valueOf', + 'hasOwnProperty', + 'isPrototypeOf', + 'propertyIsEnumerable', + 'constructor' +]; +var equalsConstructorPrototype = function (o) { + var ctor = o.constructor; + return ctor && ctor.prototype === o; +}; +var excludedKeys = { + $console: true, + $external: true, + $frame: true, + $frameElement: true, + $frames: true, + $innerHeight: true, + $innerWidth: true, + $outerHeight: true, + $outerWidth: true, + $pageXOffset: true, + $pageYOffset: true, + $parent: true, + $scrollLeft: true, + $scrollTop: true, + $scrollX: true, + $scrollY: true, + $self: true, + $webkitIndexedDB: true, + $webkitStorageInfo: true, + $window: true +}; +var hasAutomationEqualityBug = (function () { + /* global window */ + if (typeof window === 'undefined') { return false; } + for (var k in window) { + try { + if (!excludedKeys['$' + k] && has.call(window, k) && window[k] !== null && typeof window[k] === 'object') { + try { + equalsConstructorPrototype(window[k]); + } catch (e) { + return true; + } + } + } catch (e) { + return true; + } + } + return false; +}()); +var equalsConstructorPrototypeIfNotBuggy = function (o) { + /* global window */ + if (typeof window === 'undefined' || !hasAutomationEqualityBug) { + return equalsConstructorPrototype(o); + } + try { + return equalsConstructorPrototype(o); + } catch (e) { + return false; + } +}; + +var keysShim = function keys(object) { + var isObject = object !== null && typeof object === 'object'; + var isFunction = toStr.call(object) === '[object Function]'; + var isArguments = isArgs(object); + var isString = isObject && toStr.call(object) === '[object String]'; + var theKeys = []; + + if (!isObject && !isFunction && !isArguments) { + throw new TypeError('Object.keys called on a non-object'); + } + + var skipProto = hasProtoEnumBug && isFunction; + if (isString && object.length > 0 && !has.call(object, 0)) { + for (var i = 0; i < object.length; ++i) { + theKeys.push(String(i)); + } + } + + if (isArguments && object.length > 0) { + for (var j = 0; j < object.length; ++j) { + theKeys.push(String(j)); + } + } else { + for (var name in object) { + if (!(skipProto && name === 'prototype') && has.call(object, name)) { + theKeys.push(String(name)); + } + } + } + + if (hasDontEnumBug) { + var skipConstructor = equalsConstructorPrototypeIfNotBuggy(object); + + for (var k = 0; k < dontEnums.length; ++k) { + if (!(skipConstructor && dontEnums[k] === 'constructor') && has.call(object, dontEnums[k])) { + theKeys.push(dontEnums[k]); + } + } + } + return theKeys; +}; + +keysShim.shim = function shimObjectKeys() { + if (Object.keys) { + var keysWorksWithArguments = (function () { + // Safari 5.0 bug + return (Object.keys(arguments) || '').length === 2; + }(1, 2)); + if (!keysWorksWithArguments) { + var originalKeys = Object.keys; + Object.keys = function keys(object) { + if (isArgs(object)) { + return originalKeys(slice.call(object)); + } else { + return originalKeys(object); + } + }; + } + } else { + Object.keys = keysShim; + } + return Object.keys || keysShim; +}; + +module.exports = keysShim; + +},{"11":11}],11:[function(require,module,exports){ +'use strict'; + +var toStr = Object.prototype.toString; + +module.exports = function isArguments(value) { + var str = toStr.call(value); + var isArgs = str === '[object Arguments]'; + if (!isArgs) { + isArgs = str !== '[object Array]' && + value !== null && + typeof value === 'object' && + typeof value.length === 'number' && + value.length >= 0 && + toStr.call(value.callee) === '[object Function]'; + } + return isArgs; +}; + +},{}],12:[function(require,module,exports){ +// shim for using process in browser +var process = module.exports = {}; + +// cached from whatever global is present so that test runners that stub it +// don't break things. But we need to wrap it in a try catch in case it is +// wrapped in strict mode code which doesn't define any globals. It's inside a +// function because try/catches deoptimize in certain engines. + +var cachedSetTimeout; +var cachedClearTimeout; + +function defaultSetTimout() { + throw new Error('setTimeout has not been defined'); +} +function defaultClearTimeout () { + throw new Error('clearTimeout has not been defined'); +} +(function () { + try { + if (typeof setTimeout === 'function') { + cachedSetTimeout = setTimeout; + } else { + cachedSetTimeout = defaultSetTimout; + } + } catch (e) { + cachedSetTimeout = defaultSetTimout; + } + try { + if (typeof clearTimeout === 'function') { + cachedClearTimeout = clearTimeout; + } else { + cachedClearTimeout = defaultClearTimeout; + } + } catch (e) { + cachedClearTimeout = defaultClearTimeout; + } +} ()) +function runTimeout(fun) { + if (cachedSetTimeout === setTimeout) { + //normal enviroments in sane situations + return setTimeout(fun, 0); + } + // if setTimeout wasn't available but was latter defined + if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) { + cachedSetTimeout = setTimeout; + return setTimeout(fun, 0); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedSetTimeout(fun, 0); + } catch(e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedSetTimeout.call(null, fun, 0); + } catch(e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error + return cachedSetTimeout.call(this, fun, 0); + } + } + + +} +function runClearTimeout(marker) { + if (cachedClearTimeout === clearTimeout) { + //normal enviroments in sane situations + return clearTimeout(marker); + } + // if clearTimeout wasn't available but was latter defined + if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) { + cachedClearTimeout = clearTimeout; + return clearTimeout(marker); + } + try { + // when when somebody has screwed with setTimeout but no I.E. maddness + return cachedClearTimeout(marker); + } catch (e){ + try { + // When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally + return cachedClearTimeout.call(null, marker); + } catch (e){ + // same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error. + // Some versions of I.E. have different rules for clearTimeout vs setTimeout + return cachedClearTimeout.call(this, marker); + } + } + + + +} +var queue = []; +var draining = false; +var currentQueue; +var queueIndex = -1; + +function cleanUpNextTick() { + if (!draining || !currentQueue) { + return; + } + draining = false; + if (currentQueue.length) { + queue = currentQueue.concat(queue); + } else { + queueIndex = -1; + } + if (queue.length) { + drainQueue(); + } +} + +function drainQueue() { + if (draining) { + return; + } + var timeout = runTimeout(cleanUpNextTick); + draining = true; + + var len = queue.length; + while(len) { + currentQueue = queue; + queue = []; + while (++queueIndex < len) { + if (currentQueue) { + currentQueue[queueIndex].run(); + } + } + queueIndex = -1; + len = queue.length; + } + currentQueue = null; + draining = false; + runClearTimeout(timeout); +} + +process.nextTick = function (fun) { + var args = new Array(arguments.length - 1); + if (arguments.length > 1) { + for (var i = 1; i < arguments.length; i++) { + args[i - 1] = arguments[i]; + } + } + queue.push(new Item(fun, args)); + if (queue.length === 1 && !draining) { + runTimeout(drainQueue); + } +}; + +// v8 likes predictible objects +function Item(fun, array) { + this.fun = fun; + this.array = array; +} +Item.prototype.run = function () { + this.fun.apply(null, this.array); +}; +process.title = 'browser'; +process.browser = true; +process.env = {}; +process.argv = []; +process.version = ''; // empty string to avoid regexp issues +process.versions = {}; + +function noop() {} + +process.on = noop; +process.addListener = noop; +process.once = noop; +process.off = noop; +process.removeListener = noop; +process.removeAllListeners = noop; +process.emit = noop; + +process.binding = function (name) { + throw new Error('process.binding is not supported'); +}; + +process.cwd = function () { return '/' }; +process.chdir = function (dir) { + throw new Error('process.chdir is not supported'); +}; +process.umask = function() { return 0; }; + +},{}],13:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +// If obj.hasOwnProperty has been overridden, then calling +// obj.hasOwnProperty(prop) will break. +// See: https://github.com/joyent/node/issues/1707 +function hasOwnProperty(obj, prop) { + return Object.prototype.hasOwnProperty.call(obj, prop); +} + +module.exports = function(qs, sep, eq, options) { + sep = sep || '&'; + eq = eq || '='; + var obj = {}; + + if (typeof qs !== 'string' || qs.length === 0) { + return obj; + } + + var regexp = /\+/g; + qs = qs.split(sep); + + var maxKeys = 1000; + if (options && typeof options.maxKeys === 'number') { + maxKeys = options.maxKeys; + } + + var len = qs.length; + // maxKeys <= 0 means that we should not limit keys count + if (maxKeys > 0 && len > maxKeys) { + len = maxKeys; + } + + for (var i = 0; i < len; ++i) { + var x = qs[i].replace(regexp, '%20'), + idx = x.indexOf(eq), + kstr, vstr, k, v; + + if (idx >= 0) { + kstr = x.substr(0, idx); + vstr = x.substr(idx + 1); + } else { + kstr = x; + vstr = ''; + } + + k = decodeURIComponent(kstr); + v = decodeURIComponent(vstr); + + if (!hasOwnProperty(obj, k)) { + obj[k] = v; + } else if (isArray(obj[k])) { + obj[k].push(v); + } else { + obj[k] = [obj[k], v]; + } + } + + return obj; +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +},{}],14:[function(require,module,exports){ +// Copyright Joyent, Inc. and other Node contributors. +// +// Permission is hereby granted, free of charge, to any person obtaining a +// copy of this software and associated documentation files (the +// "Software"), to deal in the Software without restriction, including +// without limitation the rights to use, copy, modify, merge, publish, +// distribute, sublicense, and/or sell copies of the Software, and to permit +// persons to whom the Software is furnished to do so, subject to the +// following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN +// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +// USE OR OTHER DEALINGS IN THE SOFTWARE. + +'use strict'; + +var stringifyPrimitive = function(v) { + switch (typeof v) { + case 'string': + return v; + + case 'boolean': + return v ? 'true' : 'false'; + + case 'number': + return isFinite(v) ? v : ''; + + default: + return ''; + } +}; + +module.exports = function(obj, sep, eq, name) { + sep = sep || '&'; + eq = eq || '='; + if (obj === null) { + obj = undefined; + } + + if (typeof obj === 'object') { + return map(objectKeys(obj), function(k) { + var ks = encodeURIComponent(stringifyPrimitive(k)) + eq; + if (isArray(obj[k])) { + return map(obj[k], function(v) { + return ks + encodeURIComponent(stringifyPrimitive(v)); + }).join(sep); + } else { + return ks + encodeURIComponent(stringifyPrimitive(obj[k])); + } + }).join(sep); + + } + + if (!name) return ''; + return encodeURIComponent(stringifyPrimitive(name)) + eq + + encodeURIComponent(stringifyPrimitive(obj)); +}; + +var isArray = Array.isArray || function (xs) { + return Object.prototype.toString.call(xs) === '[object Array]'; +}; + +function map (xs, f) { + if (xs.map) return xs.map(f); + var res = []; + for (var i = 0; i < xs.length; i++) { + res.push(f(xs[i], i)); + } + return res; +} + +var objectKeys = Object.keys || function (obj) { + var res = []; + for (var key in obj) { + if (Object.prototype.hasOwnProperty.call(obj, key)) res.push(key); + } + return res; +}; + +},{}],15:[function(require,module,exports){ +'use strict'; + +exports.decode = exports.parse = require(13); +exports.encode = exports.stringify = require(14); + +},{"13":13,"14":14}],16:[function(require,module,exports){ +module.exports = AlgoliaSearch; + +var Index = require(18); +var deprecate = require(28); +var deprecatedMessage = require(29); +var AlgoliaSearchCore = require(17); +var inherits = require(7); +var errors = require(30); + +function AlgoliaSearch() { + AlgoliaSearchCore.apply(this, arguments); +} + +inherits(AlgoliaSearch, AlgoliaSearchCore); + +/* + * Delete an index + * + * @param indexName the name of index to delete + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.deleteIndex = function(indexName, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexName), + hostType: 'write', + callback: callback + }); +}; + +/** + * Move an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy of + * srcIndexName (destination will be overriten if it already exist). + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.moveIndex = function(srcIndexName, dstIndexName, callback) { + var postObj = { + operation: 'move', destination: dstIndexName + }; + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Copy an existing index. + * @param srcIndexName the name of index to copy. + * @param dstIndexName the new index name that will contains a copy + * of srcIndexName (destination will be overriten if it already exist). + * @param scope an array of scopes to copy: ['settings', 'synonyms', 'rules'] + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.copyIndex = function(srcIndexName, dstIndexName, scopeOrCallback, _callback) { + var postObj = { + operation: 'copy', + destination: dstIndexName + }; + var callback = _callback; + if (typeof scopeOrCallback === 'function') { + // oops, old behaviour of third argument being a function + callback = scopeOrCallback; + } else if (Array.isArray(scopeOrCallback) && scopeOrCallback.length > 0) { + postObj.scope = scopeOrCallback; + } else if (typeof scopeOrCallback !== 'undefined') { + throw new Error('the scope given to `copyIndex` was not an array with settings, synonyms or rules'); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(srcIndexName) + '/operation', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Return last log entries. + * @param offset Specify the first entry to retrieve (0-based, 0 is the most recent log entry). + * @param length Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param type Specify the maximum number of entries to retrieve starting + * at offset. Maximum allowed value: 1000. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer that contains the task ID + */ +AlgoliaSearch.prototype.getLogs = function(offset, length, callback) { + var clone = require(26); + var params = {}; + if (typeof offset === 'object') { + // getLogs(params) + params = clone(offset); + callback = length; + } else if (arguments.length === 0 || typeof offset === 'function') { + // getLogs([cb]) + callback = offset; + } else if (arguments.length === 1 || typeof length === 'function') { + // getLogs(1, [cb)] + callback = length; + params.offset = offset; + } else { + // getLogs(1, 2, [cb]) + params.offset = offset; + params.length = length; + } + + if (params.offset === undefined) params.offset = 0; + if (params.length === undefined) params.length = 10; + + return this._jsonRequest({ + method: 'GET', + url: '/1/logs?' + this._getSearchParams(params, ''), + hostType: 'read', + callback: callback + }); +}; + +/* + * List all existing indexes (paginated) + * + * @param page The page to retrieve, starting at 0. + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with index list + */ +AlgoliaSearch.prototype.listIndexes = function(page, callback) { + var params = ''; + + if (page === undefined || typeof page === 'function') { + callback = page; + } else { + params = '?page=' + page; + } + + return this._jsonRequest({ + method: 'GET', + url: '/1/indexes' + params, + hostType: 'read', + callback: callback + }); +}; + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearch.prototype.initIndex = function(indexName) { + return new Index(this, indexName); +}; + +AlgoliaSearch.prototype.initAnalytics = function(opts) { + // the actual require must be inside the function, when put outside then you have a cyclic dependency + // not well resolved that ends up making the main "./index.js" (main module, the agloliasearch function) + // export an object instead of a function + // Other workarounds: + // - rewrite the lib in ES6, cyclic dependencies may be better supported + // - move initAnalytics to a property on the main module (algoliasearch.initAnalytics), + // same as places. + // The current API was made mostly to mimic the one made in PHP + var createAnalyticsClient = require(27); + return createAnalyticsClient(this.applicationID, this.apiKey, opts); +}; + +/* + * @deprecated use client.listApiKeys + */ +AlgoliaSearch.prototype.listUserKeys = deprecate(function(callback) { + return this.listApiKeys(callback); +}, deprecatedMessage('client.listUserKeys()', 'client.listApiKeys()')); + +/* + * List all existing api keys with their associated ACLs + * + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with api keys list + */ +AlgoliaSearch.prototype.listApiKeys = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys', + hostType: 'read', + callback: callback + }); +}; + +/* + * @deprecated see client.getApiKey + */ +AlgoliaSearch.prototype.getUserKeyACL = deprecate(function(key, callback) { + return this.getApiKey(key, callback); +}, deprecatedMessage('client.getUserKeyACL()', 'client.getApiKey()')); + +/* + * Get an API key + * + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the right API key + */ +AlgoliaSearch.prototype.getApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/keys/' + key, + hostType: 'read', + callback: callback + }); +}; + +/* + * @deprecated see client.deleteApiKey + */ +AlgoliaSearch.prototype.deleteUserKey = deprecate(function(key, callback) { + return this.deleteApiKey(key, callback); +}, deprecatedMessage('client.deleteUserKey()', 'client.deleteApiKey()')); + +/* + * Delete an existing API key + * @param key + * @param callback the result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the date of deletion + */ +AlgoliaSearch.prototype.deleteApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'DELETE', + url: '/1/keys/' + key, + hostType: 'write', + callback: callback + }); +}; + +/** + * Restore a deleted API key + * + * @param {String} key - The key to restore + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the restored API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.restoreApiKey('APIKEY') + * @see {@link https://www.algolia.com/doc/rest-api/search/#restore-api-key|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.restoreApiKey = function(key, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/keys/' + key + '/restore', + hostType: 'write', + callback: callback + }); +}; + +/* + @deprecated see client.addApiKey + */ +AlgoliaSearch.prototype.addUserKey = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('client.addUserKey()', 'client.addApiKey()')); + +/* + * Add a new global API key + * + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the added API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.addApiKey(['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#AddKey|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.addApiKey = function(acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: client.addApiKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.indexes = params.indexes; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/keys', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * @deprecated Please use client.addApiKey() + */ +AlgoliaSearch.prototype.addUserKeyWithValidity = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('client.addUserKeyWithValidity()', 'client.addApiKey()')); + +/** + * @deprecated Please use client.updateApiKey() + */ +AlgoliaSearch.prototype.updateUserKey = deprecate(function(key, acls, params, callback) { + return this.updateApiKey(key, acls, params, callback); +}, deprecatedMessage('client.updateUserKey()', 'client.updateApiKey()')); + +/** + * Update an existing API key + * @param {string} key - The key to update + * @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that + * can contains the following values: + * - search: allow to search (https and http) + * - addObject: allows to add/update an object in the index (https only) + * - deleteObject : allows to delete an existing object (https only) + * - deleteIndex : allows to delete index content (https only) + * - settings : allows to get index settings (https only) + * - editSettings : allows to change index settings (https only) + * @param {Object} [params] - Optionnal parameters to set for the key + * @param {number} params.validity - Number of seconds after which the key will be automatically removed (0 means no time limit for this key) + * @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour + * @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call + * @param {string[]} params.indexes - Allowed targeted indexes for this key + * @param {string} params.description - A description for your key + * @param {string[]} params.referers - A list of authorized referers + * @param {Object} params.queryParameters - Force the key to use specific query parameters + * @param {Function} callback - The result callback called with two arguments + * error: null or Error('message') + * content: the server answer with the modified API key + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.updateApiKey('APIKEY', ['search'], { + * validity: 300, + * maxQueriesPerIPPerHour: 2000, + * maxHitsPerQuery: 3, + * indexes: ['fruits'], + * description: 'Eat three fruits', + * referers: ['*.algolia.com'], + * queryParameters: { + * tagFilters: ['public'], + * } + * }) + * @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} + */ +AlgoliaSearch.prototype.updateApiKey = function(key, acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.indexes = params.indexes; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this._jsonRequest({ + method: 'PUT', + url: '/1/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); +}; + +/** + * Initialize a new batch of search queries + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.startQueriesBatch = deprecate(function startQueriesBatchDeprecated() { + this._batch = []; +}, deprecatedMessage('client.startQueriesBatch()', 'client.search()')); + +/** + * Add a search query in the batch + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.addQueryInBatch = deprecate(function addQueryInBatchDeprecated(indexName, query, args) { + this._batch.push({ + indexName: indexName, + query: query, + params: args + }); +}, deprecatedMessage('client.addQueryInBatch()', 'client.search()')); + +/** + * Launch the batch of queries using XMLHttpRequest. + * @deprecated use client.search() + */ +AlgoliaSearch.prototype.sendQueriesBatch = deprecate(function sendQueriesBatchDeprecated(callback) { + return this.search(this._batch, callback); +}, deprecatedMessage('client.sendQueriesBatch()', 'client.search()')); + +/** + * Perform write operations across multiple indexes. + * + * To reduce the amount of time spent on network round trips, + * you can create, update, or delete several objects in one call, + * using the batch endpoint (all operations are done in the given order). + * + * Available actions: + * - addObject + * - updateObject + * - partialUpdateObject + * - partialUpdateObjectNoCreate + * - deleteObject + * + * https://www.algolia.com/doc/rest_api#Indexes + * @param {Object[]} operations An array of operations to perform + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.batch([{ + * action: 'addObject', + * indexName: 'clients', + * body: { + * name: 'Bill' + * } + * }, { + * action: 'udpateObject', + * indexName: 'fruits', + * body: { + * objectID: '29138', + * name: 'banana' + * } + * }], cb) + */ +AlgoliaSearch.prototype.batch = function(operations, callback) { + var isArray = require(8); + var usage = 'Usage: client.batch(operations[, callback])'; + + if (!isArray(operations)) { + throw new Error(usage); + } + + return this._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/batch', + body: { + requests: operations + }, + hostType: 'write', + callback: callback + }); +}; + +/** + * Assign or Move a userID to a cluster + * + * @param {string} data.userID The userID to assign to a new cluster + * @param {string} data.cluster The cluster to assign the user to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.assignUserID({ cluster: 'c1-test', userID: 'some-user' }); + */ +AlgoliaSearch.prototype.assignUserID = function(data, callback) { + if (!data.userID || !data.cluster) { + throw new errors.AlgoliaSearchError('You have to provide both a userID and cluster', data); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping', + hostType: 'write', + body: {cluster: data.cluster}, + callback: callback, + headers: { + 'x-algolia-user-id': data.userID + } + }); +}; + +/** + * Assign a array of userIDs to a cluster. + * + * @param {Array} data.userIDs The array of userIDs to assign to a new cluster + * @param {string} data.cluster The cluster to assign the user to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.assignUserIDs({ cluster: 'c1-test', userIDs: ['some-user-1', 'some-user-2'] }); + */ +AlgoliaSearch.prototype.assignUserIDs = function(data, callback) { + if (!data.userIDs || !data.cluster) { + throw new errors.AlgoliaSearchError('You have to provide both an array of userIDs and cluster', data); + } + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping/batch', + hostType: 'write', + body: { + cluster: data.cluster, + users: data.userIDs + }, + callback: callback + }); +}; + +/** + * Get the top userIDs + * + * (the callback is the second argument) + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getTopUserID(); + */ +AlgoliaSearch.prototype.getTopUserID = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping/top', + hostType: 'read', + callback: callback + }); +}; + +/** + * Get userID + * + * @param {string} data.userID The userID to get info about + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getUserID({ userID: 'some-user' }); + */ +AlgoliaSearch.prototype.getUserID = function(data, callback) { + if (!data.userID) { + throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data}); + } + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping/' + data.userID, + hostType: 'read', + callback: callback + }); +}; + +/** + * List all the clusters + * + * (the callback is the second argument) + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.listClusters(); + */ +AlgoliaSearch.prototype.listClusters = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters', + hostType: 'read', + callback: callback + }); +}; + +/** + * List the userIDs + * + * (the callback is the second argument) + * + * @param {string} data.hitsPerPage How many hits on every page + * @param {string} data.page The page to retrieve + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.listClusters(); + * client.listClusters({ page: 3, hitsPerPage: 30}); + */ +AlgoliaSearch.prototype.listUserIDs = function(data, callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/clusters/mapping', + body: data, + hostType: 'read', + callback: callback + }); +}; + +/** + * Remove an userID + * + * @param {string} data.userID The userID to assign to a new cluster + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.removeUserID({ userID: 'some-user' }); + */ +AlgoliaSearch.prototype.removeUserID = function(data, callback) { + if (!data.userID) { + throw new errors.AlgoliaSearchError('You have to provide a userID', {debugData: data}); + } + return this._jsonRequest({ + method: 'DELETE', + url: '/1/clusters/mapping', + hostType: 'write', + callback: callback, + headers: { + 'x-algolia-user-id': data.userID + } + }); +}; + +/** + * Search for userIDs + * + * @param {string} data.cluster The cluster to target + * @param {string} data.query The query to execute + * @param {string} data.hitsPerPage How many hits on every page + * @param {string} data.page The page to retrieve + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.searchUserIDs({ cluster: 'c1-test', query: 'some-user' }); + * client.searchUserIDs({ + * cluster: "c1-test", + * query: "some-user", + * page: 3, + * hitsPerPage: 2 + * }); + */ +AlgoliaSearch.prototype.searchUserIDs = function(data, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/clusters/mapping/search', + body: data, + hostType: 'read', + callback: callback + }); +}; + +/** + * Set strategy for personalization + * + * @param {Object} data + * @param {Object} data.eventsScoring Associate a score to an event + * @param {Object} data.eventsScoring. The name of the event + * @param {Number} data.eventsScoring..score The score to associate to + * @param {String} data.eventsScoring..type Either "click", "conversion" or "view" + * @param {Object} data.facetsScoring Associate a score to a facet + * @param {Object} data.facetsScoring. The name of the facet + * @param {Number} data.facetsScoring..score The score to associate to + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.setPersonalizationStrategy({ + * eventsScoring: { + * "Add to cart": { score: 50, type: "conversion" }, + * Purchase: { score: 100, type: "conversion" } + * }, + * facetsScoring: { + * brand: { score: 100 }, + * categories: { score: 10 } + * } + * }); + */ +AlgoliaSearch.prototype.setPersonalizationStrategy = function(data, callback) { + return this._jsonRequest({ + method: 'POST', + url: '/1/recommendation/personalization/strategy', + body: data, + hostType: 'write', + callback: callback + }); +}; + +/** + * Get strategy for personalization + * + * @return {Promise|undefined} Returns a promise if no callback given + * @example + * client.getPersonalizationStrategy(); + */ + +AlgoliaSearch.prototype.getPersonalizationStrategy = function(callback) { + return this._jsonRequest({ + method: 'GET', + url: '/1/recommendation/personalization/strategy', + hostType: 'read', + callback: callback + }); +}; + +// environment specific methods +AlgoliaSearch.prototype.destroy = notImplemented; +AlgoliaSearch.prototype.enableRateLimitForward = notImplemented; +AlgoliaSearch.prototype.disableRateLimitForward = notImplemented; +AlgoliaSearch.prototype.useSecuredAPIKey = notImplemented; +AlgoliaSearch.prototype.disableSecuredAPIKey = notImplemented; +AlgoliaSearch.prototype.generateSecuredApiKey = notImplemented; +AlgoliaSearch.prototype.getSecuredApiKeyRemainingValidity = notImplemented; + +function notImplemented() { + var message = 'Not implemented in this environment.\n' + + 'If you feel this is a mistake, write to support@algolia.com'; + + throw new errors.AlgoliaSearchError(message); +} + +},{"17":17,"18":18,"26":26,"27":27,"28":28,"29":29,"30":30,"7":7,"8":8}],17:[function(require,module,exports){ +(function (process){ +module.exports = AlgoliaSearchCore; + +var errors = require(30); +var exitPromise = require(31); +var IndexCore = require(20); +var store = require(36); + +// We will always put the API KEY in the JSON body in case of too long API KEY, +// to avoid query string being too long and failing in various conditions (our server limit, browser limit, +// proxies limit) +var MAX_API_KEY_LENGTH = 500; +var RESET_APP_DATA_TIMER = + process.env.RESET_APP_DATA_TIMER && parseInt(process.env.RESET_APP_DATA_TIMER, 10) || + 60 * 2 * 1000; // after 2 minutes reset to first host + +/* + * Algolia Search library initialization + * https://www.algolia.com/ + * + * @param {string} applicationID - Your applicationID, found in your dashboard + * @param {string} apiKey - Your API key, found in your dashboard + * @param {Object} [opts] + * @param {number} [opts.timeout=2000] - The request timeout set in milliseconds, + * another request will be issued after this timeout + * @param {string} [opts.protocol='https:'] - The protocol used to query Algolia Search API. + * Set to 'http:' to force using http. + * @param {Object|Array} [opts.hosts={ + * read: [this.applicationID + '-dsn.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]), + * write: [this.applicationID + '.algolia.net'].concat([ + * this.applicationID + '-1.algolianet.com', + * this.applicationID + '-2.algolianet.com', + * this.applicationID + '-3.algolianet.com'] + * ]) - The hosts to use for Algolia Search API. + * If you provide them, you will less benefit from our HA implementation + */ +function AlgoliaSearchCore(applicationID, apiKey, opts) { + var debug = require(1)('algoliasearch'); + + var clone = require(26); + var isArray = require(8); + var map = require(32); + + var usage = 'Usage: algoliasearch(applicationID, apiKey, opts)'; + + if (opts._allowEmptyCredentials !== true && !applicationID) { + throw new errors.AlgoliaSearchError('Please provide an application ID. ' + usage); + } + + if (opts._allowEmptyCredentials !== true && !apiKey) { + throw new errors.AlgoliaSearchError('Please provide an API key. ' + usage); + } + + this.applicationID = applicationID; + this.apiKey = apiKey; + + this.hosts = { + read: [], + write: [] + }; + + opts = opts || {}; + + this._timeouts = opts.timeouts || { + connect: 1 * 1000, // 500ms connect is GPRS latency + read: 2 * 1000, + write: 30 * 1000 + }; + + // backward compat, if opts.timeout is passed, we use it to configure all timeouts like before + if (opts.timeout) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = opts.timeout; + } + + var protocol = opts.protocol || 'https:'; + // while we advocate for colon-at-the-end values: 'http:' for `opts.protocol` + // we also accept `http` and `https`. It's a common error. + if (!/:$/.test(protocol)) { + protocol = protocol + ':'; + } + + if (protocol !== 'http:' && protocol !== 'https:') { + throw new errors.AlgoliaSearchError('protocol must be `http:` or `https:` (was `' + opts.protocol + '`)'); + } + + this._checkAppIdData(); + + if (!opts.hosts) { + var defaultHosts = map(this._shuffleResult, function(hostNumber) { + return applicationID + '-' + hostNumber + '.algolianet.com'; + }); + + // no hosts given, compute defaults + var mainSuffix = (opts.dsn === false ? '' : '-dsn') + '.algolia.net'; + this.hosts.read = [this.applicationID + mainSuffix].concat(defaultHosts); + this.hosts.write = [this.applicationID + '.algolia.net'].concat(defaultHosts); + } else if (isArray(opts.hosts)) { + // when passing custom hosts, we need to have a different host index if the number + // of write/read hosts are different. + this.hosts.read = clone(opts.hosts); + this.hosts.write = clone(opts.hosts); + } else { + this.hosts.read = clone(opts.hosts.read); + this.hosts.write = clone(opts.hosts.write); + } + + // add protocol and lowercase hosts + this.hosts.read = map(this.hosts.read, prepareHost(protocol)); + this.hosts.write = map(this.hosts.write, prepareHost(protocol)); + + this.extraHeaders = {}; + + // In some situations you might want to warm the cache + this.cache = opts._cache || {}; + + this._ua = opts._ua; + this._useCache = opts._useCache === undefined || opts._cache ? true : opts._useCache; + this._useRequestCache = this._useCache && opts._useRequestCache; + this._useFallback = opts.useFallback === undefined ? true : opts.useFallback; + + this._setTimeout = opts._setTimeout; + + debug('init done, %j', this); +} + +/* + * Get the index object initialized + * + * @param indexName the name of index + * @param callback the result callback with one argument (the Index instance) + */ +AlgoliaSearchCore.prototype.initIndex = function(indexName) { + return new IndexCore(this, indexName); +}; + +/** +* Add an extra field to the HTTP request +* +* @param name the header field name +* @param value the header field value +*/ +AlgoliaSearchCore.prototype.setExtraHeader = function(name, value) { + this.extraHeaders[name.toLowerCase()] = value; +}; + +/** +* Get the value of an extra HTTP header +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.getExtraHeader = function(name) { + return this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Remove an extra field from the HTTP request +* +* @param name the header field name +*/ +AlgoliaSearchCore.prototype.unsetExtraHeader = function(name) { + delete this.extraHeaders[name.toLowerCase()]; +}; + +/** +* Augment sent x-algolia-agent with more data, each agent part +* is automatically separated from the others by a semicolon; +* +* @param algoliaAgent the agent to add +*/ +AlgoliaSearchCore.prototype.addAlgoliaAgent = function(algoliaAgent) { + var algoliaAgentWithDelimiter = '; ' + algoliaAgent; + + if (this._ua.indexOf(algoliaAgentWithDelimiter) === -1) { + this._ua += algoliaAgentWithDelimiter; + } +}; + +/* + * Wrapper that try all hosts to maximize the quality of service + */ +AlgoliaSearchCore.prototype._jsonRequest = function(initialOpts) { + this._checkAppIdData(); + + var requestDebug = require(1)('algoliasearch:' + initialOpts.url); + + + var body; + var cacheID; + var additionalUA = initialOpts.additionalUA || ''; + var cache = initialOpts.cache; + var client = this; + var tries = 0; + var usingFallback = false; + var hasFallback = client._useFallback && client._request.fallback && initialOpts.fallback; + var headers; + + if ( + this.apiKey.length > MAX_API_KEY_LENGTH && + initialOpts.body !== undefined && + (initialOpts.body.params !== undefined || // index.search() + initialOpts.body.requests !== undefined) // client.search() + ) { + initialOpts.body.apiKey = this.apiKey; + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + withApiKey: false, + headers: initialOpts.headers + }); + } else { + headers = this._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + } + + if (initialOpts.body !== undefined) { + body = safeJSONStringify(initialOpts.body); + } + + requestDebug('request start'); + var debugData = []; + + + function doRequest(requester, reqOpts) { + client._checkAppIdData(); + + var startTime = new Date(); + + if (client._useCache && !client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && !client._useRequestCache && body) { + cacheID += '_body_' + reqOpts.body; + } + + // handle cache existence + if (isCacheValidWithCurrentID(!client._useRequestCache, cache, cacheID)) { + requestDebug('serving response from cache'); + + var responseText = cache[cacheID]; + + // Cache response must match the type of the original one + return client._promise.resolve({ + body: JSON.parse(responseText), + responseText: responseText + }); + } + + // if we reached max tries + if (tries >= client.hosts[initialOpts.hostType].length) { + if (!hasFallback || usingFallback) { + requestDebug('could not get any response'); + // then stop + return client._promise.reject(new errors.AlgoliaSearchError( + 'Cannot connect to the AlgoliaSearch API.' + + ' Send an email to support@algolia.com to report and resolve the issue.' + + ' Application id was: ' + client.applicationID, {debugData: debugData} + )); + } + + requestDebug('switching to fallback'); + + // let's try the fallback starting from here + tries = 0; + + // method, url and body are fallback dependent + reqOpts.method = initialOpts.fallback.method; + reqOpts.url = initialOpts.fallback.url; + reqOpts.jsonBody = initialOpts.fallback.body; + if (reqOpts.jsonBody) { + reqOpts.body = safeJSONStringify(reqOpts.jsonBody); + } + // re-compute headers, they could be omitting the API KEY + headers = client._computeRequestHeaders({ + additionalUA: additionalUA, + headers: initialOpts.headers + }); + + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + client._setHostIndexByType(0, initialOpts.hostType); + usingFallback = true; // the current request is now using fallback + return doRequest(client._request.fallback, reqOpts); + } + + var currentHost = client._getHostByType(initialOpts.hostType); + + var url = currentHost + reqOpts.url; + var options = { + body: reqOpts.body, + jsonBody: reqOpts.jsonBody, + method: reqOpts.method, + headers: headers, + timeouts: reqOpts.timeouts, + debug: requestDebug, + forceAuthHeaders: reqOpts.forceAuthHeaders + }; + + requestDebug('method: %s, url: %s, headers: %j, timeouts: %d', + options.method, url, options.headers, options.timeouts); + + if (requester === client._request.fallback) { + requestDebug('using fallback'); + } + + // `requester` is any of this._request or this._request.fallback + // thus it needs to be called using the client as context + return requester.call(client, url, options).then(success, tryFallback); + + function success(httpResponse) { + // compute the status of the response, + // + // When in browser mode, using XDR or JSONP, we have no statusCode available + // So we rely on our API response `status` property. + // But `waitTask` can set a `status` property which is not the statusCode (it's the task status) + // So we check if there's a `message` along `status` and it means it's an error + // + // That's the only case where we have a response.status that's not the http statusCode + var status = httpResponse && httpResponse.body && httpResponse.body.message && httpResponse.body.status || + + // this is important to check the request statusCode AFTER the body eventual + // statusCode because some implementations (jQuery XDomainRequest transport) may + // send statusCode 200 while we had an error + httpResponse.statusCode || + + // When in browser mode, using XDR or JSONP + // we default to success when no error (no response.status && response.message) + // If there was a JSON.parse() error then body is null and it fails + httpResponse && httpResponse.body && 200; + + requestDebug('received response: statusCode: %s, computed statusCode: %d, headers: %j', + httpResponse.statusCode, status, httpResponse.headers); + + var httpResponseOk = Math.floor(status / 100) === 2; + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime, + statusCode: status + }); + + if (httpResponseOk) { + if (client._useCache && !client._useRequestCache && cache) { + cache[cacheID] = httpResponse.responseText; + } + + return { + responseText: httpResponse.responseText, + body: httpResponse.body + }; + } + + var shouldRetry = Math.floor(status / 100) !== 4; + + if (shouldRetry) { + tries += 1; + return retryRequest(); + } + + requestDebug('unrecoverable error'); + + // no success and no retry => fail + var unrecoverableError = new errors.AlgoliaSearchError( + httpResponse.body && httpResponse.body.message, {debugData: debugData, statusCode: status} + ); + + return client._promise.reject(unrecoverableError); + } + + function tryFallback(err) { + // error cases: + // While not in fallback mode: + // - CORS not supported + // - network error + // While in fallback mode: + // - timeout + // - network error + // - badly formatted JSONP (script loaded, did not call our callback) + // In both cases: + // - uncaught exception occurs (TypeError) + requestDebug('error: %s, stack: %s', err.message, err.stack); + + var endTime = new Date(); + debugData.push({ + currentHost: currentHost, + headers: removeCredentials(headers), + content: body || null, + contentLength: body !== undefined ? body.length : null, + method: reqOpts.method, + timeouts: reqOpts.timeouts, + url: reqOpts.url, + startTime: startTime, + endTime: endTime, + duration: endTime - startTime + }); + + if (!(err instanceof errors.AlgoliaSearchError)) { + err = new errors.Unknown(err && err.message, err); + } + + tries += 1; + + // stop the request implementation when: + if ( + // we did not generate this error, + // it comes from a throw in some other piece of code + err instanceof errors.Unknown || + + // server sent unparsable JSON + err instanceof errors.UnparsableJSON || + + // max tries and already using fallback or no fallback + tries >= client.hosts[initialOpts.hostType].length && + (usingFallback || !hasFallback)) { + // stop request implementation for this command + err.debugData = debugData; + return client._promise.reject(err); + } + + // When a timeout occurred, retry by raising timeout + if (err instanceof errors.RequestTimeout) { + return retryRequestWithHigherTimeout(); + } + + return retryRequest(); + } + + function retryRequest() { + requestDebug('retrying request'); + client._incrementHostIndex(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + + function retryRequestWithHigherTimeout() { + requestDebug('retrying request with higher timeout'); + client._incrementHostIndex(initialOpts.hostType); + client._incrementTimeoutMultipler(); + reqOpts.timeouts = client._getTimeoutsForRequest(initialOpts.hostType); + return doRequest(requester, reqOpts); + } + } + + function isCacheValidWithCurrentID( + useRequestCache, + currentCache, + currentCacheID + ) { + return ( + client._useCache && + useRequestCache && + currentCache && + currentCache[currentCacheID] !== undefined + ); + } + + + function interopCallbackReturn(request, callback) { + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + request.catch(function() { + // Release the cache on error + delete cache[cacheID]; + }); + } + + if (typeof initialOpts.callback === 'function') { + // either we have a callback + request.then(function okCb(content) { + exitPromise(function() { + initialOpts.callback(null, callback(content)); + }, client._setTimeout || setTimeout); + }, function nookCb(err) { + exitPromise(function() { + initialOpts.callback(err); + }, client._setTimeout || setTimeout); + }); + } else { + // either we are using promises + return request.then(callback); + } + } + + if (client._useCache && client._useRequestCache) { + cacheID = initialOpts.url; + } + + // as we sometime use POST requests to pass parameters (like query='aa'), + // the cacheID must also include the body to be different between calls + if (client._useCache && client._useRequestCache && body) { + cacheID += '_body_' + body; + } + + if (isCacheValidWithCurrentID(client._useRequestCache, cache, cacheID)) { + requestDebug('serving request from cache'); + + var maybePromiseForCache = cache[cacheID]; + + // In case the cache is warmup with value that is not a promise + var promiseForCache = typeof maybePromiseForCache.then !== 'function' + ? client._promise.resolve({responseText: maybePromiseForCache}) + : maybePromiseForCache; + + return interopCallbackReturn(promiseForCache, function(content) { + // In case of the cache request, return the original value + return JSON.parse(content.responseText); + }); + } + + var request = doRequest( + client._request, { + url: initialOpts.url, + method: initialOpts.method, + body: body, + jsonBody: initialOpts.body, + timeouts: client._getTimeoutsForRequest(initialOpts.hostType), + forceAuthHeaders: initialOpts.forceAuthHeaders + } + ); + + if (client._useCache && client._useRequestCache && cache) { + cache[cacheID] = request; + } + + return interopCallbackReturn(request, function(content) { + // In case of the first request, return the JSON value + return content.body; + }); +}; + +/* +* Transform search param object in query string +* @param {object} args arguments to add to the current query string +* @param {string} params current query string +* @return {string} the final query string +*/ +AlgoliaSearchCore.prototype._getSearchParams = function(args, params) { + if (args === undefined || args === null) { + return params; + } + for (var key in args) { + if (key !== null && args[key] !== undefined && args.hasOwnProperty(key)) { + params += params === '' ? '' : '&'; + params += key + '=' + encodeURIComponent(Object.prototype.toString.call(args[key]) === '[object Array]' ? safeJSONStringify(args[key]) : args[key]); + } + } + return params; +}; + +/** + * Compute the headers for a request + * + * @param [string] options.additionalUA semi-colon separated string with other user agents to add + * @param [boolean=true] options.withApiKey Send the api key as a header + * @param [Object] options.headers Extra headers to send + */ +AlgoliaSearchCore.prototype._computeRequestHeaders = function(options) { + var forEach = require(5); + + var ua = options.additionalUA ? + this._ua + '; ' + options.additionalUA : + this._ua; + + var requestHeaders = { + 'x-algolia-agent': ua, + 'x-algolia-application-id': this.applicationID + }; + + // browser will inline headers in the url, node.js will use http headers + // but in some situations, the API KEY will be too long (big secured API keys) + // so if the request is a POST and the KEY is very long, we will be asked to not put + // it into headers but in the JSON body + if (options.withApiKey !== false) { + requestHeaders['x-algolia-api-key'] = this.apiKey; + } + + if (this.userToken) { + requestHeaders['x-algolia-usertoken'] = this.userToken; + } + + if (this.securityTags) { + requestHeaders['x-algolia-tagfilters'] = this.securityTags; + } + + forEach(this.extraHeaders, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + + if (options.headers) { + forEach(options.headers, function addToRequestHeaders(value, key) { + requestHeaders[key] = value; + }); + } + + return requestHeaders; +}; + +/** + * Search through multiple indices at the same time + * @param {Object[]} queries An array of queries you want to run. + * @param {string} queries[].indexName The index name you want to target + * @param {string} [queries[].query] The query to issue on this index. Can also be passed into `params` + * @param {Object} queries[].params Any search param like hitsPerPage, .. + * @param {Function} callback Callback to be called + * @return {Promise|undefined} Returns a promise if no callback given + */ +AlgoliaSearchCore.prototype.search = function(queries, opts, callback) { + var isArray = require(8); + var map = require(32); + + var usage = 'Usage: client.search(arrayOfQueries[, callback])'; + + if (!isArray(queries)) { + throw new Error(usage); + } + + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var client = this; + + var postObj = { + requests: map(queries, function prepareRequest(query) { + var params = ''; + + // allow query.query + // so we are mimicing the index.search(query, params) method + // {indexName:, query:, params:} + if (query.query !== undefined) { + params += 'query=' + encodeURIComponent(query.query); + } + + return { + indexName: query.indexName, + params: client._getSearchParams(query.params, params) + }; + }) + }; + + var JSONPParams = map(postObj.requests, function prepareJSONPParams(request, requestId) { + return requestId + '=' + + encodeURIComponent( + '/1/indexes/' + encodeURIComponent(request.indexName) + '?' + + request.params + ); + }).join('&'); + + var url = '/1/indexes/*/queries'; + + if (opts.strategy !== undefined) { + postObj.strategy = opts.strategy; + } + + return this._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url, + body: postObj, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/*', + body: { + params: JSONPParams + } + }, + callback: callback + }); +}; + +/** +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* This is the top-level API for SFFV. +* +* @param {object[]} queries An array of queries to run. +* @param {string} queries[].indexName Index name, name of the index to search. +* @param {object} queries[].params Query parameters. +* @param {string} queries[].params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} queries[].params.facetQuery Query for the facet search +* @param {string} [queries[].params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +*/ +AlgoliaSearchCore.prototype.searchForFacetValues = function(queries) { + var isArray = require(8); + var map = require(32); + + var usage = 'Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])'; // eslint-disable-line max-len + + if (!isArray(queries)) { + throw new Error(usage); + } + + var client = this; + + return client._promise.all(map(queries, function performQuery(query) { + if ( + !query || + query.indexName === undefined || + query.params.facetName === undefined || + query.params.facetQuery === undefined + ) { + throw new Error(usage); + } + + var clone = require(26); + var omit = require(34); + + var indexName = query.indexName; + var params = query.params; + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = client._getSearchParams(filteredParams, ''); + + return client._jsonRequest({ + cache: client.cache, + method: 'POST', + url: + '/1/indexes/' + + encodeURIComponent(indexName) + + '/facets/' + + encodeURIComponent(facetName) + + '/query', + hostType: 'read', + body: {params: searchParameters} + }); + })); +}; + +/** + * Set the extra security tagFilters header + * @param {string|array} tags The list of tags defining the current security filters + */ +AlgoliaSearchCore.prototype.setSecurityTags = function(tags) { + if (Object.prototype.toString.call(tags) === '[object Array]') { + var strTags = []; + for (var i = 0; i < tags.length; ++i) { + if (Object.prototype.toString.call(tags[i]) === '[object Array]') { + var oredTags = []; + for (var j = 0; j < tags[i].length; ++j) { + oredTags.push(tags[i][j]); + } + strTags.push('(' + oredTags.join(',') + ')'); + } else { + strTags.push(tags[i]); + } + } + tags = strTags.join(','); + } + + this.securityTags = tags; +}; + +/** + * Set the extra user token header + * @param {string} userToken The token identifying a uniq user (used to apply rate limits) + */ +AlgoliaSearchCore.prototype.setUserToken = function(userToken) { + this.userToken = userToken; +}; + +/** + * Clear all queries in client's cache + * @return undefined + */ +AlgoliaSearchCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/** +* Set the number of milliseconds a request can take before automatically being terminated. +* @deprecated +* @param {Number} milliseconds +*/ +AlgoliaSearchCore.prototype.setRequestTimeout = function(milliseconds) { + if (milliseconds) { + this._timeouts.connect = this._timeouts.read = this._timeouts.write = milliseconds; + } +}; + +/** +* Set the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.setTimeouts = function(timeouts) { + this._timeouts = timeouts; +}; + +/** +* Get the three different (connect, read, write) timeouts to be used when requesting +* @param {Object} timeouts +*/ +AlgoliaSearchCore.prototype.getTimeouts = function() { + return this._timeouts; +}; + +AlgoliaSearchCore.prototype._getAppIdData = function() { + var data = store.get(this.applicationID); + if (data !== null) this._cacheAppIdData(data); + return data; +}; + +AlgoliaSearchCore.prototype._setAppIdData = function(data) { + data.lastChange = (new Date()).getTime(); + this._cacheAppIdData(data); + return store.set(this.applicationID, data); +}; + +AlgoliaSearchCore.prototype._checkAppIdData = function() { + var data = this._getAppIdData(); + var now = (new Date()).getTime(); + if (data === null || now - data.lastChange > RESET_APP_DATA_TIMER) { + return this._resetInitialAppIdData(data); + } + + return data; +}; + +AlgoliaSearchCore.prototype._resetInitialAppIdData = function(data) { + var newData = data || {}; + newData.hostIndexes = {read: 0, write: 0}; + newData.timeoutMultiplier = 1; + newData.shuffleResult = newData.shuffleResult || shuffle([1, 2, 3]); + return this._setAppIdData(newData); +}; + +AlgoliaSearchCore.prototype._cacheAppIdData = function(data) { + this._hostIndexes = data.hostIndexes; + this._timeoutMultiplier = data.timeoutMultiplier; + this._shuffleResult = data.shuffleResult; +}; + +AlgoliaSearchCore.prototype._partialAppIdDataUpdate = function(newData) { + var foreach = require(5); + var currentData = this._getAppIdData(); + foreach(newData, function(value, key) { + currentData[key] = value; + }); + + return this._setAppIdData(currentData); +}; + +AlgoliaSearchCore.prototype._getHostByType = function(hostType) { + return this.hosts[hostType][this._getHostIndexByType(hostType)]; +}; + +AlgoliaSearchCore.prototype._getTimeoutMultiplier = function() { + return this._timeoutMultiplier; +}; + +AlgoliaSearchCore.prototype._getHostIndexByType = function(hostType) { + return this._hostIndexes[hostType]; +}; + +AlgoliaSearchCore.prototype._setHostIndexByType = function(hostIndex, hostType) { + var clone = require(26); + var newHostIndexes = clone(this._hostIndexes); + newHostIndexes[hostType] = hostIndex; + this._partialAppIdDataUpdate({hostIndexes: newHostIndexes}); + return hostIndex; +}; + +AlgoliaSearchCore.prototype._incrementHostIndex = function(hostType) { + return this._setHostIndexByType( + (this._getHostIndexByType(hostType) + 1) % this.hosts[hostType].length, hostType + ); +}; + +AlgoliaSearchCore.prototype._incrementTimeoutMultipler = function() { + var timeoutMultiplier = Math.max(this._timeoutMultiplier + 1, 4); + return this._partialAppIdDataUpdate({timeoutMultiplier: timeoutMultiplier}); +}; + +AlgoliaSearchCore.prototype._getTimeoutsForRequest = function(hostType) { + return { + connect: this._timeouts.connect * this._timeoutMultiplier, + complete: this._timeouts[hostType] * this._timeoutMultiplier + }; +}; + +function prepareHost(protocol) { + return function prepare(host) { + return protocol + '//' + host.toLowerCase(); + }; +} + +// Prototype.js < 1.7, a widely used library, defines a weird +// Array.prototype.toJSON function that will fail to stringify our content +// appropriately +// refs: +// - https://groups.google.com/forum/#!topic/prototype-core/E-SAVvV_V9Q +// - https://github.com/sstephenson/prototype/commit/038a2985a70593c1a86c230fadbdfe2e4898a48c +// - http://stackoverflow.com/a/3148441/147079 +function safeJSONStringify(obj) { + /* eslint no-extend-native:0 */ + + if (Array.prototype.toJSON === undefined) { + return JSON.stringify(obj); + } + + var toJSON = Array.prototype.toJSON; + delete Array.prototype.toJSON; + var out = JSON.stringify(obj); + Array.prototype.toJSON = toJSON; + + return out; +} + +function shuffle(array) { + var currentIndex = array.length; + var temporaryValue; + var randomIndex; + + // While there remain elements to shuffle... + while (currentIndex !== 0) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex -= 1; + + // And swap it with the current element. + temporaryValue = array[currentIndex]; + array[currentIndex] = array[randomIndex]; + array[randomIndex] = temporaryValue; + } + + return array; +} + +function removeCredentials(headers) { + var newHeaders = {}; + + for (var headerName in headers) { + if (Object.prototype.hasOwnProperty.call(headers, headerName)) { + var value; + + if (headerName === 'x-algolia-api-key' || headerName === 'x-algolia-application-id') { + value = '**hidden for security purposes**'; + } else { + value = headers[headerName]; + } + + newHeaders[headerName] = value; + } + } + + return newHeaders; +} + +}).call(this,require(12)) +},{"1":1,"12":12,"20":20,"26":26,"30":30,"31":31,"32":32,"34":34,"36":36,"5":5,"8":8}],18:[function(require,module,exports){ +var inherits = require(7); +var IndexCore = require(20); +var deprecate = require(28); +var deprecatedMessage = require(29); +var exitPromise = require(31); +var errors = require(30); + +var deprecateForwardToSlaves = deprecate( + function() {}, + deprecatedMessage('forwardToSlaves', 'forwardToReplicas') +); + +module.exports = Index; + +function Index() { + IndexCore.apply(this, arguments); +} + +inherits(Index, IndexCore); + +/* +* Add an object in this index +* +* @param content contains the javascript object to add inside the index +* @param objectID (optional) an objectID you want to attribute to this object +* (if the attribute already exist the old object will be overwrite) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.addObject = function(content, objectID, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof objectID === 'function') { + callback = objectID; + objectID = undefined; + } + + return this.as._jsonRequest({ + method: objectID !== undefined ? + 'PUT' : // update or create + 'POST', // create (API generates an objectID) + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + // create + (objectID !== undefined ? '/' + encodeURIComponent(objectID) : ''), // update or create + body: content, + hostType: 'write', + callback: callback + }); +}; + +/* +* Add several objects +* +* @param objects contains an array of objects to add +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.addObjects = function(objects, callback) { + var isArray = require(8); + var usage = 'Usage: index.addObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'addObject', + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Update partially an object (only update attributes passed in argument) +* +* @param partialObject contains the javascript attributes to override, the +* object must contains an objectID attribute +* @param createIfNotExists (optional) if false, avoid an automatic creation of the object +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.partialUpdateObject = function(partialObject, createIfNotExists, callback) { + if (arguments.length === 1 || typeof createIfNotExists === 'function') { + callback = createIfNotExists; + createIfNotExists = undefined; + } + + var indexObj = this; + var url = '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(partialObject.objectID) + '/partial'; + if (createIfNotExists === false) { + url += '?createIfNotExists=false'; + } + + return this.as._jsonRequest({ + method: 'POST', + url: url, + body: partialObject, + hostType: 'write', + callback: callback + }); +}; + +/* +* Partially Override the content of several objects +* +* @param objects contains an array of objects to update (each object must contains a objectID attribute) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.partialUpdateObjects = function(objects, createIfNotExists, callback) { + if (arguments.length === 1 || typeof createIfNotExists === 'function') { + callback = createIfNotExists; + createIfNotExists = true; + } + + var isArray = require(8); + var usage = 'Usage: index.partialUpdateObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: createIfNotExists === true ? 'partialUpdateObject' : 'partialUpdateObjectNoCreate', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Override the content of object +* +* @param object contains the javascript object to save, the object must contains an objectID attribute +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.saveObject = function(object, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(object.objectID), + body: object, + hostType: 'write', + callback: callback + }); +}; + +/* +* Override the content of several objects +* +* @param objects contains an array of objects to update (each object must contains a objectID attribute) +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that updateAt and taskID +*/ +Index.prototype.saveObjects = function(objects, callback) { + var isArray = require(8); + var usage = 'Usage: index.saveObjects(arrayOfObjects[, callback])'; + + if (!isArray(objects)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: [] + }; + for (var i = 0; i < objects.length; ++i) { + var request = { + action: 'updateObject', + objectID: objects[i].objectID, + body: objects[i] + }; + postObj.requests.push(request); + } + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete an object from the index +* +* @param objectID the unique identifier of object to delete +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.deleteObject = function(objectID, callback) { + if (typeof objectID === 'function' || typeof objectID !== 'string' && typeof objectID !== 'number') { + var err = new errors.AlgoliaSearchError( + objectID && typeof objectID !== 'function' + ? 'ObjectID must be a string' + : 'Cannot delete an object without an objectID' + ); + callback = objectID; + if (typeof callback === 'function') { + return callback(err); + } + + return this.as._promise.reject(err); + } + + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID), + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete several objects from an index +* +* @param objectIDs contains an array of objectID to delete +* @param callback (optional) the result callback called with two arguments: +* error: null or Error('message') +* content: the server answer that contains 3 elements: createAt, taskId and objectID +*/ +Index.prototype.deleteObjects = function(objectIDs, callback) { + var isArray = require(8); + var map = require(32); + + var usage = 'Usage: index.deleteObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + var postObj = { + requests: map(objectIDs, function prepareRequest(objectID) { + return { + action: 'deleteObject', + objectID: objectID, + body: { + objectID: objectID + } + }; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/batch', + body: postObj, + hostType: 'write', + callback: callback + }); +}; + +/* +* Delete all objects matching a query +* +* @param query the query string +* @param params the optional query parameters +* @param callback (optional) the result callback called with one argument +* error: null or Error('message') +* @deprecated see index.deleteBy +*/ +Index.prototype.deleteByQuery = deprecate(function(query, params, callback) { + var clone = require(26); + var map = require(32); + + var indexObj = this; + var client = indexObj.as; + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = {}; + } else { + params = clone(params); + } + + params.attributesToRetrieve = 'objectID'; + params.hitsPerPage = 1000; + params.distinct = false; + + // when deleting, we should never use cache to get the + // search results + this.clearCache(); + + // there's a problem in how we use the promise chain, + // see how waitTask is done + var promise = this + .search(query, params) + .then(stopOrDelete); + + function stopOrDelete(searchContent) { + // stop here + if (searchContent.nbHits === 0) { + // return indexObj.as._request.resolve(); + return searchContent; + } + + // continue and do a recursive call + var objectIDs = map(searchContent.hits, function getObjectID(object) { + return object.objectID; + }); + + return indexObj + .deleteObjects(objectIDs) + .then(waitTask) + .then(doDeleteByQuery); + } + + function waitTask(deleteObjectsContent) { + return indexObj.waitTask(deleteObjectsContent.taskID); + } + + function doDeleteByQuery() { + return indexObj.deleteByQuery(query, params); + } + + if (!callback) { + return promise; + } + + promise.then(success, failure); + + function success() { + exitPromise(function exit() { + callback(null); + }, client._setTimeout || setTimeout); + } + + function failure(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } +}, deprecatedMessage('index.deleteByQuery()', 'index.deleteBy()')); + +/** +* Delete all objects matching a query +* +* the query parameters that can be used are: +* - filters (numeric, facet, tag) +* - geo +* +* you can not send an empty query or filters +* +* @param params the optional query parameters +* @param callback (optional) the result callback called with one argument +* error: null or Error('message') +*/ +Index.prototype.deleteBy = function(params, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/deleteByQuery', + body: {params: indexObj.as._getSearchParams(params, '')}, + hostType: 'write', + callback: callback + }); +}; + +/* +* Browse all content from an index using events. Basically this will do +* .browse() -> .browseFrom -> .browseFrom -> .. until all the results are returned +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @return {EventEmitter} +* @example +* var browser = index.browseAll('cool songs', { +* tagFilters: 'public,comments', +* hitsPerPage: 500 +* }); +* +* browser.on('result', function resultCallback(content) { +* console.log(content.hits); +* }); +* +* // if any error occurs, you get it +* browser.on('error', function(err) { +* throw err; +* }); +* +* // when you have browsed the whole index, you get this event +* browser.on('end', function() { +* console.log('finished'); +* }); +* +* // at any point if you want to stop the browsing process, you can stop it manually +* // otherwise it will go on and on +* browser.stop(); +* +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +Index.prototype.browseAll = function(query, queryParameters) { + if (typeof query === 'object') { + queryParameters = query; + query = undefined; + } + + var merge = require(33); + + var IndexBrowser = require(19); + + var browser = new IndexBrowser(); + var client = this.as; + var index = this; + var params = client._getSearchParams( + merge({}, queryParameters || {}, { + query: query + }), '' + ); + + // start browsing + browseLoop(); + + function browseLoop(cursor) { + if (browser._stopped) { + return; + } + + var body; + + if (cursor !== undefined) { + body = { + cursor: cursor + }; + } else { + body = { + params: params + }; + } + + client._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(index.indexName) + '/browse', + hostType: 'read', + body: body, + callback: browseCallback + }); + } + + function browseCallback(err, content) { + if (browser._stopped) { + return; + } + + if (err) { + browser._error(err); + return; + } + + browser._result(content); + + // no cursor means we are finished browsing + if (content.cursor === undefined) { + browser._end(); + return; + } + + browseLoop(content.cursor); + } + + return browser; +}; + +/* +* Get a Typeahead.js adapter +* @param searchParams contains an object with query parameters (see search for details) +*/ +Index.prototype.ttAdapter = deprecate(function(params) { + var self = this; + return function ttAdapter(query, syncCb, asyncCb) { + var cb; + + if (typeof asyncCb === 'function') { + // typeahead 0.11 + cb = asyncCb; + } else { + // pre typeahead 0.11 + cb = syncCb; + } + + self.search(query, params, function searchDone(err, content) { + if (err) { + cb(err); + return; + } + + cb(content.hits); + }); + }; +}, +'ttAdapter is not necessary anymore and will be removed in the next version,\n' + +'have a look at autocomplete.js (https://github.com/algolia/autocomplete.js)'); + +/* +* Wait the publication of a task on the server. +* All server task are asynchronous and you can check with this method that the task is published. +* +* @param taskID the id of the task returned by server +* @param callback the result callback with with two arguments: +* error: null or Error('message') +* content: the server answer that contains the list of results +*/ +Index.prototype.waitTask = function(taskID, callback) { + // wait minimum 100ms before retrying + var baseDelay = 100; + // wait maximum 5s before retrying + var maxDelay = 5000; + var loop = 0; + + // waitTask() must be handled differently from other methods, + // it's a recursive method using a timeout + var indexObj = this; + var client = indexObj.as; + + var promise = retryLoop(); + + function retryLoop() { + return client._jsonRequest({ + method: 'GET', + hostType: 'read', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/task/' + taskID + }).then(function success(content) { + loop++; + var delay = baseDelay * loop * loop; + if (delay > maxDelay) { + delay = maxDelay; + } + + if (content.status !== 'published') { + return client._promise.delay(delay).then(retryLoop); + } + + return content; + }); + } + + if (!callback) { + return promise; + } + + promise.then(successCb, failureCb); + + function successCb(content) { + exitPromise(function exit() { + callback(null, content); + }, client._setTimeout || setTimeout); + } + + function failureCb(err) { + exitPromise(function exit() { + callback(err); + }, client._setTimeout || setTimeout); + } +}; + +/* +* This function deletes the index content. Settings and index specific API keys are kept untouched. +* +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the settings object or the error message if a failure occurred +*/ +Index.prototype.clearIndex = function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/clear', + hostType: 'write', + callback: callback + }); +}; + +/* +* Get settings of this index +* +* @param opts an object of options to add +* @param opts.advanced get more settings like nbShards (useful for Enterprise) +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the settings object or the error message if a failure occurred +*/ +Index.prototype.getSettings = function(opts, callback) { + if (arguments.length === 1 && typeof opts === 'function') { + callback = opts; + opts = {}; + } + opts = opts || {}; + + var indexName = encodeURIComponent(this.indexName); + return this.as._jsonRequest({ + method: 'GET', + url: + '/1/indexes/' + + indexName + + '/settings?getVersion=2' + + (opts.advanced ? '&advanced=' + opts.advanced : ''), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.searchSynonyms = function(params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } else if (params === undefined) { + params = {}; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/search', + body: params, + hostType: 'read', + callback: callback + }); +}; + +function exportData(method, _hitsPerPage, callback) { + function search(page, _previous) { + var options = { + page: page || 0, + hitsPerPage: _hitsPerPage || 100 + }; + var previous = _previous || []; + + return method(options).then(function(result) { + var hits = result.hits; + var nbHits = result.nbHits; + var current = hits.map(function(s) { + delete s._highlightResult; + return s; + }); + var synonyms = previous.concat(current); + if (synonyms.length < nbHits) { + return search(options.page + 1, synonyms); + } + return synonyms; + }); + } + return search().then(function(data) { + if (typeof callback === 'function') { + callback(data); + return undefined; + } + return data; + }); +} + +/** + * Retrieve all the synonyms in an index + * @param [number=100] hitsPerPage The amount of synonyms to retrieve per batch + * @param [function] callback will be called after all synonyms are retrieved + */ +Index.prototype.exportSynonyms = function(hitsPerPage, callback) { + return exportData(this.searchSynonyms.bind(this), hitsPerPage, callback); +}; + +Index.prototype.saveSynonym = function(synonym, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(synonym.objectID) + + '?forwardToReplicas=' + forwardToReplicas, + body: synonym, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.getSynonym = function(objectID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(objectID), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.deleteSynonym = function(objectID, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/' + encodeURIComponent(objectID) + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.clearSynonyms = function(opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/clear' + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.batchSynonyms = function(synonyms, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/synonyms/batch' + + '?forwardToReplicas=' + forwardToReplicas + + '&replaceExistingSynonyms=' + (opts.replaceExistingSynonyms ? 'true' : 'false'), + hostType: 'write', + body: synonyms, + callback: callback + }); +}; + +Index.prototype.searchRules = function(params, callback) { + if (typeof params === 'function') { + callback = params; + params = {}; + } else if (params === undefined) { + params = {}; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/search', + body: params, + hostType: 'read', + callback: callback + }); +}; +/** + * Retrieve all the query rules in an index + * @param [number=100] hitsPerPage The amount of query rules to retrieve per batch + * @param [function] callback will be called after all query rules are retrieved + * error: null or Error('message') + */ +Index.prototype.exportRules = function(hitsPerPage, callback) { + return exportData(this.searchRules.bind(this), hitsPerPage, callback); +}; + +Index.prototype.saveRule = function(rule, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + if (!rule.objectID) { + throw new errors.AlgoliaSearchError('Missing or empty objectID field for rule'); + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(rule.objectID) + + '?forwardToReplicas=' + forwardToReplicas, + body: rule, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.getRule = function(objectID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(objectID), + hostType: 'read', + callback: callback + }); +}; + +Index.prototype.deleteRule = function(objectID, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/' + encodeURIComponent(objectID) + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.clearRules = function(opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/clear' + + '?forwardToReplicas=' + forwardToReplicas, + hostType: 'write', + callback: callback + }); +}; + +Index.prototype.batchRules = function(rules, opts, callback) { + if (typeof opts === 'function') { + callback = opts; + opts = {}; + } else if (opts === undefined) { + opts = {}; + } + + var forwardToReplicas = opts.forwardToReplicas === true ? 'true' : 'false'; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/rules/batch' + + '?forwardToReplicas=' + forwardToReplicas + + '&clearExistingRules=' + (opts.clearExistingRules === true ? 'true' : 'false'), + hostType: 'write', + body: rules, + callback: callback + }); +}; + +Index.prototype.exists = function(callback) { + var result = this.getSettings().then(function() { + return true; + }).catch(function(err) { + if (err instanceof errors.AlgoliaSearchError && err.statusCode === 404) { + return false; + } + + throw err; + }); + + if (typeof callback !== 'function') { + return result; + } + + result.then(function(res) { + callback(null, res); + }).catch(function(err) { + callback(err); + }); +}; + +Index.prototype.findObject = function(findCallback, requestOptions, callback) { + requestOptions = requestOptions === undefined ? {} : requestOptions; + var paginate = requestOptions.paginate !== undefined ? requestOptions.paginate : true; + var query = requestOptions.query !== undefined ? requestOptions.query : ''; + + var that = this; + var page = 0; + + var paginateLoop = function() { + requestOptions.page = page; + + return that.search(query, requestOptions).then(function(result) { + var hits = result.hits; + + for (var position = 0; position < hits.length; position++) { + var hit = hits[position]; + if (findCallback(hit)) { + return { + object: hit, + position: position, + page: page + }; + } + } + + page += 1; + + // paginate if option was set and has next page + if (!paginate || page >= result.nbPages) { + throw new errors.ObjectNotFound('Object not found'); + } + + return paginateLoop(); + }); + }; + + var promise = paginateLoop(page); + + if (callback === undefined) { + return promise; + } + + promise + .then(function(res) { + callback(null, res); + }) + .catch(function(err) { + callback(err); + }); +}; + +Index.prototype.getObjectPosition = function(result, objectID) { + var hits = result.hits; + + for (var position = 0; position < hits.length; position++) { + if (hits[position].objectID === objectID) { + return position; + } + } + + return -1; +}; + +/* +* Set settings for this index +* +* @param settings the settings object that can contains : +* - minWordSizefor1Typo: (integer) the minimum number of characters to accept one typo (default = 3). +* - minWordSizefor2Typos: (integer) the minimum number of characters to accept two typos (default = 7). +* - hitsPerPage: (integer) the number of hits per page (default = 10). +* - attributesToRetrieve: (array of strings) default list of attributes to retrieve in objects. +* If set to null, all attributes are retrieved. +* - attributesToHighlight: (array of strings) default list of attributes to highlight. +* If set to null, all indexed attributes are highlighted. +* - attributesToSnippet**: (array of strings) default list of attributes to snippet alongside the number +* of words to return (syntax is attributeName:nbWords). +* By default no snippet is computed. If set to null, no snippet is computed. +* - attributesToIndex: (array of strings) the list of fields you want to index. +* If set to null, all textual and numerical attributes of your objects are indexed, +* but you should update it to get optimal results. +* This parameter has two important uses: +* - Limit the attributes to index: For example if you store a binary image in base64, +* you want to store it and be able to +* retrieve it but you don't want to search in the base64 string. +* - Control part of the ranking*: (see the ranking parameter for full explanation) +* Matches in attributes at the beginning of +* the list will be considered more important than matches in attributes further down the list. +* In one attribute, matching text at the beginning of the attribute will be +* considered more important than text after, you can disable +* this behavior if you add your attribute inside `unordered(AttributeName)`, +* for example attributesToIndex: ["title", "unordered(text)"]. +* - attributesForFaceting: (array of strings) The list of fields you want to use for faceting. +* All strings in the attribute selected for faceting are extracted and added as a facet. +* If set to null, no attribute is used for faceting. +* - attributeForDistinct: (string) The attribute name used for the Distinct feature. +* This feature is similar to the SQL "distinct" keyword: when enabled +* in query with the distinct=1 parameter, all hits containing a duplicate +* value for this attribute are removed from results. +* For example, if the chosen attribute is show_name and several hits have +* the same value for show_name, then only the best one is kept and others are removed. +* - ranking: (array of strings) controls the way results are sorted. +* We have six available criteria: +* - typo: sort according to number of typos, +* - geo: sort according to decreassing distance when performing a geo-location based search, +* - proximity: sort according to the proximity of query words in hits, +* - attribute: sort according to the order of attributes defined by attributesToIndex, +* - exact: +* - if the user query contains one word: sort objects having an attribute +* that is exactly the query word before others. +* For example if you search for the "V" TV show, you want to find it +* with the "V" query and avoid to have all popular TV +* show starting by the v letter before it. +* - if the user query contains multiple words: sort according to the +* number of words that matched exactly (and not as a prefix). +* - custom: sort according to a user defined formula set in **customRanking** attribute. +* The standard order is ["typo", "geo", "proximity", "attribute", "exact", "custom"] +* - customRanking: (array of strings) lets you specify part of the ranking. +* The syntax of this condition is an array of strings containing attributes +* prefixed by asc (ascending order) or desc (descending order) operator. +* For example `"customRanking" => ["desc(population)", "asc(name)"]` +* - queryType: Select how the query words are interpreted, it can be one of the following value: +* - prefixAll: all query words are interpreted as prefixes, +* - prefixLast: only the last word is interpreted as a prefix (default behavior), +* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. +* - highlightPreTag: (string) Specify the string that is inserted before +* the highlighted parts in the query result (default to ""). +* - highlightPostTag: (string) Specify the string that is inserted after +* the highlighted parts in the query result (default to ""). +* - optionalWords: (array of strings) Specify a list of words that should +* be considered as optional when found in the query. +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the server answer or the error message if a failure occurred +*/ +Index.prototype.setSettings = function(settings, opts, callback) { + if (arguments.length === 1 || typeof opts === 'function') { + callback = opts; + opts = {}; + } + + if (opts.forwardToSlaves !== undefined) deprecateForwardToSlaves(); + var forwardToReplicas = (opts.forwardToSlaves || opts.forwardToReplicas) ? 'true' : 'false'; + + var indexObj = this; + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/settings?forwardToReplicas=' + + forwardToReplicas, + hostType: 'write', + body: settings, + callback: callback + }); +}; + +/* +* @deprecated see client.listApiKeys() +*/ +Index.prototype.listUserKeys = deprecate(function(callback) { + return this.listApiKeys(callback); +}, deprecatedMessage('index.listUserKeys()', 'client.listApiKeys()')); + +/* +* List all existing API keys to this index +* +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with API keys belonging to the index +* +* @deprecated see client.listApiKeys() +*/ +Index.prototype.listApiKeys = deprecate(function(callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys', + hostType: 'read', + callback: callback + }); +}, deprecatedMessage('index.listApiKeys()', 'client.listApiKeys()')); + +/* +* @deprecated see client.getApiKey() +*/ +Index.prototype.getUserKeyACL = deprecate(function(key, callback) { + return this.getApiKey(key, callback); +}, deprecatedMessage('index.getUserKeyACL()', 'client.getApiKey()')); + + +/* +* Get an API key from this index +* +* @param key +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the right API key +* +* @deprecated see client.getApiKey() +*/ +Index.prototype.getApiKey = deprecate(function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'read', + callback: callback + }); +}, deprecatedMessage('index.getApiKey()', 'client.getApiKey()')); + +/* +* @deprecated see client.deleteApiKey() +*/ +Index.prototype.deleteUserKey = deprecate(function(key, callback) { + return this.deleteApiKey(key, callback); +}, deprecatedMessage('index.deleteUserKey()', 'client.deleteApiKey()')); + +/* +* Delete an existing API key associated to this index +* +* @param key +* @param callback the result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the deletion date +* +* @deprecated see client.deleteApiKey() +*/ +Index.prototype.deleteApiKey = deprecate(function(key, callback) { + var indexObj = this; + return this.as._jsonRequest({ + method: 'DELETE', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/keys/' + key, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.deleteApiKey()', 'client.deleteApiKey()')); + +/* +* @deprecated see client.addApiKey() +*/ +Index.prototype.addUserKey = deprecate(function(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('index.addUserKey()', 'client.addApiKey()')); + +/* +* Add a new API key to this index +* +* @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that +* can contains the following values: +* - search: allow to search (https and http) +* - addObject: allows to add/update an object in the index (https only) +* - deleteObject : allows to delete an existing object (https only) +* - deleteIndex : allows to delete index content (https only) +* - settings : allows to get index settings (https only) +* - editSettings : allows to change index settings (https only) +* @param {Object} [params] - Optionnal parameters to set for the key +* @param {number} params.validity - Number of seconds after which the key will +* be automatically removed (0 means no time limit for this key) +* @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour +* @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call +* @param {string} params.description - A description for your key +* @param {string[]} params.referers - A list of authorized referers +* @param {Object} params.queryParameters - Force the key to use specific query parameters +* @param {Function} callback - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the added API key +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.addUserKey(['search'], { +* validity: 300, +* maxQueriesPerIPPerHour: 2000, +* maxHitsPerQuery: 3, +* description: 'Eat three fruits', +* referers: ['*.algolia.com'], +* queryParameters: { +* tagFilters: ['public'], +* } +* }) +* @see {@link https://www.algolia.com/doc/rest_api#AddIndexKey|Algolia REST API Documentation} +* +* @deprecated see client.addApiKey() +*/ +Index.prototype.addApiKey = deprecate(function(acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: index.addApiKey(arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 1 || typeof params === 'function') { + callback = params; + params = null; + } + + var postObj = { + acl: acls + }; + + if (params) { + postObj.validity = params.validity; + postObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + postObj.maxHitsPerQuery = params.maxHitsPerQuery; + postObj.description = params.description; + + if (params.queryParameters) { + postObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + postObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys', + body: postObj, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.addApiKey()', 'client.addApiKey()')); + +/** +* @deprecated use client.addApiKey() +*/ +Index.prototype.addUserKeyWithValidity = deprecate(function deprecatedAddUserKeyWithValidity(acls, params, callback) { + return this.addApiKey(acls, params, callback); +}, deprecatedMessage('index.addUserKeyWithValidity()', 'client.addApiKey()')); + +/* +* @deprecated see client.updateApiKey() +*/ +Index.prototype.updateUserKey = deprecate(function(key, acls, params, callback) { + return this.updateApiKey(key, acls, params, callback); +}, deprecatedMessage('index.updateUserKey()', 'client.updateApiKey()')); + +/** +* Update an existing API key of this index +* @param {string} key - The key to update +* @param {string[]} acls - The list of ACL for this key. Defined by an array of strings that +* can contains the following values: +* - search: allow to search (https and http) +* - addObject: allows to add/update an object in the index (https only) +* - deleteObject : allows to delete an existing object (https only) +* - deleteIndex : allows to delete index content (https only) +* - settings : allows to get index settings (https only) +* - editSettings : allows to change index settings (https only) +* @param {Object} [params] - Optionnal parameters to set for the key +* @param {number} params.validity - Number of seconds after which the key will +* be automatically removed (0 means no time limit for this key) +* @param {number} params.maxQueriesPerIPPerHour - Number of API calls allowed from an IP address per hour +* @param {number} params.maxHitsPerQuery - Number of hits this API key can retrieve in one call +* @param {string} params.description - A description for your key +* @param {string[]} params.referers - A list of authorized referers +* @param {Object} params.queryParameters - Force the key to use specific query parameters +* @param {Function} callback - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with user keys list +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.updateApiKey('APIKEY', ['search'], { +* validity: 300, +* maxQueriesPerIPPerHour: 2000, +* maxHitsPerQuery: 3, +* description: 'Eat three fruits', +* referers: ['*.algolia.com'], +* queryParameters: { +* tagFilters: ['public'], +* } +* }) +* @see {@link https://www.algolia.com/doc/rest_api#UpdateIndexKey|Algolia REST API Documentation} +* +* @deprecated see client.updateApiKey() +*/ +Index.prototype.updateApiKey = deprecate(function(key, acls, params, callback) { + var isArray = require(8); + var usage = 'Usage: index.updateApiKey(key, arrayOfAcls[, params, callback])'; + + if (!isArray(acls)) { + throw new Error(usage); + } + + if (arguments.length === 2 || typeof params === 'function') { + callback = params; + params = null; + } + + var putObj = { + acl: acls + }; + + if (params) { + putObj.validity = params.validity; + putObj.maxQueriesPerIPPerHour = params.maxQueriesPerIPPerHour; + putObj.maxHitsPerQuery = params.maxHitsPerQuery; + putObj.description = params.description; + + if (params.queryParameters) { + putObj.queryParameters = this.as._getSearchParams(params.queryParameters, ''); + } + + putObj.referers = params.referers; + } + + return this.as._jsonRequest({ + method: 'PUT', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/keys/' + key, + body: putObj, + hostType: 'write', + callback: callback + }); +}, deprecatedMessage('index.updateApiKey()', 'client.updateApiKey()')); + +},{"19":19,"20":20,"26":26,"28":28,"29":29,"30":30,"31":31,"32":32,"33":33,"7":7,"8":8}],19:[function(require,module,exports){ +'use strict'; + +// This is the object returned by the `index.browseAll()` method + +module.exports = IndexBrowser; + +var inherits = require(7); +var EventEmitter = require(4).EventEmitter; + +function IndexBrowser() { +} + +inherits(IndexBrowser, EventEmitter); + +IndexBrowser.prototype.stop = function() { + this._stopped = true; + this._clean(); +}; + +IndexBrowser.prototype._end = function() { + this.emit('end'); + this._clean(); +}; + +IndexBrowser.prototype._error = function(err) { + this.emit('error', err); + this._clean(); +}; + +IndexBrowser.prototype._result = function(content) { + this.emit('result', content); +}; + +IndexBrowser.prototype._clean = function() { + this.removeAllListeners('stop'); + this.removeAllListeners('end'); + this.removeAllListeners('error'); + this.removeAllListeners('result'); +}; + +},{"4":4,"7":7}],20:[function(require,module,exports){ +var buildSearchMethod = require(25); +var deprecate = require(28); +var deprecatedMessage = require(29); + +module.exports = IndexCore; + +/* +* Index class constructor. +* You should not use this method directly but use initIndex() function +*/ +function IndexCore(algoliasearch, indexName) { + this.indexName = indexName; + this.as = algoliasearch; + this.typeAheadArgs = null; + this.typeAheadValueOption = null; + + // make sure every index instance has it's own cache + this.cache = {}; +} + +/* +* Clear all queries in cache +*/ +IndexCore.prototype.clearCache = function() { + this.cache = {}; +}; + +/* +* Search inside the index using XMLHttpRequest request (Using a POST query to +* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). +* +* @param {string} [query] the full text query +* @param {object} [args] (optional) if set, contains an object with query parameters: +* - page: (integer) Pagination parameter used to select the page to retrieve. +* Page is zero-based and defaults to 0. Thus, +* to retrieve the 10th page you need to set page=9 +* - hitsPerPage: (integer) Pagination parameter used to select the number of hits per page. Defaults to 20. +* - attributesToRetrieve: a string that contains the list of object attributes +* you want to retrieve (let you minimize the answer size). +* Attributes are separated with a comma (for example "name,address"). +* You can also use an array (for example ["name","address"]). +* By default, all attributes are retrieved. You can also use '*' to retrieve all +* values when an attributesToRetrieve setting is specified for your index. +* - attributesToHighlight: a string that contains the list of attributes you +* want to highlight according to the query. +* Attributes are separated by a comma. You can also use an array (for example ["name","address"]). +* If an attribute has no match for the query, the raw value is returned. +* By default all indexed text attributes are highlighted. +* You can use `*` if you want to highlight all textual attributes. +* Numerical attributes are not highlighted. +* A matchLevel is returned for each highlighted attribute and can contain: +* - full: if all the query terms were found in the attribute, +* - partial: if only some of the query terms were found, +* - none: if none of the query terms were found. +* - attributesToSnippet: a string that contains the list of attributes to snippet alongside +* the number of words to return (syntax is `attributeName:nbWords`). +* Attributes are separated by a comma (Example: attributesToSnippet=name:10,content:10). +* You can also use an array (Example: attributesToSnippet: ['name:10','content:10']). +* By default no snippet is computed. +* - minWordSizefor1Typo: the minimum number of characters in a query word to accept one typo in this word. +* Defaults to 3. +* - minWordSizefor2Typos: the minimum number of characters in a query word +* to accept two typos in this word. Defaults to 7. +* - getRankingInfo: if set to 1, the result hits will contain ranking +* information in _rankingInfo attribute. +* - aroundLatLng: search for entries around a given +* latitude/longitude (specified as two floats separated by a comma). +* For example aroundLatLng=47.316669,5.016670). +* You can specify the maximum distance in meters with the aroundRadius parameter (in meters) +* and the precision for ranking with aroundPrecision +* (for example if you set aroundPrecision=100, two objects that are distant of +* less than 100m will be considered as identical for "geo" ranking parameter). +* At indexing, you should specify geoloc of an object with the _geoloc attribute +* (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) +* - insideBoundingBox: search entries inside a given area defined by the two extreme points +* of a rectangle (defined by 4 floats: p1Lat,p1Lng,p2Lat,p2Lng). +* For example insideBoundingBox=47.3165,4.9665,47.3424,5.0201). +* At indexing, you should specify geoloc of an object with the _geoloc attribute +* (in the form {"_geoloc":{"lat":48.853409, "lng":2.348800}}) +* - numericFilters: a string that contains the list of numeric filters you want to +* apply separated by a comma. +* The syntax of one filter is `attributeName` followed by `operand` followed by `value`. +* Supported operands are `<`, `<=`, `=`, `>` and `>=`. +* You can have multiple conditions on one attribute like for example numericFilters=price>100,price<1000. +* You can also use an array (for example numericFilters: ["price>100","price<1000"]). +* - tagFilters: filter the query by a set of tags. You can AND tags by separating them by commas. +* To OR tags, you must add parentheses. For example, tags=tag1,(tag2,tag3) means tag1 AND (tag2 OR tag3). +* You can also use an array, for example tagFilters: ["tag1",["tag2","tag3"]] +* means tag1 AND (tag2 OR tag3). +* At indexing, tags should be added in the _tags** attribute +* of objects (for example {"_tags":["tag1","tag2"]}). +* - facetFilters: filter the query by a list of facets. +* Facets are separated by commas and each facet is encoded as `attributeName:value`. +* For example: `facetFilters=category:Book,author:John%20Doe`. +* You can also use an array (for example `["category:Book","author:John%20Doe"]`). +* - facets: List of object attributes that you want to use for faceting. +* Comma separated list: `"category,author"` or array `['category','author']` +* Only attributes that have been added in **attributesForFaceting** index setting +* can be used in this parameter. +* You can also use `*` to perform faceting on all attributes specified in **attributesForFaceting**. +* - queryType: select how the query words are interpreted, it can be one of the following value: +* - prefixAll: all query words are interpreted as prefixes, +* - prefixLast: only the last word is interpreted as a prefix (default behavior), +* - prefixNone: no query word is interpreted as a prefix. This option is not recommended. +* - optionalWords: a string that contains the list of words that should +* be considered as optional when found in the query. +* Comma separated and array are accepted. +* - distinct: If set to 1, enable the distinct feature (disabled by default) +* if the attributeForDistinct index setting is set. +* This feature is similar to the SQL "distinct" keyword: when enabled +* in a query with the distinct=1 parameter, +* all hits containing a duplicate value for the attributeForDistinct attribute are removed from results. +* For example, if the chosen attribute is show_name and several hits have +* the same value for show_name, then only the best +* one is kept and others are removed. +* - restrictSearchableAttributes: List of attributes you want to use for +* textual search (must be a subset of the attributesToIndex index setting) +* either comma separated or as an array +* @param {function} [callback] the result callback called with two arguments: +* error: null or Error('message'). If false, the content contains the error. +* content: the server answer that contains the list of results. +*/ +IndexCore.prototype.search = buildSearchMethod('query'); + +/* +* -- BETA -- +* Search a record similar to the query inside the index using XMLHttpRequest request (Using a POST query to +* minimize number of OPTIONS queries: Cross-Origin Resource Sharing). +* +* @param {string} [query] the similar query +* @param {object} [args] (optional) if set, contains an object with query parameters. +* All search parameters are supported (see search function), restrictSearchableAttributes and facetFilters +* are the two most useful to restrict the similar results and get more relevant content +*/ +IndexCore.prototype.similarSearch = deprecate( + buildSearchMethod('similarQuery'), + deprecatedMessage( + 'index.similarSearch(query[, callback])', + 'index.search({ similarQuery: query }[, callback])' + ) +); + +/* +* Browse index content. The response content will have a `cursor` property that you can use +* to browse subsequent pages for this query. Use `index.browseFrom(cursor)` when you want. +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @param {Function} [callback] - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the browse result +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.browse('cool songs', { +* tagFilters: 'public,comments', +* hitsPerPage: 500 +* }, callback); +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +IndexCore.prototype.browse = function(query, queryParameters, callback) { + var merge = require(33); + + var indexObj = this; + + var page; + var hitsPerPage; + + // we check variadic calls that are not the one defined + // .browse()/.browse(fn) + // => page = 0 + if (arguments.length === 0 || arguments.length === 1 && typeof arguments[0] === 'function') { + page = 0; + callback = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'number') { + // .browse(2)/.browse(2, 10)/.browse(2, fn)/.browse(2, 10, fn) + page = arguments[0]; + if (typeof arguments[1] === 'number') { + hitsPerPage = arguments[1]; + } else if (typeof arguments[1] === 'function') { + callback = arguments[1]; + hitsPerPage = undefined; + } + query = undefined; + queryParameters = undefined; + } else if (typeof arguments[0] === 'object') { + // .browse(queryParameters)/.browse(queryParameters, cb) + if (typeof arguments[1] === 'function') { + callback = arguments[1]; + } + queryParameters = arguments[0]; + query = undefined; + } else if (typeof arguments[0] === 'string' && typeof arguments[1] === 'function') { + // .browse(query, cb) + callback = arguments[1]; + queryParameters = undefined; + } + + // otherwise it's a .browse(query)/.browse(query, queryParameters)/.browse(query, queryParameters, cb) + + // get search query parameters combining various possible calls + // to .browse(); + queryParameters = merge({}, queryParameters || {}, { + page: page, + hitsPerPage: hitsPerPage, + query: query + }); + + var params = this.as._getSearchParams(queryParameters, ''); + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/browse', + body: {params: params}, + hostType: 'read', + callback: callback + }); +}; + +/* +* Continue browsing from a previous position (cursor), obtained via a call to `.browse()`. +* +* @param {string} query - The full text query +* @param {Object} [queryParameters] - Any search query parameter +* @param {Function} [callback] - The result callback called with two arguments +* error: null or Error('message') +* content: the server answer with the browse result +* @return {Promise|undefined} Returns a promise if no callback given +* @example +* index.browseFrom('14lkfsakl32', callback); +* @see {@link https://www.algolia.com/doc/rest_api#Browse|Algolia REST API Documentation} +*/ +IndexCore.prototype.browseFrom = function(cursor, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + encodeURIComponent(this.indexName) + '/browse', + body: {cursor: cursor}, + hostType: 'read', + callback: callback + }); +}; + +/* +* Search for facet values +* https://www.algolia.com/doc/rest-api/search#search-for-facet-values +* +* @param {string} params.facetName Facet name, name of the attribute to search for values in. +* Must be declared as a facet +* @param {string} params.facetQuery Query for the facet search +* @param {string} [params.*] Any search parameter of Algolia, +* see https://www.algolia.com/doc/api-client/javascript/search#search-parameters +* Pagination is not supported. The page and hitsPerPage parameters will be ignored. +* @param callback (optional) +*/ +IndexCore.prototype.searchForFacetValues = function(params, callback) { + var clone = require(26); + var omit = require(34); + var usage = 'Usage: index.searchForFacetValues({facetName, facetQuery, ...params}[, callback])'; + + if (params.facetName === undefined || params.facetQuery === undefined) { + throw new Error(usage); + } + + var facetName = params.facetName; + var filteredParams = omit(clone(params), function(keyName) { + return keyName === 'facetName'; + }); + var searchParameters = this.as._getSearchParams(filteredParams, ''); + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/' + + encodeURIComponent(this.indexName) + '/facets/' + encodeURIComponent(facetName) + '/query', + hostType: 'read', + body: {params: searchParameters}, + callback: callback + }); +}; + +IndexCore.prototype.searchFacet = deprecate(function(params, callback) { + return this.searchForFacetValues(params, callback); +}, deprecatedMessage( + 'index.searchFacet(params[, callback])', + 'index.searchForFacetValues(params[, callback])' +)); + +IndexCore.prototype._search = function(params, url, callback, additionalUA) { + return this.as._jsonRequest({ + cache: this.cache, + method: 'POST', + url: url || '/1/indexes/' + encodeURIComponent(this.indexName) + '/query', + body: {params: params}, + hostType: 'read', + fallback: { + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(this.indexName), + body: {params: params} + }, + callback: callback, + additionalUA: additionalUA + }); +}; + +/* +* Get an object from this index +* +* @param objectID the unique identifier of the object to retrieve +* @param attrs (optional) if set, contains the array of attribute names to retrieve +* @param callback (optional) the result callback called with two arguments +* error: null or Error('message') +* content: the object to retrieve or the error message if a failure occurred +*/ +IndexCore.prototype.getObject = function(objectID, attrs, callback) { + var indexObj = this; + + if (arguments.length === 1 || typeof attrs === 'function') { + callback = attrs; + attrs = undefined; + } + + var params = ''; + if (attrs !== undefined) { + params = '?attributes='; + for (var i = 0; i < attrs.length; ++i) { + if (i !== 0) { + params += ','; + } + params += attrs[i]; + } + } + + return this.as._jsonRequest({ + method: 'GET', + url: '/1/indexes/' + encodeURIComponent(indexObj.indexName) + '/' + encodeURIComponent(objectID) + params, + hostType: 'read', + callback: callback + }); +}; + +/* +* Get several objects from this index +* +* @param objectIDs the array of unique identifier of objects to retrieve +*/ +IndexCore.prototype.getObjects = function(objectIDs, attributesToRetrieve, callback) { + var isArray = require(8); + var map = require(32); + + var usage = 'Usage: index.getObjects(arrayOfObjectIDs[, callback])'; + + if (!isArray(objectIDs)) { + throw new Error(usage); + } + + var indexObj = this; + + if (arguments.length === 1 || typeof attributesToRetrieve === 'function') { + callback = attributesToRetrieve; + attributesToRetrieve = undefined; + } + + var body = { + requests: map(objectIDs, function prepareRequest(objectID) { + var request = { + indexName: indexObj.indexName, + objectID: objectID + }; + + if (attributesToRetrieve) { + request.attributesToRetrieve = attributesToRetrieve.join(','); + } + + return request; + }) + }; + + return this.as._jsonRequest({ + method: 'POST', + url: '/1/indexes/*/objects', + hostType: 'read', + body: body, + callback: callback + }); +}; + +IndexCore.prototype.as = null; +IndexCore.prototype.indexName = null; +IndexCore.prototype.typeAheadArgs = null; +IndexCore.prototype.typeAheadValueOption = null; + +},{"25":25,"26":26,"28":28,"29":29,"32":32,"33":33,"34":34,"8":8}],21:[function(require,module,exports){ +'use strict'; + +var AlgoliaSearch = require(16); +var createAlgoliasearch = require(22); + +module.exports = createAlgoliasearch(AlgoliaSearch, 'Browser'); + +},{"16":16,"22":22}],22:[function(require,module,exports){ +(function (process){ +'use strict'; + +var global = require(6); +var Promise = global.Promise || require(3).Promise; + +// This is the standalone browser build entry point +// Browser implementation of the Algolia Search JavaScript client, +// using XMLHttpRequest, XDomainRequest and JSONP as fallback +module.exports = function createAlgoliasearch(AlgoliaSearch, uaSuffix) { + var inherits = require(7); + var errors = require(30); + var inlineHeaders = require(23); + var jsonpRequest = require(24); + var places = require(35); + uaSuffix = uaSuffix || ''; + + if (process.env.NODE_ENV === 'debug') { + require(1).enable('algoliasearch*'); + } + + function algoliasearch(applicationID, apiKey, opts) { + var cloneDeep = require(26); + + opts = cloneDeep(opts || {}); + + opts._ua = opts._ua || algoliasearch.ua; + + return new AlgoliaSearchBrowser(applicationID, apiKey, opts); + } + + algoliasearch.version = require(37); + + algoliasearch.ua = + 'Algolia for JavaScript (' + algoliasearch.version + '); ' + uaSuffix; + + algoliasearch.initPlaces = places(algoliasearch); + + // we expose into window no matter how we are used, this will allow + // us to easily debug any website running algolia + global.__algolia = { + debug: require(1), + algoliasearch: algoliasearch + }; + + var support = { + hasXMLHttpRequest: 'XMLHttpRequest' in global, + hasXDomainRequest: 'XDomainRequest' in global + }; + + if (support.hasXMLHttpRequest) { + support.cors = 'withCredentials' in new XMLHttpRequest(); + } + + function AlgoliaSearchBrowser() { + // call AlgoliaSearch constructor + AlgoliaSearch.apply(this, arguments); + } + + inherits(AlgoliaSearchBrowser, AlgoliaSearch); + + AlgoliaSearchBrowser.prototype._request = function request(url, opts) { + return new Promise(function wrapRequest(resolve, reject) { + // no cors or XDomainRequest, no request + if (!support.cors && !support.hasXDomainRequest) { + // very old browser, not supported + reject(new errors.Network('CORS not supported')); + return; + } + + url = inlineHeaders(url, opts.headers); + + var body = opts.body; + var req = support.cors ? new XMLHttpRequest() : new XDomainRequest(); + var reqTimeout; + var timedOut; + var connected = false; + + reqTimeout = setTimeout(onTimeout, opts.timeouts.connect); + // we set an empty onprogress listener + // so that XDomainRequest on IE9 is not aborted + // refs: + // - https://github.com/algolia/algoliasearch-client-js/issues/76 + // - https://social.msdn.microsoft.com/Forums/ie/en-US/30ef3add-767c-4436-b8a9-f1ca19b4812e/ie9-rtm-xdomainrequest-issued-requests-may-abort-if-all-event-handlers-not-specified?forum=iewebdevelopment + req.onprogress = onProgress; + if ('onreadystatechange' in req) req.onreadystatechange = onReadyStateChange; + req.onload = onLoad; + req.onerror = onError; + + // do not rely on default XHR async flag, as some analytics code like hotjar + // breaks it and set it to false by default + if (req instanceof XMLHttpRequest) { + req.open(opts.method, url, true); + + // The Analytics API never accepts Auth headers as query string + // this option exists specifically for them. + if (opts.forceAuthHeaders) { + req.setRequestHeader( + 'x-algolia-application-id', + opts.headers['x-algolia-application-id'] + ); + req.setRequestHeader( + 'x-algolia-api-key', + opts.headers['x-algolia-api-key'] + ); + } + } else { + req.open(opts.method, url); + } + + // headers are meant to be sent after open + if (support.cors) { + if (body) { + if (opts.method === 'POST') { + // https://developer.mozilla.org/en-US/docs/Web/HTTP/Access_control_CORS#Simple_requests + req.setRequestHeader('content-type', 'application/x-www-form-urlencoded'); + } else { + req.setRequestHeader('content-type', 'application/json'); + } + } + req.setRequestHeader('accept', 'application/json'); + } + + if (body) { + req.send(body); + } else { + req.send(); + } + + // event object not received in IE8, at least + // but we do not use it, still important to note + function onLoad(/* event */) { + // When browser does not supports req.timeout, we can + // have both a load and timeout event, since handled by a dumb setTimeout + if (timedOut) { + return; + } + + clearTimeout(reqTimeout); + + var out; + + try { + out = { + body: JSON.parse(req.responseText), + responseText: req.responseText, + statusCode: req.status, + // XDomainRequest does not have any response headers + headers: req.getAllResponseHeaders && req.getAllResponseHeaders() || {} + }; + } catch (e) { + out = new errors.UnparsableJSON({ + more: req.responseText + }); + } + + if (out instanceof errors.UnparsableJSON) { + reject(out); + } else { + resolve(out); + } + } + + function onError(event) { + if (timedOut) { + return; + } + + clearTimeout(reqTimeout); + + // error event is trigerred both with XDR/XHR on: + // - DNS error + // - unallowed cross domain request + reject( + new errors.Network({ + more: event + }) + ); + } + + function onTimeout() { + timedOut = true; + req.abort(); + + reject(new errors.RequestTimeout()); + } + + function onConnect() { + connected = true; + clearTimeout(reqTimeout); + reqTimeout = setTimeout(onTimeout, opts.timeouts.complete); + } + + function onProgress() { + if (!connected) onConnect(); + } + + function onReadyStateChange() { + if (!connected && req.readyState > 1) onConnect(); + } + }); + }; + + AlgoliaSearchBrowser.prototype._request.fallback = function requestFallback(url, opts) { + url = inlineHeaders(url, opts.headers); + + return new Promise(function wrapJsonpRequest(resolve, reject) { + jsonpRequest(url, opts, function jsonpRequestDone(err, content) { + if (err) { + reject(err); + return; + } + + resolve(content); + }); + }); + }; + + AlgoliaSearchBrowser.prototype._promise = { + reject: function rejectPromise(val) { + return Promise.reject(val); + }, + resolve: function resolvePromise(val) { + return Promise.resolve(val); + }, + delay: function delayPromise(ms) { + return new Promise(function resolveOnTimeout(resolve/* , reject*/) { + setTimeout(resolve, ms); + }); + }, + all: function all(promises) { + return Promise.all(promises); + } + }; + + return algoliasearch; +}; + +}).call(this,require(12)) +},{"1":1,"12":12,"23":23,"24":24,"26":26,"3":3,"30":30,"35":35,"37":37,"6":6,"7":7}],23:[function(require,module,exports){ +'use strict'; + +module.exports = inlineHeaders; + +var encode = require(14); + +function inlineHeaders(url, headers) { + if (/\?/.test(url)) { + url += '&'; + } else { + url += '?'; + } + + return url + encode(headers); +} + +},{"14":14}],24:[function(require,module,exports){ +'use strict'; + +module.exports = jsonpRequest; + +var errors = require(30); + +var JSONPCounter = 0; + +function jsonpRequest(url, opts, cb) { + if (opts.method !== 'GET') { + cb(new Error('Method ' + opts.method + ' ' + url + ' is not supported by JSONP.')); + return; + } + + opts.debug('JSONP: start'); + + var cbCalled = false; + var timedOut = false; + + JSONPCounter += 1; + var head = document.getElementsByTagName('head')[0]; + var script = document.createElement('script'); + var cbName = 'algoliaJSONP_' + JSONPCounter; + var done = false; + + window[cbName] = function(data) { + removeGlobals(); + + if (timedOut) { + opts.debug('JSONP: Late answer, ignoring'); + return; + } + + cbCalled = true; + + clean(); + + cb(null, { + body: data, + responseText: JSON.stringify(data)/* , + // We do not send the statusCode, there's no statusCode in JSONP, it will be + // computed using data.status && data.message like with XDR + statusCode*/ + }); + }; + + // add callback by hand + url += '&callback=' + cbName; + + // add body params manually + if (opts.jsonBody && opts.jsonBody.params) { + url += '&' + opts.jsonBody.params; + } + + var ontimeout = setTimeout(timeout, opts.timeouts.complete); + + // script onreadystatechange needed only for + // <= IE8 + // https://github.com/angular/angular.js/issues/4523 + script.onreadystatechange = readystatechange; + script.onload = success; + script.onerror = error; + + script.async = true; + script.defer = true; + script.src = url; + head.appendChild(script); + + function success() { + opts.debug('JSONP: success'); + + if (done || timedOut) { + return; + } + + done = true; + + // script loaded but did not call the fn => script loading error + if (!cbCalled) { + opts.debug('JSONP: Fail. Script loaded but did not call the callback'); + clean(); + cb(new errors.JSONPScriptFail()); + } + } + + function readystatechange() { + if (this.readyState === 'loaded' || this.readyState === 'complete') { + success(); + } + } + + function clean() { + clearTimeout(ontimeout); + script.onload = null; + script.onreadystatechange = null; + script.onerror = null; + head.removeChild(script); + } + + function removeGlobals() { + try { + delete window[cbName]; + delete window[cbName + '_loaded']; + } catch (e) { + window[cbName] = window[cbName + '_loaded'] = undefined; + } + } + + function timeout() { + opts.debug('JSONP: Script timeout'); + timedOut = true; + clean(); + cb(new errors.RequestTimeout()); + } + + function error() { + opts.debug('JSONP: Script error'); + + if (done || timedOut) { + return; + } + + clean(); + cb(new errors.JSONPScriptError()); + } +} + +},{"30":30}],25:[function(require,module,exports){ +module.exports = buildSearchMethod; + +var errors = require(30); + +/** + * Creates a search method to be used in clients + * @param {string} queryParam the name of the attribute used for the query + * @param {string} url the url + * @return {function} the search method + */ +function buildSearchMethod(queryParam, url) { + /** + * The search method. Prepares the data and send the query to Algolia. + * @param {string} query the string used for query search + * @param {object} args additional parameters to send with the search + * @param {function} [callback] the callback to be called with the client gets the answer + * @return {undefined|Promise} If the callback is not provided then this methods returns a Promise + */ + return function search(query, args, callback) { + // warn V2 users on how to search + if (typeof query === 'function' && typeof args === 'object' || + typeof callback === 'object') { + // .search(query, params, cb) + // .search(cb, params) + throw new errors.AlgoliaSearchError('index.search usage is index.search(query, params, cb)'); + } + + // Normalizing the function signature + if (arguments.length === 0 || typeof query === 'function') { + // Usage : .search(), .search(cb) + callback = query; + query = ''; + } else if (arguments.length === 1 || typeof args === 'function') { + // Usage : .search(query/args), .search(query, cb) + callback = args; + args = undefined; + } + // At this point we have 3 arguments with values + + // Usage : .search(args) // careful: typeof null === 'object' + if (typeof query === 'object' && query !== null) { + args = query; + query = undefined; + } else if (query === undefined || query === null) { // .search(undefined/null) + query = ''; + } + + var params = ''; + + if (query !== undefined) { + params += queryParam + '=' + encodeURIComponent(query); + } + + var additionalUA; + if (args !== undefined) { + if (args.additionalUA) { + additionalUA = args.additionalUA; + delete args.additionalUA; + } + // `_getSearchParams` will augment params, do not be fooled by the = versus += from previous if + params = this.as._getSearchParams(args, params); + } + + + return this._search(params, url, callback, additionalUA); + }; +} + +},{"30":30}],26:[function(require,module,exports){ +module.exports = function clone(obj) { + return JSON.parse(JSON.stringify(obj)); +}; + +},{}],27:[function(require,module,exports){ +module.exports = createAnalyticsClient; + +var algoliasearch = require(21); + +function createAnalyticsClient(appId, apiKey, opts) { + var analytics = {}; + + opts = opts || {}; + // there need to be 4 hosts, like on the client, since if requests fail, + // the counter goes up by 1, so we need to have the same amount of hosts + // 4 because: -dsn, -1, -2, -3 + // This is done because the APPID used for search will be the same for the analytics client created, + // and since the state of available hosts is shared by APPID globally for the module, we had issues + // where the hostIndex would be 1 while the array was only one entry (you got an empty host) + opts.hosts = opts.hosts || [ + 'analytics.algolia.com', + 'analytics.algolia.com', + 'analytics.algolia.com', + 'analytics.algolia.com' + ]; + opts.protocol = opts.protocol || 'https:'; + + analytics.as = algoliasearch(appId, apiKey, opts); + + analytics.getABTests = function(_params, callback) { + var params = params || {}; + var offset = params.offset || 0; + var limit = params.limit || 10; + + return this.as._jsonRequest({ + method: 'GET', + url: '/2/abtests?offset=' + encodeURIComponent(offset) + '&limit=' + encodeURIComponent(limit), + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.getABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'GET', + url: '/2/abtests/' + encodeURIComponent(abTestID), + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.addABTest = function(abTest, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/2/abtests', + body: abTest, + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.stopABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'POST', + url: '/2/abtests/' + encodeURIComponent(abTestID) + '/stop', + hostType: 'read', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.deleteABTest = function(abTestID, callback) { + return this.as._jsonRequest({ + method: 'DELETE', + url: '/2/abtests/' + encodeURIComponent(abTestID), + hostType: 'write', + forceAuthHeaders: true, + callback: callback + }); + }; + + analytics.waitTask = function(indexName, taskID, callback) { + return this.as.initIndex(indexName).waitTask(taskID, callback); + }; + + return analytics; +} + +},{"21":21}],28:[function(require,module,exports){ +module.exports = function deprecate(fn, message) { + var warned = false; + + function deprecated() { + if (!warned) { + /* eslint no-console:0 */ + console.warn(message); + warned = true; + } + + return fn.apply(this, arguments); + } + + return deprecated; +}; + +},{}],29:[function(require,module,exports){ +module.exports = function deprecatedMessage(previousUsage, newUsage) { + var githubAnchorLink = previousUsage.toLowerCase() + .replace(/[\.\(\)]/g, ''); + + return 'algoliasearch: `' + previousUsage + '` was replaced by `' + newUsage + + '`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#' + githubAnchorLink; +}; + +},{}],30:[function(require,module,exports){ +'use strict'; + +// This file hosts our error definitions +// We use custom error "types" so that we can act on them when we need it +// e.g.: if error instanceof errors.UnparsableJSON then.. + +var inherits = require(7); + +function AlgoliaSearchError(message, extraProperties) { + var forEach = require(5); + + var error = this; + + // try to get a stacktrace + if (typeof Error.captureStackTrace === 'function') { + Error.captureStackTrace(this, this.constructor); + } else { + error.stack = (new Error()).stack || 'Cannot get a stacktrace, browser is too old'; + } + + this.name = 'AlgoliaSearchError'; + this.message = message || 'Unknown error'; + + if (extraProperties) { + forEach(extraProperties, function addToErrorObject(value, key) { + error[key] = value; + }); + } +} + +inherits(AlgoliaSearchError, Error); + +function createCustomError(name, message) { + function AlgoliaSearchCustomError() { + var args = Array.prototype.slice.call(arguments, 0); + + // custom message not set, use default + if (typeof args[0] !== 'string') { + args.unshift(message); + } + + AlgoliaSearchError.apply(this, args); + this.name = 'AlgoliaSearch' + name + 'Error'; + } + + inherits(AlgoliaSearchCustomError, AlgoliaSearchError); + + return AlgoliaSearchCustomError; +} + +// late exports to let various fn defs and inherits take place +module.exports = { + AlgoliaSearchError: AlgoliaSearchError, + UnparsableJSON: createCustomError( + 'UnparsableJSON', + 'Could not parse the incoming response as JSON, see err.more for details' + ), + RequestTimeout: createCustomError( + 'RequestTimeout', + 'Request timed out before getting a response' + ), + Network: createCustomError( + 'Network', + 'Network issue, see err.more for details' + ), + JSONPScriptFail: createCustomError( + 'JSONPScriptFail', + '"),window.ALGOLIA_SUPPORTS_DOCWRITE===!0?(document.write(''),n("document.write")()):r(o,n("DOMElement"))}catch(s){r(o,n("DOMElement"))}}function n(e){return function(){var t="AlgoliaSearch: loaded V2 script using "+e;window.console&&window.console.log&&window.console.log(t)}}t.exports=o},{1:1}],4:[function(e,t,r){"use strict";function o(){var e="-- AlgoliaSearch V2 => V3 error --\nYou are trying to use a new version of the AlgoliaSearch JavaScript client with an old notation.\nPlease read our migration guide at https://github.com/algolia/algoliasearch-client-js/wiki/Migration-guide-from-2.x.x-to-3.x.x\n-- /AlgoliaSearch V2 => V3 error --";window.AlgoliaSearch=function(){throw new Error(e)},window.AlgoliaSearchHelper=function(){throw new Error(e)},window.AlgoliaExplainResults=function(){throw new Error(e)}}t.exports=o},{}],5:[function(e,t,r){"use strict";function o(t){var r=e(2),o=e(3),n=e(4);r(t)?o(t):n()}o("algoliasearch")},{2:2,3:3,4:4}]},{},[5])(5)}),function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{var t;t="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,t.algoliasearch=e()}}(function(){var e;return function t(e,r,o){function n(s,a){if(!r[s]){if(!e[s]){var c="function"==typeof require&&require;if(!a&&c)return c(s,!0);if(i)return i(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var l=r[s]={exports:{}};e[s][0].call(l.exports,function(t){var r=e[s][1][t];return n(r?r:t)},l,l.exports,t,e,r,o)}return r[s].exports}for(var i="function"==typeof require&&require,s=0;s=31||"undefined"!=typeof navigator&&navigator.userAgent&&navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/))}function i(e){var t=this.useColors;if(e[0]=(t?"%c":"")+this.namespace+(t?" %c":" ")+e[0]+(t?"%c ":" ")+"+"+r.humanize(this.diff),t){var o="color: "+this.color;e.splice(1,0,o,"color: inherit");var n=0,i=0;e[0].replace(/%[a-zA-Z%]/g,function(e){"%%"!==e&&(n++,"%c"===e&&(i=n))}),e.splice(i,0,o)}}function s(){return"object"==typeof console&&console.log&&Function.prototype.apply.call(console.log,console,arguments)}function a(e){try{null==e?r.storage.removeItem("debug"):r.storage.debug=e}catch(t){}}function c(){var e;try{e=r.storage.debug}catch(t){}return!e&&"undefined"!=typeof o&&"env"in o&&(e=o.env.DEBUG),e}function u(){try{return window.localStorage}catch(e){}}r=t.exports=e(2),r.log=s,r.formatArgs=i,r.save=a,r.load=c,r.useColors=n,r.storage="undefined"!=typeof chrome&&"undefined"!=typeof chrome.storage?chrome.storage.local:u(),r.colors=["lightseagreen","forestgreen","goldenrod","dodgerblue","darkorchid","crimson"],r.formatters.j=function(e){try{return JSON.stringify(e)}catch(t){return"[UnexpectedJSONParseError]: "+t.message}},r.enable(c())}).call(this,e(12))},{12:12,2:2}],2:[function(e,t,r){function o(e){var t,o=0;for(t in e)o=(o<<5)-o+e.charCodeAt(t),o|=0;return r.colors[Math.abs(o)%r.colors.length]}function n(e){function t(){if(t.enabled){var e=t,o=+new Date,n=o-(u||o);e.diff=n,e.prev=u,e.curr=o,u=o;for(var i=new Array(arguments.length),s=0;s0&&this._events[e].length>r&&(this._events[e].warned=!0,console.error("(node) warning: possible EventEmitter memory leak detected. %d listeners added. Use emitter.setMaxListeners() to increase limit.",this._events[e].length),"function"==typeof console.trace&&console.trace())),this},o.prototype.on=o.prototype.addListener,o.prototype.once=function(e,t){function r(){this.removeListener(e,r),o||(o=!0,t.apply(this,arguments))}if(!n(t))throw TypeError("listener must be a function");var o=!1;return r.listener=t,this.on(e,r),this},o.prototype.removeListener=function(e,t){var r,o,i,a;if(!n(t))throw TypeError("listener must be a function");if(!this._events||!this._events[e])return this;if(r=this._events[e],i=r.length,o=-1,r===t||n(r.listener)&&r.listener===t)delete this._events[e],this._events.removeListener&&this.emit("removeListener",e,t);else if(s(r)){for(a=i;a-- >0;)if(r[a]===t||r[a].listener&&r[a].listener===t){o=a;break}if(o<0)return this;1===r.length?(r.length=0,delete this._events[e]):r.splice(o,1),this._events.removeListener&&this.emit("removeListener",e,t)}return this},o.prototype.removeAllListeners=function(e){var t,r;if(!this._events)return this;if(!this._events.removeListener)return 0===arguments.length?this._events={}:this._events[e]&&delete this._events[e],this;if(0===arguments.length){for(t in this._events)"removeListener"!==t&&this.removeAllListeners(t);return this.removeAllListeners("removeListener"),this._events={},this}if(r=this._events[e],n(r))this.removeListener(e,r);else if(r)for(;r.length;)this.removeListener(e,r[r.length-1]);return delete this._events[e],this},o.prototype.listeners=function(e){var t;return t=this._events&&this._events[e]?n(this._events[e])?[this._events[e]]:this._events[e].slice():[]},o.prototype.listenerCount=function(e){if(this._events){var t=this._events[e];if(n(t))return 1;if(t)return t.length}return 0},o.listenerCount=function(e,t){return e.listenerCount(t)}},{}],5:[function(e,t,r){var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString;t.exports=function(e,t,r){if("[object Function]"!==n.call(t))throw new TypeError("iterator must be a function");var i=e.length;if(i===+i)for(var s=0;s100)){var t=/^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(e);if(t){var r=parseFloat(t[1]),o=(t[2]||"ms").toLowerCase();switch(o){case"years":case"year":case"yrs":case"yr":case"y":return r*p;case"days":case"day":case"d":return r*l;case"hours":case"hour":case"hrs":case"hr":case"h":return r*u;case"minutes":case"minute":case"mins":case"min":case"m":return r*c;case"seconds":case"second":case"secs":case"sec":case"s":return r*a;case"milliseconds":case"millisecond":case"msecs":case"msec":case"ms":return r;default:return}}}}function n(e){return e>=l?Math.round(e/l)+"d":e>=u?Math.round(e/u)+"h":e>=c?Math.round(e/c)+"m":e>=a?Math.round(e/a)+"s":e+"ms"}function i(e){return s(e,l,"day")||s(e,u,"hour")||s(e,c,"minute")||s(e,a,"second")||e+" ms"}function s(e,t,r){if(!(e0)return o(e);if("number"===r&&isNaN(e)===!1)return t["long"]?i(e):n(e);throw new Error("val is not a non-empty string or a valid number. val="+JSON.stringify(e))}},{}],10:[function(e,t,r){"use strict";var o=Object.prototype.hasOwnProperty,n=Object.prototype.toString,i=Array.prototype.slice,s=e(11),a=Object.prototype.propertyIsEnumerable,c=!a.call({toString:null},"toString"),u=a.call(function(){},"prototype"),l=["toString","toLocaleString","valueOf","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","constructor"],p=function(e){var t=e.constructor;return t&&t.prototype===e},d={$console:!0,$external:!0,$frame:!0,$frameElement:!0,$frames:!0,$innerHeight:!0,$innerWidth:!0,$outerHeight:!0,$outerWidth:!0,$pageXOffset:!0,$pageYOffset:!0,$parent:!0,$scrollLeft:!0,$scrollTop:!0,$scrollX:!0,$scrollY:!0,$self:!0,$webkitIndexedDB:!0,$webkitStorageInfo:!0,$window:!0},h=function(){if("undefined"==typeof window)return!1;for(var e in window)try{if(!d["$"+e]&&o.call(window,e)&&null!==window[e]&&"object"==typeof window[e])try{p(window[e])}catch(t){return!0}}catch(t){return!0}return!1}(),f=function(e){if("undefined"==typeof window||!h)return p(e);try{return p(e)}catch(t){return!1}},y=function(e){var t=null!==e&&"object"==typeof e,r="[object Function]"===n.call(e),i=s(e),a=t&&"[object String]"===n.call(e),p=[];if(!t&&!r&&!i)throw new TypeError("Object.keys called on a non-object");var d=u&&r;if(a&&e.length>0&&!o.call(e,0))for(var h=0;h0)for(var y=0;y=0&&"[object Function]"===o.call(e.callee)),r}},{}],12:[function(e,t,r){function o(){throw new Error("setTimeout has not been defined")}function n(){throw new Error("clearTimeout has not been defined")}function i(e){if(p===setTimeout)return setTimeout(e,0);if((p===o||!p)&&setTimeout)return p=setTimeout,setTimeout(e,0);try{return p(e,0)}catch(t){try{return p.call(null,e,0)}catch(t){return p.call(this,e,0)}}}function s(e){if(d===clearTimeout)return clearTimeout(e);if((d===n||!d)&&clearTimeout)return d=clearTimeout,clearTimeout(e);try{return d(e)}catch(t){try{return d.call(null,e)}catch(t){return d.call(this,e)}}}function a(){m&&f&&(m=!1,f.length?y=f.concat(y):v=-1,y.length&&c())}function c(){if(!m){var e=i(a);m=!0;for(var t=y.length;t;){for(f=y,y=[];++v1)for(var r=1;r0&&u>c&&(u=c);for(var l=0;l=0?(p=y.substr(0,m),d=y.substr(m+1)):(p=y,d=""),h=decodeURIComponent(p),f=decodeURIComponent(d),o(s,h)?n(s[h])?s[h].push(f):s[h]=[s[h],f]:s[h]=f}return s};var n=Array.isArray||function(e){return"[object Array]"===Object.prototype.toString.call(e)}},{}],14:[function(e,t,r){"use strict";function o(e,t){if(e.map)return e.map(t);for(var r=[],o=0;o0)n.scope=r;else if("undefined"!=typeof r)throw new Error("the scope given to `copyIndex` was not an array with settings, synonyms or rules");return this._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(e)+"/operation",body:n,hostType:"write",callback:i})},o.prototype.getLogs=function(t,r,o){var n=e(26),i={};return"object"==typeof t?(i=n(t),o=r):0===arguments.length||"function"==typeof t?o=t:1===arguments.length||"function"==typeof r?(o=r,i.offset=t):(i.offset=t,i.length=r),void 0===i.offset&&(i.offset=0),void 0===i.length&&(i.length=10),this._jsonRequest({method:"GET",url:"/1/logs?"+this._getSearchParams(i,""),hostType:"read",callback:o})},o.prototype.listIndexes=function(e,t){var r="";return void 0===e||"function"==typeof e?t=e:r="?page="+e,this._jsonRequest({method:"GET",url:"/1/indexes"+r,hostType:"read",callback:t})},o.prototype.initIndex=function(e){return new i(this,e)},o.prototype.initAnalytics=function(t){var r=e(27);return r(this.applicationID,this.apiKey,t)},o.prototype.listUserKeys=s(function(e){return this.listApiKeys(e)},a("client.listUserKeys()","client.listApiKeys()")),o.prototype.listApiKeys=function(e){return this._jsonRequest({method:"GET",url:"/1/keys",hostType:"read",callback:e})},o.prototype.getUserKeyACL=s(function(e,t){return this.getApiKey(e,t)},a("client.getUserKeyACL()","client.getApiKey()")),o.prototype.getApiKey=function(e,t){return this._jsonRequest({method:"GET",url:"/1/keys/"+e,hostType:"read",callback:t})},o.prototype.deleteUserKey=s(function(e,t){return this.deleteApiKey(e,t)},a("client.deleteUserKey()","client.deleteApiKey()")),o.prototype.deleteApiKey=function(e,t){return this._jsonRequest({method:"DELETE",url:"/1/keys/"+e,hostType:"write",callback:t})},o.prototype.restoreApiKey=function(e,t){return this._jsonRequest({method:"POST",url:"/1/keys/"+e+"/restore",hostType:"write",callback:t})},o.prototype.addUserKey=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKey()","client.addApiKey()")),o.prototype.addApiKey=function(t,r,o){var n=e(8),i="Usage: client.addApiKey(arrayOfAcls[, params, callback])";if(!n(t))throw new Error(i);1!==arguments.length&&"function"!=typeof r||(o=r,r=null);var s={acl:t};return r&&(s.validity=r.validity,s.maxQueriesPerIPPerHour=r.maxQueriesPerIPPerHour,s.maxHitsPerQuery=r.maxHitsPerQuery,s.indexes=r.indexes,s.description=r.description,r.queryParameters&&(s.queryParameters=this._getSearchParams(r.queryParameters,"")),s.referers=r.referers),this._jsonRequest({method:"POST",url:"/1/keys",body:s,hostType:"write",callback:o})},o.prototype.addUserKeyWithValidity=s(function(e,t,r){return this.addApiKey(e,t,r)},a("client.addUserKeyWithValidity()","client.addApiKey()")),o.prototype.updateUserKey=s(function(e,t,r,o){return this.updateApiKey(e,t,r,o)},a("client.updateUserKey()","client.updateApiKey()")),o.prototype.updateApiKey=function(t,r,o,n){var i=e(8),s="Usage: client.updateApiKey(key, arrayOfAcls[, params, callback])";if(!i(r))throw new Error(s);2!==arguments.length&&"function"!=typeof o||(n=o,o=null);var a={acl:r};return o&&(a.validity=o.validity,a.maxQueriesPerIPPerHour=o.maxQueriesPerIPPerHour,a.maxHitsPerQuery=o.maxHitsPerQuery,a.indexes=o.indexes,a.description=o.description,o.queryParameters&&(a.queryParameters=this._getSearchParams(o.queryParameters,"")),a.referers=o.referers),this._jsonRequest({method:"PUT",url:"/1/keys/"+t,body:a,hostType:"write",callback:n})},o.prototype.startQueriesBatch=s(function(){this._batch=[]},a("client.startQueriesBatch()","client.search()")),o.prototype.addQueryInBatch=s(function(e,t,r){this._batch.push({indexName:e,query:t,params:r})},a("client.addQueryInBatch()","client.search()")),o.prototype.sendQueriesBatch=s(function(e){return this.search(this._batch,e)},a("client.sendQueriesBatch()","client.search()")),o.prototype.batch=function(t,r){var o=e(8),n="Usage: client.batch(operations[, callback])";if(!o(t))throw new Error(n);return this._jsonRequest({method:"POST",url:"/1/indexes/*/batch",body:{requests:t},hostType:"write",callback:r})},o.prototype.assignUserID=function(e,t){if(!e.userID||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both a userID and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping",hostType:"write",body:{cluster:e.cluster},callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.assignUserIDs=function(e,t){if(!e.userIDs||!e.cluster)throw new l.AlgoliaSearchError("You have to provide both an array of userIDs and cluster",e);return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/batch",hostType:"write",body:{cluster:e.cluster,users:e.userIDs},callback:t})},o.prototype.getTopUserID=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/top",hostType:"read",callback:e})},o.prototype.getUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"GET",url:"/1/clusters/mapping/"+e.userID,hostType:"read",callback:t})},o.prototype.listClusters=function(e){return this._jsonRequest({method:"GET",url:"/1/clusters",hostType:"read",callback:e})},o.prototype.listUserIDs=function(e,t){return this._jsonRequest({method:"GET",url:"/1/clusters/mapping",body:e,hostType:"read",callback:t})},o.prototype.removeUserID=function(e,t){if(!e.userID)throw new l.AlgoliaSearchError("You have to provide a userID",{debugData:e});return this._jsonRequest({method:"DELETE",url:"/1/clusters/mapping",hostType:"write",callback:t,headers:{"x-algolia-user-id":e.userID}})},o.prototype.searchUserIDs=function(e,t){return this._jsonRequest({method:"POST",url:"/1/clusters/mapping/search",body:e,hostType:"read",callback:t})},o.prototype.setPersonalizationStrategy=function(e,t){return this._jsonRequest({method:"POST",url:"/1/recommendation/personalization/strategy",body:e,hostType:"write",callback:t})},o.prototype.getPersonalizationStrategy=function(e){return this._jsonRequest({method:"GET",url:"/1/recommendation/personalization/strategy",hostType:"read",callback:e})},o.prototype.destroy=n,o.prototype.enableRateLimitForward=n,o.prototype.disableRateLimitForward=n,o.prototype.useSecuredAPIKey=n,o.prototype.disableSecuredAPIKey=n,o.prototype.generateSecuredApiKey=n,o.prototype.getSecuredApiKeyRemainingValidity=n},{17:17,18:18,26:26,27:27,28:28,29:29,30:30,7:7,8:8}],17:[function(e,t,r){ +(function(r){function o(t,r,o){var i=e(1)("algoliasearch"),s=e(26),a=e(8),u=e(32),l="Usage: algoliasearch(applicationID, apiKey, opts)";if(o._allowEmptyCredentials!==!0&&!t)throw new c.AlgoliaSearchError("Please provide an application ID. "+l);if(o._allowEmptyCredentials!==!0&&!r)throw new c.AlgoliaSearchError("Please provide an API key. "+l);this.applicationID=t,this.apiKey=r,this.hosts={read:[],write:[]},o=o||{},this._timeouts=o.timeouts||{connect:1e3,read:2e3,write:3e4},o.timeout&&(this._timeouts.connect=this._timeouts.read=this._timeouts.write=o.timeout);var p=o.protocol||"https:";if(/:$/.test(p)||(p+=":"),"http:"!==p&&"https:"!==p)throw new c.AlgoliaSearchError("protocol must be `http:` or `https:` (was `"+o.protocol+"`)");if(this._checkAppIdData(),o.hosts)a(o.hosts)?(this.hosts.read=s(o.hosts),this.hosts.write=s(o.hosts)):(this.hosts.read=s(o.hosts.read),this.hosts.write=s(o.hosts.write));else{var d=u(this._shuffleResult,function(e){return t+"-"+e+".algolianet.com"}),h=(o.dsn===!1?"":"-dsn")+".algolia.net";this.hosts.read=[this.applicationID+h].concat(d),this.hosts.write=[this.applicationID+".algolia.net"].concat(d)}this.hosts.read=u(this.hosts.read,n(p)),this.hosts.write=u(this.hosts.write,n(p)),this.extraHeaders={},this.cache=o._cache||{},this._ua=o._ua,this._useCache=!(void 0!==o._useCache&&!o._cache)||o._useCache,this._useRequestCache=this._useCache&&o._useRequestCache,this._useFallback=void 0===o.useFallback||o.useFallback,this._setTimeout=o._setTimeout,i("init done, %j",this)}function n(e){return function(t){return e+"//"+t.toLowerCase()}}function i(e){if(void 0===Array.prototype.toJSON)return JSON.stringify(e);var t=Array.prototype.toJSON;delete Array.prototype.toJSON;var r=JSON.stringify(e);return Array.prototype.toJSON=t,r}function s(e){for(var t,r,o=e.length;0!==o;)r=Math.floor(Math.random()*o),o-=1,t=e[o],e[o]=e[r],e[r]=t;return e}function a(e){var t={};for(var r in e)if(Object.prototype.hasOwnProperty.call(e,r)){var o;o="x-algolia-api-key"===r||"x-algolia-application-id"===r?"**hidden for security purposes**":e[r],t[r]=o}return t}t.exports=o;var c=e(30),u=e(31),l=e(20),p=e(36),d=500,h=r.env.RESET_APP_DATA_TIMER&&parseInt(r.env.RESET_APP_DATA_TIMER,10)||12e4;o.prototype.initIndex=function(e){return new l(this,e)},o.prototype.setExtraHeader=function(e,t){this.extraHeaders[e.toLowerCase()]=t},o.prototype.getExtraHeader=function(e){return this.extraHeaders[e.toLowerCase()]},o.prototype.unsetExtraHeader=function(e){delete this.extraHeaders[e.toLowerCase()]},o.prototype.addAlgoliaAgent=function(e){var t="; "+e;this._ua.indexOf(t)===-1&&(this._ua+=t)},o.prototype._jsonRequest=function(t){function r(e,n){function u(e){var t=e&&e.body&&e.body.message&&e.body.status||e.statusCode||e&&e.body&&200;h("received response: statusCode: %s, computed statusCode: %d, headers: %j",e.statusCode,t,e.headers);var r=2===Math.floor(t/100),o=new Date;if(w.push({currentHost:A,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:o,duration:o-x,statusCode:t}),r)return m._useCache&&!m._useRequestCache&&y&&(y[l]=e.responseText),{responseText:e.responseText,body:e.body};var i=4!==Math.floor(t/100);if(i)return v+=1,_();h("unrecoverable error");var u=new c.AlgoliaSearchError(e.body&&e.body.message,{debugData:w,statusCode:t});return m._promise.reject(u)}function d(e){h("error: %s, stack: %s",e.message,e.stack);var r=new Date;return w.push({currentHost:A,headers:a(p),content:s||null,contentLength:void 0!==s?s.length:null,method:n.method,timeouts:n.timeouts,url:n.url,startTime:x,endTime:r,duration:r-x}),e instanceof c.AlgoliaSearchError||(e=new c.Unknown(e&&e.message,e)),v+=1,e instanceof c.Unknown||e instanceof c.UnparsableJSON||v>=m.hosts[t.hostType].length&&(g||!b)?(e.debugData=w,m._promise.reject(e)):e instanceof c.RequestTimeout?T():_()}function _(){return h("retrying request"),m._incrementHostIndex(t.hostType),r(e,n)}function T(){return h("retrying request with higher timeout"),m._incrementHostIndex(t.hostType),m._incrementTimeoutMultipler(),n.timeouts=m._getTimeoutsForRequest(t.hostType),r(e,n)}m._checkAppIdData();var x=new Date;if(m._useCache&&!m._useRequestCache&&(l=t.url),m._useCache&&!m._useRequestCache&&s&&(l+="_body_"+n.body),o(!m._useRequestCache,y,l)){h("serving response from cache");var R=y[l];return m._promise.resolve({body:JSON.parse(R),responseText:R})}if(v>=m.hosts[t.hostType].length)return!b||g?(h("could not get any response"),m._promise.reject(new c.AlgoliaSearchError("Cannot connect to the AlgoliaSearch API. Send an email to support@algolia.com to report and resolve the issue. Application id was: "+m.applicationID,{debugData:w}))):(h("switching to fallback"),v=0,n.method=t.fallback.method,n.url=t.fallback.url,n.jsonBody=t.fallback.body,n.jsonBody&&(n.body=i(n.jsonBody)),p=m._computeRequestHeaders({additionalUA:f,headers:t.headers}),n.timeouts=m._getTimeoutsForRequest(t.hostType),m._setHostIndexByType(0,t.hostType),g=!0,r(m._request.fallback,n));var A=m._getHostByType(t.hostType),j=A+n.url,S={body:n.body,jsonBody:n.jsonBody,method:n.method,headers:p,timeouts:n.timeouts,debug:h,forceAuthHeaders:n.forceAuthHeaders};return h("method: %s, url: %s, headers: %j, timeouts: %d",S.method,j,S.headers,S.timeouts),e===m._request.fallback&&h("using fallback"),e.call(m,j,S).then(u,d)}function o(e,t,r){return m._useCache&&e&&t&&void 0!==t[r]}function n(e,r){return o(m._useRequestCache,y,l)&&e["catch"](function(){delete y[l]}),"function"!=typeof t.callback?e.then(r):void e.then(function(e){u(function(){t.callback(null,r(e))},m._setTimeout||setTimeout)},function(e){u(function(){t.callback(e)},m._setTimeout||setTimeout)})}this._checkAppIdData();var s,l,p,h=e(1)("algoliasearch:"+t.url),f=t.additionalUA||"",y=t.cache,m=this,v=0,g=!1,b=m._useFallback&&m._request.fallback&&t.fallback;this.apiKey.length>d&&void 0!==t.body&&(void 0!==t.body.params||void 0!==t.body.requests)?(t.body.apiKey=this.apiKey,p=this._computeRequestHeaders({additionalUA:f,withApiKey:!1,headers:t.headers})):p=this._computeRequestHeaders({additionalUA:f,headers:t.headers}),void 0!==t.body&&(s=i(t.body)),h("request start");var w=[];if(m._useCache&&m._useRequestCache&&(l=t.url),m._useCache&&m._useRequestCache&&s&&(l+="_body_"+s),o(m._useRequestCache,y,l)){h("serving request from cache");var _=y[l],T="function"!=typeof _.then?m._promise.resolve({responseText:_}):_;return n(T,function(e){return JSON.parse(e.responseText)})}var x=r(m._request,{url:t.url,method:t.method,body:s,jsonBody:t.body,timeouts:m._getTimeoutsForRequest(t.hostType),forceAuthHeaders:t.forceAuthHeaders});return m._useCache&&m._useRequestCache&&y&&(y[l]=x),n(x,function(e){return e.body})},o.prototype._getSearchParams=function(e,t){if(void 0===e||null===e)return t;for(var r in e)null!==r&&void 0!==e[r]&&e.hasOwnProperty(r)&&(t+=""===t?"":"&",t+=r+"="+encodeURIComponent("[object Array]"===Object.prototype.toString.call(e[r])?i(e[r]):e[r]));return t},o.prototype._computeRequestHeaders=function(t){var r=e(5),o=t.additionalUA?this._ua+"; "+t.additionalUA:this._ua,n={"x-algolia-agent":o,"x-algolia-application-id":this.applicationID};return t.withApiKey!==!1&&(n["x-algolia-api-key"]=this.apiKey),this.userToken&&(n["x-algolia-usertoken"]=this.userToken),this.securityTags&&(n["x-algolia-tagfilters"]=this.securityTags),r(this.extraHeaders,function(e,t){n[t]=e}),t.headers&&r(t.headers,function(e,t){n[t]=e}),n},o.prototype.search=function(t,r,o){var n=e(8),i=e(32),s="Usage: client.search(arrayOfQueries[, callback])";if(!n(t))throw new Error(s);"function"==typeof r?(o=r,r={}):void 0===r&&(r={});var a=this,c={requests:i(t,function(e){var t="";return void 0!==e.query&&(t+="query="+encodeURIComponent(e.query)),{indexName:e.indexName,params:a._getSearchParams(e.params,t)}})},u=i(c.requests,function(e,t){return t+"="+encodeURIComponent("/1/indexes/"+encodeURIComponent(e.indexName)+"?"+e.params)}).join("&"),l="/1/indexes/*/queries";return void 0!==r.strategy&&(c.strategy=r.strategy),this._jsonRequest({cache:this.cache,method:"POST",url:l,body:c,hostType:"read",fallback:{method:"GET",url:"/1/indexes/*",body:{params:u}},callback:o})},o.prototype.searchForFacetValues=function(t){var r=e(8),o=e(32),n="Usage: client.searchForFacetValues([{indexName, params: {facetName, facetQuery, ...params}}, ...queries])";if(!r(t))throw new Error(n);var i=this;return i._promise.all(o(t,function(t){if(!t||void 0===t.indexName||void 0===t.params.facetName||void 0===t.params.facetQuery)throw new Error(n);var r=e(26),o=e(34),s=t.indexName,a=t.params,c=a.facetName,u=o(r(a),function(e){return"facetName"===e}),l=i._getSearchParams(u,"");return i._jsonRequest({cache:i.cache,method:"POST",url:"/1/indexes/"+encodeURIComponent(s)+"/facets/"+encodeURIComponent(c)+"/query",hostType:"read",body:{params:l}})}))},o.prototype.setSecurityTags=function(e){if("[object Array]"===Object.prototype.toString.call(e)){for(var t=[],r=0;rh?this._resetInitialAppIdData(e):e},o.prototype._resetInitialAppIdData=function(e){var t=e||{};return t.hostIndexes={read:0,write:0},t.timeoutMultiplier=1,t.shuffleResult=t.shuffleResult||s([1,2,3]),this._setAppIdData(t)},o.prototype._cacheAppIdData=function(e){this._hostIndexes=e.hostIndexes,this._timeoutMultiplier=e.timeoutMultiplier,this._shuffleResult=e.shuffleResult},o.prototype._partialAppIdDataUpdate=function(t){var r=e(5),o=this._getAppIdData();return r(t,function(e,t){o[t]=e}),this._setAppIdData(o)},o.prototype._getHostByType=function(e){return this.hosts[e][this._getHostIndexByType(e)]},o.prototype._getTimeoutMultiplier=function(){return this._timeoutMultiplier},o.prototype._getHostIndexByType=function(e){return this._hostIndexes[e]},o.prototype._setHostIndexByType=function(t,r){var o=e(26),n=o(this._hostIndexes);return n[r]=t,this._partialAppIdDataUpdate({hostIndexes:n}),t},o.prototype._incrementHostIndex=function(e){return this._setHostIndexByType((this._getHostIndexByType(e)+1)%this.hosts[e].length,e)},o.prototype._incrementTimeoutMultipler=function(){var e=Math.max(this._timeoutMultiplier+1,4);return this._partialAppIdDataUpdate({timeoutMultiplier:e})},o.prototype._getTimeoutsForRequest=function(e){return{connect:this._timeouts.connect*this._timeoutMultiplier,complete:this._timeouts[e]*this._timeoutMultiplier}}}).call(this,e(12))},{1:1,12:12,20:20,26:26,30:30,31:31,32:32,34:34,36:36,5:5,8:8}],18:[function(e,t,r){function o(){s.apply(this,arguments)}function n(e,t,r){function o(r,n){var i={page:r||0,hitsPerPage:t||100},s=n||[];return e(i).then(function(e){var t=e.hits,r=e.nbHits,n=t.map(function(e){return delete e._highlightResult,e}),a=s.concat(n);return a.lengths&&(t=s),"published"!==e.status?l._promise.delay(t).then(r):e})}function o(e){u(function(){t(null,e)},l._setTimeout||setTimeout)}function n(e){u(function(){t(e)},l._setTimeout||setTimeout)}var i=100,s=5e3,a=0,c=this,l=c.as,p=r();return t?void p.then(o,n):p},o.prototype.clearIndex=function(e){var t=this;return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(t.indexName)+"/clear",hostType:"write",callback:e})},o.prototype.getSettings=function(e,t){1===arguments.length&&"function"==typeof e&&(t=e,e={}),e=e||{};var r=encodeURIComponent(this.indexName);return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+r+"/settings?getVersion=2"+(e.advanced?"&advanced="+e.advanced:""),hostType:"read",callback:t})},o.prototype.searchSynonyms=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/search",body:e,hostType:"read",callback:t})},o.prototype.exportSynonyms=function(e,t){return n(this.searchSynonyms.bind(this),e,t)},o.prototype.saveSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getSynonym=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteSynonym=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearSynonyms=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),void 0!==e.forwardToSlaves&&p();var r=e.forwardToSlaves||e.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchSynonyms=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={}),void 0!==t.forwardToSlaves&&p();var o=t.forwardToSlaves||t.forwardToReplicas?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/synonyms/batch?forwardToReplicas="+o+"&replaceExistingSynonyms="+(t.replaceExistingSynonyms?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.searchRules=function(e,t){return"function"==typeof e?(t=e,e={}):void 0===e&&(e={}),this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/search",body:e,hostType:"read",callback:t})},o.prototype.exportRules=function(e,t){return n(this.searchRules.bind(this),e,t)},o.prototype.saveRule=function(e,t,r){if("function"==typeof t?(r=t,t={}):void 0===t&&(t={}),!e.objectID)throw new l.AlgoliaSearchError("Missing or empty objectID field for rule");var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"PUT",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e.objectID)+"?forwardToReplicas="+o,body:e,hostType:"write",callback:r})},o.prototype.getRule=function(e,t){return this.as._jsonRequest({method:"GET",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e),hostType:"read",callback:t})},o.prototype.deleteRule=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"DELETE",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/"+encodeURIComponent(e)+"?forwardToReplicas="+o,hostType:"write",callback:r})},o.prototype.clearRules=function(e,t){"function"==typeof e?(t=e,e={}):void 0===e&&(e={});var r=e.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/clear?forwardToReplicas="+r,hostType:"write",callback:t})},o.prototype.batchRules=function(e,t,r){"function"==typeof t?(r=t,t={}):void 0===t&&(t={});var o=t.forwardToReplicas===!0?"true":"false";return this.as._jsonRequest({method:"POST",url:"/1/indexes/"+encodeURIComponent(this.indexName)+"/rules/batch?forwardToReplicas="+o+"&clearExistingRules="+(t.clearExistingRules===!0?"true":"false"),hostType:"write",body:e,callback:r})},o.prototype.exists=function(e){var t=this.getSettings().then(function(){return!0})["catch"](function(e){if(e instanceof l.AlgoliaSearchError&&404===e.statusCode)return!1;throw e});return"function"!=typeof e?t:void t.then(function(t){e(null,t)})["catch"](function(t){e(t)})},o.prototype.findObject=function(e,t,r){t=void 0===t?{}:t;var o=void 0===t.paginate||t.paginate,n=void 0!==t.query?t.query:"",i=this,s=0,a=function(){return t.page=s,i.search(n,t).then(function(t){for(var r=t.hits,n=0;n=t.nbPages)throw new l.ObjectNotFound("Object not found");return a()})},c=a(s);return void 0===r?c:void c.then(function(e){r(null,e)})["catch"](function(e){r(e)})},o.prototype.getObjectPosition=function(e,t){for(var r=e.hits,o=0;o1&&a()}if(!h.cors&&!h.hasXDomainRequest)return void o(new u.Network("CORS not supported"));e=l(e,t.headers);var d,f,y=t.body,m=h.cors?new XMLHttpRequest:new XDomainRequest,v=!1;d=setTimeout(s,t.timeouts.connect),m.onprogress=c,"onreadystatechange"in m&&(m.onreadystatechange=p),m.onload=n,m.onerror=i,m instanceof XMLHttpRequest?(m.open(t.method,e,!0),t.forceAuthHeaders&&(m.setRequestHeader("x-algolia-application-id",t.headers["x-algolia-application-id"]),m.setRequestHeader("x-algolia-api-key",t.headers["x-algolia-api-key"]))):m.open(t.method,e),h.cors&&(y&&("POST"===t.method?m.setRequestHeader("content-type","application/x-www-form-urlencoded"):m.setRequestHeader("content-type","application/json")),m.setRequestHeader("accept","application/json")),y?m.send(y):m.send()})},a.prototype._request.fallback=function(e,t){return e=l(e,t.headers),new n(function(r,o){p(e,t,function(e,t){return e?void o(e):void r(t)})})},a.prototype._promise={reject:function(e){return n.reject(e)},resolve:function(e){return n.resolve(e)},delay:function(e){return new n(function(t){setTimeout(t,e)})},all:function(e){return n.all(e)}},s}}).call(this,e(12))},{1:1,12:12,23:23,24:24,26:26,3:3,30:30,35:35,37:37,6:6,7:7}],23:[function(e,t,r){"use strict";function o(e,t){return e+=/\?/.test(e)?"&":"?",e+n(t)}t.exports=o;var n=e(14)},{14:14}],24:[function(e,t,r){"use strict";function o(e,t,r){function o(){t.debug("JSONP: success"),m||d||(m=!0,p||(t.debug("JSONP: Fail. Script loaded but did not call the callback"), +a(),r(new n.JSONPScriptFail)))}function s(){"loaded"!==this.readyState&&"complete"!==this.readyState||o()}function a(){clearTimeout(v),f.onload=null,f.onreadystatechange=null,f.onerror=null,h.removeChild(f)}function c(){try{delete window[y],delete window[y+"_loaded"]}catch(e){window[y]=window[y+"_loaded"]=void 0}}function u(){t.debug("JSONP: Script timeout"),d=!0,a(),r(new n.RequestTimeout)}function l(){t.debug("JSONP: Script error"),m||d||(a(),r(new n.JSONPScriptError))}if("GET"!==t.method)return void r(new Error("Method "+t.method+" "+e+" is not supported by JSONP."));t.debug("JSONP: start");var p=!1,d=!1;i+=1;var h=document.getElementsByTagName("head")[0],f=document.createElement("script"),y="algoliaJSONP_"+i,m=!1;window[y]=function(e){return c(),d?void t.debug("JSONP: Late answer, ignoring"):(p=!0,a(),void r(null,{body:e,responseText:JSON.stringify(e)}))},e+="&callback="+y,t.jsonBody&&t.jsonBody.params&&(e+="&"+t.jsonBody.params);var v=setTimeout(u,t.timeouts.complete);f.onreadystatechange=s,f.onload=o,f.onerror=l,f.async=!0,f.defer=!0,f.src=e,h.appendChild(f)}t.exports=o;var n=e(30),i=0},{30:30}],25:[function(e,t,r){function o(e,t){return function(r,o,i){if("function"==typeof r&&"object"==typeof o||"object"==typeof i)throw new n.AlgoliaSearchError("index.search usage is index.search(query, params, cb)");0===arguments.length||"function"==typeof r?(i=r,r=""):1!==arguments.length&&"function"!=typeof o||(i=o,o=void 0),"object"==typeof r&&null!==r?(o=r,r=void 0):void 0!==r&&null!==r||(r="");var s="";void 0!==r&&(s+=e+"="+encodeURIComponent(r));var a;return void 0!==o&&(o.additionalUA&&(a=o.additionalUA,delete o.additionalUA),s=this.as._getSearchParams(o,s)),this._search(s,t,i,a)}}t.exports=o;var n=e(30)},{30:30}],26:[function(e,t,r){t.exports=function(e){return JSON.parse(JSON.stringify(e))}},{}],27:[function(e,t,r){function o(e,t,r){var o={};return r=r||{},r.hosts=r.hosts||["analytics.algolia.com","analytics.algolia.com","analytics.algolia.com","analytics.algolia.com"],r.protocol=r.protocol||"https:",o.as=n(e,t,r),o.getABTests=function(e,t){var r=r||{},o=r.offset||0,n=r.limit||10;return this.as._jsonRequest({method:"GET",url:"/2/abtests?offset="+encodeURIComponent(o)+"&limit="+encodeURIComponent(n),hostType:"read",forceAuthHeaders:!0,callback:t})},o.getABTest=function(e,t){return this.as._jsonRequest({method:"GET",url:"/2/abtests/"+encodeURIComponent(e),hostType:"read",forceAuthHeaders:!0,callback:t})},o.addABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests",body:e,hostType:"read",forceAuthHeaders:!0,callback:t})},o.stopABTest=function(e,t){return this.as._jsonRequest({method:"POST",url:"/2/abtests/"+encodeURIComponent(e)+"/stop",hostType:"read",forceAuthHeaders:!0,callback:t})},o.deleteABTest=function(e,t){return this.as._jsonRequest({method:"DELETE",url:"/2/abtests/"+encodeURIComponent(e),hostType:"write",forceAuthHeaders:!0,callback:t})},o.waitTask=function(e,t,r){return this.as.initIndex(e).waitTask(t,r)},o}t.exports=o;var n=e(21)},{21:21}],28:[function(e,t,r){t.exports=function(e,t){function r(){return o||(console.warn(t),o=!0),e.apply(this,arguments)}var o=!1;return r}},{}],29:[function(e,t,r){t.exports=function(e,t){var r=e.toLowerCase().replace(/[\.\(\)]/g,"");return"algoliasearch: `"+e+"` was replaced by `"+t+"`. Please see https://github.com/algolia/algoliasearch-client-javascript/wiki/Deprecated#"+r}},{}],30:[function(e,t,r){"use strict";function o(t,r){var o=e(5),n=this;"function"==typeof Error.captureStackTrace?Error.captureStackTrace(this,this.constructor):n.stack=(new Error).stack||"Cannot get a stacktrace, browser is too old",this.name="AlgoliaSearchError",this.message=t||"Unknown error",r&&o(r,function(e,t){n[t]=e})}function n(e,t){function r(){var r=Array.prototype.slice.call(arguments,0);"string"!=typeof r[0]&&r.unshift(t),o.apply(this,r),this.name="AlgoliaSearch"+e+"Error"}return i(r,o),r}var i=e(7);i(o,Error),t.exports={AlgoliaSearchError:o,UnparsableJSON:n("UnparsableJSON","Could not parse the incoming response as JSON, see err.more for details"),RequestTimeout:n("RequestTimeout","Request timed out before getting a response"),Network:n("Network","Network issue, see err.more for details"),JSONPScriptFail:n("JSONPScriptFail","
正在自动验证中...

以编程方式调用验证方式

如果您希望更好地控制 reCAPTCHA 的运行时间,可以在 grecaptcha 对象中使用 execute 方法。为此,您需要在 reCAPTCHA 脚本加载中添加 render 参数。

  • 使用您的网站密钥加载 JavaScript API。

    1
    2
    3
    <script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>
    // 如无法连接 Google 服务器,请使用以下地址。
    <script src="https://recaptcha.net/recaptcha/api.js?render=reCAPTCHA_site_key"></script>
  • 针对您要保护的每项操作调用 grecaptcha.execute。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function onClick(e) {
    e.preventDefault();
    grecaptcha.ready(function () {
    grecaptcha
    .execute("reCAPTCHA_site_key", { action: "submit" })
    .then(function (token) {
    // Add your logic to submit to your backend server here.
    });
    });
    }
  • 在您的后端验证 reCAPTCHA 令牌。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const {
    "success": true|false, // whether this request was a valid reCAPTCHA token for your site
    "score": number // the score for this request (0.0 - 1.0)
    "action": string // the action name for this request (important to verify)
    "challenge_ts": timestamp, // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
    "hostname": string, // the hostname of the site where the reCAPTCHA was solved
    "error-codes": [...] // optional
    } = await fetch("https://www.google.com/recaptcha/api/siteverify", {
    method: "POST",
    headers: {
    "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
    },
    body: `secret=your_secret&response=${token}`,
    })

基于验证方式 (v2)

示例

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<html>
<head>
<title>reCAPTCHA</title>
<script
src="https://www.recaptcha.net/recaptcha/api.js"
async
defer
></script>
</head>
<body>
<div id="html_element"></div>
<script>
function open() {
grecaptcha.render("html_element", {
sitekey: "6Lf2irwpAAAAAIfBI_Bjo7TAccpnUAPsiI01rF7x",
callback: async function (response) {
await fetch("https://api.zhangsifan.com/google/getReCAPTCHA", {
method: "POST",
body: JSON.stringify({
token: response,
id: "6Lf2irwpAAAAAIfBI_Bjo7TAccpnUAPsiI01rF7x",
}),
headers: {
"Content-Type": "application/json",
},
}).then(async (response) => {
const { result } = await response.json();
if (result.success) {
if (window.confirm("验证成功,是否重置验证器?")) {
grecaptcha.reset();
}
} else {
window.alert("验证失败,是否重置验证器?");
grecaptcha.reset();
}
});
},
});
}
setTimeout(() => open(), 3000);
</script>
</body>
</html>
]]> + + + reCAPTCHA使用先进的风险分析引擎和自适应挑战来防止恶意软件在您的网站上进行滥用活动。与此同时,合法用户将能够登录、购买、查看页面或创建帐户,而虚假用户将被屏蔽。 + + + + + + + + + + + + 通行密钥开发 Passkey + + https://blog.zhangsifan.com/posts/20230806pa/ + 2023-08-06T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 使用文档

网站通过使用通行密钥代替密码,可提高用户帐号的安全性并简化用户帐号的管理和使用。借助通行密钥,用户可以使用设备的屏幕锁定功能(例如指纹锁、人脸识别锁或设备 PIN 码)来登录网站或应用。必须先创建通行密钥、将其与用户帐号关联,并将其公钥存储在服务器上,之后用户才能使用该通行密钥进行登录。

示例

示例网站

代码实现

前端代码

安装依赖库

1
npm install @simplewebauthn/browser

后端需实现的接口

注册验证器

1
2
3
从依赖方(您的服务器)获取注册选项 (/passkey/generate-registration-options)
将身份验证者的回复提交给依赖方进行验证 (/passkey/verify-registration)

使用验证器

1
2
从依赖方(您的服务器)获取身份验证选项 (/passkey/generate-authentication-options)
将身份验证者的回复提交给依赖方进行验证 (/passkey/verify-authentication)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
<template>
<div class="container">
<el-input
v-model.trim="state.uniid"
style="width: 300px"
placeholder="请输入用户唯一标识符/邮箱账号"
/>
<el-button @click="init" :disabled="!state.uniid">创建通行密钥</el-button>
<el-button @click="login">通过通行密钥登录</el-button>
</div>
</template>
<script setup lang="ts">
import {
reactive,
onMounted,
getCurrentInstance,
type ComponentInternalInstance,
} from "vue";
import {
startRegistration,
startAuthentication,
} from "@simplewebauthn/browser";
import { ElMessage } from "element-plus";
const currentInstance = getCurrentInstance() as ComponentInternalInstance;
const { $UtilsHttp } = currentInstance.appContext.config.globalProperties;
const state = reactive({
uniid: "",
});
const init = async () => {
try {
const { result } = await $UtilsHttp(
"/passkey/generate-registration-options",
"get",
{
uniid: state.uniid,
}
);
const challenge = result.challenge;
let attResp;
try {
attResp = await startRegistration(result);
} catch (error) {
if (error.name === "InvalidStateError") {
ElMessage.error(
"Authenticator was probably already registered by user"
);
} else {
ElMessage.error(error.toString());
}
throw error;
}
try {
const { result } = await $UtilsHttp(
"/passkey/verify-registration",
"post",
{
attResp,
challenge,
}
);
if (result) ElMessage.success("已成功添加通行密钥(PassKey)!");
} catch (error) {
ElMessage.error(error.response.data.error);
}
} catch (error) {
ElMessage.error(error.response.data.message);
}
};
const login = async () => {
const { result } = await $UtilsHttp(
"/passkey/generate-authentication-options",
"get"
);
const challenge = result.challenge;
let asseResp;
try {
asseResp = await startAuthentication(result);
} catch (error) {
ElMessage.error(error.toString());
throw error;
}
try {
const {
result: { flag, token },
} = await $UtilsHttp("/passkey/verify-authentication", "post", {
asseResp,
challenge,
});
if (flag) ElMessage.success("使用通行密钥(PassKey)登录成功!!");
} catch (error) {
ElMessage.error(error.response.data.message);
}
};
onMounted(() => {});
</script>
<style lang="less" scoped></style>

后端代码

安装依赖库

1
npm install @simplewebauthn/server
]]>
+ + + 在 Web 应用中使用表单自动填充功能实现通行密钥 + + + + + + +
+ + + Tauri使用教程 + + https://blog.zhangsifan.com/posts/20230505tr/ + 2023-05-05T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方网站

Tauri 是什么?

Tauri 是一个构建适用于所有主流桌面和移动平台的轻快二进制文件的框架。开发者们可以集成任何用于创建用户界面的可以被编译成 HTML、JavaScript 和 CSS 的前端框架,同时可以在必要时使用 Rust、Swift 和 Kotlin 等语言编写后端逻辑。

前置要求

系统依赖项

Windows

Tauri 使用 Microsoft C++ 构建工具进行开发以及 Microsoft Edge WebView2。这些都是在 Windows 上进行开发所必需的。

按照以下步骤安装所需的依赖项。

Microsoft C++ 构建工具

  • 下载 Microsoft C++ 构建工具 安装程序并打开它以开始安装。
  • 在安装过程中,选中“使用 C++ 进行桌面开发”选项。

Visual Studio C++ 构建工具 安装程序 截图

WebView2

WebView 2 已安装在 Windows 10(从版本 1803 开始)和更高版本的 Windows 上。如果你正在这些版本之一上进行开发,则可以跳过此步骤并直接转到 下载并安装 Rust。

Tauri 使用 Microsoft Edge WebView2 在 Windows 上呈现内容。

通过访问 WebView2 Runtime 下载区 安装 WebView2。下载“常青版独立安装程序(Evergreen Boostrapper)”并安装它。

MacOS

Tauri 使用 Xcode 以及各种 macOS 和 iOS 开发依赖项。

从以下位置之一下载并安装 Xcode:

请务必在安装后启动 Xcode,以使它完成设置。

下载并安装 Rust

Tauri 使用 Rust 构建并需要它进行开发。使用以下方法之一安装 Rust。你可以在 https://www.rust-lang.org/tools/install 查看更多安装方法。

Linux/MacOs

1
curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Windows

前往 https://www.rust-lang.org/tools/install 下载 rustup。

创建项目

使用 create-tauri-app

1
2
3
4
5
6
npm create tauri-app@latest


cd tauri-app
npm install
npm run tauri dev
]]>
+ + + Tauri + + + + + + +
+ + + GeeTest3.0使用教程 + + https://blog.zhangsifan.com/posts/20230330gt/ + 2023-03-30T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方网站

示例

前端

准备工作:确保已经在极验用户后台获取到了 captchaId

配置参数

1.引入初始化函数

1
<script src="https://static.geetest.com/static/js/gt.0.4.9.js"></script>

2.初始化

1
<div id="captcha"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ajax({
url: "API1接口(详见服务端部署)",
type: "get",
dataType: "json",
success: function (data) {
//请检测data的数据结构, 保证data.gt, data.challenge, data.success有值
initGeetest(
{
// 以下配置参数来自服务端 SDK
gt: data.gt,
challenge: data.challenge,
offline: !data.success,
new_captcha: true,
},
function (captchaObj) {
captchaObj.appendTo("#captcha");
}
);
},
});

3.二次验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
initGeetest(
{
// 省略配置参数
},
function (captchaObj) {
// 省略其他方法的调用
captchaObj.onSuccess(function () {
var result = captchaObj.getValidate();
// ajax 伪代码
$.ajax({
url: "服务端",
data: result,
dataType: "json",
success: function (res) {
console.log(res.result);
},
});
});
}
);

重置

1
captchaObj.reset();

后端

后端使用 Nodejs + Express

官方 Demo

]]>
+ + + GeeTest3.0使用教程。极验「行为验证」是一项可以帮助你的网站与APP识别与拦截机器程序批量自动化操作的SaaS应用。它是由极验开发的新一代人机验证产品,它不基于传统“问题-答案”的检测模式,而是通过利用深度学习对验证过程中产生的行为数据进行高维分析,发现人机行为模式与行为特征的差异,更加精准地区分人机行为。 + + + + + + + + +
+ + + GeeTest4.0使用教程 + + https://blog.zhangsifan.com/posts/20230329gt/ + 2023-03-29T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方网站

示例

前端

准备工作:确保已经在极验用户后台获取到了 captchaId

配置参数

1.引入初始化函数

1
<script src="https://static.geetest.com/v4/gt4.js"></script>

2.初始化

1
<div id="captcha"></div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
initGeetest4(
{
captchaId: "您的captchaId",
nativeButton: {
width: "300px",
height: "40px",
}, // 极验按钮样式设置
userInfo: "user@geetest.com", // 用户信息
},
function (captcha) {
// captcha为验证码实例
captcha.appendTo("#captcha"); // 调用appendTo将验证码插入到页的某一个元素中,这个元素用户可以自定义
}
);

3.二次验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
initGeetest4(
{
// 省略配置参数
},
function (captchaObj) {
// 省略其他方法的调用

// 这里调用了 onSuccess 方法,该方法介绍见下文
captchaObj.onSuccess(function () {
var result = captchaObj.getValidate();

// ajax 伪代码
$.ajax({
url: "服务端",
data: result,
dataType: "json",
success: function (res) {
console.log(res.result);
},
});
});
}
);

重置

1
captchaObj.reset();

后端

后端使用 Nodejs + Express

官方 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
var express = require("express");
var querystring = require("querystring");
const crypto = require("crypto");
var axios = require("axios");
var router = express.Router();

// geetest 公钥
// geetest public key
const CAPTCHA_ID = "";

// geetest 密钥
// geetest secret key
const CAPTCHA_KEY = "";

// geetest 服务地址
// geetest server url
const API_SERVER = "http://gcaptcha4.geetest.com";

// geetest 验证接口
// geetest server interface
const API_URL = API_SERVER + "/validate" + "?captcha_id=" + CAPTCHA_ID;

/* GET home page. */
router.get("/", function (req, res, next) {
res.render("index");
});

router.get("/login", function (req, res, next) {
req.query = querystring.parse(req.url.split("?")[1]);
// 前端参数
// web parameter
var lot_number = req.query["lot_number"];
var captcha_output = req.query["captcha_output"];
var pass_token = req.query["pass_token"];
var gen_time = req.query["gen_time"];

// 生成签名, 使用标准的hmac算法,使用用户当前完成验证的流水号lot_number作为原始消息message,使用客户验证私钥作为key
// 采用sha256散列算法将message和key进行单向散列生成最终的 “sign_token” 签名
// use lot_number + CAPTCHA_KEY, generate the signature
var sign_token = hmac_sha256_encode(lot_number, CAPTCHA_KEY);

// 向极验转发前端数据 + “sign_token” 签名
// send web parameter and “sign_token” to geetest server
var datas = {
lot_number: lot_number,
captcha_output: captcha_output,
pass_token: pass_token,
gen_time: gen_time,
sign_token: sign_token,
};

// post request
// 根据极验返回的用户验证状态, 网站主进行自己的业务逻辑
// According to the user authentication status returned by the geetest, the website owner carries out his own business logic
post_form(datas, API_URL)
.then((result) => {
if (result["result"] == "success") {
console.log("validate success");
res.send("success");
} else {
console.log("validate fail:" + result["reason"]);
res.send("fail");
}
})
.catch((err) => {
// 当请求Geetest服务接口出现异常,应放行通过,以免阻塞正常业务。
// When the request geetest service interface is abnormal, it shall be released to avoid blocking normal business.
console.log("Geetest server error:" + err);
res.send("success");
});
});

// 生成签名
// Generate signature
function hmac_sha256_encode(value, key) {
var hash = crypto
.createHmac("sha256", key)
.update(value, "utf8")
.digest("hex");
return hash;
}

// 发送post请求, 响应json数据如:{"result": "success", "reason": "", "captcha_args": {}}
// Send a post request and respond to JSON data, such as: {result ":" success "," reason ":" "," captcha_args ": {}}
async function post_form(datas, url) {
var options = {
url: url,
method: "POST",
params: datas,
timeout: 5000,
};

var result = await axios(options);

if (result.status != 200) {
// geetest服务响应异常
// geetest service response exception
console.log("Geetest Response Error, StatusCode:" + result.status);
throw new Error("Geetest Response Error");
}
return result.data;
}

module.exports = router;
]]>
+ + + GeeTest4.0使用教程。行为验证4.0产品是极验于2022年6月正式推出的最新一代验证码产品,结合环境检测、行为特征、POW工作量证明、视觉模型热更等多项技术,在注册、登录、下单、防作弊等多种场景提供人机智能分流验证服务,为企业安全保驾护航。 + + + + + + + + +
+ + + 利用 kkFileView 实现在线预览文件 + + https://blog.zhangsifan.com/posts/20221021ov/ + 2022-10-21T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方文档
KK 开源社区

Docker 容器环境运行

1
2
3
4
5
# 拉取镜像
docker pull keking/kkfileview
# 运行
docker run -it -p 8012:8012 keking/kkfileview
# 浏览器访问容器8012端口 http://172.0.0.1:8012 即可看到项目演示用首页

前端使用

1
2
3
4
5
6
7
8
9
10
<script
type="text/javascript"
src="https://gcore.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"
></script>;

const previewUrl = "http://127.0.0.1:8080/file/test.txt"; //要预览文件的访问地址
window.open(
"http://127.0.0.1:8012/onlinePreview?url=" +
encodeURIComponent(Base64.encode(previewUrl))
);

示例

]]>
+ + + kkFileView为文件文档在线预览解决方案,该项目使用流行的spring boot搭建,易上手和部署,基本支持主流办公文档的在线预览,如doc,docx,xls,xlsx,ppt,pptx,pdf,txt,zip,rar,图片,视频,音频等等 + + + + + + + + + + +
+ + + WPS WEB Office 前端使用教程 + + https://blog.zhangsifan.com/posts/20221010bg/ + 2022-10-10T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 相关链接

下载 JS-SDK

在使用之前,请先下载最新版本的 js-sdk 代码。

引用 JS-SDK

  • 非模块化
1
<script src="web-office-sdk.umd.js"></script>
  • CommonJS 规范
1
let WebOfficeSDK = require("./web-office-sdk.cjs.js");
  • 非模块化
1
import WebOfficeSDK from "./web-office-sdk.es.js";

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<div class="wps"><div class="custom-mount"></div></div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
import WebOfficeSDK from './web-office-sdk.es.js';
const init = async () => {
const url = ''; // 后端提供的链接
const token = "";
const wps = WebOfficeSDK.config({
url: url,
mount: document.querySelector(".custom-mount")!,
});
wps.setToken({
token: token,
timeout: 10000,
});
};
onMounted(() => {
init();
});
</script>
<style lang="less" scoped>
.wps {
width: 100%;
height: 900px;
background-color: #66ccff;
.custom-mount {
width: 100%;
height: 100%;
}
}
</style>
]]>
+ + + 使用WPS WEB Office,可以在浏览器中打开WPS Office文档,并且可以在线编辑,保存,打印等操作,本文将介绍如何在前端使用WPS WEB Office. + + + + + + + + +
+ + + Vue3 El-upload 开启上传文件夹功能 + + https://blog.zhangsifan.com/posts/20220915up/ + 2022-09-15T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<template>
<el-upload
multiple
action="http://192.168.1.8:3030/file/upload"
:auto-upload="false"
:show-file-list="false"
:on-change="handleChange"
>
<template #trigger>
<el-button type="primary" @click="folderMode(false)">上传文件</el-button>
<el-button type="primary" @click="folderMode(true)">上传文件夹</el-button>
</template>
</el-upload>
</template>
<script setup lang="ts">
import { reactive, onMounted, nextTick } from "vue";
import type { UploadFile, UploadFiles } from "element-plus";
const state = reactive({
uploadEle: null as Element | null,
uploadList: [],
});
const folderMode = (type: boolean) => {
if (state.uploadEle) {
state.uploadEle.webkitdirectory = type;
}
};
const handleChange = async (
uploadFile: UploadFile,
uploadFiles: UploadFiles
) => {
console.log(uploadFile, uploadFiles);
};
onMounted(() => {
nextTick(() => {
state.uploadEle = document.querySelector(".el-upload__input");
});
});
</script>
<style lang="less" scoped></style>

示例

]]>
+ + + 为 El-upload 组件 开启文件夹上传功能 + + + + + + + + + + +
+ + + vue.draggable vue3-拖拽排序组件 + + https://blog.zhangsifan.com/posts/20220909px/ + 2022-09-09T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方文档

安装

1
2
3
yarn add vuedraggable@next

npm i -S vuedraggable@next

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<template>
<draggable
:list="state.form_list"
ghost-class="ghost"
:force-fallback="true"
chosen-class="chosenClass"
animation="200"
@start="onStart"
@end="onEnd"
>
<template #item="{ element, index }">
<div class="item_box">{{ element }}{{ index }}</div>
</template>
</draggable>
</template>
<script setup lang="ts">
import draggable from "vuedraggable";
import { reactive, onMounted } from "vue";

const state = reactive({
form_list: [{ title: "肯德基" }, { title: "麦当劳" }],
});
//开始拖拽事件
const onStart = () => {};
//结束拖拽事件
const onEnd = () => {};
onMounted(() => {});
</script>
<style lang="less" scoped></style>

示例

]]>
+ + + 为方便用户访问本博客内容,提供文章导航服务,请选择对应的文章进行浏览! + + + + + + + + + + + + +
+ + + Vue3使用高德地图 + + https://blog.zhangsifan.com/posts/20220905ap/ + 2022-09-05T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方文档

安装 Loader

1
npm i @amap/amap-jsapi-loader

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
 <!-- MyMap.vue -->
<template>
<div :id="state.id"></div>
</template>
<script setup lang="ts">
import { onMounted, reactive } from "vue";
import AMapLoader from "@amap/amap-jsapi-loader";
const props = defineProps({
modelValue: null, // 坐标
});
const state = reactive({
id: "",
map: null as any,
});
const initMap = () => {
state.id =
Math.random().toString(36).substring(7) + Math.floor(Math.random() * 100);
AMapLoader.load({
key: "", // 申请好的Web端开发者Key,首次调用 load 时必填
version: "2.0", // 指定要加载的 JSAPI 的版本
plugins: ["AMap.Scale", "AMap.Marker"], // 需要使用的的插件列表
})
.then((AMap) => {
state.map = new AMap.Map(state.id, {
//设置地图容器id
viewMode: "3D", //是否为3D地图模式
zoom: 18, //初始化地图级别
center: props.modelValue, //初始化地图中心点位置
doubleClickZoom: false, // 禁止双击放大地图
layers: [],
});
if (state.map) {
// 比例尺
const scale = new AMap.Scale({
visible: true,
});
state.map.addControl(scale);
// 点
const marker = new AMap.Marker({
position: props.modelValue,
});
state.map.add(marker);
}
})
.catch((e) => {
console.log(e);
});
};
onMounted(() => {
initMap();
});
</script>
<style lang="less" scoped>
#container {
padding: 0px;
margin: 0px;
width: 100%;
height: 800px;
}
</style>
]]>
+ + + 为方便用户访问本博客内容,提供文章导航服务,请选择对应的文章进行浏览! + + + + + + + + +
+ + + Nprogress使用教程 + + https://blog.zhangsifan.com/posts/20220903nr/ + 2022-09-03T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方文档

安装

1
npm install nprogress

使用

1
2
3
4
5
6
7
8
9
10
11
import "nprogress/nprogress.css";
import NProgress from "nprogress";
NProgress.configure({ showSpinner: false });
// 显示进度条
NProgress.start();
// 隐藏进度条
NProgress.done();

NProgress.set(0.0); // Sorta same as .start()
NProgress.set(0.4);
NProgress.set(1.0); // Sorta same as .done()
]]>
+ + + 为方便用户访问本博客内容,提供文章导航服务,请选择对应的文章进行浏览! + + + + +
+ + + Fingerprintjs使用教程 + + https://blog.zhangsifan.com/posts/20220903sg/ + 2022-09-03T00:00:00.000Z + 2024-12-02T02:16:22.536Z + + 官方网站

安装

1
npm i @fingerprintjs/fingerprintjs

使用

1
2
3
4
5
6
7
8
9
10
11
import FingerprintJS from "@fingerprintjs/fingerprintjs";

const getFingerPrintID = async () => {
const fpPromise = await FingerprintJS.load();
const result = await fpPromise.get();
return result.visitorId;
},

setTimeout(() => {
console.log(await getFingerPrintID())
}, 1000);

示例

]]>
+ + + 为方便用户访问本博客内容,提供文章导航服务,请选择对应的文章进行浏览! + + + + + + + + +
+ + + TS基础 + + https://blog.zhangsifan.com/posts/TS-TS%E5%9F%BA%E7%A1%80%20/ + 2021-03-02T20:00:00.000Z + 2024-12-02T02:16:22.544Z + + 初识 TypeScript

TypeScript 的介绍

TypeScript 是一种由微软开发的开源、跨平台的编程语言。它是 JavaScript 的超集,最终会被编译为 JavaScript 代码。

2012 年 10 月,微软发布了首个公开版本的 TypeScript,2013 年 6 月 19 日,在经历了一个预览版之后微软正式发布了正式版 TypeScript

TypeScript 的作者是安德斯·海尔斯伯格,C#的首席架构师。它是开源和跨平台的编程语言。

TypeScript 扩展了 JavaScript 的语法,所以任何现有的 JavaScript 程序可以运行在 TypeScript 环境中。

TypeScript 是为大型应用的开发而设计,并且可以编译为 JavaScript。

TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持**,它由 Microsoft 开发,代码开源于 GitHub 上

TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持,它由 Microsoft 开发,代码开源于 GitHub (opens new window)上

TypeScript 的特点

TypeScript 主要有 3 大特点:

  • 始于 JavaScript,归于 JavaScript
    TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的 JavaScript 引擎中。

  • 强大的类型系统
    类型系统允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。

  • 先进的 JavaScript
    TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。

安装 TypeScript

命令行运行如下命令,全局安装 TypeScript:

1
npm install -g typescript

安装完成后,在控制台运行如下命令,检查安装是否成功:

1
tsc -V

HelloWorld

1
2
3
4
5
function sayHi(str) {
return "Hello" + str;
}
let username = "World";
console.log(sayHi(username));

手动编译代码

1
tsc helloworld.ts
]]>
+ + + + + <h1 id="初识-TypeScript"><a href="#初识-TypeScript" class="headerlink" title="初识 TypeScript"></a>初识 TypeScript</h1><h2 id="TypeScript-的介绍"><a hr + + + + + + + + + +
+ + + React基础 + + https://blog.zhangsifan.com/posts/React-React%E5%9F%BA%E7%A1%80/ + 2020-01-06T00:00:00.000Z + 2024-12-02T02:16:22.544Z + +

React:用于构建用户界面的 JavaScript 库(框架)
英文网站 > 中文网站

特征:

  • 声明式视图
    • 对于声明式组件,当数据变更的时候,React 低层负责高效更新。这种方式代码更加可预见并且更容易调试。
  • 组件化
    • 封装管理数据的组件,通过组合的方式实现复杂的 UI,组件的逻辑采用 js 实现而不是模板,这样可以保持数据在 DOM 之外。
  • 一次学习,随处编写
    • React 可以进行服务端渲染,也可以用于移动 APP 开发(React Native)

Hello World

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Document</title>
<!-- 1.导入相关库文件 -->
<script src="https://static.zhangsifan.com/react.js"></script>
<script src="https://static.zhangsifan.com/react-dom.js"></script>
<script src="https://static.zhangsifan.com/babel.min.js"></script>
</head>
<body>
<!-- 2.添加一个容器 -->
<div id="app"></div>
<!-- 3.基于React实现业务 -->
<script type="text/babel">
let content = <h1>Hello World</h1>;
ReactDOM.render(content, document.getElementById("app"));
</script>
</body>
</html>

开发工具

VS Code 插件

  • JS JSX Snippets
  • jsx-beautify
  • Live Server
1
2
3
4
5
"emmet.triggerExpansionOnTab": true,
"emmet.includeLanguages": {
"javascript": "javascriptreact",
"wxml": "html"
},

JSX 基础语法

JSX 是什么

1
let element = <h1>Hello World</h1>;

JSX 元素中动态插入数据

1
2
let name = "世界";
let element = <h1>你好,{name}</h1>;

JSX 设置动态属性

1
2
3
4
5
6
let info = "bt";
let element = (
<h1 className="title" abc={info}>
你好,世界
</h1>
);

JSX 可以包含子元素

1
2
3
4
5
6
let element = (
<div>
<h1>你好</h1>
<h1>世界</h1>
</div>
);
]]>
+ + + + + <blockquote> +<p>React:用于构建用户界面的 JavaScript 库(框架)<br><a href="https://reactjs.org/">英文网站</a> &gt; <a href="https://react.docschina.org/">中文网站 + + + + + + + + + + + +
+ + + Uni-App开发项目 + + https://blog.zhangsifan.com/posts/Wechat-Uni-App%E5%BC%80%E5%8F%91%E9%A1%B9%E7%9B%AE/ + 2019-12-06T21:11:43.000Z + 2024-12-02T02:16:22.544Z + + uni-app—官方网站

uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉)等多个平台。
即使不跨端,uni-app同时也是更好的小程序开发框架。

使用 uni-app 创建项目

环境安装

1
npm install -g @vue/cli

创建 uni-app

1
vue create -p dcloudio/uni-preset-vue myapp

配置 AppID

manifest.json中填入 appid

运行并发布 uni-app

1
npm run dev:%PLATFORM%

%PLATFORM% 可取值如下:

平台
h5H5
mp-alipay支付宝小程序
mp-baidu百度小程序
mp-weixin微信小程序
mp-toutiao头条小程序
mp-qqqq 小程序

导入 less

1
npm i less less-loader --save
]]>
+ + + + + <h1 id="uni-app—官方网站"><a href="#uni-app—官方网站" class="headerlink" title="uni-app—官方网站"></a>uni-app—<a href="https://uniapp.dcloud.io/">官方网站</ + + + + + + + + + +
+ + + ESLint + + https://blog.zhangsifan.com/posts/DOC-ESLint/ + 2019-12-01T21:30:49.000Z + 2024-12-02T02:16:22.536Z + + vscode中的vscode的配置代码
1
2
3
4
5
6
7
8
9
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "vue",
"autoFix": true
}
],
"eslint.autoFixOnSave": true,
]]>
+ + + + + <h1 id="vscode中的vscode的配置代码"><a href="#vscode中的vscode的配置代码" class="headerlink" title="vscode中的vscode的配置代码"></a>vscode中的vscode的配置代码</h1><figu + + + + + + + + + +
+ + + 微信小程序基础 + + https://blog.zhangsifan.com/posts/Wechat-%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%9F%BA%E7%A1%80/ + 2019-11-30T08:00:00.000Z + 2024-12-02T02:16:22.544Z + + 微信小程序开发准备

编辑器

VSCode 下载地址

微信小程序开发工具 下载地址

官方 API 文档

官方文档

VSCode 推荐安装的插件

minapp

小程序的结构目录

小程序文件结构和传统 web 对比

结构传统 web微信小程序
结构HTMLWXML
样式CSSWXSS
逻辑JavaScriptJavaScript
配置JSON

基本的项目目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
│  app.js              # 全局的js(入口文件)
│ app.json # 全局的配置文件
│ app.wxss # 全局的样式文件
│ project.config.json # 整个项目的描述文件 类似node中的package.json

├─pages # 小程序对应的页面的目录
│ ├─index # 首页
│ │ index.js # js文件
│ │ index.json # 配置文件
│ │ index.wxml # 类似html
│ │ index.wxss # 样式文件 类似CSS
│ │
│ └─logs # 子页面
│ logs.js
│ logs.json
│ logs.wxml
│ logs.wxss

└─utils # 自己封装的工具函数
util.js

配置介绍

注意:配置文件中不能出现注释

 

全局配置 app.json

通过 app.json 文件对小程序进行全局配置,如页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
官方文档

app.json 配置清单

属性类型必填描述
pagesString Array设置页面路径
windowObject设置默认窗口表现
tabBarObject设置底部 tab 表现
networkTimeoutObject设置网络超时时间
debugBoolean设置是否开启调试模式

pages-(页面路径)

用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径(含文件名) 信息。文件名不需要写文件后缀,框架会自动去寻找对于位置的 .json, .js, .wxml, .wxss 四个文件进行处理。
官方文档

1
2
3
{
"pages": ["pages/index/index", "pages/detail/detail"]
}

window-(默认窗口表现)

用于设置小程序的状态栏、导航条、标题、窗口背景色。
官方文档

属性类型默认值描述
navigationBarBackgroundColorHexColor#000000导航栏背景颜色,如”#000000”
navigationBarTextStyleStringwhite导航栏标题颜色,仅支持 black/white
navigationBarTitleTextString导航栏标题文字内容
backgroundColorHexColor#ffffff窗口的背景色
1
2
3
4
5
6
7
8
{
"window": {
"navigationBarBackgroundColor": "#66ccff",
"navigationBarTitleText": "名称",
"navigationBarTextStyle": "white",
"backgroundColor": "#F0F0F0"
}
}

tabBar-( 底部 tab 栏的表现 )

如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。
官方文档

对象类型,配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

属性类型必填默认值描述
colorHexColortab 上的文字默认颜色
selectedColorHexColortab 上的文字选中时的颜色
backgroundColorHexColortab 的背景色
borderStyleStringblacktabbar 上边框的颜色, 仅支持 black/white
listArraytab 的列表,最少 2 个、最多 5 个
positionStringbottomtab 的位置 可选值 bottom、top

其中 list 接受一个数组,数组中的每个项都是一个对象,其属性值如下:

属性类型必填描述
pagePathString页面路径,必须在 pages 中先定义
textStringtab 上按钮文字
iconPathString图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,当 postion 为 top 时,此参数无效,不支持网络图片
selectedIconPathString选中时的图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px ,当 postion 为 top 时,此参数无效
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
"tabBar": {
"color": "#D78B09",
"selectedColor": "#FFF",
"backgroundColor": "#FECA49",
"borderStyle": "white",
"list": [
{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "icons/home-default.png",
"selectedIconPath": "icons/home-active.png"
},
{
"text": "日志",
"pagePath": "pages/logs/logs",
"iconPath": "icons/cards-default.png",
"selectedIconPath": "icons/cards-active.png"
}
]
}

页面配置 page.json

每个页面可以有不同的表现,通过 pages 目录下的 .json 文件,如 logs.json ,实现页面的局部配置。但是只能设置 app.json 中的 window 配置项的内容,页面中配置项会覆盖 app.json 的 window 中相同的配置项。
官方文档

常用配置

属性类型描述
navigationBarTitleTextHexColor导航栏标题文字内容
navigationBarBackgroundColorHexColor导航栏背景颜色
navigationBarTextStyleString字体颜色 只支持black / white
1
2
3
4
5
{
"navigationBarTitleText": "页面标题",
"navigationBarBackgroundColor": "#6cf",
"navigationBarTextStyle": "white"
}

静态资源

小程序打包体积不允许超过 2M
通过配置 project.config.json 文件,可以忽略某些文件(如图片等)以减少预览发布资源所占空间的大小。

1
2
3
4
5
6
7
8
"packOptions": {
"ignore": [
{
"type": "folder",
"value": "static"
}
]
}

字体图标

在小程序中可以像网页一样使用字体图标,并且使用方式基本一致。唯一的不同在于小程序中字体图标所引入的字体文件路径为网络路径,且必须为 https 协议。

1
2
3
4
5
6
7
8
9
10
11
@font-face {
font-family: "icomoon";
/* wxss 不支持本地资源(图片、字体) */ /* 服务器地址需为 https 协议 */
src: url("https://botue.github.io/85qi/fonts/icomoon.eot?lzaqut");
src: url("https://botue.github.io/85qi/fonts/icomoon.eot?lzaqut#iefix") format("embedded-opentype"),
url("https://botue.github.io/85qi/fonts/icomoon.ttf?lzaqut") format("truetype"),
url("https://botue.github.io/85qi/fonts/icomoon.woff?lzaqut") format("woff"),
url("https://botue.github.io/85qi/fonts/icomoon.svg?lzaqut#icomoon") format("svg");
font-weight: normal;
font-style: normal;
}

视图层

数据绑定

所谓数据绑定是指数据与页面中组件的关联关系。使用 Mustache 语法(双大括号)将数据变量包起来

简单数据—-官方文档

数据绑定使用 Mustache 语法(双大括号)将变量包起来,可以作用于:

内容

1
<view> {{ message }} </view>
1
2
3
4
5
Page({
data: {
message: "Hello MINA!",
},
});

组件属性(需要在双引号之内)

1
<view id="item-{{id}}"> </view>
1
2
3
4
5
Page({
data: {
id: 0,
},
});

控制属性(需要在双引号之内)

1
<view wx:if="{{condition}}">文本</view>
1
2
3
4
5
Page({
data: {
condition: true,
},
});

关键字(需要在双引号之内)

1
<checkbox checked="{{false}}"> </checkbox>

特别注意:不要直接写 `checked="false"`,其计算结果是一个字符串,转成 boolean 类型后代表真值。

 

复杂数据

1
2
3
<text
>我叫{{user.name}},我今年{{user.age}}岁了,我在学习{{courses[0]}}课程。</text
>
1
2
3
4
5
6
7
8
9
10
Page({
// 通过 data 属性,初始化页面中用到的数据
data: {
user: {
name: "小明",
age: 16,
},
courses: ["wxml", "wxss", "javascript"],
},
});

运算

1
<text>{{a}} + {{b}} = {{a + b}}</text> <text>{{flag ? '是': '否'}}</text>
1
2
3
4
5
6
7
8
Page({
// 通过 data 属性,初始化页面中用到的数据
data: {
a: 10,
b: 5,
flag: true,
},
});

列表渲染

将数组数据遍历绑定到组件中。通过 wx:for 控制属(类似 vue 中的指令)性实现。
项的变量名默认为item 可以通过wx:for-item="value"修改变量名
下标变量名默认为index 可以通过wx:for-index="key"修改变量名 0
通过 block 可以将多个组件元素“包”到一起进行渲染,block 只是提供一种结构,并不会被渲染到页面中。一般这样做的目的是可以将多个组件按统一的逻辑进行控制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<view>
<view wx:for="{{arr}}" wx:key="{{index}}">{{index}}:{{item}}</view>
<view wx:for="{{person}}" wx:key="{{index}}">{{index}}:{{item}}</view>
<view
wx:for="{{person}}"
wx:key="{{index}}"
wx:for-index="key"
wx:for-item="value"
>
{{key}}:{{value}}
</view>
<!-- block最终不会变成真正的dom元素 -->
<block wx:for="{{arr}}" wx:key="{{index}}">{{index}}:{{item}}</block>
</view>
1
2
3
4
5
6
7
8
9
Page({
data: {
arr: ["苹果", "香蕉", "菠萝", "火龙果"],
person: {
name: "小张",
age: 22,
},
},
});

条件渲染

根据条件控制是否渲染某个(些)组件,通过 wx:if 属性实现。

基本用法

1
2
3
<view wx:if="{{true}}">
<text>锄禾日当午</text>
</view>

多分支

1
2
3
<view wx:if="{{view == '小明'}}">小明</view>
<view wx:elif="{{view == '小张'}}">小张</view>
<view wx:else="{{view == '小李'}}">小李</view>
1
2
3
4
5
Page({
data: {
view: "小张",
},
});

hidden—显示/隐藏

为小程序组件添加 hidden 属性也可以控制组件是否显示,其效果类似于 vue 中的 v-show,它与 wx:if 的不同之处是 wx:if 通过添加/移除节点实现元素的显示/隐藏,而 hidden 是对过样式 display 属性实现的。

1
2
3
4
5
6
<view hidden="{{true}}">
<text>hidden=true</text>
</view>
<view hidden="{{false}}">
<text>hidden=false</text>
</view>

小程序中组件属性的值如果为布尔类型时,只要包含这个属性即为 true,要表达 false 时,需要通过 { { } } 表达,原因是 { { } } 中的内容为被小程序当成表达式解析,所以 hidden="{{false}}"会被解析成数据类型的布尔类型,而如果写成 hidden=”false” 则将 false 当成字符串解析。

样式 WXSS

wxss 是一套样式语言,用于描述 WXML 的样式组件

数据

获取 data 的值

1
this.data.name;

修改 data 数据

1
2
3
this.setData({
name: "张三",
});

事件对象

1
2
3
in(e){
console.log(e)
}

获取输入框的值是通过事件源对象获取的e.detail.value

传参要动过自定义属性传参

发送请求

小程序不支持XMLHTTPRequest,$.ajax,axios

开发者所请求的

1
2
3
4
5
6
7
wx.request({
url: "",
method: "",
data: {},
success() {},
fail() {},
});

事件

事件绑定

使用bind事件名称="事件回调"或者bind:事件类型="事件回调"

1
2
3
<button bind:tap="getTime">获取时间</button>
<!-- 或者 -->
<button bindtap="getTime">获取时间</button>

事件冒泡

盒子嵌套的事件会触发事件冒泡.
里面的事件先触发,随后外面的事件触发

1
2
3
<view class="father" bind:tap="father">
<view class="son" bind:tap="son"></view>
</view>

阻止事件冒泡

实现监听的同时,阻止冒泡
使用 catch:事件名称="事件回调"或者catch事件名称="事件回调"

1
2
3
4
5
<view class="father" bind:tap="father">
<view class="son" catch:tap="son"></view>
<!-- 或者 -->
<view class="son" catchtap="son"></view>
</view>

事件捕获

点击里面盒子,外面的事件先执行,里面的事件在执行
使用capture-bind:事件名称="事件回调"

1
2
3
<view class="father" capture-bind:tap="father">
<view class="son" capture-bind:tap="son"></view>
</view>

阻止捕获

在哪里阻止,里面的事件就不会执行
使用capture-catch:事件名称="事件回调"

1
2
3
<view class="father" capture-catch:tap="father">
<view class="son" capture-bind:tap="son"></view>
</view>

事件互斥

有 mut 的互斥
使用mut-bind:事件名称="事件回调"

1
2
3
4
5
<view class="box" bind:tap="fn">
<view class="father" mut-bind:tap="father">
<view class="son" mut-bind:tap="son"></view>
</view>
</view>

事件回调

1
2
3
fn(e){
console.log(e)
}

生命周期

生命周期就是函数,只是会自己执行

APP—应用级别

onLaunch

小程序启动时会自动执行该函数(只会执行一次)

1
2
3
4
5
App({
onLaunch() {
console.log("小程序启动会执行");
},
});

onShow

小程序前台运行时会自动执行

1
2
3
4
5
App({
onShow() {
console.log("小程序在前台会执行");
},
});

onHide

小程序后台运行时会自动执行

1
2
3
4
5
App({
onHide() {
console.log("小程序在后台会执行");
},
});

onError

小程序错误会自动执行

1
2
3
4
5
App({
onError() {
console.log("小程序错误会执行");
},
});

onPageNotFound

小程序启动时,页面找不到会自动执行

1
2
3
4
5
App({
onPageNotFound() {
console.log("页面找不到呀!!!!");
},
});

Page—页面级别

onLoad

当前页面加载时会自动加载该函数 (只会执行一次)

1
2
3
4
5
Page({
onLoad() {
console.log("页面加载会调用");
},
});

onShow

当前页面加载完毕,显示时会自动加载该函数

1
2
3
4
5
Page({
onShow() {
console.log("页面加载完毕,显示会调用");
},
});

onReady

当前页面初次渲染时,显示时会自动加载该函数 (只会执行一次)

1
2
3
4
5
Page({
onReady() {
console.log("页面渲染完成会调用");
},
});

onHide

页面隐藏会调用该函数

1
2
3
4
5
Page({
onHide() {
console.log("页面隐藏会调用");
},
});

场景值—官方文档

场景值用来描述用户进入小程序的路径
只能通过App.js下的onLaunchonShow的事件回调来获取

1
2
3
4
5
6
7
8
9
10
App({
onLaunch(e) {
// 只会执行一次
console.log(e.scene);
},
// 或者
onShow(e) {
console.log(e.scene);
},
});

地址参数

使用 navigator 的地址穿参,在被传的 JS 文件调用 onLoad 的回调获取

1
<navigator url="../index3/index?a=1&b=2">跳转到下个页面</navigator>
1
2
3
4
5
Page({
onLoad(e) {
console.log(e);
},
});

组件

view—官方文档

类似 div

属性名类型默认值说明
hover-classstringnone指定按下去的样式类。当 hover-class="none" 时,没有点击态效果
1
2
3
4
<!-- WXML -->
<view hover-class="hover_class">view</view>
<!-- WXSS -->
.hover_class{ background-color: red }

scroll-view—官方文档

可滚动视图区域

属性类型默认值说明
scroll-xbooleanfalse允许横向滚动
scroll-ybooleanfalse允许纵向滚动
bindscrolltouppereventhandle滚动到顶部/左边时触发
bindscrolltolowereventhandle滚动到底部/右边时触发
1
2
3
4
5
<scroll-view scroll-y style="height:400rpx ;width:200rpx;border:1px solid red">
<view>文是文本</view>
...
<view>文是文本</view>
</scroll-view>

text —官方文档

显示普通文本的标签 text 只能嵌套 text

属性类型默认值说明
selectablebooleanfalse文本是否可选
decodebooleandalse是否解码
1
<text class="" selectable="false" space="false" decode="false">text</text>

image—官方文档

图片标签 image 组件默认宽度 320px 高度 240ppx 不支持背景图片的写法 > image组件中二维码/小程序码图片不支持长按识别。仅在wx.previewImage中支持长按识别

属性类型默认值说明
srcString图片资源地址
modestringscaleToFill图片裁剪、缩放的模式参数见官方文档
lazy-loadbooleanfalse图片懒加载,在即将进入一定范围(上下三屏)时才开始加载

mode 的合法值

说明
scaleToFill缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
aspectFit缩放模式,保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
aspectFill缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
widthFix缩放模式,宽度不变,高度自动变化,保持原图宽高比不变
top裁剪模式,不缩放图片,只显示图片的顶部区域
bottom裁剪模式,不缩放图片,只显示图片的底部区域
center裁剪模式,不缩放图片,只显示图片的中间区域
left裁剪模式,不缩放图片,只显示图片的左边区域
right裁剪模式,不缩放图片,只显示图片的右边区域
top left裁剪模式,不缩放图片,只显示图片的左上边区域
top right裁剪模式,不缩放图片,只显示图片的右上边区域
bottom left裁剪模式,不缩放图片,只显示图片的左下边区域
bottom right裁剪模式,不缩放图片,只显示图片的右下边区域
1
2
3
4
5
6
<image
class=""
src="../../images/private.png"
mode="aspectFit|aspectFill|widthFix"
lazy-load="false"
></image>

swiper(轮播图)—官方文档

微信内置的轮播图插件swiper的默认高度是150rpx

swiper 的高度=swiper 的宽 * 原图的高 / 原图的宽

属性类型默认值说明
indicator-dotsbooleanfalse是否显示面板指示点
indicator-colorcolorrgba(0, 0, 0, .3)指示点颜色
indicator-active-colorcolor#000000当前选中的指示点颜色
autoplaybooleanfalse是否自动切换
intervalnumber5000自动切换时间间隔
circularbooleanfalse是否采用衔接滑动
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<swiper
autoplay
circular
indicator-dots
indicator-color="#ccc"
indicator-active-color="#6cf"
>
<swiper-item>
<image class="swiper-image " src="../../images/01.jpg " />
</swiper-item>
<swiper-item>
<image class="swiper-image " src="../../images/02.jpg " />
</swiper-item>
<swiper-item>
<image class="swiper-image " src="../../images/03.jpg " />
</swiper-item>
<swiper-item>
<image class="swiper-image " src="../../images/04.jpg " />
</swiper-item>
</swiper>

导航组件 类似超链接标签

属性类型默认值说明
targetStringseif在哪个目标上发生跳转,默认当前小程序
urlString当前小程序内的跳转链接
open-typeStringnavigate跳转方式(见下表)

open-type 的有效值:

说明
navigate保留当前页面,跳转到页面的某页 但是不能跳转到 tabbar 页面
redirect关闭当前页面,跳转到页面的某页 但是不能跳转到 tabbar 页面
switchTab跳转到 tabbar 页面 关闭其他菲 tabbar 页面
reLaunch关闭所有页面,打开到页面的某个页面
navigateBack关闭当前页面,返回上一页面或多级页面
exit退出小程序,target="miniProgram"时生效
1
2
3
4
5
6
7
8
<!-- 保留当前页面,跳转到页面的某页 但是不能跳转到tabbar页面 -->
<navigator url="../swiper/index" open-type="navigate">swiper</navigator>
<!-- 关闭当前页面,跳转到页面的某页 但是不能跳转到tabbar页面 -->
<navigator url="../swiper/index" open-type="redirect">swiper</navigator>
类型 跳转到tabbar页面 关闭其他菲tabbar页面-->
<navigator url="../index/index" open-type="switchTab">swiper</navigator>
<!-- 关闭所有页面,打开到页面的某个页面 -->
<navigator url="../swiper/index" open-type="reLaunch">swiper</navigator>

audio—官方文档

音频组件
mp3 音乐链接来自[刘志进实验室]

属性类型默认值说明
idstringaudio 组件的唯一标识符
srcstring要播放音频的资源地址
loopbooleanfalse是否循环播放
controlsbooleanfalse是否显示默认控件
1
2
3
4
5
6
7
8
<audio
id="my"
src="{{audioSrc}}"
poster="{{audioPoster}}"
name="{{audioName}}"
author="{{audioAuthor}}"
controls
></audio>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Page({
data: {
audioPoster:
"http://p1.music.126.net/ka7kZIHdviNfYO9lqBaOEQ==/109951163906385177.jpg?param=300x300",
audioName: "前世今生",
audioAuthor: "十三里夏天,龚子笑",
audioSrc:
"http://m10.music.126.net/20191130104652/2466ef3c0dc4c3a159cf970e0f16c14b/ymusic/075e/025c/520c/049c0bb5455612be6aebef319191d974.mp3",
},
onReady: function (e) {
// 使用 wx.createAudioContext 获取 audio 上下文 context
this.audioCtx = wx.createAudioContext("my");
// 强制播放
this.audioCtx.play();
},
});

video—官方文档

视频组件

属性类型默认值说明
srcString要播放视频的资源地址
durationnumber指定视频时长 /s
controlsbooleantrue是否显示默认播放控件(播放/暂停按钮、播放进度、时间)
autoplaybooleanfalse是否自动播放
loopbooleanfalse是否循环播放
mutedbooleanfalse是否静音播放
1
2
3
4
5
6
<video
src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400"
loop
autoplay
controls
></video>

rich-text—官方文档

富文本

1
<rich-text nodes="{{html}}"></rich-text>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Page({
data: {
nodes: [
{
name: "div",
attrs: {
class: "div_class",
style: "line-height: 60px; color: red;",
},
children: [
{
type: "text",
text: "Hello&nbsp;World!",
},
],
},
],
},
});

button—官方文档

按钮

web-view—官方文档

承载网页的容器。会自动铺满整个小程序页面,个人类型的小程序暂不支持使用

属性类型说明
srcStringwebview 指向网页的链接。可打开关联的公众号的文章,其它网页需登录小程序管理后台配置业务域名。
1
<web-view src="https://zhangsifan.com"></web-view>

自定义组件

新建自定义组件

根目录==>components==>新建 Component

目录结构

1
2
3
4
5
6
├── components .................................................. 组件目录
| ├── header
| | ├── index.js ............................................ 组件header业务逻辑
| | ├── index.json .......................................... 组件header配置
| | ├── index.wxml .......................................... 组件header布局结构
| | └── index.wxss .......................................... 组件header布局样式

导入自定义组件

1
2
3
4
5
6
7
// pages/index/index.json
{
usingComponents: {
// 导入自定义组件
header: "../../components/header/index";
}
}
1
2
3
4
5
<!-- pages/index/index.wxml -->
<view class="box">
<!-- 应用自定义组件 -->
<header />
</view>

父组件给子组件传值

父组件将数据传给子组件时,通过子组件定义的属性实现

父组件代码

1
<myheader list="{{list}}"></myheader>
1
2
3
4
5
Page({
data: {
list: [1, 2, 3, 4, 5],
},
});

子组件代码

1
<view wx:for="{{list}}" wx:key="*this">{{item}}</view>
1
2
3
4
5
6
7
8
9
10
11
Component({
/**
* 组件的属性列表
*/
properties: {
list: {
type: Array,
value: [],
},
},
});

子组件给父组件传值

子组件将数据传给父组件时通过自定义事件实现
父组件定义一个事件
子组件来触发父组件自定义的事件

父组件代码

1
2
3
<myheader bind:myevent="mycallback" list="{{list}}"></myheader>
<view>{{student.name}}</view>
<view>{{student.age}}</view>
1
2
3
4
5
6
7
8
9
10
11
12
Page({
data: {
student: {},
},
// 自定义事件的回调函数
mycallback(e) {
console.log(e.detail);
this.setData({
student: e.detail,
});
},
});

子组件代码

1
2
<view wx:for="{{list}}" wx:key="*this">{{item}}</view>
<view bind:tap="mmm">点我传值</view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Component({
/**
* 组件的属性列表
*/
properties: {
list: {
type: Array,
value: [],
},
},

/**
* 组件的初始数据
*/
data: {
msg: "我是自定义组件的参数",
student: {
name: "小张",
gender: "男",
age: 16,
},
},

/**
* 组件的方法列表
*/
methods: {
mmm() {
this.triggerEvent("myevent", this.data.student);
},
},
});

API

Application Program Interface 官方文档

消息提示框—官方文档

1
2
3
4
5
wx.showToast({
title: "成功", // 标题
icon: "loading", // 图标 有效值 success loading none
duration: 2000, // 提示的延迟时间
});

Loading 提示框—官方文档

显示 loading 提示框。需主动调用 wx.hideLoading 才能关闭提示框

1
2
3
4
5
6
wx.showLoading({
title: "标题",
});
setTimeout(() => {
wx.hideLoading();
}, 3000);

确认对话框—官方文档

显示模态对话框

1
2
3
4
5
6
7
8
9
10
11
wx.showModal({
title: "大标题",
content: "小标题",
success(res) {
if (res.confirm) {
console.log("确定");
} else if (res.cancel) {
console.log("取消");
}
},
});

选择框—官方文档

显示操作菜单

1
2
3
4
5
6
wx.showActionSheet({
itemList: ["拍照", "从相册获取"],
success(res) {
console.log(res.tapIndex); // 用户点击的按钮序号,从上到下的顺序,从0开始
},
});

拍照—官方文档

从本地相册选择图片或使用相机拍照

1
2
3
4
5
wx.chooseImage({
success(res) {
console.log(res.tempFiles[0].path);
},
});

文件上传接口—官方文档

1
2
3
4
5
6
wx.uploadFile({
url: "",
filePath: null,
name: "",
success(res) {},
});

下拉刷新—官方文档

1
2
3
4
async onPullDownRefresh() {
await this.getFloorList();
wx.stopPullDownRefresh();
}

监听页面是否快到底部

1
2
3
4
5
Page({
onReachBottom() {
console.log("快到底部了");
},
});

分享小程序—官方文档

1
2
3
4
5
6
7
8
9
Page({
onShareAppMessage() {
return {
title: "标题:瞧一瞧,看一看啦",
path: "/pages/index/index?id=888",
imageUrl: "https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg",
};
},
});

模块化

小程序遵循的是类似 CommonJS 的规范。

规范

1
2
3
4
5
// pages/index/test.js
function abc(arg) {
console.log("我是模块的函数,传入的参数为", arg);
}
module.exports.abc = abc;
1
2
3
4
5
6
7
8
9
10
// pages/index/index.js
const mk = require("./test.js");
Page({
data: {
msg: "233",
},
onLoad: function (options) {
mk.abc(this.data.msg);
},
});

npm

小程序默认不支持 npm 的模块,必须经过构建后才可以使用
微信开发工具==>工具==>构建 npm==>详情==>本地管理==>使用 npm

1
2
3
4
# 初始化npm
npm init -y
# 安装模块
npm install mime
1
2
3
// pages/index/index.js
// 当通过开发工具进行构建后,才可以将 npm 模块导入,这时导入的是 miniprogram_npm 中的模块
const mime = require("mime");

文件作用域

在 JavaScript 文件中声明的变量和函数只在该文件中有效;不同的文件中可以声明相同名字的变量和函数,不会互相影响。
通过全局函数 getApp 可以获取全局的应用实例,如果需要全局的数据可以在 App() 中设置如:

1
2
3
4
5
6
7
//app.js
App({
// 定义全局数据
name: "小张",
// 定义生命周期
onLaunch() {},
});
1
2
3
4
5
6
7
// pages/index/index.js
const app = getApp();
Page({
data: {
name: app.name,
},
});

WXS—官方文档

WXS.jpg

由上图我们可以知道 JsCore(Javascript)和 界面(WXML、WXSS)是互相隔离的,它们之间的通信是通过 Native(微信)中转实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
Page({
data: {
msg: "学习小程序!",
},

foo: function () {
console.log("wxml无法调用该函数...");
},

sayHi: function () {
console.log("你好,小程序!");
},
});
1
2
3
4
5
6
7
8
<view class="msg">{{msg}}</view>
<!-- 将sayHi注册为事件回调,当事件触发时会被调用 -->
<button type="primary" bind:tap="sayHi">打招呼</button>
<!-- 直接调用函数,无效!!! -->
<view class="demo">{{foo()}}</view>

<!-- 由于无法直接调用函数,故下面的写法是不允许的!!! -->
<button type="primary" bind:tap="sayHi()">打招呼</button>

基本用法

视图层和逻辑层的隔离性给开发带来了不便,通过 WXS 可以解决这个问题,WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。
WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致,但类似!!!!WXS 声明变量只能使用 var 、变量名不能为 $、通过 getDate 获取时间对象等,这些都是与 Javascript 不一致的方面。

内联式

1
2
3
4
5
<view>{{m1.sayHi()}}</view>
<wxs module="m1">
var name = '小明'; function sayHi(){ console.log('Hi') } //
将内部封装的功能导出 module.exports.sayHi = sayHi
</wxs>

引入式

1
2
3
4
5
// wxs/m1.wxs
module.exports.name = "小张";
module.exports.sayHello = function (name) {
return "你好!" + name + "!";
};
1
2
3
4
5
<!-- pages/index/index.wxml -->
<view>{{m2.name}}</view>
<view>{{m2.sayHello('小王')}}</view>

<wxs module="m2" src="../../wxs/m2.wxs"></wxs>

语法

WXS 一般是结合 WXML 使用的,它通过被用来格式展示数据,类似于 Vue 中过滤器的功能。

1
2
3
4
<!-- 将 now 时间戳传入 date.format 方法 -->
<view class="now">{{date.format(now)}}</view>
<!-- 模块 date 暴露了 format 方法 -->
<wxs module="date" src="../../wxs/date.wxs"></wxs>
1
2
3
4
Page({
// 获取当前时间(时间戳)
now: Date.now(),
});
1
2
3
4
5
6
module.exports.format = function (timestamp) {
// wxs 中通过 getDate 函数获得时间对象
var d = getDate(timestamp);
// 返回值会在 wxml 中被渲染展示
return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' d.getDate() + '日';
}

其他

Vant Weapp—官方网站

Vant Weapp 是移动端 Vue 组件库 Vant 的小程序版本,两者基于相同的视觉规范,提供一致的 API 接口,助力开发者快速搭建小程序应用。

安装

1
npm i @vant/weapp -S --production

构建 npm

构建和使用方法已在上面讲过,就不在重复讲述了

导入组件

1
2
3
4
5
6
// app.json
{
"usingComponents": {
"van-button": "@vant/weapp/button"
}
}
1
2
<!--index.wxml-->
<van-button type="primary">按钮</van-button>

F2—官方网站

F2 是一个专注于移动,开箱即用的可视化解决方案,完美支持 H5 环境同时兼容多种环境(Node, 小程序,Weex),完备的图形语法理论,满足你的各种可视化需求,专业的移动设计指引为你带来最佳的移动端图表体验。

安装

1
npm i @antv/f2-canvas

构建 npm

构建和使用方法已在上面讲过,就不在重复讲述了

导入组件

1
2
3
4
5
6
// pages/index/index.json
{
"usingComponents": {
"ff-canvas": "@antv/f2-canvas"
}
}
1
2
3
4
<!--pages/index/index.wxml-->
<view class="container">
<ff-canvas id="column-dom" canvas-id="column" opts="{{ opts }}"></ff-canvas>
</view>
1
2
3
4
5
/* pages/index/index.wxss */
ff-canvas {
width: 750rpx;
height: 640rpx;
}

绘制图表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import F2 from "@antv/wx-f2";

let chart = null;

function initChart(canvas, width, height, F2) {
// 图表数据(也可通过请求后端获得)
const data = [
{ year: "1951 年", sales: 38 },
{ year: "1952 年", sales: 52 },
{ year: "1956 年", sales: 61 },
{ year: "1957 年", sales: 145 },
{ year: "1958 年", sales: 48 },
{ year: "1959 年", sales: 38 },
{ year: "1960 年", sales: 38 },
{ year: "1962 年", sales: 38 },
];

// 实例化图表
chart = new F2.Chart({
el: canvas,
width,
height,
});

// 配置图表数据
chart.source(data, {
sales: {
tickCount: 5,
},
});

chart.interval().position("year*sales");
chart.render();
return chart;
}

Page({
data: {
// 图表参数
opts: {
onInit: initChart,
},
},
});

日历

一个非常好用的小程序日历组件。

下载并解压缩,拷贝 calendar 文件到小程序项目下

导入组件

1
2
3
4
5
6
<!--pages/index/index.json-->
{
"usingComponents": {
"calendar": "/components/calendar/index"
}
}

在 wxml 中引入组件

1
2
<!--pages/index/index.wxml-->
<calendar />
]]>
+ + + + + <h1 id="微信小程序开发准备"><a href="#微信小程序开发准备" class="headerlink" title="微信小程序开发准备"></a>微信小程序开发准备</h1><h2 id="编辑器"><a href="#编辑器" class="headerlink + + + + + + + + + +
+ + + VueX + + https://blog.zhangsifan.com/posts/Vue-Vuex/ + 2019-11-12T14:30:00.000Z + 2024-12-02T02:16:22.544Z + + VueX 介绍

官方网站
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。(管理数据共享的工具)

看图结论:

  • state 管理数据,管理的数据是响应式的,当数据改变时驱动视图更新。
  • mutations 更新数据,state 中的数据只能使用 mutations 去改变数据。
  • actions 请求数据,响应成功后把数据提交给 mutations

初始化功能

安装

1
npm i vuex --save

新建 store.js 文件

1
2
3
4
5
6
7
8
9
10
11
12
// 初始化一个vuex的实例(数据仓库) 导出即可
import Vue from "vue";
import Vuex from "vuex";
// 使用安装
Vue.use(Vuex);
// 初始化
const store = new Vuex.Store({
state: null,
mutations: null,
actions: null,
});
export default store;

在 main.js 配置

1
2
3
4
5
6
7
...
import store from '@/store'

new Vue({
store,
render: h => h(App)
}).$mount("#app");

state (管理数据)

普通写法

store.js文件下写入

1
2
3
4
5
6
const store = new Vuex.Store({
state: {
// 管理数据
count: 0,
},
});

在组件获取state的数据:原始用法插值表达式

1
<p>{{ $store.state.count }}</p>

计算属性写法

1
<p>{{ count }}</p>

常规写法

正常写法

1
2
3
4
5
6
7
8
export default {
...
computed: {
count: function() {
return this.$store.state.count;
}
}
};

缩写

1
2
3
4
5
6
7
8
export default {
...
computed: {
count() {
return this.$store.state.count;
}
}
};

不能使用箭头函数 this指向的不是vue实例

 ### mapState

导入

1
import { mapState } from "vuex";

使用:mapState(对象)

基本写法

1
2
3
4
5
computed: mapState({
count: function (state) {
return state.count;
},
});

箭头函数写法

1
2
3
computed: mapState({
count: (state) => state.count,
});

vuex 提供写法

1
2
3
computed: mapState({
count: "count",
});

当你的计算属性,需要依赖 vuex 中的数据,同时,依赖组件中 data 的数据

1
2
3
4
5
computed: mapState({
myCount(state) {
return state.count + 233;
},
});

使用:mapState(数组)

1
computed: mapState(["count"]);

如果组件自己有计算属性,state 的字段映射成计算属性

1
2
3
4
5
6
computed: {
myCount() {
return 1;
},
...mapState(["count"])
}

mutations (修改数据)

常规写法

store.js文件下写入

1
2
3
4
5
6
7
8
9
10
const store = new Vuex.Store({
...
mutations: {
//state:state中的值
//payload:传入的值
increment(state, payload) {
state.count = state.count + payload.num;
}
}
});

要使用的组件文件下写入

1
2
3
4
5
6
7
methods: {
fn() {
//第一项 函数名
//第二项 传入的参数
this.$store.commit("increment", { num: 22 });
}
}

mapMutations

要使用的组件文件下写入

函数要传的参数在调用处的括号里传入

1
<button @click="increment({num:123})">点我</button>
1
2
3
methods: {
...mapMutations(["increment"])
}

actions (异步获取数据)

常规写法

store.js文件下写入

1
2
3
4
5
6
7
8
9
actions: {
getData({ commit }, num) {
//模拟拿到后端数据
window.setTimeout(() => {
const data = num;
commit("add", data);
}, 2000);
}
}

要使用的组件文件下写入

1
2
3
getData() {
this.$store.dispatch("getData", 10086);
}

mapActions

要使用的组件文件下写入

函数要传的参数在调用处的括号里传入

1
<button @click="getData(123)">点我</button>
1
2
3
methods: {
...mapActions(["getData"])
}

modules

在 store 全局数据 是可以分模块化管理的

1
2
3
4
5
6
7
8
modules:{
m1:{
namespaced:true, // 开启命名空间
state:{
count:1
}
}
}
1
<h2>{{$store.state.m1.count}}</h2>
]]>
+ + + + + <h1 id="VueX-介绍"><a href="#VueX-介绍" class="headerlink" title="VueX 介绍"></a>VueX 介绍</h1><blockquote> +<p><a href="https://vuex.vuejs.org/zh/"> + + + + + + + + + + + +
+ + diff --git a/books/index.html b/books/index.html new file mode 100644 index 00000000..38b14210 --- /dev/null +++ b/books/index.html @@ -0,0 +1,283 @@ +我看过的书 | JCAlways + + + + + + + + + +
我看过的书
+ + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/ES6/index.html b/categories/JavaScript/ES6/index.html new file mode 100644 index 00000000..9b920b93 --- /dev/null +++ b/categories/JavaScript/ES6/index.html @@ -0,0 +1,279 @@ +分类: ES6 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/ES6/page/2/index.html b/categories/JavaScript/ES6/page/2/index.html new file mode 100644 index 00000000..579a8b91 --- /dev/null +++ b/categories/JavaScript/ES6/page/2/index.html @@ -0,0 +1,279 @@ +分类: ES6 | JCAlways + + + + + + + + + + + +
分类 - ES6
2019
Promise 对象
Promise 对象
Proxy
Proxy
Reflect
Reflect
SIMD
SIMD
Set 和 Map 数据结构
Set 和 Map 数据结构
Symbol
Symbol
async 函数
async 函数
let 和 const 命令
let 和 const 命令
修饰器
修饰器
函数式编程
函数式编程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/ES6/page/3/index.html b/categories/JavaScript/ES6/page/3/index.html new file mode 100644 index 00000000..03370caf --- /dev/null +++ b/categories/JavaScript/ES6/page/3/index.html @@ -0,0 +1,279 @@ +分类: ES6 | JCAlways + + + + + + + + + + + +
分类 - ES6
2019
函数的扩展
函数的扩展
变量的解构赋值
变量的解构赋值
参考链接
参考链接
字符串的扩展
字符串的扩展
对象的扩展
对象的扩展
数值的扩展
数值的扩展
数组的扩展
数组的扩展
最新提案
最新提案
正则的扩展
正则的扩展
编程风格
编程风格
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/ES6/page/4/index.html b/categories/JavaScript/ES6/page/4/index.html new file mode 100644 index 00000000..f0544d4d --- /dev/null +++ b/categories/JavaScript/ES6/page/4/index.html @@ -0,0 +1,279 @@ +分类: ES6 | JCAlways + + + + + + + + + + + +
分类 - ES6
2019
读懂 ECMAScript 规格
读懂 ECMAScript 规格
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/JQuery/index.html b/categories/JavaScript/JQuery/index.html new file mode 100644 index 00000000..ffee51ec --- /dev/null +++ b/categories/JavaScript/JQuery/index.html @@ -0,0 +1,279 @@ +分类: JQuery | JCAlways + + + + + + + + + + + +
分类 - JQuery
2019
JQuery基础
JQuery基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/Node-js/index.html b/categories/JavaScript/Node-js/index.html new file mode 100644 index 00000000..c7a3d5d7 --- /dev/null +++ b/categories/JavaScript/Node-js/index.html @@ -0,0 +1,279 @@ +分类: Node.js | JCAlways + + + + + + + + + + + +
分类 - Node.js
2019
Promise
Promise
Node操作MySQL
Node操作MySQL
Node中的会话技术
Node中的会话技术
Node中的跨域
Node中的跨域
Express框架
Express框架
Node基础
Node基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/React-js/index.html b/categories/JavaScript/React-js/index.html new file mode 100644 index 00000000..f6c6cf85 --- /dev/null +++ b/categories/JavaScript/React-js/index.html @@ -0,0 +1,279 @@ +分类: React.js | JCAlways + + + + + + + + + + + +
分类 - React.js
2020
React基础
React基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/Vue-js/index.html b/categories/JavaScript/Vue-js/index.html new file mode 100644 index 00000000..a9a2abf2 --- /dev/null +++ b/categories/JavaScript/Vue-js/index.html @@ -0,0 +1,279 @@ +分类: Vue.js | JCAlways + + + + + + + + + + + +
分类 - Vue.js
2019
VueX
VueX
Vue的钩子函数
Vue的钩子函数
Vue-Cli的使用
Vue-Cli的使用
Vue基础
Vue基础
MVVM原理
MVVM原理
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/index.html b/categories/JavaScript/index.html new file mode 100644 index 00000000..263c2b03 --- /dev/null +++ b/categories/JavaScript/index.html @@ -0,0 +1,279 @@ +分类: JavaScript | JCAlways + + + + + + + + + + + +
分类 - JavaScript
2020
React基础
React基础
2019
VueX
VueX
Vue的钩子函数
Vue的钩子函数
Vue-Cli的使用
Vue-Cli的使用
Vue基础
Vue基础
MVVM原理
MVVM原理
Promise
Promise
Node操作MySQL
Node操作MySQL
Node中的会话技术
Node中的会话技术
Node中的跨域
Node中的跨域
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/page/2/index.html b/categories/JavaScript/page/2/index.html new file mode 100644 index 00000000..dad98902 --- /dev/null +++ b/categories/JavaScript/page/2/index.html @@ -0,0 +1,279 @@ +分类: JavaScript | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/page/3/index.html b/categories/JavaScript/page/3/index.html new file mode 100644 index 00000000..91a97c37 --- /dev/null +++ b/categories/JavaScript/page/3/index.html @@ -0,0 +1,279 @@ +分类: JavaScript | JCAlways + + + + + + + + + + + +
分类 - JavaScript
2019
Promise 对象
Promise 对象
Proxy
Proxy
Reflect
Reflect
SIMD
SIMD
Set 和 Map 数据结构
Set 和 Map 数据结构
Symbol
Symbol
async 函数
async 函数
let 和 const 命令
let 和 const 命令
修饰器
修饰器
函数式编程
函数式编程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/page/4/index.html b/categories/JavaScript/page/4/index.html new file mode 100644 index 00000000..98f429e2 --- /dev/null +++ b/categories/JavaScript/page/4/index.html @@ -0,0 +1,279 @@ +分类: JavaScript | JCAlways + + + + + + + + + + + +
分类 - JavaScript
2019
函数的扩展
函数的扩展
变量的解构赋值
变量的解构赋值
参考链接
参考链接
字符串的扩展
字符串的扩展
对象的扩展
对象的扩展
数值的扩展
数值的扩展
数组的扩展
数组的扩展
最新提案
最新提案
正则的扩展
正则的扩展
编程风格
编程风格
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/JavaScript/page/5/index.html b/categories/JavaScript/page/5/index.html new file mode 100644 index 00000000..dadbf942 --- /dev/null +++ b/categories/JavaScript/page/5/index.html @@ -0,0 +1,279 @@ +分类: JavaScript | JCAlways + + + + + + + + + + + +
分类 - JavaScript
2019
读懂 ECMAScript 规格
读懂 ECMAScript 规格
Express框架
Express框架
Node基础
Node基础
JS高级
JS高级
JQuery基础
JQuery基础
Web Apis
Web Apis
JS基础
JS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/TypeScript/index.html b/categories/TypeScript/index.html new file mode 100644 index 00000000..7a06aae2 --- /dev/null +++ b/categories/TypeScript/index.html @@ -0,0 +1,279 @@ +分类: TypeScript | JCAlways + + + + + + + + + + + +
分类 - TypeScript
2021
TS基础
TS基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/Web-Office/index.html b/categories/Web-Office/index.html new file mode 100644 index 00000000..b5f387a1 --- /dev/null +++ b/categories/Web-Office/index.html @@ -0,0 +1,279 @@ +分类: Web Office | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/categories/index.html b/categories/index.html new file mode 100644 index 00000000..f3a4760a --- /dev/null +++ b/categories/index.html @@ -0,0 +1,281 @@ +分类 | JCAlways + + + + + + + + + + + + + +
\ No newline at end of file diff --git "a/categories/\344\275\277\347\224\250\346\226\207\346\241\243/Element-Plus/index.html" "b/categories/\344\275\277\347\224\250\346\226\207\346\241\243/Element-Plus/index.html" new file mode 100644 index 00000000..e9fd0639 --- /dev/null +++ "b/categories/\344\275\277\347\224\250\346\226\207\346\241\243/Element-Plus/index.html" @@ -0,0 +1,279 @@ +分类: Element Plus | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\344\275\277\347\224\250\346\226\207\346\241\243/index.html" "b/categories/\344\275\277\347\224\250\346\226\207\346\241\243/index.html" new file mode 100644 index 00000000..c1ac9e66 --- /dev/null +++ "b/categories/\344\275\277\347\224\250\346\226\207\346\241\243/index.html" @@ -0,0 +1,279 @@ +分类: 使用文档 | JCAlways + + + + + + + + + + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\345\234\260\345\233\276\347\261\273/index.html" "b/categories/\345\234\260\345\233\276\347\261\273/index.html" new file mode 100644 index 00000000..df794fa8 --- /dev/null +++ "b/categories/\345\234\260\345\233\276\347\261\273/index.html" @@ -0,0 +1,279 @@ +分类: 地图类 | JCAlways + + + + + + + + + + + +
分类 - 地图类
2022
Vue3使用高德地图
Vue3使用高德地图
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\345\274\200\345\217\221\345\267\245\345\205\267/index.html" "b/categories/\345\274\200\345\217\221\345\267\245\345\205\267/index.html" new file mode 100644 index 00000000..cbd99ba2 --- /dev/null +++ "b/categories/\345\274\200\345\217\221\345\267\245\345\205\267/index.html" @@ -0,0 +1,279 @@ +分类: 开发工具 | JCAlways + + + + + + + + + + + +
分类 - 开发工具
2024
PageSpy使用教程
PageSpy使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" "b/categories/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" new file mode 100644 index 00000000..ee235e73 --- /dev/null +++ "b/categories/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" @@ -0,0 +1,279 @@ +分类: 微信小程序 | JCAlways + + + + + + + + + + + +
分类 - 微信小程序
2019
Uni-App开发项目
Uni-App开发项目
微信小程序基础
微信小程序基础
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\346\212\200\346\234\257\346\226\207\346\241\243/index.html" "b/categories/\346\212\200\346\234\257\346\226\207\346\241\243/index.html" new file mode 100644 index 00000000..3035108e --- /dev/null +++ "b/categories/\346\212\200\346\234\257\346\226\207\346\241\243/index.html" @@ -0,0 +1,279 @@ +分类: 技术文档 | JCAlways + + + + + + + + + + + +
分类 - 技术文档
2019
ESLint
Jquery的常见问题
Jquery的常见问题
Xmind笔记汇总
24道JavaScript算法题
24道JavaScript算法题
JavaScript常见问题
JavaScript常见问题
Hexo博客搭建流程
Hexo博客搭建流程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\350\276\205\345\212\251\347\261\273/index.html" "b/categories/\350\276\205\345\212\251\347\261\273/index.html" new file mode 100644 index 00000000..3230c639 --- /dev/null +++ "b/categories/\350\276\205\345\212\251\347\261\273/index.html" @@ -0,0 +1,279 @@ +分类: 辅助类 | JCAlways + + + + + + + + + + + +
分类 - 辅助类
2024
风灵月影下载
风灵月影下载
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git "a/categories/\351\252\214\350\257\201\347\261\273/index.html" "b/categories/\351\252\214\350\257\201\347\261\273/index.html" new file mode 100644 index 00000000..0375e060 --- /dev/null +++ "b/categories/\351\252\214\350\257\201\347\261\273/index.html" @@ -0,0 +1,279 @@ +分类: 验证类 | JCAlways + + + + + + + + + + + +
分类 - 验证类
2024
Google reCAPTCHA使用教程
Google reCAPTCHA使用教程
2023
GeeTest3.0使用教程
GeeTest3.0使用教程
GeeTest4.0使用教程
GeeTest4.0使用教程
2022
Fingerprintjs使用教程
Fingerprintjs使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/css/index.css b/css/index.css new file mode 100644 index 00000000..7376f814 --- /dev/null +++ b/css/index.css @@ -0,0 +1,6302 @@ +/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ +html { + line-height: 1.15; + -webkit-text-size-adjust: 100% +} + +body { + margin: 0 +} + +main { + display: block +} + +h1 { + font-size: 2em; + margin: .67em 0 +} + +hr { + box-sizing: content-box; + height: 0; + overflow: visible +} + +pre { + font-family: monospace, monospace; + font-size: 1em +} + +a { + background-color: transparent +} + +abbr[title] { + border-bottom: none; + text-decoration: underline; + text-decoration: underline dotted +} + +b, +strong { + font-weight: bolder +} + +code, +kbd, +samp { + font-family: monospace, monospace; + font-size: 1em +} + +small { + font-size: 80% +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline +} + +sub { + bottom: -.25em +} + +sup { + top: -.5em +} + +img { + border-style: none +} + +button, +input, +optgroup, +select, +textarea { + font-family: inherit; + font-size: 100%; + line-height: 1.15; + margin: 0 +} + +button, +input { + overflow: visible +} + +button, +select { + text-transform: none +} + +[type=button], +[type=reset], +[type=submit], +button { + -webkit-appearance: button +} + +[type=button]::-moz-focus-inner, +[type=reset]::-moz-focus-inner, +[type=submit]::-moz-focus-inner, +button::-moz-focus-inner { + border-style: none; + padding: 0 +} + +[type=button]:-moz-focusring, +[type=reset]:-moz-focusring, +[type=submit]:-moz-focusring, +button:-moz-focusring { + outline: 1px dotted ButtonText +} + +fieldset { + padding: .35em .75em .625em +} + +legend { + box-sizing: border-box; + color: inherit; + display: table; + max-width: 100%; + padding: 0; + white-space: normal +} + +progress { + vertical-align: baseline +} + +textarea { + overflow: auto +} + +[type=checkbox], +[type=radio] { + box-sizing: border-box; + padding: 0 +} + +[type=number]::-webkit-inner-spin-button, +[type=number]::-webkit-outer-spin-button { + height: auto +} + +[type=search] { + -webkit-appearance: textfield; + outline-offset: -2px +} + +[type=search]::-webkit-search-decoration { + -webkit-appearance: none +} + +::-webkit-file-upload-button { + -webkit-appearance: button; + font: inherit +} + +details { + display: block +} + +summary { + display: list-item +} + +template { + display: none +} + +[hidden] { + display: none +} +.limit-one-line, +.container .flink .flink-item-name, +.container .flink .flink-item-desc, +#aside-content .card-archives ul.card-archive-list > .card-archive-list-item a span, +#aside-content .card-categories ul.card-category-list > .card-category-list-item a span, +.site-data > a .headline, +#nav #blog-info, +#sidebar #sidebar-menus .menus_items .site-page { + overflow: hidden; + -o-text-overflow: ellipsis; + text-overflow: ellipsis; + white-space: nowrap; +} +.limit-more-line, +.article-sort-item-title, +#recent-posts .recent-post-item >.recent-post-info > .article-title, +#recent-posts .recent-post-item >.recent-post-info > .content, +#aside-content .aside-list > .aside-list-item .content > .name, +#aside-content .aside-list > .aside-list-item .content > .title, +#aside-content .aside-list > .aside-list-item .content > .comment, +#post-info .post-title, +.pagination-related .info .info-1 .info-item-2, +.pagination-related .info .info-2 .info-item-1, +.container figure.gallery-group p, +.container figure.gallery-group .gallery-group-name { + display: -webkit-box; + overflow: hidden; + -webkit-box-orient: vertical; +} +.fontawesomeIcon, +.custom-hr:before, +#post .post-copyright:before, +#post #post-outdate-notice:before, +.note:not(.no-icon)::before, +.search-dialog hr:before { + display: inline-block; + font-weight: 600; + font-family: 'Font Awesome 6 Free'; + text-rendering: auto; + -webkit-font-smoothing: antialiased; +} +.cardHover, +.layout > div:first-child:not(.nc), +#recent-posts .recent-post-item, +#article-container .shuoshuo-item, +#aside-content .card-widget, +.layout .pagination > *:not(.space) { + background: var(--card-bg); + -webkit-box-shadow: var(--card-box-shadow); + box-shadow: var(--card-box-shadow); + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; + border-radius: 8px; +} +.cardHover:hover, +.layout > div:first-child:not(.nc):hover, +#recent-posts .recent-post-item:hover, +#article-container .shuoshuo-item:hover, +#aside-content .card-widget:hover, +.layout .pagination > *:not(.space):hover { + -webkit-box-shadow: var(--card-hover-box-shadow); + box-shadow: var(--card-hover-box-shadow); +} +.imgHover, +.article-sort-item-img :first-child, +#recent-posts .recent-post-item .post_cover .post-bg, +#aside-content .aside-list > .aside-list-item .thumbnail :first-child { + width: 100%; + height: 100%; + -webkit-transition: filter 375ms ease-in 0.2s, -webkit-transform 0.6s; + -moz-transition: filter 375ms ease-in 0.2s, -moz-transform 0.6s; + -o-transition: filter 375ms ease-in 0.2s, -o-transform 0.6s; + -ms-transition: filter 375ms ease-in 0.2s, -ms-transform 0.6s; + transition: filter 375ms ease-in 0.2s, transform 0.6s; + object-fit: cover; +} +.imgHover:hover, +.article-sort-item-img :first-child:hover, +#recent-posts .recent-post-item .post_cover .post-bg:hover, +#aside-content .aside-list > .aside-list-item .thumbnail :first-child:hover { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -o-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); +} +.postImgHover:hover .cover, +.pagination-related:hover .cover { + opacity: 0.5; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -o-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); +} +.postImgHover .cover, +.pagination-related .cover { + width: 100%; + height: 100%; + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transition: all 0.6s, filter 375ms ease-in 0.2s; + -moz-transition: all 0.6s, filter 375ms ease-in 0.2s; + -o-transition: all 0.6s, filter 375ms ease-in 0.2s; + -ms-transition: all 0.6s, filter 375ms ease-in 0.2s; + transition: all 0.6s, filter 375ms ease-in 0.2s; + object-fit: cover; +} +.list-beauty, +.category-lists ul, +#algolia-search .search-dialog .ais-Hits-list { + list-style: none; +} +.list-beauty li, +.category-lists ul li, +#algolia-search .search-dialog .ais-Hits-list li { + position: relative; + padding: 0.12em 0.4em 0.12em 1.4em; +} +.list-beauty li:hover:before, +.category-lists ul li:hover:before, +#algolia-search .search-dialog .ais-Hits-list li:hover:before { + border-color: var(--pseudo-hover); +} +.list-beauty li:before, +.category-lists ul li:before, +#algolia-search .search-dialog .ais-Hits-list li:before { + position: absolute; + top: 0.67em; + left: 0; + width: 0.43em; + height: 0.43em; + border: 0.215em solid #49b1f5; + border-radius: 0.43em; + background: transparent; + content: ''; + cursor: pointer; + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} +.custom-hr, +.search-dialog hr { + position: relative; + margin: 40px auto; + border: 2px dashed var(--hr-border); + width: calc(100% - 4px); +} +.custom-hr:hover:before, +.search-dialog hr:hover:before { + left: calc(95% - 20px); +} +.custom-hr:before, +.search-dialog hr:before { + position: absolute; + top: -10px; + left: 5%; + z-index: 1; + color: var(--hr-before-color); + content: '\f0c4'; + font-size: 20px; + line-height: 1; + -webkit-transition: all 1s ease-in-out; + -moz-transition: all 1s ease-in-out; + -o-transition: all 1s ease-in-out; + -ms-transition: all 1s ease-in-out; + transition: all 1s ease-in-out; +} +.verticalCenter, +.pagination-related .info .info-1, +.pagination-related .info .info-2 { + position: absolute; + top: 50%; + width: 100%; + -webkit-transform: translate(0, -50%); + -moz-transform: translate(0, -50%); + -o-transform: translate(0, -50%); + -ms-transform: translate(0, -50%); + transform: translate(0, -50%); +} +#content-inner, +#footer { + -webkit-animation: bottom-top 1s; + -moz-animation: bottom-top 1s; + -o-animation: bottom-top 1s; + -ms-animation: bottom-top 1s; + animation: bottom-top 1s; +} +#page-header:not(.full_page), +#nav.show { + -webkit-animation: header-effect 1s; + -moz-animation: header-effect 1s; + -o-animation: header-effect 1s; + -ms-animation: header-effect 1s; + animation: header-effect 1s; +} +#site-title, +#site-subtitle { + -webkit-animation: titleScale 1s; + -moz-animation: titleScale 1s; + -o-animation: titleScale 1s; + -ms-animation: titleScale 1s; + animation: titleScale 1s; +} +canvas:not(#ribbon-canvas), +#web_bg { + -webkit-animation: to_show 4s; + -moz-animation: to_show 4s; + -o-animation: to_show 4s; + -ms-animation: to_show 4s; + animation: to_show 4s; +} +#ribbon-canvas { + -webkit-animation: ribbon_to_show 4s; + -moz-animation: ribbon_to_show 4s; + -o-animation: ribbon_to_show 4s; + -ms-animation: ribbon_to_show 4s; + animation: ribbon_to_show 4s; +} +#sidebar-menus.open > :nth-child(1) { + -webkit-animation: sidebarItem 0.2s; + -moz-animation: sidebarItem 0.2s; + -o-animation: sidebarItem 0.2s; + -ms-animation: sidebarItem 0.2s; + animation: sidebarItem 0.2s; +} +#sidebar-menus.open > :nth-child(2) { + -webkit-animation: sidebarItem 0.4s; + -moz-animation: sidebarItem 0.4s; + -o-animation: sidebarItem 0.4s; + -ms-animation: sidebarItem 0.4s; + animation: sidebarItem 0.4s; +} +#sidebar-menus.open > :nth-child(3) { + -webkit-animation: sidebarItem 0.6s; + -moz-animation: sidebarItem 0.6s; + -o-animation: sidebarItem 0.6s; + -ms-animation: sidebarItem 0.6s; + animation: sidebarItem 0.6s; +} +#sidebar-menus.open > :nth-child(4) { + -webkit-animation: sidebarItem 0.8s; + -moz-animation: sidebarItem 0.8s; + -o-animation: sidebarItem 0.8s; + -ms-animation: sidebarItem 0.8s; + animation: sidebarItem 0.8s; +} +.scroll-down-effects { + -webkit-animation: scroll-down-effect 1.5s infinite; + -moz-animation: scroll-down-effect 1.5s infinite; + -o-animation: scroll-down-effect 1.5s infinite; + -ms-animation: scroll-down-effect 1.5s infinite; + animation: scroll-down-effect 1.5s infinite; +} +.reward-main { + -webkit-animation: donate_effcet 0.3s 0.1s ease both; + -moz-animation: donate_effcet 0.3s 0.1s ease both; + -o-animation: donate_effcet 0.3s 0.1s ease both; + -ms-animation: donate_effcet 0.3s 0.1s ease both; + animation: donate_effcet 0.3s 0.1s ease both; +} +@-moz-keyframes scroll-down-effect { + 0% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } + 50% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate(0, -16px); + -moz-transform: translate(0, -16px); + -o-transform: translate(0, -16px); + -ms-transform: translate(0, -16px); + transform: translate(0, -16px); + } + 100% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } +} +@-webkit-keyframes scroll-down-effect { + 0% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } + 50% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate(0, -16px); + -moz-transform: translate(0, -16px); + -o-transform: translate(0, -16px); + -ms-transform: translate(0, -16px); + transform: translate(0, -16px); + } + 100% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } +} +@-o-keyframes scroll-down-effect { + 0% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } + 50% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate(0, -16px); + -moz-transform: translate(0, -16px); + -o-transform: translate(0, -16px); + -ms-transform: translate(0, -16px); + transform: translate(0, -16px); + } + 100% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } +} +@keyframes scroll-down-effect { + 0% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } + 50% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate(0, -16px); + -moz-transform: translate(0, -16px); + -o-transform: translate(0, -16px); + -ms-transform: translate(0, -16px); + transform: translate(0, -16px); + } + 100% { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); + } +} +@-moz-keyframes header-effect { + 0% { + -webkit-transform: translateY(-35px); + -moz-transform: translateY(-35px); + -o-transform: translateY(-35px); + -ms-transform: translateY(-35px); + transform: translateY(-35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-webkit-keyframes header-effect { + 0% { + -webkit-transform: translateY(-35px); + -moz-transform: translateY(-35px); + -o-transform: translateY(-35px); + -ms-transform: translateY(-35px); + transform: translateY(-35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-o-keyframes header-effect { + 0% { + -webkit-transform: translateY(-35px); + -moz-transform: translateY(-35px); + -o-transform: translateY(-35px); + -ms-transform: translateY(-35px); + transform: translateY(-35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes header-effect { + 0% { + -webkit-transform: translateY(-35px); + -moz-transform: translateY(-35px); + -o-transform: translateY(-35px); + -ms-transform: translateY(-35px); + transform: translateY(-35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-moz-keyframes bottom-top { + 0% { + -webkit-transform: translateY(35px); + -moz-transform: translateY(35px); + -o-transform: translateY(35px); + -ms-transform: translateY(35px); + transform: translateY(35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-webkit-keyframes bottom-top { + 0% { + -webkit-transform: translateY(35px); + -moz-transform: translateY(35px); + -o-transform: translateY(35px); + -ms-transform: translateY(35px); + transform: translateY(35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-o-keyframes bottom-top { + 0% { + -webkit-transform: translateY(35px); + -moz-transform: translateY(35px); + -o-transform: translateY(35px); + -ms-transform: translateY(35px); + transform: translateY(35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes bottom-top { + 0% { + -webkit-transform: translateY(35px); + -moz-transform: translateY(35px); + -o-transform: translateY(35px); + -ms-transform: translateY(35px); + transform: translateY(35px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-moz-keyframes titleScale { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@-webkit-keyframes titleScale { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@-o-keyframes titleScale { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@keyframes titleScale { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@-moz-keyframes search_close { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@-webkit-keyframes search_close { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@-o-keyframes search_close { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@keyframes search_close { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@-moz-keyframes to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + } +} +@-webkit-keyframes to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + } +} +@-o-keyframes to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + } +} +@keyframes to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + } +} +@-moz-keyframes to_hide { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } +} +@-webkit-keyframes to_hide { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } +} +@-o-keyframes to_hide { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } +} +@keyframes to_hide { + 0% { + opacity: 1; + -ms-filter: none; + filter: none; + } + 100% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } +} +@-moz-keyframes ribbon_to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 0.6; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; + filter: alpha(opacity=60); + } +} +@-webkit-keyframes ribbon_to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 0.6; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; + filter: alpha(opacity=60); + } +} +@-o-keyframes ribbon_to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 0.6; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; + filter: alpha(opacity=60); + } +} +@keyframes ribbon_to_show { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + } + 100% { + opacity: 0.6; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=60)"; + filter: alpha(opacity=60); + } +} +@-moz-keyframes avatar_turn_around { + from { + -webkit-transform: rotate(0); + -moz-transform: rotate(0); + -o-transform: rotate(0); + -ms-transform: rotate(0); + transform: rotate(0); + } + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-webkit-keyframes avatar_turn_around { + from { + -webkit-transform: rotate(0); + -moz-transform: rotate(0); + -o-transform: rotate(0); + -ms-transform: rotate(0); + transform: rotate(0); + } + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-o-keyframes avatar_turn_around { + from { + -webkit-transform: rotate(0); + -moz-transform: rotate(0); + -o-transform: rotate(0); + -ms-transform: rotate(0); + transform: rotate(0); + } + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@keyframes avatar_turn_around { + from { + -webkit-transform: rotate(0); + -moz-transform: rotate(0); + -o-transform: rotate(0); + -ms-transform: rotate(0); + transform: rotate(0); + } + to { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); + } +} +@-moz-keyframes sub_menus { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(10px); + -moz-transform: translateY(10px); + -o-transform: translateY(10px); + -ms-transform: translateY(10px); + transform: translateY(10px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-webkit-keyframes sub_menus { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(10px); + -moz-transform: translateY(10px); + -o-transform: translateY(10px); + -ms-transform: translateY(10px); + transform: translateY(10px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-o-keyframes sub_menus { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(10px); + -moz-transform: translateY(10px); + -o-transform: translateY(10px); + -ms-transform: translateY(10px); + transform: translateY(10px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes sub_menus { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(10px); + -moz-transform: translateY(10px); + -o-transform: translateY(10px); + -ms-transform: translateY(10px); + transform: translateY(10px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-moz-keyframes donate_effcet { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(-20px); + -moz-transform: translateY(-20px); + -o-transform: translateY(-20px); + -ms-transform: translateY(-20px); + transform: translateY(-20px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-webkit-keyframes donate_effcet { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(-20px); + -moz-transform: translateY(-20px); + -o-transform: translateY(-20px); + -ms-transform: translateY(-20px); + transform: translateY(-20px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-o-keyframes donate_effcet { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(-20px); + -moz-transform: translateY(-20px); + -o-transform: translateY(-20px); + -ms-transform: translateY(-20px); + transform: translateY(-20px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes donate_effcet { + 0% { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translateY(-20px); + -moz-transform: translateY(-20px); + -o-transform: translateY(-20px); + -ms-transform: translateY(-20px); + transform: translateY(-20px); + } + 100% { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-moz-keyframes sidebarItem { + 0% { + -webkit-transform: translateX(200px); + -moz-transform: translateX(200px); + -o-transform: translateX(200px); + -ms-transform: translateX(200px); + transform: translateX(200px); + } + 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } +} +@-webkit-keyframes sidebarItem { + 0% { + -webkit-transform: translateX(200px); + -moz-transform: translateX(200px); + -o-transform: translateX(200px); + -ms-transform: translateX(200px); + transform: translateX(200px); + } + 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } +} +@-o-keyframes sidebarItem { + 0% { + -webkit-transform: translateX(200px); + -moz-transform: translateX(200px); + -o-transform: translateX(200px); + -ms-transform: translateX(200px); + transform: translateX(200px); + } + 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } +} +@keyframes sidebarItem { + 0% { + -webkit-transform: translateX(200px); + -moz-transform: translateX(200px); + -o-transform: translateX(200px); + -ms-transform: translateX(200px); + transform: translateX(200px); + } + 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } +} +:root { + --global-font-size: 14px; + --global-bg: #fff; + --font-color: #4c4948; + --hr-border: #a4d8fa; + --hr-before-color: #80c8f8; + --search-bg: #f6f8fa; + --search-input-color: #4c4948; + --search-a-color: #4c4948; + --preloader-bg: #37474f; + --preloader-color: #fff; + --tab-border-color: #f0f0f0; + --tab-botton-bg: #f0f0f0; + --tab-botton-color: #1f2d3d; + --tab-button-hover-bg: #dcdcdc; + --tab-button-active-bg: #fff; + --card-bg: #fff; + --card-meta: #858585; + --sidebar-bg: #f6f8fa; + --sidebar-menu-bg: #fff; + --btn-hover-color: #ff7242; + --btn-color: #fff; + --btn-bg: #49b1f5; + --text-bg-hover: rgba(73,177,245,0.7); + --light-grey: #eee; + --dark-grey: #cacaca; + --white: #fff; + --text-highlight-color: #1f2d3d; + --blockquote-color: #6a737d; + --blockquote-bg: rgba(73,177,245,0.1); + --reward-pop: #f5f5f5; + --toc-link-color: #666261; + --card-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.05); + --card-hover-box-shadow: 0 3px 8px 6px rgba(7,17,27,0.09); + --pseudo-hover: #ff7242; + --headline-presudo: #a0a0a0; + --scrollbar-color: #49b1f5; + --default-bg-color: #49b1f5; + --zoom-bg: #fff; + --mark-bg: rgba(0,0,0,0.3); +} +body { + position: relative; + overflow-y: scroll; + min-height: 100%; + background: var(--global-bg); + color: var(--font-color); + font-size: var(--global-font-size); + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Helvetica Neue', Lato, Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif; + line-height: 2; + -webkit-tap-highlight-color: rgba(0,0,0,0); + scroll-behavior: smooth; +} +@-moz-document url-prefix() { + * { + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-color) transparent; + } +} +*::-webkit-scrollbar { + width: 5px; + height: 5px; +} +*::-webkit-scrollbar-thumb { + background: var(--scrollbar-color); +} +*::-webkit-scrollbar-track { + background-color: transparent; +} +input::placeholder { + color: var(--font-color); +} +h1, +h2, +h3, +h4, +h5, +h6 { + position: relative; + margin: 20px 0 14px; + color: var(--text-highlight-color); + font-weight: bold; +} +h1 code, +h2 code, +h3 code, +h4 code, +h5 code, +h6 code { + font-size: inherit !important; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.table-wrap { + overflow-x: scroll; + margin: 0 0 20px; + border-radius: 5px; +} +.table-wrap table { + border-radius: 5px; +} +.table-wrap table thead > tr:first-child th:first-child { + border-top-left-radius: 5px; +} +.table-wrap table thead > tr:first-child th:last-child { + border-top-right-radius: 5px; +} +.table-wrap table tbody > tr:last-child td:first-child { + border-bottom-left-radius: 5px; +} +.table-wrap table tbody > tr:last-child td:last-child { + border-bottom-right-radius: 5px; +} +table { + display: table; + width: 100%; + border-spacing: 0; + border-collapse: separate; + border-top: 1px solid var(--light-grey); + border-left: 1px solid var(--light-grey); + empty-cells: show; +} +table thead { + background: rgba(153,169,191,0.1); +} +table th, +table td { + padding: 6px 12px; + border: 1px solid var(--light-grey); + border-top: none; + border-left: none; + vertical-align: middle; +} +*::selection { + background: #00c4b6; + color: #f7f7f7; +} +button { + padding: 0; + outline: 0; + border: none; + background: none; + cursor: pointer; + touch-action: manipulation; +} +a { + color: #99a9bf; + text-decoration: none; + word-wrap: break-word; + -webkit-transition: all 0.2s; + -moz-transition: all 0.2s; + -o-transition: all 0.2s; + -ms-transition: all 0.2s; + transition: all 0.2s; + overflow-wrap: break-word; +} +a:hover { + color: #49b1f5; +} +.text-center { + text-align: center; +} +.text-right { + text-align: right; +} +img[src=''], +img:not([src]) { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); +} +.img-alt { + margin: -10px 0 10px; + color: #858585; +} +.img-alt:hover { + text-decoration: none !important; +} +blockquote { + margin: 0 0 20px; + padding: 7px 15px; + border-left: 4px solid #49b1f5; + background-color: var(--blockquote-bg); + color: var(--blockquote-color); + border-radius: 6px; +} +blockquote footer cite:before { + padding: 0 5px; + content: '—'; +} +blockquote > :last-child { + margin-bottom: 0 !important; +} +:root { + --hl-color: #90a4ae; + --hl-bg: #f6f8fa; + --hltools-bg: #e6ebf1; + --hltools-color: #90a4ae; + --hlnumber-bg: #f6f8fa; + --hlnumber-color: rgba(144,164,174,0.5); + --hlscrollbar-bg: #dce4eb; + --hlexpand-bg: linear-gradient(180deg, rgba(246,248,250,0.6), rgba(246,248,250,0.9)); +} +[data-theme='dark'] { + --hl-color: rgba(255,255,255,0.7); + --hl-bg: #171717; + --hltools-bg: #1a1a1a; + --hltools-color: #90a4ae; + --hlnumber-bg: #171717; + --hlnumber-color: rgba(255,255,255,0.4); + --hlscrollbar-bg: #1f1f1f; + --hlexpand-bg: linear-gradient(180deg, rgba(23,23,23,0.6), rgba(23,23,23,0.9)); +} +@-moz-document url-prefix() { + scrollbar-color: var(--hlscrollbar-bg) transparent; +} +figure.highlight table::-webkit-scrollbar-thumb { + background: var(--hlscrollbar-bg); +} +figure.highlight pre .deletion { + color: #bf42bf; +} +figure.highlight pre .addition { + color: #105ede; +} +figure.highlight pre .meta { + color: #7c4dff; +} +figure.highlight pre .comment { + color: rgba(149,165,166,0.8); +} +figure.highlight pre .variable, +figure.highlight pre .attribute, +figure.highlight pre .regexp, +figure.highlight pre .ruby .constant, +figure.highlight pre .xml .tag .title, +figure.highlight pre .xml .pi, +figure.highlight pre .xml .doctype, +figure.highlight pre .html .doctype, +figure.highlight pre .css .id, +figure.highlight pre .tag .name, +figure.highlight pre .css .class, +figure.highlight pre .css .pseudo { + color: #e53935; +} +figure.highlight pre .tag { + color: #39adb5; +} +figure.highlight pre .number, +figure.highlight pre .preprocessor, +figure.highlight pre .literal, +figure.highlight pre .params, +figure.highlight pre .constant, +figure.highlight pre .command { + color: #f76d47; +} +figure.highlight pre .built_in { + color: #ffb62c; +} +figure.highlight pre .ruby .class .title, +figure.highlight pre .css .rules .attribute, +figure.highlight pre .string, +figure.highlight pre .value, +figure.highlight pre .inheritance, +figure.highlight pre .header, +figure.highlight pre .ruby .symbol, +figure.highlight pre .xml .cdata, +figure.highlight pre .special, +figure.highlight pre .number, +figure.highlight pre .formula { + color: #91b859; +} +figure.highlight pre .keyword, +figure.highlight pre .title, +figure.highlight pre .css .hexcolor { + color: #39adb5; +} +figure.highlight pre .function, +figure.highlight pre .python .decorator, +figure.highlight pre .python .title, +figure.highlight pre .ruby .function .title, +figure.highlight pre .ruby .title .keyword, +figure.highlight pre .perl .sub, +figure.highlight pre .javascript .title, +figure.highlight pre .coffeescript .title { + color: #6182b8; +} +figure.highlight pre .tag .attr, +figure.highlight pre .javascript .function { + color: #7c4dff; +} +.container figure.highlight .line.marked { + background-color: rgba(128,203,196,0.251); +} +.container figure.highlight table { + display: block; + overflow: auto; + border: none; +} +.container figure.highlight table td { + padding: 0; + border: none; +} +.container figure.highlight .gutter pre { + padding-right: 10px; + padding-left: 10px; + background-color: var(--hlnumber-bg); + color: var(--hlnumber-color); + text-align: right; +} +.container figure.highlight .code pre { + padding-right: 10px; + padding-left: 10px; + width: 100%; +} +.container pre, +.container figure.highlight { + overflow: auto; + margin: 0 0 20px; + padding: 0; + background: var(--hl-bg); + color: var(--hl-color); + line-height: 1.6; +} +.container pre, +.container code { + font-size: var(--global-font-size); + font-family: consolas, Menlo, 'PingFang SC', 'Microsoft YaHei', sans-serif !important; + border-radius: 6px; +} +.container code { + padding: 2px 5px; + background: rgba(27,31,35,0.05); + color: #f47466; +} +.container pre { + padding: 10px 20px; +} +.container pre code { + padding: 0; + background: none; + color: var(--hl-color); + text-shadow: none; +} +.container figure.highlight { + position: relative; + border-radius: 6px; +} +.container figure.highlight pre { + margin: 0; + padding: 8px 0; + border: none; +} +.container figure.highlight figcaption, +.container figure.highlight .caption { + padding: 6px 0 2px 14px; + font-size: var(--global-font-size); + line-height: 1em; +} +.container figure.highlight figcaption a, +.container figure.highlight .caption a { + float: right; + padding-right: 10px; + color: var(--hl-color); +} +.container figure.highlight figcaption a:hover, +.container figure.highlight .caption a:hover { + border-bottom-color: var(--hl-color); +} +.container figure.highlight.copy-true { + -webkit-user-select: all; + -moz-user-select: all; + -ms-user-select: all; + user-select: all; + -webkit-user-select: all; +} +.container figure.highlight.copy-true > table, +.container figure.highlight.copy-true > pre { + display: block !important; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); +} +.container .highlight-tools { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + padding: 0 8px; + min-height: 24px; + height: 2.15em; + background: var(--hltools-bg); + color: var(--hltools-color); + font-size: var(--global-font-size); + overflow: hidden; +} +.container .highlight-tools > * { + padding: 5px; +} +.container .highlight-tools i { + cursor: pointer; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} +.container .highlight-tools i:hover { + color: #49b1f5; +} +.container .highlight-tools.closed ~ * { + display: none; +} +.container .highlight-tools.closed .expand { + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + transform: rotate(-90deg); +} +.container .highlight-tools > .macStyle { + padding: 0; +} +.container .highlight-tools .code-lang { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + text-transform: uppercase; + font-weight: bold; + font-size: 1.15em; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; + padding: 2px; +} +.container .highlight-tools .copy-notice { + padding-right: 2px; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: opacity 0.4s; + -moz-transition: opacity 0.4s; + -o-transition: opacity 0.4s; + -ms-transition: opacity 0.4s; + transition: opacity 0.4s; +} +.container .highlight-tools .code-lang { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +.container .gutter { + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-user-select: none; +} +.container .gist table { + width: auto; +} +.container .gist table td { + border: none; +} +.article-sort { + margin-left: 10px; + padding-left: 20px; + border-left: 2px solid #aadafa; +} +.article-sort-title { + position: relative; + margin-left: 10px; + padding-bottom: 20px; + padding-left: 20px; + font-size: 1.72em; +} +.article-sort-title:hover:before { + border-color: var(--pseudo-hover); +} +.article-sort-title:before { + position: absolute; + top: calc(((100% - 36px) / 2)); + left: -9px; + z-index: 1; + width: 10px; + height: 10px; + border: 5px solid #49b1f5; + border-radius: 10px; + background: var(--card-bg); + content: ''; + line-height: 10px; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.article-sort-title:after { + position: absolute; + bottom: 0; + left: 0; + z-index: 0; + width: 2px; + height: 1.5em; + background: #aadafa; + content: ''; +} +.article-sort-item { + position: relative; + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + margin: 0 0 20px 10px; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.article-sort-item:hover:before { + border-color: var(--pseudo-hover); +} +.article-sort-item:before { + position: absolute; + left: calc(-20px - 17px); + width: 6px; + height: 6px; + border: 3px solid #49b1f5; + border-radius: 6px; + background: var(--card-bg); + content: ''; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +.article-sort-item.no-article-cover { + height: 80px; +} +.article-sort-item.no-article-cover .article-sort-item-info { + padding: 0; +} +.article-sort-item.year { + font-size: 1.43em; + margin-bottom: 10px; +} +.article-sort-item.year:hover:before { + border-color: #49b1f5; +} +.article-sort-item.year:before { + border-color: var(--pseudo-hover); +} +.article-sort-item-time { + color: var(--card-meta); + font-size: 0.85em; +} +.article-sort-item-time time { + padding-left: 6px; + cursor: default; +} +.article-sort-item-title { + color: var(--font-color); + font-size: 1.05em; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; + -webkit-line-clamp: 2; +} +.article-sort-item-title:hover { + color: #49b1f5; + -webkit-transform: translateX(10px); + -moz-transform: translateX(10px); + -o-transform: translateX(10px); + -ms-transform: translateX(10px); + transform: translateX(10px); +} +.article-sort-item-img { + overflow: hidden; + width: 100px; + height: 70px; + border-radius: 6px; +} +@media screen and (max-width: 768px) { + .article-sort-item-img { + width: 70px; + height: 70px; + } +} +.article-sort-item-info { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + padding: 0 16px; +} +.category-lists .category-title { + font-size: 2.57em; +} +@media screen and (max-width: 768px) { + .category-lists .category-title { + font-size: 2em; + } +} +.category-lists .category-list { + margin-bottom: 0; +} +.category-lists .category-list a { + color: var(--font-color); +} +.category-lists .category-list a:hover { + color: #49b1f5; +} +.category-lists .category-list .category-list-count { + margin-left: 8px; + color: var(--card-meta); +} +.category-lists .category-list .category-list-count:before { + content: '('; +} +.category-lists .category-list .category-list-count:after { + content: ')'; +} +.category-lists ul { + padding: 0 0 0 20px; +} +.category-lists ul ul { + padding-left: 4px; +} +.category-lists ul li { + position: relative; + margin: 6px 0; + padding: 0.12em 0.4em 0.12em 1.4em; +} +#body-wrap { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + -o-box-orient: vertical; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + min-height: 100vh; +} +.layout { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1 auto; + -ms-flex: 1 auto; + flex: 1 auto; + margin: 0 auto; + padding: 40px 15px; + max-width: 1200px; + width: 100%; +} +@media screen and (max-width: 900px) { + .layout { + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + -o-box-orient: vertical; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + } +} +@media screen and (max-width: 768px) { + .layout { + padding: 20px 5px; + } +} +@media screen and (min-width: 2000px) { + .layout { + max-width: 70%; + } +} +.layout > div:first-child:not(.nc) { + -webkit-align-self: flex-start; + align-self: flex-start; + -ms-flex-item-align: start; + padding: 50px 40px; +} +@media screen and (max-width: 768px) { + .layout > div:first-child:not(.nc) { + padding: 36px 14px; + } +} +.layout > div:first-child { + width: 74%; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} +@media screen and (max-width: 900px) { + .layout > div:first-child { + width: 100% !important; + } +} +.layout.hide-aside { + max-width: 1000px; +} +@media screen and (min-width: 2000px) { + .layout.hide-aside { + max-width: 1300px; + } +} +.layout.hide-aside > div { + width: 100% !important; +} +.apple #page-header.full_page { + background-attachment: scroll !important; +} +.apple .recent-post-item, +.apple .avatar-img, +.apple .flink-item-icon { + -webkit-transform: translateZ(0); + -moz-transform: translateZ(0); + -o-transform: translateZ(0); + -ms-transform: translateZ(0); + transform: translateZ(0); +} +.container .flink { + margin-bottom: 20px; +} +.container .flink .flink-list { + overflow: auto; + padding: 10px 10px 0; + text-align: center; +} +.container .flink .flink-list > .flink-list-item { + position: relative; + float: left; + overflow: hidden; + margin: 15px 7px; + width: calc(100% / 3 - 15px); + height: 90px; + line-height: 17px; + -webkit-transform: translateZ(0); + border-radius: 8px; +} +@media screen and (max-width: 1024px) { + .container .flink .flink-list > .flink-list-item { + width: calc(50% - 15px) !important; + } +} +@media screen and (max-width: 600px) { + .container .flink .flink-list > .flink-list-item { + width: calc(100% - 15px) !important; + } +} +.container .flink .flink-list > .flink-list-item:hover .flink-item-icon { + margin-left: -10px; + width: 0; +} +.container .flink .flink-list > .flink-list-item:before { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + background: var(--text-bg-hover); + content: ''; + -webkit-transition: -webkit-transform 0.3s ease-out; + -moz-transition: -moz-transform 0.3s ease-out; + -o-transition: -o-transform 0.3s ease-out; + -ms-transition: -ms-transform 0.3s ease-out; + transition: transform 0.3s ease-out; + -webkit-transform: scale(0); + -moz-transform: scale(0); + -o-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); +} +.container .flink .flink-list > .flink-list-item:hover:before, +.container .flink .flink-list > .flink-list-item:focus:before, +.container .flink .flink-list > .flink-list-item:active:before { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); +} +.container .flink .flink-list > .flink-list-item a { + color: var(--font-color); + text-decoration: none; +} +.container .flink .flink-list > .flink-list-item a .flink-item-icon { + float: left; + overflow: hidden; + margin: 15px 10px; + width: 60px; + height: 60px; + border-radius: 7px; + -webkit-transition: width 0.3s ease-out; + -moz-transition: width 0.3s ease-out; + -o-transition: width 0.3s ease-out; + -ms-transition: width 0.3s ease-out; + transition: width 0.3s ease-out; +} +.container .flink .flink-list > .flink-list-item a .flink-item-icon img { + width: 100%; + height: 100%; + -webkit-transition: filter 375ms ease-in 0.2s, -webkit-transform 0.3s; + -moz-transition: filter 375ms ease-in 0.2s, -moz-transform 0.3s; + -o-transition: filter 375ms ease-in 0.2s, -o-transform 0.3s; + -ms-transition: filter 375ms ease-in 0.2s, -ms-transform 0.3s; + transition: filter 375ms ease-in 0.2s, transform 0.3s; + object-fit: cover; +} +.container .flink .flink-list > .flink-list-item a .img-alt { + display: none; +} +.container .flink .flink-item-name { + padding: 16px 10px 0 0; + height: 40px; + font-weight: bold; + font-size: 1.43em; +} +.container .flink .flink-item-desc { + padding: 16px 10px 16px 0; + height: 50px; + font-size: 0.93em; +} +.container .flink .flink-name { + margin-bottom: 5px; + font-weight: bold; + font-size: 1.5em; +} +#recent-posts .recent-post-item { + position: relative; + overflow: hidden; + margin-bottom: 20px; + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-orient: horizontal; + -moz-box-orient: horizontal; + -o-box-orient: horizontal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + height: 16.8em; +} +@media screen and (max-width: 768px) { + #recent-posts .recent-post-item { + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + -o-box-orient: vertical; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; + height: auto; + } +} +@media screen and (min-width: 2000px) { + #recent-posts .recent-post-item { + height: 18.8em; + } +} +#recent-posts .recent-post-item:hover .post-bg { + -webkit-transform: scale(1.1); + -moz-transform: scale(1.1); + -o-transform: scale(1.1); + -ms-transform: scale(1.1); + transform: scale(1.1); +} +#recent-posts .recent-post-item.ads-wrap { + display: block !important; + height: auto !important; +} +#recent-posts .recent-post-item .post_cover { + overflow: hidden; + width: 42%; + height: 100%; +} +@media screen and (max-width: 768px) { + #recent-posts .recent-post-item .post_cover { + width: 100%; + height: 230px; + } +} +#recent-posts .recent-post-item .post_cover.right { + -webkit-box-ordinal-group: 1; + -moz-box-ordinal-group: 1; + -o-box-ordinal-group: 1; + -ms-flex-order: 1; + -webkit-order: 1; + order: 1; +} +@media screen and (max-width: 768px) { + #recent-posts .recent-post-item .post_cover.right { + -webkit-box-ordinal-group: 0; + -moz-box-ordinal-group: 0; + -o-box-ordinal-group: 0; + -ms-flex-order: 0; + -webkit-order: 0; + order: 0; + } +} +#recent-posts .recent-post-item .post_cover .post-bg { + z-index: -4; +} +#recent-posts .recent-post-item >.recent-post-info { + padding: 0 40px; + width: 58%; +} +@media screen and (max-width: 768px) { + #recent-posts .recent-post-item >.recent-post-info { + padding: 20px 20px 30px; + width: 100%; + } +} +#recent-posts .recent-post-item >.recent-post-info.no-cover { + width: 100%; +} +@media screen and (max-width: 768px) { + #recent-posts .recent-post-item >.recent-post-info.no-cover { + padding: 30px 20px; + } +} +#recent-posts .recent-post-item >.recent-post-info > .article-title { + color: var(--text-highlight-color); + font-size: 1.55em; + line-height: 1.4; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + -webkit-line-clamp: 2; +} +#recent-posts .recent-post-item >.recent-post-info > .article-title .sticky { + margin-right: 10px; + color: #ff7242; + -webkit-transform: rotate(45deg); + -moz-transform: rotate(45deg); + -o-transform: rotate(45deg); + -ms-transform: rotate(45deg); + transform: rotate(45deg); +} +@media screen and (max-width: 768px) { + #recent-posts .recent-post-item >.recent-post-info > .article-title { + font-size: 1.43em; + } +} +#recent-posts .recent-post-item >.recent-post-info > .article-title:hover { + color: #49b1f5; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap { + margin: 6px 0; + color: var(--card-meta); + font-size: 0.9em; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap > .post-meta-date { + cursor: default; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap i { + margin: 0 4px 0 0; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap .fa-spinner { + margin: 0; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap .article-meta-label { + padding-right: 4px; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap .article-meta-separator { + margin: 0 6px; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap .article-meta-link { + margin: 0 4px; +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap a { + color: var(--card-meta); +} +#recent-posts .recent-post-item >.recent-post-info > .article-meta-wrap a:hover { + color: #49b1f5; + text-decoration: underline; +} +#recent-posts .recent-post-item >.recent-post-info > .content { + -webkit-line-clamp: 2; +} +#article-container .shuoshuo-item { + margin-bottom: 20px; + padding: 35px 30px 30px; +} +@media screen and (max-width: 768px) { + #article-container .shuoshuo-item { + padding: 25px 20px 20px; + } +} +#article-container .shuoshuo-item-header { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + cursor: default; +} +#article-container .shuoshuo-avatar { + overflow: hidden; + width: 40px; + height: 40px; + border-radius: 40px; +} +#article-container .shuoshuo-avatar img { + margin: 0; + width: 100%; + height: 100%; +} +#article-container .shuoshuo-info { + margin-left: 10px; + line-height: 1.5; +} +#article-container .shuoshuo-date { + color: #858585; + font-size: 0.8em; +} +#article-container .shuoshuo-content { + padding: 15px 0 10px; +} +#article-container .shuoshuo-content > *:last-child { + margin-bottom: 0; +} +#article-container .shuoshuo-footer { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; +} +#article-container .shuoshuo-footer.flex-between { + -webkit-box-pack: justify; + -moz-box-pack: justify; + -o-box-pack: justify; + -ms-flex-pack: justify; + -webkit-justify-content: space-between; + justify-content: space-between; +} +#article-container .shuoshuo-footer.flex-end { + -webkit-box-pack: end; + -moz-box-pack: end; + -o-box-pack: end; + -ms-flex-pack: end; + -webkit-justify-content: flex-end; + justify-content: flex-end; +} +#article-container .shuoshuo-footer .shuoshuo-tag { + display: inline-block; + margin-right: 8px; + padding: 0 8px; + width: fit-content; + border: 1px solid #49b1f5; + border-radius: 12px; + color: #49b1f5; + font-size: 0.85em; + cursor: default; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +#article-container .shuoshuo-footer .shuoshuo-tag:hover { + background: #49b1f5; + color: var(--white); +} +#article-container .shuoshuo-footer .shuoshuo-comment-btn { + padding: 2px; + color: #90a4ae; + cursor: pointer; +} +#article-container .shuoshuo-footer .shuoshuo-comment-btn:hover { + color: #49b1f5; +} +#article-container .shuoshuo-comment { + padding-top: 10px; +} +#article-container .shuoshuo-comment.no-comment { + display: none; +} +.tag-cloud-list a { + display: inline-block; + margin: 2px; + padding: 2px 7px; + line-height: 1.7; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; + border-radius: 5px; +} +.tag-cloud-list a:hover { + background: var(--btn-bg) !important; + -webkit-box-shadow: 2px 2px 6px rgba(0,0,0,0.2); + box-shadow: 2px 2px 6px rgba(0,0,0,0.2); + color: var(--btn-color) !important; +} +@media screen and (max-width: 768px) { + .tag-cloud-list a { + zoom: 0.85; + } +} +.tag-cloud-title { + font-size: 2.57em; +} +@media screen and (max-width: 768px) { + .tag-cloud-title { + font-size: 2em; + } +} +.page-title + .tag-cloud-list { + text-align: left; +} +#aside-content { + width: 26%; +} +@media screen and (min-width: 900px) { + #aside-content { + padding-left: 15px; + } +} +@media screen and (max-width: 900px) { + #aside-content { + margin-top: 20px; + width: 100%; + } +} +#aside-content .card-widget { + position: relative; + overflow: hidden; + margin-bottom: 20px; + padding: 20px 24px; +} +#aside-content .card-widget:last-child { + margin-bottom: 0; +} +#aside-content .card-info .author-info-name { + font-weight: 500; + font-size: 1.57em; +} +#aside-content .card-info .author-info-description { + margin-top: -0.42em; +} +#aside-content .card-info .site-data { + margin: 14px 0 4px; +} +#aside-content .card-info .card-info-social-icons { + margin: 6px 0 -6px; +} +#aside-content .card-info .card-info-social-icons .social-icon { + margin: 0 10px; + color: var(--font-color); + font-size: 1.4em; +} +#aside-content .card-info .card-info-social-icons i { + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} +#aside-content .card-info .card-info-social-icons i:hover { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); +} +#aside-content .card-info #card-info-btn { + display: block; + margin-top: 14px; + background-color: var(--btn-bg); + color: var(--btn-color); + text-align: center; + line-height: 2.4; + border-radius: 7px; +} +#aside-content .card-info #card-info-btn:hover { + background-color: var(--btn-hover-color); +} +#aside-content .card-info #card-info-btn span { + padding-left: 10px; +} +#aside-content .item-headline { + padding-bottom: 6px; + font-size: 1.2em; +} +#aside-content .item-headline span { + margin-left: 6px; +} +@media screen and (min-width: 900px) { + #aside-content .sticky_layout { + position: sticky; + position: -webkit-sticky; + top: 20px; + -webkit-transition: top 0.3s; + -moz-transition: top 0.3s; + -o-transition: top 0.3s; + -ms-transition: top 0.3s; + transition: top 0.3s; + } +} +#aside-content .card-tag-cloud a { + display: inline-block; + padding: 0 4px; + line-height: 1.8; +} +#aside-content .card-tag-cloud a:hover { + color: #49b1f5 !important; +} +#aside-content .aside-list > span { + display: block; + margin-bottom: 10px; + text-align: center; +} +#aside-content .aside-list > .aside-list-item { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + padding: 6px 0; +} +#aside-content .aside-list > .aside-list-item:first-child { + padding-top: 0; +} +#aside-content .aside-list > .aside-list-item:not(:last-child) { + border-bottom: 1px dashed #f5f5f5; +} +#aside-content .aside-list > .aside-list-item:last-child { + padding-bottom: 0; +} +#aside-content .aside-list > .aside-list-item .thumbnail { + overflow: hidden; + width: 4em; + height: 4em; + border-radius: 6px; +} +#aside-content .aside-list > .aside-list-item .content { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + padding-left: 10px; + word-break: break-all; +} +#aside-content .aside-list > .aside-list-item .content > .name { + -webkit-line-clamp: 1; +} +#aside-content .aside-list > .aside-list-item .content > time, +#aside-content .aside-list > .aside-list-item .content > .name { + display: block; + color: var(--card-meta); + font-size: 0.85em; +} +#aside-content .aside-list > .aside-list-item .content > .title, +#aside-content .aside-list > .aside-list-item .content > .comment { + color: var(--font-color); + line-height: 1.5; + -webkit-line-clamp: 2; +} +#aside-content .aside-list > .aside-list-item .content > .title:hover, +#aside-content .aside-list > .aside-list-item .content > .comment:hover { + color: #49b1f5; +} +#aside-content .aside-list > .aside-list-item.no-cover { + min-height: 4.4em; +} +#aside-content .card-archives ul.card-archive-list, +#aside-content .card-categories ul.card-category-list { + margin: 0; + padding: 0; + list-style: none; +} +#aside-content .card-archives ul.card-archive-list > .card-archive-list-item a, +#aside-content .card-categories ul.card-category-list > .card-category-list-item a { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-orient: horizontal; + -moz-box-orient: horizontal; + -o-box-orient: horizontal; + -webkit-flex-direction: row; + -ms-flex-direction: row; + flex-direction: row; + margin: 2px 0; + padding: 2px 8px; + color: var(--font-color); + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; + border-radius: 6px; +} +#aside-content .card-archives ul.card-archive-list > .card-archive-list-item a:hover, +#aside-content .card-categories ul.card-category-list > .card-category-list-item a:hover { + padding: 2px 12px; + background-color: var(--text-bg-hover); + color: var(--white); +} +#aside-content .card-archives ul.card-archive-list > .card-archive-list-item a span:first-child, +#aside-content .card-categories ul.card-category-list > .card-category-list-item a span:first-child { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; +} +#aside-content .card-categories .card-category-list.child { + padding: 0 0 0 16px; +} +#aside-content .card-categories .card-category-list > .parent > a.expand i { + -webkit-transform: rotate(-90deg); + -moz-transform: rotate(-90deg); + -o-transform: rotate(-90deg); + -ms-transform: rotate(-90deg); + transform: rotate(-90deg); +} +#aside-content .card-categories .card-category-list > .parent > a.expand + .child { + display: block; +} +#aside-content .card-categories .card-category-list > .parent > a .card-category-list-name { + width: 70% !important; +} +#aside-content .card-categories .card-category-list > .parent > a .card-category-list-count { + width: calc(100% - 70% - 20px); + text-align: right; +} +#aside-content .card-categories .card-category-list > .parent > a i { + float: right; + margin-right: -0.5em; + padding: 0.5em; + -webkit-transition: -webkit-transform 0.3s; + -moz-transition: -moz-transform 0.3s; + -o-transition: -o-transform 0.3s; + -ms-transition: -ms-transform 0.3s; + transition: transform 0.3s; + -webkit-transform: rotate(0); + -moz-transform: rotate(0); + -o-transform: rotate(0); + -ms-transform: rotate(0); + transform: rotate(0); +} +#aside-content .card-webinfo .webinfo .webinfo-item { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + padding: 2px 10px 0; +} +#aside-content .card-webinfo .webinfo .webinfo-item div:first-child { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + padding-right: 20px; +} +@media screen and (min-width: 901px) { + #aside-content #card-toc { + right: 0 !important; + } +} +@media screen and (max-width: 900px) { + #aside-content #card-toc { + position: fixed; + right: 55px; + bottom: 30px; + z-index: 100; + max-width: 380px; + max-height: calc(100% - 60px); + width: calc(100% - 80px); + -webkit-transition: none; + -moz-transition: none; + -o-transition: none; + -ms-transition: none; + transition: none; + -webkit-transform: scale(0); + -moz-transform: scale(0); + -o-transform: scale(0); + -ms-transform: scale(0); + transform: scale(0); + -webkit-transform-origin: right bottom; + -moz-transform-origin: right bottom; + -o-transform-origin: right bottom; + -ms-transform-origin: right bottom; + transform-origin: right bottom; + } + #aside-content #card-toc.open { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +#aside-content #card-toc .toc-percentage { + float: right; + margin-top: -9px; + color: #a9a9a9; + font-style: italic; + font-size: 140%; +} +#aside-content #card-toc .toc-content { + overflow-y: scroll; + overflow-y: overlay; + margin: 0 -24px; + max-height: calc(100vh - 120px); + width: calc(100% + 48px); +} +@media screen and (max-width: 900px) { + #aside-content #card-toc .toc-content { + max-height: calc(100vh - 140px); + } +} +#aside-content #card-toc .toc-content > * { + margin: 0 20px !important; +} +#aside-content #card-toc .toc-content > * > .toc-item > .toc-child { + margin-left: 10px; + padding-left: 10px; + border-left: 1px solid var(--dark-grey); +} +#aside-content #card-toc .toc-content:not(.is-expand) .toc-child { + display: none; +} +@media screen and (max-width: 900px) { + #aside-content #card-toc .toc-content:not(.is-expand) .toc-child { + display: block !important; + } +} +#aside-content #card-toc .toc-content:not(.is-expand) .toc-item.active .toc-child { + display: block; +} +#aside-content #card-toc .toc-content ol, +#aside-content #card-toc .toc-content li { + list-style: none; +} +#aside-content #card-toc .toc-content > ol { + padding: 0 !important; +} +#aside-content #card-toc .toc-content ol { + margin: 0; + padding-left: 18px; +} +#aside-content #card-toc .toc-content .toc-link { + display: block; + margin: 4px 0; + padding: 1px 8px; + color: var(--toc-link-color); + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; + border-radius: 6px; +} +#aside-content #card-toc .toc-content .toc-link:hover { + color: #49b1f5; +} +#aside-content #card-toc .toc-content .toc-link.active { + background: #00c4b6; + color: #fff; +} +#aside-content .sticky_layout:only-child > :first-child { + margin-top: 0; +} +#aside-content .card-more-btn { + float: right; + color: inherit; +} +#aside-content .card-more-btn:hover { + -webkit-animation: more-btn-move 1s infinite; + -moz-animation: more-btn-move 1s infinite; + -o-animation: more-btn-move 1s infinite; + -ms-animation: more-btn-move 1s infinite; + animation: more-btn-move 1s infinite; +} +#aside-content .card-announcement .item-headline i { + color: #f00; +} +.avatar-img { + overflow: hidden; + margin: 0 auto; + width: 110px; + height: 110px; + border-radius: 70px; +} +.avatar-img img { + width: 100%; + height: 100%; + -webkit-transition: filter 375ms ease-in 0.2s, -webkit-transform 0.3s; + -moz-transition: filter 375ms ease-in 0.2s, -moz-transform 0.3s; + -o-transition: filter 375ms ease-in 0.2s, -o-transform 0.3s; + -ms-transition: filter 375ms ease-in 0.2s, -ms-transform 0.3s; + transition: filter 375ms ease-in 0.2s, transform 0.3s; + object-fit: cover; +} +.avatar-img img:hover { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); +} +.site-data { + display: table; + width: 100%; + table-layout: fixed; +} +.site-data > a { + display: table-cell; +} +.site-data > a div { + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} +.site-data > a:hover div { + color: #49b1f5 !important; +} +.site-data > a .headline { + color: var(--font-color); + font-size: 0.95em; +} +.site-data > a .length-num { + margin-top: -0.45em; + color: var(--text-highlight-color); + font-size: 1.2em; +} +@media screen and (min-width: 900px) { + html.hide-aside .layout { + -webkit-box-pack: center; + -moz-box-pack: center; + -o-box-pack: center; + -ms-flex-pack: center; + -webkit-justify-content: center; + justify-content: center; + } + html.hide-aside .layout > .aside-content { + display: none; + } + html.hide-aside .layout > div:first-child { + width: 80%; + } +} +.page .sticky_layout { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-orient: vertical; + -moz-box-orient: vertical; + -o-box-orient: vertical; + -webkit-flex-direction: column; + -ms-flex-direction: column; + flex-direction: column; +} +@-moz-keyframes more-btn-move { + 0%, 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(3px); + -moz-transform: translateX(3px); + -o-transform: translateX(3px); + -ms-transform: translateX(3px); + transform: translateX(3px); + } +} +@-webkit-keyframes more-btn-move { + 0%, 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(3px); + -moz-transform: translateX(3px); + -o-transform: translateX(3px); + -ms-transform: translateX(3px); + transform: translateX(3px); + } +} +@-o-keyframes more-btn-move { + 0%, 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(3px); + -moz-transform: translateX(3px); + -o-transform: translateX(3px); + -ms-transform: translateX(3px); + transform: translateX(3px); + } +} +@keyframes more-btn-move { + 0%, 100% { + -webkit-transform: translateX(0); + -moz-transform: translateX(0); + -o-transform: translateX(0); + -ms-transform: translateX(0); + transform: translateX(0); + } + 50% { + -webkit-transform: translateX(3px); + -moz-transform: translateX(3px); + -o-transform: translateX(3px); + -ms-transform: translateX(3px); + transform: translateX(3px); + } +} +@-moz-keyframes toc-open { + 0% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@-webkit-keyframes toc-open { + 0% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@-o-keyframes toc-open { + 0% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@keyframes toc-open { + 0% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } + 100% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } +} +@-moz-keyframes toc-close { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@-webkit-keyframes toc-close { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@-o-keyframes toc-close { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +@keyframes toc-close { + 0% { + -webkit-transform: scale(1); + -moz-transform: scale(1); + -o-transform: scale(1); + -ms-transform: scale(1); + transform: scale(1); + } + 100% { + -webkit-transform: scale(0.7); + -moz-transform: scale(0.7); + -o-transform: scale(0.7); + -ms-transform: scale(0.7); + transform: scale(0.7); + } +} +#post-comment .comment-head { + margin-bottom: 20px; +} +#post-comment .comment-head:after { + display: block; + clear: both; + content: ''; +} +#post-comment .comment-head .comment-headline { + display: inline-block; + vertical-align: middle; + font-weight: 700; + font-size: 1.43em; +} +#post-comment .comment-head .comment-switch { + display: inline-block; + float: right; + margin: 2px auto 0; + padding: 4px 16px; + width: max-content; + border-radius: 8px; + background: #f6f8fa; +} +#post-comment .comment-head .comment-switch .first-comment { + color: #49b1f5; +} +#post-comment .comment-head .comment-switch .second-comment { + color: #ff7242; +} +#post-comment .comment-head .comment-switch #switch-btn { + position: relative; + display: inline-block; + margin: -4px 8px 0; + width: 42px; + height: 22px; + border-radius: 34px; + background-color: #49b1f5; + vertical-align: middle; + cursor: pointer; + -webkit-transition: 0.4s; + -moz-transition: 0.4s; + -o-transition: 0.4s; + -ms-transition: 0.4s; + transition: 0.4s; +} +#post-comment .comment-head .comment-switch #switch-btn:before { + position: absolute; + bottom: 4px; + left: 4px; + width: 14px; + height: 14px; + border-radius: 50%; + background-color: #fff; + content: ''; + -webkit-transition: 0.4s; + -moz-transition: 0.4s; + -o-transition: 0.4s; + -ms-transition: 0.4s; + transition: 0.4s; +} +#post-comment .comment-wrap > div { + -webkit-animation: tabshow 0.5s; + -moz-animation: tabshow 0.5s; + -o-animation: tabshow 0.5s; + -ms-animation: tabshow 0.5s; + animation: tabshow 0.5s; +} +#post-comment .comment-wrap > div:nth-child(2) { + display: none; +} +#post-comment.move #switch-btn { + background-color: #ff7242; +} +#post-comment.move #switch-btn:before { + -webkit-transform: translateX(20px); + -moz-transform: translateX(20px); + -o-transform: translateX(20px); + -ms-transform: translateX(20px); + transform: translateX(20px); +} +#post-comment.move .comment-wrap > div:first-child { + display: none; +} +#post-comment.move .comment-wrap > div:last-child { + display: block; +} +#footer { + position: relative; + background-color: #49b1f5; + background-attachment: scroll; + background-position: bottom; + background-size: cover; +} +#footer-wrap { + position: relative; + padding: 40px 20px; + color: var(--light-grey); + text-align: center; +} +#footer-wrap a { + color: var(--light-grey); +} +#footer-wrap a:hover { + text-decoration: underline; +} +#footer-wrap .footer-separator { + margin: 0 4px; +} +#footer-wrap .icp-icon { + padding: 0 4px; + max-height: 1.4em; + width: auto; + vertical-align: text-bottom; +} +#page-header { + position: relative; + width: 100%; + background-color: #49b1f5; + background-position: center center; + background-size: cover; + background-repeat: no-repeat; + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + -ms-transition: all 0.5s; + transition: all 0.5s; +} +#page-header:not(.not-top-img):before { + position: absolute; + width: 100%; + height: 100%; + background-color: var(--mark-bg); + content: ''; +} +#page-header.full_page { + height: 100vh; + background-attachment: fixed; +} +#page-header.full_page #site-info { + position: absolute; + top: 43%; + padding: 0 10px; + width: 100%; +} +#page-header #site-title, +#page-header #site-subtitle, +#page-header #scroll-down .scroll-down-effects { + text-align: center; + text-shadow: 2px 2px 4px rgba(0,0,0,0.15); + line-height: 1.5; +} +#page-header #site-title { + margin: 0; + color: var(--white); + font-size: 1.85em; +} +@media screen and (min-width: 768px) { + #page-header #site-title { + font-size: 2.85em; + } +} +#page-header #site-subtitle { + color: var(--light-grey); + font-size: 1.15em; +} +@media screen and (min-width: 768px) { + #page-header #site-subtitle { + font-size: 1.72em; + } +} +#page-header #site_social_icons { + display: none; + margin: 0 auto; + text-align: center; +} +@media screen and (max-width: 768px) { + #page-header #site_social_icons { + display: block; + } +} +#page-header #site_social_icons .social-icon { + margin: 0 10px; + color: var(--light-grey); + text-shadow: 2px 2px 4px rgba(0,0,0,0.15); + font-size: 1.43em; +} +#page-header #scroll-down { + position: absolute; + bottom: 10px; + width: 100%; + cursor: pointer; +} +#page-header #scroll-down .scroll-down-effects { + position: relative; + width: 100%; + color: var(--light-grey); + font-size: 20px; +} +#page-header.not-home-page { + height: 400px; +} +@media screen and (max-width: 768px) { + #page-header.not-home-page { + height: 280px; + } +} +#page-header #page-site-info { + position: absolute; + top: 200px; + padding: 0 10px; + width: 100%; +} +@media screen and (max-width: 768px) { + #page-header #page-site-info { + top: 140px; + } +} +#page-header.post-bg { + height: 400px; +} +@media screen and (max-width: 768px) { + #page-header.post-bg { + height: 360px; + } +} +#page-header #post-info { + position: absolute; + width: 100%; + bottom: 30px; +} +#page-header #post-info > * { + margin: 0 auto; + padding: 0 15px; + max-width: 1200px; +} +@media screen and (min-width: 768px) and (max-width: 1300px) { + #page-header #post-info > * { + padding: 0 30px; + } +} +@media screen and (min-width: 2000px) { + #page-header #post-info > * { + max-width: 70%; + } +} +#page-header.not-top-img { + margin-bottom: 10px; + height: 60px; + background: 0; +} +#page-header.not-top-img .title-seo { + display: none; +} +#page-header.not-top-img #nav { + background: rgba(255,255,255,0.8); + -webkit-box-shadow: 0 5px 6px -5px rgba(133,133,133,0.6); + box-shadow: 0 5px 6px -5px rgba(133,133,133,0.6); +} +#page-header.not-top-img #nav a, +#page-header.not-top-img #nav span.site-page, +#page-header.not-top-img #nav .site-name { + color: var(--font-color); + text-shadow: none; +} +#page-header.nav-fixed #nav { + position: fixed; + top: -60px; + z-index: 91; + background: rgba(255,255,255,0.7); + -webkit-box-shadow: 0 5px 6px -5px rgba(133,133,133,0.6); + box-shadow: 0 5px 6px -5px rgba(133,133,133,0.6); + -webkit-transition: -webkit-transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + -moz-transition: -moz-transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + -o-transition: -o-transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + -ms-transition: -ms-transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + transition: transform 0.2s ease-in-out, opacity 0.2s ease-in-out; + will-change: transform; + backdrop-filter: blur(7px); +} +#page-header.nav-fixed #nav #blog-info { + color: var(--font-color); +} +#page-header.nav-fixed #nav #blog-info:hover { + color: #49b1f5; +} +#page-header.nav-fixed #nav #blog-info .site-name { + text-shadow: none; +} +#page-header.nav-fixed #nav #blog-info > a:first-child { + display: none; +} +#page-header.nav-fixed #nav #blog-info > a:last-child { + display: inline; +} +#page-header.nav-fixed #nav a, +#page-header.nav-fixed #nav span.site-page, +#page-header.nav-fixed #nav #toggle-menu { + color: var(--font-color); + text-shadow: none; +} +#page-header.nav-fixed #nav a:hover, +#page-header.nav-fixed #nav span.site-page:hover, +#page-header.nav-fixed #nav #toggle-menu:hover { + color: #49b1f5; +} +#page-header.nav-fixed.fixed #nav { + top: 0; + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + -ms-transition: all 0.5s; + transition: all 0.5s; +} +#page-header.nav-visible:not(.fixed) #nav { + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + -ms-transition: all 0.5s; + transition: all 0.5s; + -webkit-transform: translate3d(0, 100%, 0); + -moz-transform: translate3d(0, 100%, 0); + -o-transform: translate3d(0, 100%, 0); + -ms-transform: translate3d(0, 100%, 0); + transform: translate3d(0, 100%, 0); +} +#page-header.nav-visible:not(.fixed) + .layout > .aside-content > .sticky_layout { + top: 70px; + -webkit-transition: top 0.5s; + -moz-transition: top 0.5s; + -o-transition: top 0.5s; + -ms-transition: top 0.5s; + transition: top 0.5s; +} +#page-header.fixed #nav { + position: fixed; +} +#page-header.fixed + .layout > .aside-content > .sticky_layout { + top: 70px; + -webkit-transition: top 0.5s; + -moz-transition: top 0.5s; + -o-transition: top 0.5s; + -ms-transition: top 0.5s; + transition: top 0.5s; +} +#page-header.fixed + .layout #card-toc .toc-content { + max-height: calc(100vh - 170px); +} +#page .page-title { + margin: 0 0 10px; + font-weight: bold; + font-size: 2em; +} +#post > #post-info { + margin-bottom: 30px; +} +#post > #post-info .post-title { + padding-bottom: 4px; + border-bottom: 1px solid var(--light-grey); + color: var(--text-highlight-color); +} +#post > #post-info .post-title .post-edit-link { + float: right; +} +#post > #post-info #post-meta, +#post > #post-info #post-meta a { + color: #78818a; +} +#post-info .post-title { + margin-bottom: 8px; + color: var(--white); + font-weight: normal; + font-size: 2.5em; + line-height: 1.5; + -webkit-line-clamp: 3; +} +@media screen and (max-width: 768px) { + #post-info .post-title { + font-size: 2.1em; + } +} +#post-info .post-title .post-edit-link { + padding-left: 10px; +} +#post-info #post-meta { + color: var(--light-grey); + font-size: 95%; +} +@media screen and (min-width: 768px) { + #post-info #post-meta > .meta-secondline > span:first-child { + display: none; + } +} +@media screen and (max-width: 768px) { + #post-info #post-meta { + font-size: 90%; + } + #post-info #post-meta > .meta-firstline, + #post-info #post-meta > .meta-secondline { + display: inline; + } +} +#post-info #post-meta .post-meta-separator { + margin: 0 5px; +} +#post-info #post-meta .post-meta-icon { + margin-right: 4px; +} +#post-info #post-meta .post-meta-label { + margin-right: 4px; +} +#post-info #post-meta a { + color: var(--light-grey); + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + -ms-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} +#post-info #post-meta a:hover { + color: #49b1f5; + text-decoration: underline; +} +#nav { + position: absolute; + top: 0; + z-index: 90; + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-align: center; + -moz-box-align: center; + -o-box-align: center; + -ms-flex-align: center; + -webkit-align-items: center; + align-items: center; + padding: 0 36px; + width: 100%; + height: 60px; + font-size: 1.3em; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + -ms-transition: all 0.5s; + transition: all 0.5s; +} +@media screen and (max-width: 768px) { + #nav { + padding: 0 16px; + } +} +#nav.show { + opacity: 1; + -ms-filter: none; + filter: none; +} +#nav #blog-info { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + box-flex: 1; + -webkit-flex: 1; + -ms-flex: 1; + flex: 1; + color: var(--light-grey); +} +#nav #blog-info .site-icon { + margin-right: 6px; + height: 36px; + vertical-align: middle; +} +#nav #blog-info .nav-page-title { + display: none; +} +#nav #toggle-menu { + display: none; + padding: 2px 0 0 6px; + vertical-align: top; +} +#nav #toggle-menu:hover { + color: var(--white); +} +#nav a, +#nav span.site-page { + color: var(--light-grey); +} +#nav a:hover, +#nav span.site-page:hover { + color: var(--white); +} +#nav .site-name { + text-shadow: 2px 2px 4px rgba(0,0,0,0.15); + font-weight: bold; +} +#nav .menus_items { + display: inline; +} +#nav .menus_items .menus_item { + position: relative; + display: inline-block; + padding: 0 0 0 14px; +} +#nav .menus_items .menus_item:hover .menus_item_child { + display: block; +} +#nav .menus_items .menus_item:hover > span > i:last-child { + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -o-transform: rotate(180deg); + -ms-transform: rotate(180deg); + transform: rotate(180deg); +} +#nav .menus_items .menus_item > span > i:last-child { + padding: 4px; + -webkit-transition: -webkit-transform 0.3s; + -moz-transition: -moz-transform 0.3s; + -o-transition: -o-transform 0.3s; + -ms-transition: -ms-transform 0.3s; + transition: transform 0.3s; +} +#nav .menus_items .menus_item .menus_item_child { + position: absolute; + right: 0; + display: none; + margin-top: 8px; + padding: 0; + width: max-content; + background-color: var(--sidebar-bg); + -webkit-box-shadow: 0 5px 20px -4px rgba(0,0,0,0.5); + box-shadow: 0 5px 20px -4px rgba(0,0,0,0.5); + -webkit-animation: sub_menus 0.3s 0.1s ease both; + -moz-animation: sub_menus 0.3s 0.1s ease both; + -o-animation: sub_menus 0.3s 0.1s ease both; + -ms-animation: sub_menus 0.3s 0.1s ease both; + animation: sub_menus 0.3s 0.1s ease both; + border-radius: 5px; +} +#nav .menus_items .menus_item .menus_item_child:before { + position: absolute; + top: -8px; + left: 0; + width: 100%; + height: 20px; + content: ''; +} +#nav .menus_items .menus_item .menus_item_child li { + list-style: none; +} +#nav .menus_items .menus_item .menus_item_child li:hover { + background: var(--text-bg-hover); +} +#nav .menus_items .menus_item .menus_item_child li:first-child { + border-top-left-radius: 5px; + border-top-right-radius: 5px; +} +#nav .menus_items .menus_item .menus_item_child li:last-child { + border-bottom-right-radius: 5px; + border-bottom-left-radius: 5px; +} +#nav .menus_items .menus_item .menus_item_child li a { + display: inline-block; + padding: 8px 16px; + width: 100%; + color: var(--font-color) !important; + text-shadow: none !important; +} +#nav.hide-menu #toggle-menu { + display: inline-block !important; +} +#nav.hide-menu #toggle-menu .site-page { + font-size: inherit; +} +#nav.hide-menu .menus_items { + display: none; +} +#nav.hide-menu #search-button span:not(.site-page) { + display: none; +} +#nav #search-button { + display: inline; + padding: 0 0 0 14px; +} +#nav .site-page { + position: relative; + padding-bottom: 6px; + text-shadow: 1px 1px 2px rgba(0,0,0,0.3); + font-size: 0.78em; + cursor: pointer; +} +#nav .site-page:not(.child):after { + position: absolute; + bottom: 0; + left: 0; + z-index: -1; + width: 0; + height: 3px; + background-color: #80c8f8; + content: ''; + -webkit-transition: all 0.3s ease-in-out; + -moz-transition: all 0.3s ease-in-out; + -o-transition: all 0.3s ease-in-out; + -ms-transition: all 0.3s ease-in-out; + transition: all 0.3s ease-in-out; + border-radius: 6px; +} +#nav .site-page:not(.child):hover:after { + width: 100%; +} +#pagination .pagination { + margin-top: 20px; + text-align: center; +} +#pagination .page-number.current { + background: #00c4b6; + color: var(--white); +} +#pagination .full-width { + width: 100% !important; +} +#pagination .pagination-related { + width: 50%; + height: 150px; +} +@media screen and (max-width: 768px) { + #pagination .pagination-related { + width: 100%; + } +} +#pagination .pagination-related .info-1 .info-item-2 { + -webkit-line-clamp: 1; +} +#pagination .pagination-related .info-2 .info-item-1 { + -webkit-line-clamp: 2; +} +#pagination.pagination-post { + overflow: hidden; + margin-top: 40px; + width: 100%; + border-radius: 6px; +} +.layout .pagination > * { + display: inline-block; + margin: 0 6px; + width: 2.5em; + height: 2.5em; + line-height: 2.5em; +} +.layout .pagination > *:not(.space):hover { + background: var(--btn-hover-color); + color: var(--btn-color); +} +#archive .pagination { + margin-top: 30px; +} +#archive .pagination > *:not(.space) { + -webkit-box-shadow: none; + box-shadow: none; +} +.pagination-related { + position: relative; + display: inline-block; + overflow: hidden; + background: #000; + vertical-align: bottom; +} +.pagination-related.next-post .info { + text-align: right; +} +.pagination-related .info .info-1, +.pagination-related .info .info-2 { + padding: 20px 40px; + color: var(--white); + -webkit-transition: -webkit-transform 0.3s, opacity 0.3s; + -moz-transition: -moz-transform 0.3s, opacity 0.3s; + -o-transition: -o-transform 0.3s, opacity 0.3s; + -ms-transition: -ms-transform 0.3s, opacity 0.3s; + transition: transform 0.3s, opacity 0.3s; +} +.pagination-related .info .info-1 .info-item-1 { + color: var(--light-grey); + text-transform: uppercase; + font-size: 90%; +} +.pagination-related .info .info-1 .info-item-2 { + color: var(--white); + font-weight: 500; +} +.pagination-related .info .info-2 { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} +.pagination-related:not(.no-desc):hover .info-1 { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transform: translate(0, -100%); + -moz-transform: translate(0, -100%); + -o-transform: translate(0, -100%); + -ms-transform: translate(0, -100%); + transform: translate(0, -100%); +} +.pagination-related:not(.no-desc):hover .info-2 { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate(0, -50%); + -moz-transform: translate(0, -50%); + -o-transform: translate(0, -50%); + -ms-transform: translate(0, -50%); + transform: translate(0, -50%); +} +.container { + word-wrap: break-word; + overflow-wrap: break-word; +} +.container a { + color: #49b1f5; +} +.container a:hover { + text-decoration: underline; +} +.container img { + display: block; + margin: 0 auto 20px; + max-width: 100%; + -webkit-transition: filter 375ms ease-in 0.2s; + -moz-transition: filter 375ms ease-in 0.2s; + -o-transition: filter 375ms ease-in 0.2s; + -ms-transition: filter 375ms ease-in 0.2s; + transition: filter 375ms ease-in 0.2s; + border-radius: 6px; +} +.container p { + margin: 0 0 16px; +} +.container iframe { + margin: 0 0 20px; +} +.container kbd { + margin: 0 3px; + padding: 3px 5px; + border: 1px solid #b4b4b4; + background-color: #f8f8f8; + -webkit-box-shadow: 0 1px 3px rgba(0,0,0,0.25), 0 2px 1px 0 rgba(255,255,255,0.6) inset; + box-shadow: 0 1px 3px rgba(0,0,0,0.25), 0 2px 1px 0 rgba(255,255,255,0.6) inset; + color: #34495e; + white-space: nowrap; + font-weight: 600; + font-size: 0.9em; + font-family: Monaco, 'Ubuntu Mono', monospace; + line-height: 1em; + border-radius: 3px; +} +.container ol ol, +.container ul ol, +.container ol ul, +.container ul ul { + padding-left: 20px; +} +.container ol li, +.container ul li { + margin: 4px 0; +} +.container ol p, +.container ul p { + margin: 0 0 8px; +} +.container > :last-child { + margin-bottom: 0 !important; +} +.container hr { + margin: 20px 0; +} +#post .tag_share:after { + display: block; + clear: both; + content: ''; +} +#post .tag_share .post-meta__tag-list { + display: inline-block; +} +#post .tag_share .post-meta__tags { + display: inline-block; + margin: 8px 8px 8px 0; + padding: 0 12px; + width: fit-content; + border: 1px solid #49b1f5; + border-radius: 12px; + color: #49b1f5; + font-size: 0.85em; + -webkit-transition: all 0.2s ease-in-out; + -moz-transition: all 0.2s ease-in-out; + -o-transition: all 0.2s ease-in-out; + -ms-transition: all 0.2s ease-in-out; + transition: all 0.2s ease-in-out; +} +#post .tag_share .post-meta__tags:hover { + background: #49b1f5; + color: var(--white); +} +#post .tag_share .post-share { + display: inline-block; + float: right; + margin: 8px 0 0; + width: fit-content; +} +#post .tag_share .post-share .social-share { + font-size: 0.85em; +} +#post .tag_share .post-share .social-share .social-share-icon { + margin: 0 4px; + width: 1.85em; + height: 1.85em; + font-size: 1.2em; + line-height: 1.85em; +} +#post .post-copyright { + position: relative; + margin: 40px 0 10px; + padding: 10px 16px; + border: 1px solid var(--light-grey); + -webkit-transition: box-shadow 0.3s ease-in-out; + -moz-transition: box-shadow 0.3s ease-in-out; + -o-transition: box-shadow 0.3s ease-in-out; + -ms-transition: box-shadow 0.3s ease-in-out; + transition: box-shadow 0.3s ease-in-out; + border-radius: 6px; +} +#post .post-copyright:before { + position: absolute; + top: 2px; + right: 12px; + color: #49b1f5; + content: '\f1f9'; + font-size: 1.3em; +} +#post .post-copyright:hover { + -webkit-box-shadow: 0 0 8px 0 rgba(232,237,250,0.6), 0 2px 4px 0 rgba(232,237,250,0.5); + box-shadow: 0 0 8px 0 rgba(232,237,250,0.6), 0 2px 4px 0 rgba(232,237,250,0.5); +} +#post .post-copyright .post-copyright-meta { + color: #49b1f5; + font-weight: bold; +} +#post .post-copyright .post-copyright-meta i { + margin-right: 3px; +} +#post .post-copyright .post-copyright-info { + padding-left: 6px; +} +#post .post-copyright .post-copyright-info a { + text-decoration: underline; + word-break: break-word; +} +#post .post-copyright .post-copyright-info a:hover { + text-decoration: none; +} +#post #post-outdate-notice { + position: relative; + margin: 0 0 20px; + padding: 0.5em 1.2em; + background-color: #ffe6e6; + color: #f66; + border-radius: 3px; + padding: 0.5em 1em 0.5em 2.6em; + border-left: 5px solid #ff8080; +} +#post #post-outdate-notice .num { + padding: 0 4px; +} +#post #post-outdate-notice:before { + position: absolute; + top: 50%; + left: 0.9em; + color: #ff8080; + content: '\f071'; + -webkit-transform: translateY(-50%); + -moz-transform: translateY(-50%); + -o-transform: translateY(-50%); + -ms-transform: translateY(-50%); + transform: translateY(-50%); +} +#post .ads-wrap { + margin: 40px 0; +} +.relatedPosts { + margin-top: 40px; +} +.relatedPosts > .headline { + margin-bottom: 5px; + font-weight: 700; + font-size: 1.43em; +} +.relatedPosts > .relatedPosts-list > a { + margin: 3px; + width: calc(33.333% - 6px); + height: 200px; + border-radius: 6px; +} +@media screen and (max-width: 768px) { + .relatedPosts > .relatedPosts-list > a { + margin: 2px; + width: calc(50% - 4px); + height: 150px; + } +} +@media screen and (max-width: 600px) { + .relatedPosts > .relatedPosts-list > a { + width: calc(100% - 4px); + } +} +.relatedPosts > .relatedPosts-list .info .info-1 .info-item-2 { + -webkit-line-clamp: 2; +} +.relatedPosts > .relatedPosts-list .info .info-2 .info-item-1 { + -webkit-line-clamp: 3; +} +.post-reward { + position: relative; + margin-top: 80px; + width: 100%; + text-align: center; + pointer-events: none; +} +.post-reward > * { + pointer-events: auto; +} +.post-reward .reward-button { + display: inline-block; + padding: 4px 24px; + background: var(--btn-bg); + color: var(--btn-color); + cursor: pointer; + border-radius: 6px; +} +.post-reward .reward-button i { + margin-right: 5px; +} +.post-reward:hover .reward-button { + background: var(--btn-hover-color); +} +.post-reward:hover > .reward-main { + display: block; +} +.post-reward .reward-main { + position: absolute; + bottom: 40px; + left: 0; + z-index: 100; + display: none; + padding: 0 0 15px; + width: 100%; + border-radius: 6px; +} +.post-reward .reward-main .reward-all { + display: inline-block; + margin: 0; + padding: 20px 10px; + background: var(--reward-pop); +} +.post-reward .reward-main .reward-all:before { + position: absolute; + bottom: -10px; + left: 0; + width: 100%; + height: 20px; + content: ''; +} +.post-reward .reward-main .reward-all:after { + position: absolute; + right: 0; + bottom: 2px; + left: 0; + margin: 0 auto; + width: 0; + height: 0; + border-top: 13px solid var(--reward-pop); + border-right: 13px solid transparent; + border-left: 13px solid transparent; + content: ''; +} +.post-reward .reward-main .reward-all .reward-item { + display: inline-block; + padding: 0 8px; + list-style-type: none; + vertical-align: top; +} +.post-reward .reward-main .reward-all .reward-item img { + width: 130px; + height: 130px; +} +.post-reward .reward-main .reward-all .reward-item .post-qr-code-desc { + width: 130px; + color: #858585; +} +#rightside { + position: fixed; + right: -48px; + bottom: 40px; + z-index: 100; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + -ms-transition: all 0.5s; + transition: all 0.5s; +} +#rightside.rightside-show { + opacity: 0.8; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; + filter: alpha(opacity=80); + -webkit-transform: translate(-58px, 0); + -moz-transform: translate(-58px, 0); + -o-transform: translate(-58px, 0); + -ms-transform: translate(-58px, 0); + transform: translate(-58px, 0); +} +#rightside #rightside-config-hide { + height: 0; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: -webkit-transform 0.4s; + -moz-transition: -moz-transform 0.4s; + -o-transition: -o-transform 0.4s; + -ms-transition: -ms-transform 0.4s; + transition: transform 0.4s; + -webkit-transform: translate(45px, 0); + -moz-transform: translate(45px, 0); + -o-transform: translate(45px, 0); + -ms-transform: translate(45px, 0); + transform: translate(45px, 0); +} +#rightside #rightside-config-hide.show { + height: auto; + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate(0, 0); + -moz-transform: translate(0, 0); + -o-transform: translate(0, 0); + -ms-transform: translate(0, 0); + transform: translate(0, 0); +} +#rightside #rightside-config-hide.status { + height: auto; + opacity: 1; + -ms-filter: none; + filter: none; +} +#rightside > div > button, +#rightside > div > a { + display: block; + margin-bottom: 5px; + width: 35px; + height: 35px; + background-color: var(--btn-bg); + color: var(--btn-color); + text-align: center; + font-size: 16px; + line-height: 35px; + border-radius: 5px; +} +#rightside > div > button:hover, +#rightside > div > a:hover { + background-color: var(--btn-hover-color); +} +#rightside #mobile-toc-button { + display: none; +} +@media screen and (max-width: 900px) { + #rightside #mobile-toc-button { + display: block; + } +} +@media screen and (max-width: 900px) { + #rightside #hide-aside-btn { + display: none; + } +} +#rightside #go-up .scroll-percent { + display: none; +} +#rightside #go-up.show-percent .scroll-percent { + display: block; +} +#rightside #go-up.show-percent .scroll-percent + i { + display: none; +} +#rightside #go-up:hover .scroll-percent { + display: none; +} +#rightside #go-up:hover .scroll-percent + i { + display: block; +} +#sidebar #menu-mask { + position: fixed; + z-index: 102; + display: none; + width: 100%; + height: 100%; + background: rgba(0,0,0,0.8); +} +#sidebar #sidebar-menus { + position: fixed; + top: 0; + right: -330px; + z-index: 103; + overflow-x: hidden; + overflow-y: scroll; + padding-left: 5px; + width: 330px; + height: 100%; + background: var(--sidebar-bg); + -webkit-transition: all 0.5s; + -moz-transition: all 0.5s; + -o-transition: all 0.5s; + -ms-transition: all 0.5s; + transition: all 0.5s; +} +#sidebar #sidebar-menus.open { + -webkit-transform: translate3d(-100%, 0, 0); + -moz-transform: translate3d(-100%, 0, 0); + -o-transform: translate3d(-100%, 0, 0); + -ms-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); +} +#sidebar #sidebar-menus > .avatar-img { + margin: 20px auto; +} +#sidebar #sidebar-menus .site-data { + padding: 0 10px; +} +#sidebar #sidebar-menus hr { + margin: 20px auto; +} +#sidebar #sidebar-menus .menus_items { + margin: 20px; + padding: 15px; + background: var(--sidebar-menu-bg); + -webkit-box-shadow: 0 0 1px 1px rgba(7,17,27,0.05); + box-shadow: 0 0 1px 1px rgba(7,17,27,0.05); + border-radius: 10px; +} +#sidebar #sidebar-menus .menus_items .site-page { + position: relative; + display: block; + margin: 4px 0; + padding: 2px 23px 2px 15px; + color: var(--font-color); + font-size: 1.15em; + cursor: pointer; + border-radius: 6px; +} +#sidebar #sidebar-menus .menus_items .site-page:hover { + background: var(--text-bg-hover); + color: var(--white); +} +#sidebar #sidebar-menus .menus_items .site-page i:first-child { + width: 15%; + text-align: left; +} +#sidebar #sidebar-menus .menus_items .site-page.group > i:last-child { + position: absolute; + top: 0.6em; + right: 10px; + -webkit-transition: -webkit-transform 0.3s; + -moz-transition: -moz-transform 0.3s; + -o-transition: -o-transform 0.3s; + -ms-transition: -ms-transform 0.3s; + transition: transform 0.3s; +} +#sidebar #sidebar-menus .menus_items .site-page.group.hide > i:last-child { + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -o-transform: rotate(90deg); + -ms-transform: rotate(90deg); + transform: rotate(90deg); +} +#sidebar #sidebar-menus .menus_items .site-page.group.hide + .menus_item_child { + display: none; +} +#sidebar #sidebar-menus .menus_items .menus_item_child { + margin: 0; + padding-left: 25px; + list-style: none; +} +#vcomment { + font-size: 1.1em; +} +#vcomment .vbtn { + border: none; + background: var(--btn-bg); + color: var(--btn-color); +} +#vcomment .vbtn:hover { + background: var(--btn-hover-color); +} +#vcomment .vimg { + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} +#vcomment .vimg:hover { + -webkit-transform: rotate(360deg); + -moz-transform: rotate(360deg); + -o-transform: rotate(360deg); + -ms-transform: rotate(360deg); + transform: rotate(360deg); +} +#vcomment .vcards .vcard .vcontent.expand:before, +#vcomment .vcards .vcard .vcontent.expand:after { + z-index: 22; +} +#waline-wrap { + --waline-font-size: 1.1em; + --waline-theme-color: #49b1f5; + --waline-active-color: #ff7242; +} +#waline-wrap .wl-comment-actions > button:not(last-child) { + padding-right: 4px; +} +.twikoo .tk-content p { + margin: 3px 0; +} +.fireworks { + position: fixed; + top: 0; + left: 0; + z-index: 9999; + pointer-events: none; +} +.medium-zoom-image--opened { + z-index: 99999 !important; + margin: 0 !important; +} +.medium-zoom-overlay { + z-index: 99999 !important; +} +.utterances, +.fb-comments iframe { + width: 100% !important; +} +#gitalk-container .gt-meta { + margin: 0 0 0.8em; + padding: 6px 0 16px; +} +.aplayer { + color: #4c4948; +} +.container .aplayer { + margin: 0 0 20px; +} +.snackbar-container.snackbar-css { + border-radius: 5px; + opacity: 0.85 !important; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=85)" !important; + filter: alpha(opacity=85) !important; +} +.abc-music-sheet { + margin: 0 0 20px; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: opacity 0.3s; + -moz-transition: opacity 0.3s; + -o-transition: opacity 0.3s; + -ms-transition: opacity 0.3s; + transition: opacity 0.3s; +} +.abc-music-sheet.abcjs-container { + opacity: 1; + -ms-filter: none; + filter: none; +} +@media screen and (max-width: 768px) { + .fancybox__toolbar__column.is-middle { + display: none; + } +} +.container .btn-center { + margin: 0 0 20px; + text-align: center; +} +.container .btn-beautify { + display: inline-block; + margin: 0 4px 6px; + padding: 0 15px; + background-color: var(--btn-beautify-color, #777); + color: #fff; + line-height: 2; + border-radius: 6px; +} +.container .btn-beautify.blue { + --btn-beautify-color: #428bca; +} +.container .btn-beautify.pink { + --btn-beautify-color: #ff69b4; +} +.container .btn-beautify.red { + --btn-beautify-color: #f00; +} +.container .btn-beautify.purple { + --btn-beautify-color: #6f42c1; +} +.container .btn-beautify.orange { + --btn-beautify-color: #ff8c00; +} +.container .btn-beautify.green { + --btn-beautify-color: #5cb85c; +} +.container .btn-beautify:hover { + background-color: var(--btn-hover-color); +} +.container .btn-beautify i + span { + margin-left: 6px; +} +.container .btn-beautify:not(.block) + .btn-beautify:not(.block) { + margin: 0 4px 20px; +} +.container .btn-beautify.block { + display: block; + margin: 0 0 20px; + width: fit-content; + width: -moz-fit-content; +} +.container .btn-beautify.block.center { + margin: 0 auto 20px; +} +.container .btn-beautify.block.right { + margin: 0 0 20px auto; +} +.container .btn-beautify.larger { + padding: 6px 15px; +} +.container .btn-beautify:hover { + text-decoration: none; +} +.container .btn-beautify.outline { + border: 1px solid transparent; + border-color: var(--btn-beautify-color, #777); + background-color: transparent; + color: var(--btn-beautify-color, #777); +} +.container .btn-beautify.outline:hover { + background-color: var(--btn-beautify-color, #777); +} +.container .btn-beautify.outline:hover { + color: #fff !important; +} +.container figure.gallery-group { + position: relative; + float: left; + overflow: hidden; + margin: 6px 4px; + width: calc(50% - 8px); + height: 250px; + border-radius: 10px; + background: #000; + -webkit-transform: translate3d(0, 0, 0); +} +@media screen and (max-width: 600px) { + .container figure.gallery-group { + width: calc(100% - 8px); + } +} +@media screen and (min-width: 1024px) { + .container figure.gallery-group { + width: calc(100% / 3 - 8px); + } +} +.container figure.gallery-group:hover img { + opacity: 0.4; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=40)"; + filter: alpha(opacity=40); + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +.container figure.gallery-group:hover .gallery-group-name::after { + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +.container figure.gallery-group:hover p { + opacity: 1; + -ms-filter: none; + filter: none; + -webkit-transform: translate3d(0, 0, 0); + -moz-transform: translate3d(0, 0, 0); + -o-transform: translate3d(0, 0, 0); + -ms-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); +} +.container figure.gallery-group img { + position: relative; + margin: 0; + max-width: none; + width: calc(100% + 20px); + height: 250px; + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -ms-backface-visibility: hidden; + backface-visibility: hidden; + opacity: 0.8; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; + filter: alpha(opacity=80); + -webkit-transition: all 0.3s, filter 375ms ease-in 0.2s; + -moz-transition: all 0.3s, filter 375ms ease-in 0.2s; + -o-transition: all 0.3s, filter 375ms ease-in 0.2s; + -ms-transition: all 0.3s, filter 375ms ease-in 0.2s; + transition: all 0.3s, filter 375ms ease-in 0.2s; + -webkit-transform: translate3d(-10px, 0, 0); + -moz-transform: translate3d(-10px, 0, 0); + -o-transform: translate3d(-10px, 0, 0); + -ms-transform: translate3d(-10px, 0, 0); + transform: translate3d(-10px, 0, 0); + object-fit: cover; +} +.container figure.gallery-group figcaption { + position: absolute; + top: 0; + left: 0; + padding: 30px; + width: 100%; + height: 100%; + color: #fff; + text-transform: uppercase; + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -ms-backface-visibility: hidden; + backface-visibility: hidden; +} +.container figure.gallery-group figcaption > a { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1000; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); +} +.container figure.gallery-group p { + margin: 0; + padding: 8px 0 0; + letter-spacing: 1px; + font-size: 1.1em; + line-height: 1.5; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); + -webkit-transition: opacity 0.35s, -webkit-transform 0.35s; + -moz-transition: opacity 0.35s, -moz-transform 0.35s; + -o-transition: opacity 0.35s, -o-transform 0.35s; + -ms-transition: opacity 0.35s, -ms-transform 0.35s; + transition: opacity 0.35s, transform 0.35s; + -webkit-transform: translate3d(100%, 0, 0); + -moz-transform: translate3d(100%, 0, 0); + -o-transform: translate3d(100%, 0, 0); + -ms-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + -webkit-line-clamp: 4; +} +.container figure.gallery-group .gallery-group-name { + position: relative; + margin: 0; + padding: 8px 0; + font-weight: bold; + font-size: 1.65em; + line-height: 1.5; + -webkit-line-clamp: 2; +} +.container figure.gallery-group .gallery-group-name:after { + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 2px; + background: #fff; + content: ''; + -webkit-transition: -webkit-transform 0.35s; + -moz-transition: -moz-transform 0.35s; + -o-transition: -o-transform 0.35s; + -ms-transition: -ms-transform 0.35s; + transition: transform 0.35s; + -webkit-transform: translate3d(-100%, 0, 0); + -moz-transform: translate3d(-100%, 0, 0); + -o-transform: translate3d(-100%, 0, 0); + -ms-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); +} +.container .gallery-group-main { + overflow: auto; + padding: 0 0 16px; +} +.container .gallery-container { + margin: 0 0 20px; + text-align: center; + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); +} +.container .gallery-container.loaded { + opacity: 1; + -ms-filter: none; + filter: none; +} +.container .gallery-container img { + display: initial; + margin: 0; + width: 100%; + height: 100%; +} +.container .gallery-container .gallery-data { + display: none; +} +.container .gallery-container button { + margin-top: 25px; + padding: 8px 14px; + background: var(--btn-bg); + color: var(--btn-color); + font-weight: bold; + font-size: 1.1em; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; + border-radius: 5px; +} +.container .gallery-container button:hover { + background: var(--btn-hover-color); +} +.container .gallery-container button:hover i { + margin-left: 8px; +} +.container .gallery-container button i { + margin-left: 4px; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; +} +.container .loading-container { + display: inline-block; + overflow: hidden; + width: 154px; + height: 154px; +} +.container .loading-container .loading-item { + position: relative; + width: 100%; + height: 100%; + -webkit-backface-visibility: hidden; + -moz-backface-visibility: hidden; + -ms-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-transform: translateZ(0) scale(1); + -moz-transform: translateZ(0) scale(1); + -o-transform: translateZ(0) scale(1); + -ms-transform: translateZ(0) scale(1); + transform: translateZ(0) scale(1); + -webkit-transform-origin: 0 0; + -moz-transform-origin: 0 0; + -o-transform-origin: 0 0; + -ms-transform-origin: 0 0; + transform-origin: 0 0; +} +.container .loading-container .loading-item div { + position: absolute; + width: 30.8px; + height: 30.8px; + border-radius: 50%; + background: #e15b64; + -webkit-transform: translate(61.6px, 61.6px) scale(1); + -moz-transform: translate(61.6px, 61.6px) scale(1); + -o-transform: translate(61.6px, 61.6px) scale(1); + -ms-transform: translate(61.6px, 61.6px) scale(1); + transform: translate(61.6px, 61.6px) scale(1); + -webkit-animation: loading-ball 1.92s infinite cubic-bezier(0, 0.5, 0.5, 1); + -moz-animation: loading-ball 1.92s infinite cubic-bezier(0, 0.5, 0.5, 1); + -o-animation: loading-ball 1.92s infinite cubic-bezier(0, 0.5, 0.5, 1); + -ms-animation: loading-ball 1.92s infinite cubic-bezier(0, 0.5, 0.5, 1); + animation: loading-ball 1.92s infinite cubic-bezier(0, 0.5, 0.5, 1); +} +.container .loading-container .loading-item div:nth-child(1) { + background: #f47e60; + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + -webkit-animation: loading-ball-r 0.48s infinite cubic-bezier(0, 0.5, 0.5, 1), loading-ball-c 1.92s infinite step-start; + -moz-animation: loading-ball-r 0.48s infinite cubic-bezier(0, 0.5, 0.5, 1), loading-ball-c 1.92s infinite step-start; + -o-animation: loading-ball-r 0.48s infinite cubic-bezier(0, 0.5, 0.5, 1), loading-ball-c 1.92s infinite step-start; + -ms-animation: loading-ball-r 0.48s infinite cubic-bezier(0, 0.5, 0.5, 1), loading-ball-c 1.92s infinite step-start; + animation: loading-ball-r 0.48s infinite cubic-bezier(0, 0.5, 0.5, 1), loading-ball-c 1.92s infinite step-start; +} +.container .loading-container .loading-item div:nth-child(2) { + background: #e15b64; + -webkit-animation-delay: -0.48s; + -moz-animation-delay: -0.48s; + -o-animation-delay: -0.48s; + -ms-animation-delay: -0.48s; + animation-delay: -0.48s; +} +.container .loading-container .loading-item div:nth-child(3) { + background: #f47e60; + -webkit-animation-delay: -0.96s; + -moz-animation-delay: -0.96s; + -o-animation-delay: -0.96s; + -ms-animation-delay: -0.96s; + animation-delay: -0.96s; +} +.container .loading-container .loading-item div:nth-child(4) { + background: #f8b26a; + -webkit-animation-delay: -1.44s; + -moz-animation-delay: -1.44s; + -o-animation-delay: -1.44s; + -ms-animation-delay: -1.44s; + animation-delay: -1.44s; +} +.container .loading-container .loading-item div:nth-child(5) { + background: #abbd81; + -webkit-animation-delay: -1.92s; + -moz-animation-delay: -1.92s; + -o-animation-delay: -1.92s; + -ms-animation-delay: -1.92s; + animation-delay: -1.92s; +} +@-moz-keyframes loading-ball { + 0% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 25% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 50% { + -webkit-transform: translate(9.24px, 61.6px) scale(1); + -moz-transform: translate(9.24px, 61.6px) scale(1); + -o-transform: translate(9.24px, 61.6px) scale(1); + -ms-transform: translate(9.24px, 61.6px) scale(1); + transform: translate(9.24px, 61.6px) scale(1); + } + 75% { + -webkit-transform: translate(61.6px, 61.6px) scale(1); + -moz-transform: translate(61.6px, 61.6px) scale(1); + -o-transform: translate(61.6px, 61.6px) scale(1); + -ms-transform: translate(61.6px, 61.6px) scale(1); + transform: translate(61.6px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } +} +@-webkit-keyframes loading-ball { + 0% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 25% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 50% { + -webkit-transform: translate(9.24px, 61.6px) scale(1); + -moz-transform: translate(9.24px, 61.6px) scale(1); + -o-transform: translate(9.24px, 61.6px) scale(1); + -ms-transform: translate(9.24px, 61.6px) scale(1); + transform: translate(9.24px, 61.6px) scale(1); + } + 75% { + -webkit-transform: translate(61.6px, 61.6px) scale(1); + -moz-transform: translate(61.6px, 61.6px) scale(1); + -o-transform: translate(61.6px, 61.6px) scale(1); + -ms-transform: translate(61.6px, 61.6px) scale(1); + transform: translate(61.6px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } +} +@-o-keyframes loading-ball { + 0% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 25% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 50% { + -webkit-transform: translate(9.24px, 61.6px) scale(1); + -moz-transform: translate(9.24px, 61.6px) scale(1); + -o-transform: translate(9.24px, 61.6px) scale(1); + -ms-transform: translate(9.24px, 61.6px) scale(1); + transform: translate(9.24px, 61.6px) scale(1); + } + 75% { + -webkit-transform: translate(61.6px, 61.6px) scale(1); + -moz-transform: translate(61.6px, 61.6px) scale(1); + -o-transform: translate(61.6px, 61.6px) scale(1); + -ms-transform: translate(61.6px, 61.6px) scale(1); + transform: translate(61.6px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } +} +@keyframes loading-ball { + 0% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 25% { + -webkit-transform: translate(9.24px, 61.6px) scale(0); + -moz-transform: translate(9.24px, 61.6px) scale(0); + -o-transform: translate(9.24px, 61.6px) scale(0); + -ms-transform: translate(9.24px, 61.6px) scale(0); + transform: translate(9.24px, 61.6px) scale(0); + } + 50% { + -webkit-transform: translate(9.24px, 61.6px) scale(1); + -moz-transform: translate(9.24px, 61.6px) scale(1); + -o-transform: translate(9.24px, 61.6px) scale(1); + -ms-transform: translate(9.24px, 61.6px) scale(1); + transform: translate(9.24px, 61.6px) scale(1); + } + 75% { + -webkit-transform: translate(61.6px, 61.6px) scale(1); + -moz-transform: translate(61.6px, 61.6px) scale(1); + -o-transform: translate(61.6px, 61.6px) scale(1); + -ms-transform: translate(61.6px, 61.6px) scale(1); + transform: translate(61.6px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } +} +@-moz-keyframes loading-ball-r { + 0% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(0); + -moz-transform: translate(113.96px, 61.6px) scale(0); + -o-transform: translate(113.96px, 61.6px) scale(0); + -ms-transform: translate(113.96px, 61.6px) scale(0); + transform: translate(113.96px, 61.6px) scale(0); + } +} +@-webkit-keyframes loading-ball-r { + 0% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(0); + -moz-transform: translate(113.96px, 61.6px) scale(0); + -o-transform: translate(113.96px, 61.6px) scale(0); + -ms-transform: translate(113.96px, 61.6px) scale(0); + transform: translate(113.96px, 61.6px) scale(0); + } +} +@-o-keyframes loading-ball-r { + 0% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(0); + -moz-transform: translate(113.96px, 61.6px) scale(0); + -o-transform: translate(113.96px, 61.6px) scale(0); + -ms-transform: translate(113.96px, 61.6px) scale(0); + transform: translate(113.96px, 61.6px) scale(0); + } +} +@keyframes loading-ball-r { + 0% { + -webkit-transform: translate(113.96px, 61.6px) scale(1); + -moz-transform: translate(113.96px, 61.6px) scale(1); + -o-transform: translate(113.96px, 61.6px) scale(1); + -ms-transform: translate(113.96px, 61.6px) scale(1); + transform: translate(113.96px, 61.6px) scale(1); + } + 100% { + -webkit-transform: translate(113.96px, 61.6px) scale(0); + -moz-transform: translate(113.96px, 61.6px) scale(0); + -o-transform: translate(113.96px, 61.6px) scale(0); + -ms-transform: translate(113.96px, 61.6px) scale(0); + transform: translate(113.96px, 61.6px) scale(0); + } +} +@-moz-keyframes loading-ball-c { + 0% { + background: #e15b64; + } + 25% { + background: #abbd81; + } + 50% { + background: #f8b26a; + } + 75% { + background: #f47e60; + } + 100% { + background: #e15b64; + } +} +@-webkit-keyframes loading-ball-c { + 0% { + background: #e15b64; + } + 25% { + background: #abbd81; + } + 50% { + background: #f8b26a; + } + 75% { + background: #f47e60; + } + 100% { + background: #e15b64; + } +} +@-o-keyframes loading-ball-c { + 0% { + background: #e15b64; + } + 25% { + background: #abbd81; + } + 50% { + background: #f8b26a; + } + 75% { + background: #f47e60; + } + 100% { + background: #e15b64; + } +} +@keyframes loading-ball-c { + 0% { + background: #e15b64; + } + 25% { + background: #abbd81; + } + 50% { + background: #f8b26a; + } + 75% { + background: #f47e60; + } + 100% { + background: #e15b64; + } +} +blockquote.pullquote { + position: relative; + max-width: 45%; + font-size: 110%; +} +blockquote.pullquote.left { + float: left; + margin: 1em 0.5em 0 0; +} +blockquote.pullquote.right { + float: right; + margin: 1em 0 0 0.5em; +} +.video-container { + position: relative; + overflow: hidden; + margin-bottom: 16px; + padding-top: 56.25%; + height: 0; +} +.video-container iframe { + position: absolute; + top: 0; + left: 0; + margin-top: 0; + width: 100%; + height: 100%; +} +.hide-inline > .hide-button, +.hide-block > .hide-button { + display: inline-block; + padding: 5px 18px; + background: #49b1f5; + color: var(--white); + border-radius: 6px; +} +.hide-inline > .hide-button:hover, +.hide-block > .hide-button:hover { + background-color: var(--btn-hover-color); +} +.hide-inline > .hide-button.open, +.hide-block > .hide-button.open { + display: none; +} +.hide-inline > .hide-button.open + div, +.hide-block > .hide-button.open + div { + display: block; +} +.hide-inline > .hide-button.open + span, +.hide-block > .hide-button.open + span { + display: inline; +} +.hide-inline > .hide-content, +.hide-block > .hide-content { + display: none; +} +.hide-inline > .hide-button { + margin: 0 6px; +} +.hide-inline > .hide-content { + margin: 0 6px; +} +.hide-block { + margin: 0 0 16px; +} +.toggle { + margin-bottom: 20px; + border: 1px solid #f0f0f0; + border-radius: 5px; + overflow: hidden; +} +.toggle > .toggle-button { + padding: 6px 15px; + background: #f0f0f0; + color: #1f2d3d; + cursor: pointer; +} +.toggle > .toggle-content { + margin: 30px 24px; +} +.container .inline-img { + display: inline; + margin: 0 3px; + height: 1.1em; + vertical-align: text-bottom; +} +.hl-label { + padding: 2px 4px; + color: #fff; + border-radius: 3px; +} +.hl-label.default { + background-color: #777; +} +.hl-label.blue { + background-color: #428bca; +} +.hl-label.pink { + background-color: #ff69b4; +} +.hl-label.red { + background-color: #f00; +} +.hl-label.purple { + background-color: #6f42c1; +} +.hl-label.orange { + background-color: #ff8c00; +} +.hl-label.green { + background-color: #5cb85c; +} +.note { + position: relative; + margin: 0 0 20px; + padding: 15px; + border-radius: 3px; +} +.note.icon-padding { + padding-left: 3em; +} +.note > .note-icon { + position: absolute; + top: calc(50% - 0.5em); + left: 0.8em; + font-size: larger; +} +.note.blue:not(.disabled) { + border-left-color: #428bca !important; +} +.note.blue:not(.disabled).modern { + border-left-color: transparent !important; + color: #428bca; +} +.note.blue:not(.disabled):not(.simple) { + background: #e3eef7 !important; +} +.note.blue > .note-icon { + color: #428bca; +} +.note.pink:not(.disabled) { + border-left-color: #ff69b4 !important; +} +.note.pink:not(.disabled).modern { + border-left-color: transparent !important; + color: #ff69b4; +} +.note.pink:not(.disabled):not(.simple) { + background: #ffe9f4 !important; +} +.note.pink > .note-icon { + color: #ff69b4; +} +.note.red:not(.disabled) { + border-left-color: #f00 !important; +} +.note.red:not(.disabled).modern { + border-left-color: transparent !important; + color: #f00; +} +.note.red:not(.disabled):not(.simple) { + background: #ffd9d9 !important; +} +.note.red > .note-icon { + color: #f00; +} +.note.purple:not(.disabled) { + border-left-color: #6f42c1 !important; +} +.note.purple:not(.disabled).modern { + border-left-color: transparent !important; + color: #6f42c1; +} +.note.purple:not(.disabled):not(.simple) { + background: #e9e3f6 !important; +} +.note.purple > .note-icon { + color: #6f42c1; +} +.note.orange:not(.disabled) { + border-left-color: #ff8c00 !important; +} +.note.orange:not(.disabled).modern { + border-left-color: transparent !important; + color: #ff8c00; +} +.note.orange:not(.disabled):not(.simple) { + background: #ffeed9 !important; +} +.note.orange > .note-icon { + color: #ff8c00; +} +.note.green:not(.disabled) { + border-left-color: #5cb85c !important; +} +.note.green:not(.disabled).modern { + border-left-color: transparent !important; + color: #5cb85c; +} +.note.green:not(.disabled):not(.simple) { + background: #e7f4e7 !important; +} +.note.green > .note-icon { + color: #5cb85c; +} +.note.simple { + border: 1px solid #eee; + border-left-width: 5px; +} +.note.modern { + border: 1px solid transparent !important; + background-color: #f5f5f5; + color: #4c4948; +} +.note.flat { + border: initial; + border-left: 5px solid #eee; + background-color: #f9f9f9; + color: #4c4948; +} +.note h2, +.note h3, +.note h4, +.note h5, +.note h6 { + margin-top: 3px; + margin-bottom: 0; + padding-top: 0 !important; + border-bottom: initial; +} +.note p:first-child, +.note ul:first-child, +.note ol:first-child, +.note table:first-child, +.note pre:first-child, +.note blockquote:first-child, +.note img:first-child { + margin-top: 0 !important; +} +.note p:last-child, +.note ul:last-child, +.note ol:last-child, +.note table:last-child, +.note pre:last-child, +.note blockquote:last-child, +.note img:last-child { + margin-bottom: 0 !important; +} +.note .img-alt { + margin: 5px 0 10px; +} +.note:not(.no-icon) { + padding-left: 3em; +} +.note:not(.no-icon)::before { + position: absolute; + top: calc(50% - 0.95em); + left: 0.8em; + font-size: larger; +} +.note.default.flat { + background: #f7f7f7; +} +.note.default.modern { + border-color: #e1e1e1; + background: #f3f3f3; + color: #666; +} +.note.default.modern a:not(.btn) { + color: #666; +} +.note.default.modern a:not(.btn):hover { + color: #454545; +} +.note.default:not(.modern) { + border-left-color: #777; +} +.note.default:not(.modern) h2, +.note.default:not(.modern) h3, +.note.default:not(.modern) h4, +.note.default:not(.modern) h5, +.note.default:not(.modern) h6 { + color: #777; +} +.note.default:not(.no-icon)::before { + content: '\f0a9'; +} +.note.default:not(.no-icon):not(.modern)::before { + color: #777; +} +.note.primary.flat { + background: #f5f0fa; +} +.note.primary.modern { + border-color: #e1c2ff; + background: #f3daff; + color: #6f42c1; +} +.note.primary.modern a:not(.btn) { + color: #6f42c1; +} +.note.primary.modern a:not(.btn):hover { + color: #453298; +} +.note.primary:not(.modern) { + border-left-color: #6f42c1; +} +.note.primary:not(.modern) h2, +.note.primary:not(.modern) h3, +.note.primary:not(.modern) h4, +.note.primary:not(.modern) h5, +.note.primary:not(.modern) h6 { + color: #6f42c1; +} +.note.primary:not(.no-icon)::before { + content: '\f055'; +} +.note.primary:not(.no-icon):not(.modern)::before { + color: #6f42c1; +} +.note.info.flat { + background: #eef7fa; +} +.note.info.modern { + border-color: #b3e5ef; + background: #d9edf7; + color: #31708f; +} +.note.info.modern a:not(.btn) { + color: #31708f; +} +.note.info.modern a:not(.btn):hover { + color: #215761; +} +.note.info:not(.modern) { + border-left-color: #428bca; +} +.note.info:not(.modern) h2, +.note.info:not(.modern) h3, +.note.info:not(.modern) h4, +.note.info:not(.modern) h5, +.note.info:not(.modern) h6 { + color: #428bca; +} +.note.info:not(.no-icon)::before { + content: '\f05a'; +} +.note.info:not(.no-icon):not(.modern)::before { + color: #428bca; +} +.note.success.flat { + background: #eff8f0; +} +.note.success.modern { + border-color: #d0e6be; + background: #dff0d8; + color: #3c763d; +} +.note.success.modern a:not(.btn) { + color: #3c763d; +} +.note.success.modern a:not(.btn):hover { + color: #32562c; +} +.note.success:not(.modern) { + border-left-color: #5cb85c; +} +.note.success:not(.modern) h2, +.note.success:not(.modern) h3, +.note.success:not(.modern) h4, +.note.success:not(.modern) h5, +.note.success:not(.modern) h6 { + color: #5cb85c; +} +.note.success:not(.no-icon)::before { + content: '\f058'; +} +.note.success:not(.no-icon):not(.modern)::before { + color: #5cb85c; +} +.note.warning.flat { + background: #fdf8ea; +} +.note.warning.modern { + border-color: #fae4cd; + background: #fcf4e3; + color: #8a6d3b; +} +.note.warning.modern a:not(.btn) { + color: #8a6d3b; +} +.note.warning.modern a:not(.btn):hover { + color: #714f30; +} +.note.warning:not(.modern) { + border-left-color: #f0ad4e; +} +.note.warning:not(.modern) h2, +.note.warning:not(.modern) h3, +.note.warning:not(.modern) h4, +.note.warning:not(.modern) h5, +.note.warning:not(.modern) h6 { + color: #f0ad4e; +} +.note.warning:not(.no-icon)::before { + content: '\f06a'; +} +.note.warning:not(.no-icon):not(.modern)::before { + color: #f0ad4e; +} +.note.danger.flat { + background: #fcf1f2; +} +.note.danger.modern { + border-color: #ebcdd2; + background: #f2dfdf; + color: #a94442; +} +.note.danger.modern a:not(.btn) { + color: #a94442; +} +.note.danger.modern a:not(.btn):hover { + color: #84333f; +} +.note.danger:not(.modern) { + border-left-color: #d9534f; +} +.note.danger:not(.modern) h2, +.note.danger:not(.modern) h3, +.note.danger:not(.modern) h4, +.note.danger:not(.modern) h5, +.note.danger:not(.modern) h6 { + color: #d9534f; +} +.note.danger:not(.no-icon)::before { + content: '\f056'; +} +.note.danger:not(.no-icon):not(.modern)::before { + color: #d9534f; +} +.container .series-items a:hover { + color: var(--pseudo-hover); +} +.container .tabs { + position: relative; + margin: 0 0 20px; + border-right: 1px solid var(--tab-border-color); + border-bottom: 1px solid var(--tab-border-color); + border-left: 1px solid var(--tab-border-color); + border-radius: 6px; + overflow: hidden; +} +.container .tabs > .nav-tabs { + display: -webkit-box; + display: -moz-box; + display: -webkit-flex; + display: -ms-flexbox; + display: box; + display: flex; + -webkit-box-lines: multiple; + -moz-box-lines: multiple; + -o-box-lines: multiple; + -webkit-flex-wrap: wrap; + -ms-flex-wrap: wrap; + flex-wrap: wrap; + margin: 0; + padding: 0; + background: var(--tab-botton-bg); +} +.container .tabs > .nav-tabs > .tab { + -webkit-box-flex: 1; + -moz-box-flex: 1; + -o-box-flex: 1; + -ms-box-flex: 1; + box-flex: 1; + -webkit-flex-grow: 1; + flex-grow: 1; + padding: 8px 18px; + border-top: 2px solid var(--tab-border-color); + background: var(--tab-botton-bg); + color: var(--tab-botton-color); + line-height: 2; + -webkit-transition: all 0.4s; + -moz-transition: all 0.4s; + -o-transition: all 0.4s; + -ms-transition: all 0.4s; + transition: all 0.4s; +} +.container .tabs > .nav-tabs > .tab i { + width: 1.5em; +} +.container .tabs > .nav-tabs > .tab.active { + border-top: 2px solid #49b1f5; + background: var(--tab-button-active-bg); + cursor: default; +} +.container .tabs > .nav-tabs > .tab:not(.active):hover { + border-top: 2px solid var(--tab-button-hover-bg); + background: var(--tab-button-hover-bg); +} +.container .tabs > .nav-tabs.no-default ~ .tab-to-top { + display: none; +} +.container .tabs > .tab-contents .tab-item-content { + position: relative; + display: none; + padding: 36px 24px 10px; +} +@media screen and (max-width: 768px) { + .container .tabs > .tab-contents .tab-item-content { + padding: 24px 14px; + } +} +.container .tabs > .tab-contents .tab-item-content.active { + display: block; + -webkit-animation: tabshow 0.5s; + -moz-animation: tabshow 0.5s; + -o-animation: tabshow 0.5s; + -ms-animation: tabshow 0.5s; + animation: tabshow 0.5s; +} +.container .tabs > .tab-contents .tab-item-content > :last-child { + margin-bottom: 0; +} +.container .tabs > .tab-to-top { + padding: 0 16px 10px 0; + width: 100%; + text-align: right; +} +.container .tabs > .tab-to-top button { + color: #99a9bf; +} +.container .tabs > .tab-to-top button:hover { + color: #49b1f5; +} +@-moz-keyframes tabshow { + 0% { + -webkit-transform: translateY(15px); + -moz-transform: translateY(15px); + -o-transform: translateY(15px); + -ms-transform: translateY(15px); + transform: translateY(15px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-webkit-keyframes tabshow { + 0% { + -webkit-transform: translateY(15px); + -moz-transform: translateY(15px); + -o-transform: translateY(15px); + -ms-transform: translateY(15px); + transform: translateY(15px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@-o-keyframes tabshow { + 0% { + -webkit-transform: translateY(15px); + -moz-transform: translateY(15px); + -o-transform: translateY(15px); + -ms-transform: translateY(15px); + transform: translateY(15px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +@keyframes tabshow { + 0% { + -webkit-transform: translateY(15px); + -moz-transform: translateY(15px); + -o-transform: translateY(15px); + -ms-transform: translateY(15px); + transform: translateY(15px); + } + 100% { + -webkit-transform: translateY(0); + -moz-transform: translateY(0); + -o-transform: translateY(0); + -ms-transform: translateY(0); + transform: translateY(0); + } +} +.container .timeline { + margin: 0 10px 20px; + padding: 14px 0 5px 20px; + border-left: 2px solid var(--timeline-color, #49b1f5); +} +.container .timeline.blue { + --timeline-color: #428bca; + --timeline-bg: rgba(66,139,202, 0.2); +} +.container .timeline.pink { + --timeline-color: #ff69b4; + --timeline-bg: rgba(255,105,180, 0.2); +} +.container .timeline.red { + --timeline-color: #f00; + --timeline-bg: rgba(255,0,0, 0.2); +} +.container .timeline.purple { + --timeline-color: #6f42c1; + --timeline-bg: rgba(111,66,193, 0.2); +} +.container .timeline.orange { + --timeline-color: #ff8c00; + --timeline-bg: rgba(255,140,0, 0.2); +} +.container .timeline.green { + --timeline-color: #5cb85c; + --timeline-bg: rgba(92,184,92, 0.2); +} +.container .timeline .timeline-item { + margin: 0 0 15px; +} +.container .timeline .timeline-item:hover .item-circle:before { + border-color: var(--timeline-color, #49b1f5); +} +.container .timeline .timeline-item.headline .timeline-item-title .item-circle > p { + font-weight: 600; + font-size: 1.2em; +} +.container .timeline .timeline-item.headline .timeline-item-title .item-circle:before { + left: -28px; + border: 4px solid var(--timeline-color, #49b1f5); +} +.container .timeline .timeline-item.headline:hover .item-circle:before { + border-color: var(--pseudo-hover); +} +.container .timeline .timeline-item .timeline-item-title { + position: relative; +} +.container .timeline .timeline-item .item-circle:before { + position: absolute; + top: 50%; + left: -27px; + width: 6px; + height: 6px; + border: 3px solid var(--pseudo-hover); + border-radius: 50%; + background: var(--card-bg); + content: ''; + -webkit-transition: all 0.3s; + -moz-transition: all 0.3s; + -o-transition: all 0.3s; + -ms-transition: all 0.3s; + transition: all 0.3s; + -webkit-transform: translate(0, -50%); + -moz-transform: translate(0, -50%); + -o-transform: translate(0, -50%); + -ms-transform: translate(0, -50%); + transform: translate(0, -50%); +} +.container .timeline .timeline-item .item-circle > p { + margin: 0 0 8px; + font-weight: 500; +} +.container .timeline .timeline-item .timeline-item-content { + position: relative; + padding: 12px 15px; + border-radius: 8px; + background: var(--timeline-bg, #e4f3fd); + font-size: 0.93em; +} +.container .timeline .timeline-item .timeline-item-content > :last-child { + margin-bottom: 0; +} +.container .timeline + .timeline { + margin-top: -20px; +} +[data-theme='dark'] { + --global-bg: #0d0d0d; + --font-color: rgba(255,255,255,0.7); + --hr-border: rgba(255,255,255,0.4); + --hr-before-color: rgba(255,255,255,0.7); + --search-bg: #121212; + --search-input-color: rgba(255,255,255,0.7); + --search-a-color: rgba(255,255,255,0.7); + --preloader-bg: #0d0d0d; + --preloader-color: rgba(255,255,255,0.7); + --tab-border-color: #2c2c2c; + --tab-botton-bg: #2c2c2c; + --tab-botton-color: rgba(255,255,255,0.7); + --tab-button-hover-bg: #383838; + --tab-button-active-bg: #121212; + --card-bg: #121212; + --sidebar-bg: #121212; + --sidebar-menu-bg: #1f1f1f; + --btn-hover-color: #787878; + --btn-color: rgba(255,255,255,0.7); + --btn-bg: #1f1f1f; + --text-bg-hover: #383838; + --light-grey: rgba(255,255,255,0.7); + --dark-grey: rgba(255,255,255,0.2); + --white: rgba(255,255,255,0.9); + --text-highlight-color: rgba(255,255,255,0.9); + --blockquote-color: rgba(255,255,255,0.7); + --blockquote-bg: #2c2c2c; + --reward-pop: #2c2c2c; + --toc-link-color: rgba(255,255,255,0.6); + --scrollbar-color: #525252; + --timeline-bg: #1f1f1f; + --zoom-bg: #121212; + --mark-bg: rgba(0,0,0,0.6); +} +[data-theme='dark'] #web_bg:before { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(0,0,0,0.7); + content: ''; +} +[data-theme='dark'] .container code { + background: #2c2c2c; +} +[data-theme='dark'] .container pre > code { + background: #171717; +} +[data-theme='dark'] .container figure.highlight { + -webkit-box-shadow: none; + box-shadow: none; +} +[data-theme='dark'] .container .note code { + background: rgba(27,31,35,0.05); +} +[data-theme='dark'] .container .aplayer { + filter: brightness(0.8); +} +[data-theme='dark'] .container kbd { + border-color: #696969; + background-color: #525252; + color: #e2f1ff; +} +[data-theme='dark'] #page-header.nav-fixed > #nav, +[data-theme='dark'] #page-header.not-top-img > #nav { + background: rgba(18,18,18,0.8); + -webkit-box-shadow: 0 5px 6px -5px rgba(133,133,133,0); + box-shadow: 0 5px 6px -5px rgba(133,133,133,0); +} +[data-theme='dark'] #post-comment .comment-switch { + background: #2c2c2c !important; +} +[data-theme='dark'] #post-comment .comment-switch #switch-btn { + filter: brightness(0.8); +} +[data-theme='dark'] .note { + filter: brightness(0.8); +} +[data-theme='dark'] .hide-button, +[data-theme='dark'] .btn-beautify, +[data-theme='dark'] .hl-label, +[data-theme='dark'] #post-outdate-notice, +[data-theme='dark'] .error-img, +[data-theme='dark'] .container iframe, +[data-theme='dark'] .gist, +[data-theme='dark'] .ads-wrap { + filter: brightness(0.8); +} +[data-theme='dark'] img { + filter: brightness(0.8); +} +[data-theme='dark'] #aside-content .aside-list > .aside-list-item:not(:last-child) { + border-bottom: 1px dashed rgba(255,255,255,0.1); +} +[data-theme='dark'] #gitalk-container { + filter: brightness(0.8); +} +[data-theme='dark'] #gitalk-container svg { + fill: rgba(255,255,255,0.9) !important; +} +[data-theme='dark'] #disqusjs #dsqjs:hover, +[data-theme='dark'] #disqusjs #dsqjs:focus, +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-tab-active, +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-no-comment { + color: rgba(255,255,255,0.7); +} +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-order-label { + background-color: #1f1f1f; +} +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-post-body { + color: rgba(255,255,255,0.7); +} +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-post-body code, +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-post-body pre { + background: #2c2c2c; +} +[data-theme='dark'] #disqusjs #dsqjs .dsqjs-post-body blockquote { + color: rgba(255,255,255,0.7); +} +[data-theme='dark'] #artitalk_main #lazy { + background: #121212; +} +[data-theme='dark'] #operare_artitalk .c2 { + background: #121212; +} +@media screen and (max-width: 900px) { + [data-theme='dark'] #card-toc { + background: #1f1f1f; + } +} +[data-theme='dark'] .artalk.atk-dark-mode, +[data-theme='dark'] .atk-layer-wrap.atk-dark-mode { + --at-color-font: rgba(255,255,255,0.7); + --at-color-meta: rgba(255,255,255,0.7); + --at-color-grey: rgba(255,255,255,0.7); +} +[data-theme='dark'] .atk-send-btn, +[data-theme='dark'] .atk-badge { + color: rgba(255,255,255,0.7) !important; +} +[data-theme='dark'] #waline-wrap { + --waline-color: rgba(255,255,255,0.7); + --waline-dark-grey: rgba(255,255,255,0.7); + --waline-info-color: rgba(255,255,255,0.5); +} +.read-mode { + --font-color: #4c4948; + --readmode-light-color: #fff; + --white: #4c4948; + --light-grey: #4c4948; + --gray: #d6dbdf; + --hr-border: #d6dbdf; + --hr-before-color: #b9c2c9; + --highlight-bg: #f7f7f7; + --exit-btn-bg: #c0c0c0; + --exit-btn-color: #fff; + --exit-btn-hover: #8d8d8d; + --pseudo-hover: none; +} +[data-theme='dark'] .read-mode { + --font-color: rgba(255,255,255,0.7); + --readmode-light-color: #0d0d0d; + --white: rgba(255,255,255,0.9); + --light-grey: rgba(255,255,255,0.7); + --gray: rgba(255,255,255,0.7); + --hr-border: rgba(255,255,255,0.5); + --hr-before-color: rgba(255,255,255,0.7); + --highlight-bg: #171717; + --exit-btn-bg: #1f1f1f; + --exit-btn-color: rgba(255,255,255,0.9); + --exit-btn-hover: #525252; +} +.read-mode { + background: var(--readmode-light-color); +} +.read-mode .exit-readmode { + position: fixed; + top: 30px; + right: 30px; + z-index: 100; + width: 40px; + height: 40px; + background: var(--exit-btn-bg); + color: var(--exit-btn-color); + font-size: 16px; + -webkit-transition: background 0.3s; + -moz-transition: background 0.3s; + -o-transition: background 0.3s; + -ms-transition: background 0.3s; + transition: background 0.3s; + border-radius: 8px; +} +@media screen and (max-width: 768px) { + .read-mode .exit-readmode { + top: initial; + bottom: 30px; + } +} +.read-mode .exit-readmode:hover { + background: var(--exit-btn-hover); +} +.read-mode #aside-content { + display: none; +} +.read-mode #page-header.post-bg { + background: none !important; +} +.read-mode #page-header.post-bg:before { + opacity: 0; + -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=0)"; + filter: alpha(opacity=0); +} +.read-mode #page-header.post-bg > #post-info { + text-align: center; +} +.read-mode #post { + margin: 0 auto; + background: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.read-mode #post:hover { + -webkit-box-shadow: none; + box-shadow: none; +} +.read-mode > canvas { + display: none !important; +} +.read-mode .highlight-tools, +.read-mode #footer, +.read-mode #post > *:not(#post-info):not(.post-content), +.read-mode #nav, +.read-mode #post-outdate-notice, +.read-mode #web_bg, +.read-mode #rightside, +.read-mode .not-top-img { + display: none !important; +} +.read-mode .container a { + color: #99a9bf; +} +.read-mode .container pre, +.read-mode .container .highlight:not(.js-file-line-container) { + background: var(--highlight-bg) !important; +} +.read-mode .container pre *, +.read-mode .container .highlight:not(.js-file-line-container) * { + color: var(--font-color) !important; +} +.read-mode .container figure.highlight { + border-radius: 0 !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; +} +.read-mode .container figure.highlight > :not(.highlight-tools) { + display: block !important; +} +.read-mode .container figure.highlight .line:before { + color: var(--font-color) !important; +} +.read-mode .container figure.highlight .hljs { + background: var(--highlight-bg) !important; +} +.read-mode .container h1, +.read-mode .container h2, +.read-mode .container h3, +.read-mode .container h4, +.read-mode .container h5, +.read-mode .container h6 { + padding: 0; +} +.read-mode .container h1:before, +.read-mode .container h2:before, +.read-mode .container h3:before, +.read-mode .container h4:before, +.read-mode .container h5:before, +.read-mode .container h6:before { + content: ''; +} +.read-mode .container h1:hover, +.read-mode .container h2:hover, +.read-mode .container h3:hover, +.read-mode .container h4:hover, +.read-mode .container h5:hover, +.read-mode .container h6:hover { + padding: 0; +} +.read-mode .container ul:hover:before, +.read-mode .container li:hover:before, +.read-mode .container ol:hover:before { + -webkit-transform: none !important; + -moz-transform: none !important; + -o-transform: none !important; + -ms-transform: none !important; + transform: none !important; +} +.read-mode .container ol:before, +.read-mode .container li:before { + background: transparent !important; + color: var(--font-color) !important; +} +.read-mode .container ul >li:before { + border-color: var(--gray) !important; +} +.read-mode .container .tabs { + border: 2px solid var(--tab-border-color); +} +.read-mode .container .tabs > .nav-tabs { + background: transparent; +} +.read-mode .container .tabs > .nav-tabs > .tab { + border-top: none !important; +} +.read-mode .container .tabs > .tab-contents .tab-item-content.active { + -webkit-animation: none; + -moz-animation: none; + -o-animation: none; + -ms-animation: none; + animation: none; +} +.read-mode .container code { + color: var(--font-color); +} +.read-mode .container blockquote { + border-color: var(--gray); + background-color: var(--readmode-light-color); +} +.read-mode .container kbd { + border: 1px solid var(--gray); + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; + color: var(--font-color); +} +.read-mode .container .hide-toggle { + border: 1px solid var(--gray) !important; +} +.read-mode .container .hide-button, +.read-mode .container .btn-beautify, +.read-mode .container .hl-label { + border: 1px solid var(--gray) !important; + background: var(--readmode-light-color) !important; + color: var(--font-color) !important; +} +.read-mode .container .note { + border: 2px solid var(--gray); + border-left-color: var(--gray) !important; + filter: none; + background-color: var(--readmode-light-color) !important; + color: var(--font-color); +} +.read-mode .container .note:before, +.read-mode .container .note .note-icon { + color: var(--font-color); +} +.search-dialog { + position: fixed; + top: 10%; + left: 50%; + z-index: 1001; + display: none; + margin-left: -300px; + padding: 20px; + width: 600px; + background: var(--search-bg); + --search-height: 100vh; + border-radius: 8px; +} +@media screen and (max-width: 768px) { + .search-dialog { + top: 0; + left: 0; + margin: 0; + width: 100%; + height: 100%; + border-radius: 0; + } +} +.search-dialog .search-nav { + margin: 0 0 14px; + color: #49b1f5; + font-size: 1.4em; + line-height: 1; +} +.search-dialog .search-nav .search-dialog-title { + margin-right: 10px; +} +.search-dialog .search-nav .search-close-button { + float: right; + color: #858585; + -webkit-transition: color 0.2s ease-in-out; + -moz-transition: color 0.2s ease-in-out; + -o-transition: color 0.2s ease-in-out; + -ms-transition: color 0.2s ease-in-out; + transition: color 0.2s ease-in-out; +} +.search-dialog .search-nav .search-close-button:hover { + color: #49b1f5; +} +.search-dialog hr { + margin: 15px auto; +} +#search-mask { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1000; + display: none; + background: rgba(0,0,0,0.6); +} +#algolia-search .search-dialog .ais-SearchBox input { + padding: 5px 14px; + width: 100%; + outline: none; + border: 2px solid #49b1f5; + border-radius: 40px; + background: var(--search-bg); + color: var(--search-input-color); +} +#algolia-search .search-dialog .ais-SearchBox .ais-SearchBox-loadingIndicator { + position: absolute; + top: 18px; + left: 67px; +} +#algolia-search .search-dialog .ais-Hits-list { + margin: 0; + padding: 0; +} +#algolia-search .search-dialog .ais-Hits-list a { + color: var(--search-a-color); +} +#algolia-search .search-dialog .ais-Hits-list a:hover { + color: #49b1f5; +} +#algolia-search .search-dialog .ais-Hits-list mark { + background: transparent; + color: #f47466; + font-weight: bold; +} +#algolia-search .search-dialog .algolia-hits-item-title { + font-weight: 600; +} +#algolia-search .search-dialog .algolia-hit-item-content { + margin: 0 0 8px; + word-break: break-word; +} +#algolia-search .search-dialog .ais-Pagination { + margin: 15px 0 0; + padding: 0; + text-align: center; +} +#algolia-search .search-dialog .ais-Pagination .ais-Pagination-list { + margin: 0; + padding: 0; + list-style: none; +} +#algolia-search .search-dialog .ais-Pagination .ais-Pagination-item { + display: inline; + margin: 0 4px; + padding: 0; +} +#algolia-search .search-dialog .ais-Pagination .ais-Pagination-item .ais-Pagination-link { + display: inline-block; + min-width: 24px; + height: 24px; + text-align: center; + line-height: 24px; + border-radius: 6px; +} +#algolia-search .search-dialog .ais-Pagination .ais-Pagination-item--selected a { + background: #00c4b6; + color: #eee; + cursor: default; +} +#algolia-search .search-dialog .ais-Pagination .ais-Pagination-item--disabled { + visibility: hidden; +} +#algolia-search .search-dialog #algolia-hits > div { + overflow-y: overlay; + margin: 0 -20px; + padding: 0 22px; + max-height: calc(80vh - 220px); +} +@media screen and (max-width: 768px) { + #algolia-search .search-dialog #algolia-hits > div { + max-height: none; + height: calc(var(--search-height) - 235px); + } +} +#algolia-search .search-dialog #algolia-info div { + display: inline; +} +#algolia-search .search-dialog #algolia-info .algolia-poweredBy { + float: right; + vertical-align: text-top; +} +#algolia-search .search-dialog #algolia-info .algolia-poweredBy svg { + height: 1.1em; +} diff --git a/css/var.css b/css/var.css new file mode 100644 index 00000000..e69de29b diff --git a/games/index.html b/games/index.html new file mode 100644 index 00000000..4bd92659 --- /dev/null +++ b/games/index.html @@ -0,0 +1,283 @@ +我玩过的游戏 | JCAlways + + + + + + + + + +
我玩过的游戏
+ + + +
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/img/404.jpg b/img/404.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4bab3c3f20a1e04b7e611f1b8e1d28357009007d GIT binary patch literal 16393 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 literal 0 HcmV?d00001 diff --git a/img/butterfly-icon.png b/img/butterfly-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3992d7740488b06e1e1b2e268f3a6af72dc23110 GIT binary patch literal 275383 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 z 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%0DKg^36uq^-XBeg839qsa&^Ed6%fof>heHs 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%0A0(jhwyD;_b%)-E)}3Y7b1?VJ7k^0$Rr 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\VyKE(sbCH4xy^MF(yCFm!QFekL6|Fbx(pytL+ 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 TGK9|&00000NkvXXu0mjfIGrd$ literal 0 HcmV?d00001 diff --git a/img/error-page.png b/img/error-page.png new file mode 100644 index 0000000000000000000000000000000000000000..9d1de96575394474a91f6b05b7e935245be3869c GIT binary patch literal 35850 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 literal 0 HcmV?d00001 diff --git a/img/favicon.ico b/img/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..56d07f553d8146f3ab6e7105dd1c53ba51082363 GIT binary patch literal 15406 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

K7CuJ&%|?G?@*^}@_>b;isra+!+)w(A{tmG( z*|O{s6E}#Ct-Y2#6Stv{SjXd$UPDvHEt~~>3rEs!gKhVp5Gr4)^vDd$`1|!cZCz{C zVx4Id8|q5ENMk|Cl}3rbJmXE`nt~$cn9|P|{wMlu%vWOkZ|Y$;=~2KcAI& z#P-RB#x@hbbHguUwlao2N!O&G>Sn*!x&1)|tJb0VH+KzH&Vbu-j4L73G-w1o&eAtWEDvqf9 z#y+3eobmEDGBScl$=e#A82+D+fY?#4FSCyY3m8w5|CQT5GPPCmJ6#TLcqJDQDd_CYH)y{+FR z)^Y8Al(EEpjBmPm&G266c4xoFSTqsGi7$}8qWC}z+SCF4f;b6?S(@vz8I`4;xwx;fvQ zir!8$55iGbM*NI3 z#dk?AEcnO7pkemlv)FTAM1MKVxN&tnM%Q_p%{Nmv>Xo0uKe5d{v>nb(zkqA|J?NKN zP+srwABVSQm&$q@=TW^!I19vkMfTepGtsi*`=sF&%4TAAP39(iS-;WVzU;I1WmjLN zv%u{EC#o_((bz|LZ{?5%zp2eyHRaZ57z6#BV+|jpt+|@>uO{YnrhkhwA7yc#No=Lq z*ZlHrb1mi%6W3%OX@vG_GwhtH5#Q@czeweN4Bv=f za)S`(U4*w%6BK?+&C04_S#?jDACuVDv*9OWcps-h-p}7U>N&3^v3nAa|Mt&k&`p2h z+4ev5U*C-Hx9+*>actg=GZb1=qWmOdw`dTWgf^+ohDv6T$3}j{?l>hT*)bVyRSOca z!`}tF*k3thUO$R9%8x(uF3Qut>ORQYS|eJ^x=dRO`UXd|PiSFIK5Gq;xmj=T0Dbl# z{TAoq7@N1%twJAd(GX{IG`<|?cmH<2TiAIPTg*66Y?I_ZeUW4O{|nKw_nGT0;oOZK z!~DIYlQZ|^b24u0v*4FQdYip{gBTk5OlM7H-gIR2&+MD@z+SpT=Pp9c)w-8|5A35S zzd%ksE{Ir$q;GDyg>|IPnQR+*u zRpqN3TWhz{rbsV&yv|yDH~lZ^ftg-_g&)msY$l5mQ%FsS>&*9B(ot^I~MZ)jT+THTJ!Po7)`NIZjx+{ z!EZ2+?>~;ALF&>H$%4>_}@GUuPnd79_ZT(}ugR|9(5!i;&^ z*wgFKI*i2P9U=BwIlo@LZ7%D3)3i)a zjq^8@52Y1AVL$mmw+?Nzu z$L<4TJ*C6&MrcqvXtU&hXk4rh%Un{$mboZSdl?58Jg)Qcx$_?8?DaF;ueVW~wF35< zLjAwAAp4A3Lr`8M_^o}EuZZ2^_r&;3Z)pq-u~=2}X5C{$gYr*!XdN>56Z>pr!hF}y zyl%~RNKZ@TL}i8VHrP#EVDxWdnK?IJOMa5i=;{3AY%ILYJD{;PB{N*_w_f8<+6Jr*;p5Ud0 U+d9i{91r@(-#<9;H*?^B0n>H*u>b%7 literal 0 HcmV?d00001 diff --git a/img/friend_404.gif b/img/friend_404.gif new file mode 100644 index 0000000000000000000000000000000000000000..91dd56a289327b6d869daa157912ee1f94eeec67 GIT binary patch literal 65097 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 literal 0 HcmV?d00001 diff --git a/index.html b/index.html new file mode 100644 index 00000000..d225efd5 --- /dev/null +++ b/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +

文章导航
PageSpy使用教程
风灵月影下载
Google reCAPTCHA使用教程
通行密钥开发 Passkey
Tauri使用教程
GeeTest3.0使用教程
GeeTest4.0使用教程
利用 kkFileView 实现在线预览文件
WPS WEB Office 前端使用教程
avatar
JCAlways
分享开发过程中遇到的问题,以及一些技术文章!
Follow Me
公告
欢迎访问本站!遇到有用的文章记得分享哦!
\ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 00000000..42cfd976 --- /dev/null +++ b/js/main.js @@ -0,0 +1,923 @@ +document.addEventListener('DOMContentLoaded', () => { + let headerContentWidth, $nav + let mobileSidebarOpen = false + + const adjustMenu = init => { + const getAllWidth = ele => Array.from(ele).reduce((width, i) => width + i.offsetWidth, 0) + + if (init) { + const blogInfoWidth = getAllWidth(document.querySelector('#blog-info > a').children) + const menusWidth = getAllWidth(document.getElementById('menus').children) + headerContentWidth = blogInfoWidth + menusWidth + $nav = document.getElementById('nav') + } + + const hideMenuIndex = window.innerWidth <= 768 || headerContentWidth > $nav.offsetWidth - 120 + $nav.classList.toggle('hide-menu', hideMenuIndex) + } + + // 初始化header + const initAdjust = () => { + adjustMenu(true) + $nav.classList.add('show') + } + + // sidebar menus + const sidebarFn = { + open: () => { + btf.overflowPaddingR.add() + btf.animateIn(document.getElementById('menu-mask'), 'to_show 0.5s') + document.getElementById('sidebar-menus').classList.add('open') + mobileSidebarOpen = true + }, + close: () => { + btf.overflowPaddingR.remove() + btf.animateOut(document.getElementById('menu-mask'), 'to_hide 0.5s') + document.getElementById('sidebar-menus').classList.remove('open') + mobileSidebarOpen = false + } + } + + /** + * 首頁top_img底下的箭頭 + */ + const scrollDownInIndex = () => { + const handleScrollToDest = () => { + btf.scrollToDest(document.getElementById('content-inner').offsetTop, 300) + } + + const $scrollDownEle = document.getElementById('scroll-down') + $scrollDownEle && btf.addEventListenerPjax($scrollDownEle, 'click', handleScrollToDest) + } + + /** + * 代碼 + * 只適用於Hexo默認的代碼渲染 + */ + const addHighlightTool = () => { + const highLight = GLOBAL_CONFIG.highlight + if (!highLight) return + + const { highlightCopy, highlightLang, highlightHeightLimit, highlightFullpage, highlightMacStyle, plugin } = highLight + const isHighlightShrink = GLOBAL_CONFIG_SITE.isHighlightShrink + const isShowTool = highlightCopy || highlightLang || isHighlightShrink !== undefined || highlightFullpage || highlightMacStyle + const $figureHighlight = plugin === 'highlight.js' ? document.querySelectorAll('figure.highlight') : document.querySelectorAll('pre[class*="language-"]') + + if (!((isShowTool || highlightHeightLimit) && $figureHighlight.length)) return + + const isPrismjs = plugin === 'prismjs' + const highlightShrinkClass = isHighlightShrink === true ? 'closed' : '' + const highlightShrinkEle = isHighlightShrink !== undefined ? '' : '' + const highlightCopyEle = highlightCopy ? '
' : '' + const highlightMacStyleEle = '
' + const highlightFullpageEle = highlightFullpage ? '' : '' + + const alertInfo = (ele, text) => { + if (GLOBAL_CONFIG.Snackbar !== undefined) { + btf.snackbarShow(text) + } else { + ele.textContent = text + ele.style.opacity = 1 + setTimeout(() => { ele.style.opacity = 0 }, 800) + } + } + + const copy = async (text, ctx) => { + try { + await navigator.clipboard.writeText(text) + alertInfo(ctx, GLOBAL_CONFIG.copy.success) + } catch (err) { + console.error('Failed to copy: ', err) + alertInfo(ctx, GLOBAL_CONFIG.copy.noSupport) + } + } + + // click events + const highlightCopyFn = (ele, clickEle) => { + const $buttonParent = ele.parentNode + $buttonParent.classList.add('copy-true') + const preCodeSelector = isPrismjs ? 'pre code' : 'table .code pre' + const codeElement = $buttonParent.querySelector(preCodeSelector) + if (!codeElement) return + copy(codeElement.innerText, clickEle.previousElementSibling) + $buttonParent.classList.remove('copy-true') + } + + const highlightShrinkFn = ele => ele.classList.toggle('closed') + + const codeFullpage = (item, clickEle) => { + const wrapEle = item.closest('figure.highlight') + const isFullpage = wrapEle.classList.toggle('code-fullpage') + + document.body.style.overflow = isFullpage ? 'hidden' : '' + clickEle.classList.toggle('fa-down-left-and-up-right-to-center', isFullpage) + clickEle.classList.toggle('fa-up-right-and-down-left-from-center', !isFullpage) + } + + const highlightToolsFn = e => { + const $target = e.target.classList + const currentElement = e.currentTarget + if ($target.contains('expand')) highlightShrinkFn(currentElement) + else if ($target.contains('copy-button')) highlightCopyFn(currentElement, e.target) + else if ($target.contains('fullpage-button')) codeFullpage(currentElement, e.target) + } + + const expandCode = e => e.currentTarget.classList.toggle('expand-done') + + // 獲取隱藏狀態下元素的真實高度 + const getActualHeight = item => { + const hiddenElements = new Map() + + const fix = () => { + let current = item + while (current !== document.body && current != null) { + if (window.getComputedStyle(current).display === 'none') { + hiddenElements.set(current, current.getAttribute('style') || '') + } + current = current.parentNode + } + + const style = 'visibility: hidden !important; display: block !important;' + hiddenElements.forEach((originalStyle, elem) => { + elem.setAttribute('style', originalStyle ? originalStyle + ';' + style : style) + }) + } + + const restore = () => { + hiddenElements.forEach((originalStyle, elem) => { + if (originalStyle === '') elem.removeAttribute('style') + else elem.setAttribute('style', originalStyle) + }) + } + + fix() + const height = item.offsetHeight + restore() + return height + } + + const createEle = (lang, item) => { + const fragment = document.createDocumentFragment() + + if (isShowTool) { + const hlTools = document.createElement('div') + hlTools.className = `highlight-tools ${highlightShrinkClass}` + hlTools.innerHTML = highlightMacStyleEle + highlightShrinkEle + lang + highlightCopyEle + highlightFullpageEle + btf.addEventListenerPjax(hlTools, 'click', highlightToolsFn) + fragment.appendChild(hlTools) + } + + if (highlightHeightLimit && getActualHeight(item) > highlightHeightLimit + 30) { + const ele = document.createElement('div') + ele.className = 'code-expand-btn' + ele.innerHTML = '' + btf.addEventListenerPjax(ele, 'click', expandCode) + fragment.appendChild(ele) + } + + isPrismjs ? item.parentNode.insertBefore(fragment, item) : item.insertBefore(fragment, item.firstChild) + } + + $figureHighlight.forEach(item => { + let langName = '' + if (isPrismjs) btf.wrap(item, 'figure', { class: 'highlight' }) + + if (!highlightLang) { + createEle('', item) + return + } + + if (isPrismjs) { + langName = item.getAttribute('data-language') || 'Code' + } else { + langName = item.getAttribute('class').split(' ')[1] + if (langName === 'plain' || langName === undefined) langName = 'Code' + } + createEle(`
${langName}
`, item) + }) + } + + /** + * PhotoFigcaption + */ + const addPhotoFigcaption = () => { + if (!GLOBAL_CONFIG.isPhotoFigcaption) return + document.querySelectorAll('#article-container img').forEach(item => { + const altValue = item.title || item.alt + if (!altValue) return + const ele = document.createElement('div') + ele.className = 'img-alt text-center' + ele.textContent = altValue + item.insertAdjacentElement('afterend', ele) + }) + } + + /** + * Lightbox + */ + const runLightbox = () => { + btf.loadLightbox(document.querySelectorAll('#article-container img:not(.no-lightbox)')) + } + + /** + * justified-gallery 圖庫排版 + */ + + const fetchUrl = async url => { + const response = await fetch(url) + return await response.json() + } + + const runJustifiedGallery = (item, data, isButton = false, tabs) => { + const dataLength = data.length + + const ig = new InfiniteGrid.JustifiedInfiniteGrid(item, { + gap: 5, + isConstantSize: true, + sizeRange: [150, 600], + // useResizeObserver: true, + // observeChildren: true, + useTransform: true + // useRecycle: false + }) + + const replaceDq = str => str.replace(/"/g, '"') // replace double quotes to " + + const getItems = (nextGroupKey, count) => { + const nextItems = [] + const startCount = (nextGroupKey - 1) * count + + for (let i = 0; i < count; ++i) { + const num = startCount + i + if (num >= dataLength) { + break + } + + const item = data[num] + const alt = item.alt ? `alt="${replaceDq(item.alt)}"` : '' + const title = item.title ? `title="${replaceDq(item.title)}"` : '' + + nextItems.push(`
+ +
`) + } + return nextItems + } + + const buttonText = GLOBAL_CONFIG.infinitegrid.buttonText + const addButton = item => { + const button = document.createElement('button') + button.innerHTML = buttonText + '' + + button.addEventListener('click', e => { + e.target.closest('button').remove() + btf.setLoading.add(item) + appendItem(ig.getGroups().length + 1, 10) + }, { once: true }) + + item.insertAdjacentElement('afterend', button) + } + + const appendItem = (nextGroupKey, count) => { + ig.append(getItems(nextGroupKey, count), nextGroupKey) + } + + const maxGroupKey = Math.ceil(dataLength / 10) + let isLayoutHidden = false + + const completeFn = e => { + if (tabs) { + const parentNode = item.parentNode + + if (isLayoutHidden) { + parentNode.style.visibility = 'visible' + } + + if (item.offsetHeight === 0) { + parentNode.style.visibility = 'hidden' + isLayoutHidden = true + } + } + + const { updated, isResize, mounted } = e + if (!updated.length || !mounted.length || isResize) { + return + } + + btf.loadLightbox(item.querySelectorAll('img:not(.medium-zoom-image)')) + + if (ig.getGroups().length === maxGroupKey) { + btf.setLoading.remove(item) + !tabs && ig.off('renderComplete', completeFn) + return + } + + if (isButton) { + btf.setLoading.remove(item) + addButton(item) + } + } + + const requestAppendFn = btf.debounce(e => { + const nextGroupKey = (+e.groupKey || 0) + 1 + appendItem(nextGroupKey, 10) + + if (nextGroupKey === maxGroupKey) { + ig.off('requestAppend', requestAppendFn) + } + }, 300) + + btf.setLoading.add(item) + ig.on('renderComplete', completeFn) + + if (isButton) { + appendItem(1, 10) + } else { + ig.on('requestAppend', requestAppendFn) + ig.renderItems() + } + + btf.addGlobalFn('pjaxSendOnce', () => { ig.destroy() }) + } + + const addJustifiedGallery = async (ele, tabs = false) => { + if (!ele.length) return + const init = async () => { + for (const item of ele) { + if (btf.isHidden(item) || item.classList.contains('loaded')) continue + + const isButton = item.getAttribute('data-button') === 'true' + const children = item.firstElementChild + const text = children.textContent + children.textContent = '' + item.classList.add('loaded') + try { + const content = item.getAttribute('data-type') === 'url' ? await fetchUrl(text) : JSON.parse(text) + runJustifiedGallery(children, content, isButton, tabs) + } catch (e) { + console.error('Gallery data parsing failed:', e) + } + } + } + + if (typeof InfiniteGrid === 'function') { + init() + } else { + await btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`) + init() + } + } + + /** + * rightside scroll percent + */ + const rightsideScrollPercent = currentTop => { + const scrollPercent = btf.getScrollPercent(currentTop, document.body) + const goUpElement = document.getElementById('go-up') + + if (scrollPercent < 95) { + goUpElement.classList.add('show-percent') + goUpElement.querySelector('.scroll-percent').textContent = scrollPercent + } else { + goUpElement.classList.remove('show-percent') + } + } + + /** + * 滾動處理 + */ + const scrollFn = () => { + const $rightside = document.getElementById('rightside') + const innerHeight = window.innerHeight + 56 + let initTop = 0 + const $header = document.getElementById('page-header') + const isChatBtn = typeof chatBtn !== 'undefined' + const isShowPercent = GLOBAL_CONFIG.percent.rightside + + // 檢查文檔高度是否小於視窗高度 + const checkDocumentHeight = () => { + if (document.body.scrollHeight <= innerHeight) { + $rightside.classList.add('rightside-show') + return true + } + return false + } + + // 如果文檔高度小於視窗高度,直接返回 + if (checkDocumentHeight()) return + + // find the scroll direction + const scrollDirection = currentTop => { + const result = currentTop > initTop // true is down & false is up + initTop = currentTop + return result + } + + let flag = '' + const scrollTask = btf.throttle(() => { + const currentTop = window.scrollY || document.documentElement.scrollTop + const isDown = scrollDirection(currentTop) + if (currentTop > 56) { + if (flag === '') { + $header.classList.add('nav-fixed') + $rightside.classList.add('rightside-show') + } + + if (isDown) { + if (flag !== 'down') { + $header.classList.remove('nav-visible') + isChatBtn && window.chatBtn.hide() + flag = 'down' + } + } else { + if (flag !== 'up') { + $header.classList.add('nav-visible') + isChatBtn && window.chatBtn.show() + flag = 'up' + } + } + } else { + flag = '' + if (currentTop === 0) { + $header.classList.remove('nav-fixed', 'nav-visible') + } + $rightside.classList.remove('rightside-show') + } + + isShowPercent && rightsideScrollPercent(currentTop) + checkDocumentHeight() + }, 300) + + btf.addEventListenerPjax(window, 'scroll', scrollTask, { passive: true }) + } + + /** + * toc,anchor + */ + const scrollFnToDo = () => { + const isToc = GLOBAL_CONFIG_SITE.isToc + const isAnchor = GLOBAL_CONFIG.isAnchor + const $article = document.getElementById('article-container') + + if (!($article && (isToc || isAnchor))) return + + let $tocLink, $cardToc, autoScrollToc, $tocPercentage, isExpand + + if (isToc) { + const $cardTocLayout = document.getElementById('card-toc') + $cardToc = $cardTocLayout.querySelector('.toc-content') + $tocLink = $cardToc.querySelectorAll('.toc-link') + $tocPercentage = $cardTocLayout.querySelector('.toc-percentage') + isExpand = $cardToc.classList.contains('is-expand') + + // toc元素點擊 + const tocItemClickFn = e => { + const target = e.target.closest('.toc-link') + if (!target) return + + e.preventDefault() + btf.scrollToDest(btf.getEleTop(document.getElementById(decodeURI(target.getAttribute('href')).replace('#', ''))), 300) + if (window.innerWidth < 900) { + $cardTocLayout.classList.remove('open') + } + } + + btf.addEventListenerPjax($cardToc, 'click', tocItemClickFn) + + autoScrollToc = item => { + const sidebarHeight = $cardToc.clientHeight + const itemOffsetTop = item.offsetTop + const itemHeight = item.clientHeight + const scrollTop = $cardToc.scrollTop + const offset = itemOffsetTop - scrollTop + const middlePosition = (sidebarHeight - itemHeight) / 2 + + if (offset !== middlePosition) { + $cardToc.scrollTop = scrollTop + (offset - middlePosition) + } + } + + // 處理 hexo-blog-encrypt 事件 + $cardToc.style.display = 'block' + } + + // find head position & add active class + const $articleList = $article.querySelectorAll('h1,h2,h3,h4,h5,h6') + let detectItem = '' + + const findHeadPosition = top => { + if (top === 0) return false + + let currentId = '' + let currentIndex = '' + + for (let i = 0; i < $articleList.length; i++) { + const ele = $articleList[i] + if (top > btf.getEleTop(ele) - 80) { + const id = ele.id + currentId = id ? '#' + encodeURI(id) : '' + currentIndex = i + } else { + break + } + } + + if (detectItem === currentIndex) return + + if (isAnchor) btf.updateAnchor(currentId) + + detectItem = currentIndex + + if (isToc) { + $cardToc.querySelectorAll('.active').forEach(i => i.classList.remove('active')) + + if (currentId) { + const currentActive = $tocLink[currentIndex] + currentActive.classList.add('active') + + setTimeout(() => autoScrollToc(currentActive), 0) + + if (!isExpand) { + let parent = currentActive.parentNode + while (!parent.matches('.toc')) { + if (parent.matches('li')) parent.classList.add('active') + parent = parent.parentNode + } + } + } + } + } + + // main of scroll + const tocScrollFn = btf.throttle(() => { + const currentTop = window.scrollY || document.documentElement.scrollTop + if (isToc && GLOBAL_CONFIG.percent.toc) { + $tocPercentage.textContent = btf.getScrollPercent(currentTop, $article) + } + findHeadPosition(currentTop) + }, 100) + + btf.addEventListenerPjax(window, 'scroll', tocScrollFn, { passive: true }) + } + + const handleThemeChange = mode => { + const globalFn = window.globalFn || {} + const themeChange = globalFn.themeChange || {} + if (!themeChange) { + return + } + + Object.keys(themeChange).forEach(key => { + const themeChangeFn = themeChange[key] + if (['disqus', 'disqusjs'].includes(key)) { + setTimeout(() => themeChangeFn(mode), 300) + } else { + themeChangeFn(mode) + } + }) + } + + /** + * Rightside + */ + const rightSideFn = { + readmode: () => { // read mode + const $body = document.body + const newEle = document.createElement('button') + + const exitReadMode = () => { + $body.classList.remove('read-mode') + newEle.remove() + newEle.removeEventListener('click', exitReadMode) + } + + $body.classList.add('read-mode') + newEle.type = 'button' + newEle.className = 'fas fa-sign-out-alt exit-readmode' + newEle.addEventListener('click', exitReadMode) + $body.appendChild(newEle) + }, + darkmode: () => { // switch between light and dark mode + const willChangeMode = document.documentElement.getAttribute('data-theme') === 'dark' ? 'light' : 'dark' + if (willChangeMode === 'dark') { + btf.activateDarkMode() + GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.day_to_night) + } else { + btf.activateLightMode() + GLOBAL_CONFIG.Snackbar !== undefined && btf.snackbarShow(GLOBAL_CONFIG.Snackbar.night_to_day) + } + btf.saveToLocal.set('theme', willChangeMode, 2) + handleThemeChange(willChangeMode) + }, + 'rightside-config': item => { // Show or hide rightside-hide-btn + const hideLayout = item.firstElementChild + if (hideLayout.classList.contains('show')) { + hideLayout.classList.add('status') + setTimeout(() => { + hideLayout.classList.remove('status') + }, 300) + } + + hideLayout.classList.toggle('show') + }, + 'go-up': () => { // Back to top + btf.scrollToDest(0, 500) + }, + 'hide-aside-btn': () => { // Hide aside + const $htmlDom = document.documentElement.classList + const saveStatus = $htmlDom.contains('hide-aside') ? 'show' : 'hide' + btf.saveToLocal.set('aside-status', saveStatus, 2) + $htmlDom.toggle('hide-aside') + }, + 'mobile-toc-button': (p, item) => { // Show mobile toc + const tocEle = document.getElementById('card-toc') + tocEle.style.transition = 'transform 0.3s ease-in-out' + + const tocEleHeight = tocEle.clientHeight + const btData = item.getBoundingClientRect() + + const tocEleBottom = window.innerHeight - btData.bottom - 30 + if (tocEleHeight > tocEleBottom) { + tocEle.style.transformOrigin = `right ${tocEleHeight - tocEleBottom - btData.height / 2}px` + } + + tocEle.classList.toggle('open') + tocEle.addEventListener('transitionend', () => { + tocEle.style.cssText = '' + }, { once: true }) + }, + 'chat-btn': () => { // Show chat + window.chatBtnFn() + }, + translateLink: () => { // switch between traditional and simplified chinese + window.translateFn.translatePage() + } + } + + document.getElementById('rightside').addEventListener('click', e => { + const $target = e.target.closest('[id]') + if ($target && rightSideFn[$target.id]) { + rightSideFn[$target.id](e.currentTarget, $target) + } + }) + + /** + * menu + * 側邊欄sub-menu 展開/收縮 + */ + const clickFnOfSubMenu = () => { + const handleClickOfSubMenu = e => { + const target = e.target.closest('.site-page.group') + if (!target) return + target.classList.toggle('hide') + } + + const menusItems = document.querySelector('#sidebar-menus .menus_items') + menusItems && menusItems.addEventListener('click', handleClickOfSubMenu) + } + + /** + * 手机端目录点击 + */ + const openMobileMenu = () => { + const toggleMenu = document.getElementById('toggle-menu') + if (!toggleMenu) return + btf.addEventListenerPjax(toggleMenu, 'click', () => { sidebarFn.open() }) + } + + /** + * 複製時加上版權信息 + */ + const addCopyright = () => { + const { limitCount, languages } = GLOBAL_CONFIG.copyright + + const handleCopy = (e) => { + e.preventDefault() + const copyFont = window.getSelection(0).toString() + let textFont = copyFont + if (copyFont.length > limitCount) { + textFont = `${copyFont}\n\n\n${languages.author}\n${languages.link}${window.location.href}\n${languages.source}\n${languages.info}` + } + if (e.clipboardData) { + return e.clipboardData.setData('text', textFont) + } else { + return window.clipboardData.setData('text', textFont) + } + } + + document.body.addEventListener('copy', handleCopy) + } + + /** + * 網頁運行時間 + */ + const addRuntime = () => { + const $runtimeCount = document.getElementById('runtimeshow') + if ($runtimeCount) { + const publishDate = $runtimeCount.getAttribute('data-publishDate') + $runtimeCount.textContent = `${btf.diffDate(publishDate)} ${GLOBAL_CONFIG.runtime}` + } + } + + /** + * 最後一次更新時間 + */ + const addLastPushDate = () => { + const $lastPushDateItem = document.getElementById('last-push-date') + if ($lastPushDateItem) { + const lastPushDate = $lastPushDateItem.getAttribute('data-lastPushDate') + $lastPushDateItem.textContent = btf.diffDate(lastPushDate, true) + } + } + + /** + * table overflow + */ + const addTableWrap = () => { + const $table = document.querySelectorAll('#article-container table') + if (!$table.length) return + + $table.forEach(item => { + if (!item.closest('.highlight')) { + btf.wrap(item, 'div', { class: 'table-wrap' }) + } + }) + } + + /** + * tag-hide + */ + const clickFnOfTagHide = () => { + const hideButtons = document.querySelectorAll('#article-container .hide-button') + if (!hideButtons.length) return + hideButtons.forEach(item => item.addEventListener('click', e => { + const currentTarget = e.currentTarget + currentTarget.classList.add('open') + addJustifiedGallery(currentTarget.nextElementSibling.querySelectorAll('.gallery-container')) + }, { once: true })) + } + + const tabsFn = () => { + const navTabsElements = document.querySelectorAll('#article-container .tabs') + if (!navTabsElements.length) return + + const setActiveClass = (elements, activeIndex) => { + elements.forEach((el, index) => { + el.classList.toggle('active', index === activeIndex) + }) + } + + const handleNavClick = e => { + const target = e.target.closest('button') + if (!target || target.classList.contains('active')) return + + const navItems = [...e.currentTarget.children] + const tabContents = [...e.currentTarget.nextElementSibling.children] + const indexOfButton = navItems.indexOf(target) + setActiveClass(navItems, indexOfButton) + e.currentTarget.classList.remove('no-default') + setActiveClass(tabContents, indexOfButton) + addJustifiedGallery(tabContents[indexOfButton].querySelectorAll('.gallery-container'), true) + } + + const handleToTopClick = tabElement => e => { + if (e.target.closest('button')) { + btf.scrollToDest(btf.getEleTop(tabElement), 300) + } + } + + navTabsElements.forEach(tabElement => { + btf.addEventListenerPjax(tabElement.firstElementChild, 'click', handleNavClick) + btf.addEventListenerPjax(tabElement.lastElementChild, 'click', handleToTopClick(tabElement)) + }) + } + + const toggleCardCategory = () => { + const cardCategory = document.querySelector('#aside-cat-list.expandBtn') + if (!cardCategory) return + + const handleToggleBtn = e => { + const target = e.target + if (target.nodeName === 'I') { + e.preventDefault() + target.parentNode.classList.toggle('expand') + } + } + btf.addEventListenerPjax(cardCategory, 'click', handleToggleBtn, true) + } + + const addPostOutdateNotice = () => { + const ele = document.getElementById('post-outdate-notice') + if (!ele) return + + const { limitDay, messagePrev, messageNext, postUpdate } = JSON.parse(ele.getAttribute('data')) + const diffDay = btf.diffDate(postUpdate) + if (diffDay >= limitDay) { + ele.textContent = `${messagePrev} ${diffDay} ${messageNext}` + ele.hidden = false + } + } + + const lazyloadImg = () => { + window.lazyLoadInstance = new LazyLoad({ + elements_selector: 'img', + threshold: 0, + data_src: 'lazy-src' + }) + + btf.addGlobalFn('pjaxComplete', () => { + window.lazyLoadInstance.update() + }, 'lazyload') + } + + const relativeDate = selector => { + selector.forEach(item => { + item.textContent = btf.diffDate(item.getAttribute('datetime'), true) + item.style.display = 'inline' + }) + } + + const justifiedIndexPostUI = () => { + const recentPostsElement = document.getElementById('recent-posts') + if (!(recentPostsElement && recentPostsElement.classList.contains('masonry'))) return + + const init = () => { + const masonryItem = new InfiniteGrid.MasonryInfiniteGrid('.recent-post-items', { + gap: { horizontal: 10, vertical: 20 }, + useTransform: true, + useResizeObserver: true + }) + masonryItem.renderItems() + btf.addGlobalFn('pjaxCompleteOnce', () => { masonryItem.destroy() }, 'removeJustifiedIndexPostUI') + } + + typeof InfiniteGrid === 'function' ? init() : btf.getScript(`${GLOBAL_CONFIG.infinitegrid.js}`).then(init) + } + + const unRefreshFn = () => { + window.addEventListener('resize', () => { + adjustMenu(false) + mobileSidebarOpen && btf.isHidden(document.getElementById('toggle-menu')) && sidebarFn.close() + }) + + const menuMask = document.getElementById('menu-mask') + menuMask && menuMask.addEventListener('click', () => { sidebarFn.close() }) + + clickFnOfSubMenu() + GLOBAL_CONFIG.islazyload && lazyloadImg() + GLOBAL_CONFIG.copyright !== undefined && addCopyright() + + if (GLOBAL_CONFIG.autoDarkmode) { + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => { + if (btf.saveToLocal.get('theme') !== undefined) return + e.matches ? handleThemeChange('dark') : handleThemeChange('light') + }) + } + } + + const forPostFn = () => { + addHighlightTool() + addPhotoFigcaption() + addJustifiedGallery(document.querySelectorAll('#article-container .gallery-container')) + runLightbox() + scrollFnToDo() + addTableWrap() + clickFnOfTagHide() + tabsFn() + } + + const refreshFn = () => { + initAdjust() + justifiedIndexPostUI() + + if (GLOBAL_CONFIG_SITE.isPost) { + addPostOutdateNotice() + GLOBAL_CONFIG.relativeDate.post && relativeDate(document.querySelectorAll('#post-meta time')) + } else { + GLOBAL_CONFIG.relativeDate.homepage && relativeDate(document.querySelectorAll('#recent-posts time')) + GLOBAL_CONFIG.runtime && addRuntime() + addLastPushDate() + toggleCardCategory() + } + + GLOBAL_CONFIG_SITE.isHome && scrollDownInIndex() + scrollFn() + + forPostFn() + !GLOBAL_CONFIG_SITE.isShuoshuo && btf.switchComments(document) + openMobileMenu() + } + + btf.addGlobalFn('pjaxComplete', refreshFn, 'refreshFn') + refreshFn() + unRefreshFn() + + // 處理 hexo-blog-encrypt 事件 + window.addEventListener('hexo-blog-decrypt', e => { + forPostFn() + window.translateFn.translateInitialization() + Object.values(window.globalFn.encrypt).forEach(fn => { + fn() + }) + }) +}) diff --git a/js/search/algolia.js b/js/search/algolia.js new file mode 100644 index 00000000..8624f52f --- /dev/null +++ b/js/search/algolia.js @@ -0,0 +1,174 @@ +window.addEventListener('load', () => { + const { algolia } = GLOBAL_CONFIG + const { appId, apiKey, indexName, hitsPerPage = 5, languages } = algolia + + if (!appId || !apiKey || !indexName) { + return console.error('Algolia setting is invalid!') + } + + const $searchMask = document.getElementById('search-mask') + const $searchDialog = document.querySelector('#algolia-search .search-dialog') + + const animateElements = show => { + const action = show ? 'animateIn' : 'animateOut' + const maskAnimation = show ? 'to_show 0.5s' : 'to_hide 0.5s' + const dialogAnimation = show ? 'titleScale 0.5s' : 'search_close .5s' + btf[action]($searchMask, maskAnimation) + btf[action]($searchDialog, dialogAnimation) + } + + const fixSafariHeight = () => { + if (window.innerWidth < 768) { + $searchDialog.style.setProperty('--search-height', `${window.innerHeight}px`) + } + } + + const openSearch = () => { + btf.overflowPaddingR.add() + animateElements(true) + setTimeout(() => { document.querySelector('#algolia-search .ais-SearchBox-input').focus() }, 100) + + const handleEscape = event => { + if (event.code === 'Escape') { + closeSearch() + document.removeEventListener('keydown', handleEscape) + } + } + + document.addEventListener('keydown', handleEscape) + fixSafariHeight() + window.addEventListener('resize', fixSafariHeight) + } + + const closeSearch = () => { + btf.overflowPaddingR.remove() + animateElements(false) + window.removeEventListener('resize', fixSafariHeight) + } + + const searchClickFn = () => { + btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch) + } + + const searchFnOnce = () => { + $searchMask.addEventListener('click', closeSearch) + document.querySelector('#algolia-search .search-close-button').addEventListener('click', closeSearch) + } + + const cutContent = (content) => { + if (!content) return '' + const firstOccur = content.indexOf('') + let start = firstOccur - 30 + let end = firstOccur + 120 + let pre = '' + let post = '' + + if (start <= 0) { + start = 0 + end = 140 + } else { + pre = '...' + } + + if (end > content.length) { + end = content.length + } else { + post = '...' + } + + return `${pre}${content.substring(start, end)}${post}` + } + + const disableDiv = [ + document.getElementById('algolia-hits'), + document.getElementById('algolia-pagination'), + document.querySelector('#algolia-info .algolia-stats') + ] + + const searchClient = typeof algoliasearch === 'function' ? algoliasearch : window['algoliasearch/lite'].liteClient + const search = instantsearch({ + indexName, + searchClient: searchClient(appId, apiKey), + searchFunction (helper) { + disableDiv.forEach(item => { + item.style.display = helper.state.query ? '' : 'none' + }) + if (helper.state.query) helper.search() + } + }) + + const widgets = [ + instantsearch.widgets.configure({ hitsPerPage }), + instantsearch.widgets.searchBox({ + container: '#algolia-search-input', + showReset: false, + showSubmit: false, + placeholder: languages.input_placeholder, + showLoadingIndicator: true + }), + instantsearch.widgets.hits({ + container: '#algolia-hits', + templates: { + item (data) { + const link = data.permalink || (GLOBAL_CONFIG.root + data.path) + const result = data._highlightResult + const content = result.contentStripTruncate + ? cutContent(result.contentStripTruncate.value) + : result.contentStrip + ? cutContent(result.contentStrip.value) + : result.content + ? cutContent(result.content.value) + : '' + return ` + + ${result.title.value || 'no-title'} + ${content ? `
${content}
` : ''} +
` + }, + empty (data) { + return `
${languages.hits_empty.replace(/\$\{query}/, data.query)}
` + } + } + }), + instantsearch.widgets.stats({ + container: '#algolia-info > .algolia-stats', + templates: { + text (data) { + const stats = languages.hits_stats + .replace(/\$\{hits}/, data.nbHits) + .replace(/\$\{time}/, data.processingTimeMS) + return `
${stats}` + } + } + }), + instantsearch.widgets.poweredBy({ + container: '#algolia-info > .algolia-poweredBy' + }), + instantsearch.widgets.pagination({ + container: '#algolia-pagination', + totalPages: 5, + templates: { + first: '', + last: '', + previous: '', + next: '' + } + }) + ] + + search.addWidgets(widgets) + search.start() + searchClickFn() + searchFnOnce() + + window.addEventListener('pjax:complete', () => { + if (!btf.isHidden($searchMask)) closeSearch() + searchClickFn() + }) + + if (window.pjax) { + search.on('render', () => { + window.pjax.refresh(document.getElementById('algolia-hits')) + }) + } +}) diff --git a/js/search/local-search.js b/js/search/local-search.js new file mode 100644 index 00000000..1d3f2687 --- /dev/null +++ b/js/search/local-search.js @@ -0,0 +1,360 @@ +/** + * Refer to hexo-generator-searchdb + * https://github.com/next-theme/hexo-generator-searchdb/blob/main/dist/search.js + * Modified by hexo-theme-butterfly + */ + +class LocalSearch { + constructor ({ + path = '', + unescape = false, + top_n_per_article = 1 + }) { + this.path = path + this.unescape = unescape + this.top_n_per_article = top_n_per_article + this.isfetched = false + this.datas = null + } + + getIndexByWord (words, text, caseSensitive = false) { + const index = [] + const included = new Set() + + if (!caseSensitive) { + text = text.toLowerCase() + } + words.forEach(word => { + if (this.unescape) { + const div = document.createElement('div') + div.innerText = word + word = div.innerHTML + } + const wordLen = word.length + if (wordLen === 0) return + let startPosition = 0 + let position = -1 + if (!caseSensitive) { + word = word.toLowerCase() + } + while ((position = text.indexOf(word, startPosition)) > -1) { + index.push({ position, word }) + included.add(word) + startPosition = position + wordLen + } + }) + // Sort index by position of keyword + index.sort((left, right) => { + if (left.position !== right.position) { + return left.position - right.position + } + return right.word.length - left.word.length + }) + return [index, included] + } + + // Merge hits into slices + mergeIntoSlice (start, end, index) { + let item = index[0] + let { position, word } = item + const hits = [] + const count = new Set() + while (position + word.length <= end && index.length !== 0) { + count.add(word) + hits.push({ + position, + length: word.length + }) + const wordEnd = position + word.length + + // Move to next position of hit + index.shift() + while (index.length !== 0) { + item = index[0] + position = item.position + word = item.word + if (wordEnd > position) { + index.shift() + } else { + break + } + } + } + return { + hits, + start, + end, + count: count.size + } + } + + // Highlight title and content + highlightKeyword (val, slice) { + let result = '' + let index = slice.start + for (const { position, length } of slice.hits) { + result += val.substring(index, position) + index = position + length + result += `${val.substr(position, length)}` + } + result += val.substring(index, slice.end) + return result + } + + getResultItems (keywords) { + const resultItems = [] + this.datas.forEach(({ title, content, url }) => { + // The number of different keywords included in the article. + const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title) + const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content) + const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size + + // Show search results + const hitCount = indexOfTitle.length + indexOfContent.length + if (hitCount === 0) return + + const slicesOfTitle = [] + if (indexOfTitle.length !== 0) { + slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle)) + } + + let slicesOfContent = [] + while (indexOfContent.length !== 0) { + const item = indexOfContent[0] + const { position } = item + // Cut out 120 characters. The maxlength of .search-input is 80. + const start = Math.max(0, position - 20) + const end = Math.min(content.length, position + 100) + slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent)) + } + + // Sort slices in content by included keywords' count and hits' count + slicesOfContent.sort((left, right) => { + if (left.count !== right.count) { + return right.count - left.count + } else if (left.hits.length !== right.hits.length) { + return right.hits.length - left.hits.length + } + return left.start - right.start + }) + + // Select top N slices in content + const upperBound = parseInt(this.top_n_per_article, 10) + if (upperBound >= 0) { + slicesOfContent = slicesOfContent.slice(0, upperBound) + } + + let resultItem = '' + + url = new URL(url, location.origin) + url.searchParams.append('highlight', keywords.join(' ')) + + if (slicesOfTitle.length !== 0) { + resultItem += `
  • ${this.highlightKeyword(title, slicesOfTitle[0])}` + } else { + resultItem += `
  • ${title}` + } + + slicesOfContent.forEach(slice => { + resultItem += `

    ${this.highlightKeyword(content, slice)}...

    ` + }) + + resultItem += '
  • ' + resultItems.push({ + item: resultItem, + id: resultItems.length, + hitCount, + includedCount + }) + }) + return resultItems + } + + fetchData () { + const isXml = !this.path.endsWith('json') + fetch(this.path) + .then(response => response.text()) + .then(res => { + // Get the contents from search data + this.isfetched = true + this.datas = isXml + ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({ + title: element.querySelector('title').textContent, + content: element.querySelector('content').textContent, + url: element.querySelector('url').textContent + })) + : JSON.parse(res) + // Only match articles with non-empty titles + this.datas = this.datas.filter(data => data.title).map(data => { + data.title = data.title.trim() + data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '' + data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/') + return data + }) + // Remove loading animation + window.dispatchEvent(new Event('search:loaded')) + }) + } + + // Highlight by wrapping node in mark elements with the given class name + highlightText (node, slice, className) { + const val = node.nodeValue + let index = slice.start + const children = [] + for (const { position, length } of slice.hits) { + const text = document.createTextNode(val.substring(index, position)) + index = position + length + const mark = document.createElement('mark') + mark.className = className + mark.appendChild(document.createTextNode(val.substr(position, length))) + children.push(text, mark) + } + node.nodeValue = val.substring(index, slice.end) + children.forEach(element => { + node.parentNode.insertBefore(element, node) + }) + } + + // Highlight the search words provided in the url in the text + highlightSearchWords (body) { + const params = new URL(location.href).searchParams.get('highlight') + const keywords = params ? params.split(' ') : [] + if (!keywords.length || !body) return + const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null) + const allNodes = [] + while (walk.nextNode()) { + if (!walk.currentNode.parentNode.matches('button, select, textarea, .mermaid')) allNodes.push(walk.currentNode) + } + allNodes.forEach(node => { + const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue) + if (!indexOfNode.length) return + const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode) + this.highlightText(node, slice, 'search-keyword') + }) + } +} + +window.addEventListener('load', () => { +// Search + const { path, top_n_per_article, unescape, languages } = GLOBAL_CONFIG.localSearch + const localSearch = new LocalSearch({ + path, + top_n_per_article, + unescape + }) + + const input = document.querySelector('#local-search-input input') + const statsItem = document.getElementById('local-search-stats-wrap') + const $loadingStatus = document.getElementById('loading-status') + const isXml = !path.endsWith('json') + + const inputEventFunction = () => { + if (!localSearch.isfetched) return + let searchText = input.value.trim().toLowerCase() + isXml && (searchText = searchText.replace(//g, '>')) + if (searchText !== '') $loadingStatus.innerHTML = '' + const keywords = searchText.split(/[-\s]+/) + const container = document.getElementById('local-search-results') + let resultItems = [] + if (searchText.length > 0) { + // Perform local searching + resultItems = localSearch.getResultItems(keywords) + } + if (keywords.length === 1 && keywords[0] === '') { + container.textContent = '' + statsItem.textContent = '' + } else if (resultItems.length === 0) { + container.textContent = '' + const statsDiv = document.createElement('div') + statsDiv.className = 'search-result-stats' + statsDiv.textContent = languages.hits_empty.replace(/\$\{query}/, searchText) + statsItem.innerHTML = statsDiv.outerHTML + } else { + resultItems.sort((left, right) => { + if (left.includedCount !== right.includedCount) { + return right.includedCount - left.includedCount + } else if (left.hitCount !== right.hitCount) { + return right.hitCount - left.hitCount + } + return right.id - left.id + }) + + const stats = languages.hits_stats.replace(/\$\{hits}/, resultItems.length) + + container.innerHTML = `
      ${resultItems.map(result => result.item).join('')}
    ` + statsItem.innerHTML = `
    ${stats}
    ` + window.pjax && window.pjax.refresh(container) + } + + $loadingStatus.textContent = '' + } + + let loadFlag = false + const $searchMask = document.getElementById('search-mask') + const $searchDialog = document.querySelector('#local-search .search-dialog') + + // fix safari + const fixSafariHeight = () => { + if (window.innerWidth < 768) { + $searchDialog.style.setProperty('--search-height', window.innerHeight + 'px') + } + } + + const openSearch = () => { + btf.overflowPaddingR.add() + btf.animateIn($searchMask, 'to_show 0.5s') + btf.animateIn($searchDialog, 'titleScale 0.5s') + setTimeout(() => { input.focus() }, 300) + if (!loadFlag) { + !localSearch.isfetched && localSearch.fetchData() + input.addEventListener('input', inputEventFunction) + loadFlag = true + } + // shortcut: ESC + document.addEventListener('keydown', function f (event) { + if (event.code === 'Escape') { + closeSearch() + document.removeEventListener('keydown', f) + } + }) + + fixSafariHeight() + window.addEventListener('resize', fixSafariHeight) + } + + const closeSearch = () => { + btf.overflowPaddingR.remove() + btf.animateOut($searchDialog, 'search_close .5s') + btf.animateOut($searchMask, 'to_hide 0.5s') + window.removeEventListener('resize', fixSafariHeight) + } + + const searchClickFn = () => { + btf.addEventListenerPjax(document.querySelector('#search-button > .search'), 'click', openSearch) + } + + const searchFnOnce = () => { + document.querySelector('#local-search .search-close-button').addEventListener('click', closeSearch) + $searchMask.addEventListener('click', closeSearch) + if (GLOBAL_CONFIG.localSearch.preload) { + localSearch.fetchData() + } + localSearch.highlightSearchWords(document.getElementById('article-container')) + } + + window.addEventListener('search:loaded', () => { + const $loadDataItem = document.getElementById('loading-database') + $loadDataItem.nextElementSibling.style.display = 'block' + $loadDataItem.remove() + }) + + searchClickFn() + searchFnOnce() + + // pjax + window.addEventListener('pjax:complete', () => { + !btf.isHidden($searchMask) && closeSearch() + localSearch.highlightSearchWords(document.getElementById('article-container')) + searchClickFn() + }) +}) diff --git a/js/tw_cn.js b/js/tw_cn.js new file mode 100644 index 00000000..c19d69cd --- /dev/null +++ b/js/tw_cn.js @@ -0,0 +1,117 @@ +document.addEventListener('DOMContentLoaded', () => { + const { defaultEncoding, translateDelay, msgToTraditionalChinese, msgToSimplifiedChinese } = GLOBAL_CONFIG.translate + const snackbarData = GLOBAL_CONFIG.Snackbar + const targetEncodingCookie = 'translate-chn-cht' + + let currentEncoding = defaultEncoding + let targetEncoding = Number(btf.saveToLocal.get(targetEncodingCookie)) || defaultEncoding + const translateButtonObject = document.getElementById('translateLink') + const isSnackbar = snackbarData !== undefined + + const setLang = () => { + document.documentElement.lang = targetEncoding === 1 ? 'zh-TW' : 'zh-CN' + } + + const translateText = (txt) => { + if (!txt) return '' + if (currentEncoding === 1 && targetEncoding === 2) return Simplized(txt) + if (currentEncoding === 2 && targetEncoding === 1) return Traditionalized(txt) + return txt + } + + const translateBody = (fobj) => { + const nodes = typeof fobj === 'object' ? fobj.childNodes : document.body.childNodes + + for (const node of nodes) { + // Skip BR, HR tags, or the translate button object + if (['BR', 'HR'].includes(node.tagName) || node === translateButtonObject) continue + + if (node.nodeType === Node.ELEMENT_NODE) { + const { tagName, title, alt, placeholder, value, type } = node + + // Translate title, alt, placeholder + if (title) node.title = translateText(title) + if (alt) node.alt = translateText(alt) + if (placeholder) node.placeholder = translateText(placeholder) + + // Translate input value except text and hidden types + if (tagName === 'INPUT' && value && type !== 'text' && type !== 'hidden') { + node.value = translateText(value) + } + + // Recursively translate child nodes + translateBody(node) + } else if (node.nodeType === Node.TEXT_NODE) { + // Translate text node data + node.data = translateText(node.data) + } + } + } + + const translatePage = () => { + if (targetEncoding === 1) { + currentEncoding = 1 + targetEncoding = 2 + translateButtonObject.textContent = msgToTraditionalChinese + isSnackbar && btf.snackbarShow(snackbarData.cht_to_chs) + } else if (targetEncoding === 2) { + currentEncoding = 2 + targetEncoding = 1 + translateButtonObject.textContent = msgToSimplifiedChinese + isSnackbar && btf.snackbarShow(snackbarData.chs_to_cht) + } + btf.saveToLocal.set(targetEncodingCookie, targetEncoding, 2) + setLang() + translateBody() + } + + const JTPYStr = () => '万与丑专业丛东丝丢两严丧个丬丰临为丽举么义乌乐乔习乡书买乱争于亏云亘亚产亩亲亵亸亿仅从仑仓仪们价众优伙会伛伞伟传伤伥伦伧伪伫体余佣佥侠侣侥侦侧侨侩侪侬俣俦俨俩俪俭债倾偬偻偾偿傥傧储傩儿兑兖党兰关兴兹养兽冁内冈册写军农冢冯冲决况冻净凄凉凌减凑凛几凤凫凭凯击凼凿刍划刘则刚创删别刬刭刽刿剀剂剐剑剥剧劝办务劢动励劲劳势勋勐勚匀匦匮区医华协单卖卢卤卧卫却卺厂厅历厉压厌厍厕厢厣厦厨厩厮县参叆叇双发变叙叠叶号叹叽吁后吓吕吗吣吨听启吴呒呓呕呖呗员呙呛呜咏咔咙咛咝咤咴咸哌响哑哒哓哔哕哗哙哜哝哟唛唝唠唡唢唣唤唿啧啬啭啮啰啴啸喷喽喾嗫呵嗳嘘嘤嘱噜噼嚣嚯团园囱围囵国图圆圣圹场坂坏块坚坛坜坝坞坟坠垄垅垆垒垦垧垩垫垭垯垱垲垴埘埙埚埝埯堑堕塆墙壮声壳壶壸处备复够头夸夹夺奁奂奋奖奥妆妇妈妩妪妫姗姜娄娅娆娇娈娱娲娴婳婴婵婶媪嫒嫔嫱嬷孙学孪宁宝实宠审宪宫宽宾寝对寻导寿将尔尘尧尴尸尽层屃屉届属屡屦屿岁岂岖岗岘岙岚岛岭岳岽岿峃峄峡峣峤峥峦崂崃崄崭嵘嵚嵛嵝嵴巅巩巯币帅师帏帐帘帜带帧帮帱帻帼幂幞干并广庄庆庐庑库应庙庞废庼廪开异弃张弥弪弯弹强归当录彟彦彻径徕御忆忏忧忾怀态怂怃怄怅怆怜总怼怿恋恳恶恸恹恺恻恼恽悦悫悬悭悯惊惧惨惩惫惬惭惮惯愍愠愤愦愿慑慭憷懑懒懔戆戋戏戗战戬户扎扑扦执扩扪扫扬扰抚抛抟抠抡抢护报担拟拢拣拥拦拧拨择挂挚挛挜挝挞挟挠挡挢挣挤挥挦捞损捡换捣据捻掳掴掷掸掺掼揸揽揿搀搁搂搅携摄摅摆摇摈摊撄撑撵撷撸撺擞攒敌敛数斋斓斗斩断无旧时旷旸昙昼昽显晋晒晓晔晕晖暂暧札术朴机杀杂权条来杨杩杰极构枞枢枣枥枧枨枪枫枭柜柠柽栀栅标栈栉栊栋栌栎栏树栖样栾桊桠桡桢档桤桥桦桧桨桩梦梼梾检棂椁椟椠椤椭楼榄榇榈榉槚槛槟槠横樯樱橥橱橹橼檐檩欢欤欧歼殁殇残殒殓殚殡殴毁毂毕毙毡毵氇气氢氩氲汇汉污汤汹沓沟没沣沤沥沦沧沨沩沪沵泞泪泶泷泸泺泻泼泽泾洁洒洼浃浅浆浇浈浉浊测浍济浏浐浑浒浓浔浕涂涌涛涝涞涟涠涡涢涣涤润涧涨涩淀渊渌渍渎渐渑渔渖渗温游湾湿溃溅溆溇滗滚滞滟滠满滢滤滥滦滨滩滪漤潆潇潋潍潜潴澜濑濒灏灭灯灵灾灿炀炉炖炜炝点炼炽烁烂烃烛烟烦烧烨烩烫烬热焕焖焘煅煳熘爱爷牍牦牵牺犊犟状犷犸犹狈狍狝狞独狭狮狯狰狱狲猃猎猕猡猪猫猬献獭玑玙玚玛玮环现玱玺珉珏珐珑珰珲琎琏琐琼瑶瑷璇璎瓒瓮瓯电画畅畲畴疖疗疟疠疡疬疮疯疱疴痈痉痒痖痨痪痫痴瘅瘆瘗瘘瘪瘫瘾瘿癞癣癫癯皑皱皲盏盐监盖盗盘眍眦眬着睁睐睑瞒瞩矫矶矾矿砀码砖砗砚砜砺砻砾础硁硅硕硖硗硙硚确硷碍碛碜碱碹磙礼祎祢祯祷祸禀禄禅离秃秆种积称秽秾稆税稣稳穑穷窃窍窑窜窝窥窦窭竖竞笃笋笔笕笺笼笾筑筚筛筜筝筹签简箓箦箧箨箩箪箫篑篓篮篱簖籁籴类籼粜粝粤粪粮糁糇紧絷纟纠纡红纣纤纥约级纨纩纪纫纬纭纮纯纰纱纲纳纴纵纶纷纸纹纺纻纼纽纾线绀绁绂练组绅细织终绉绊绋绌绍绎经绐绑绒结绔绕绖绗绘给绚绛络绝绞统绠绡绢绣绤绥绦继绨绩绪绫绬续绮绯绰绱绲绳维绵绶绷绸绹绺绻综绽绾绿缀缁缂缃缄缅缆缇缈缉缊缋缌缍缎缏缐缑缒缓缔缕编缗缘缙缚缛缜缝缞缟缠缡缢缣缤缥缦缧缨缩缪缫缬缭缮缯缰缱缲缳缴缵罂网罗罚罢罴羁羟羡翘翙翚耢耧耸耻聂聋职聍联聩聪肃肠肤肷肾肿胀胁胆胜胧胨胪胫胶脉脍脏脐脑脓脔脚脱脶脸腊腌腘腭腻腼腽腾膑臜舆舣舰舱舻艰艳艹艺节芈芗芜芦苁苇苈苋苌苍苎苏苘苹茎茏茑茔茕茧荆荐荙荚荛荜荞荟荠荡荣荤荥荦荧荨荩荪荫荬荭荮药莅莜莱莲莳莴莶获莸莹莺莼萚萝萤营萦萧萨葱蒇蒉蒋蒌蓝蓟蓠蓣蓥蓦蔷蔹蔺蔼蕲蕴薮藁藓虏虑虚虫虬虮虽虾虿蚀蚁蚂蚕蚝蚬蛊蛎蛏蛮蛰蛱蛲蛳蛴蜕蜗蜡蝇蝈蝉蝎蝼蝾螀螨蟏衅衔补衬衮袄袅袆袜袭袯装裆裈裢裣裤裥褛褴襁襕见观觃规觅视觇览觉觊觋觌觍觎觏觐觑觞触觯詟誉誊讠计订讣认讥讦讧讨让讪讫训议讯记讱讲讳讴讵讶讷许讹论讻讼讽设访诀证诂诃评诅识诇诈诉诊诋诌词诎诏诐译诒诓诔试诖诗诘诙诚诛诜话诞诟诠诡询诣诤该详诧诨诩诪诫诬语诮误诰诱诲诳说诵诶请诸诹诺读诼诽课诿谀谁谂调谄谅谆谇谈谊谋谌谍谎谏谐谑谒谓谔谕谖谗谘谙谚谛谜谝谞谟谠谡谢谣谤谥谦谧谨谩谪谫谬谭谮谯谰谱谲谳谴谵谶谷豮贝贞负贠贡财责贤败账货质贩贪贫贬购贮贯贰贱贲贳贴贵贶贷贸费贺贻贼贽贾贿赀赁赂赃资赅赆赇赈赉赊赋赌赍赎赏赐赑赒赓赔赕赖赗赘赙赚赛赜赝赞赟赠赡赢赣赪赵赶趋趱趸跃跄跖跞践跶跷跸跹跻踊踌踪踬踯蹑蹒蹰蹿躏躜躯车轧轨轩轪轫转轭轮软轰轱轲轳轴轵轶轷轸轹轺轻轼载轾轿辀辁辂较辄辅辆辇辈辉辊辋辌辍辎辏辐辑辒输辔辕辖辗辘辙辚辞辩辫边辽达迁过迈运还这进远违连迟迩迳迹适选逊递逦逻遗遥邓邝邬邮邹邺邻郁郄郏郐郑郓郦郧郸酝酦酱酽酾酿释里鉅鉴銮錾钆钇针钉钊钋钌钍钎钏钐钑钒钓钔钕钖钗钘钙钚钛钝钞钟钠钡钢钣钤钥钦钧钨钩钪钫钬钭钮钯钰钱钲钳钴钵钶钷钸钹钺钻钼钽钾钿铀铁铂铃铄铅铆铈铉铊铋铍铎铏铐铑铒铕铗铘铙铚铛铜铝铞铟铠铡铢铣铤铥铦铧铨铪铫铬铭铮铯铰铱铲铳铴铵银铷铸铹铺铻铼铽链铿销锁锂锃锄锅锆锇锈锉锊锋锌锍锎锏锐锑锒锓锔锕锖锗错锚锜锞锟锠锡锢锣锤锥锦锨锩锫锬锭键锯锰锱锲锳锴锵锶锷锸锹锺锻锼锽锾锿镀镁镂镃镆镇镈镉镊镌镍镎镏镐镑镒镕镖镗镙镚镛镜镝镞镟镠镡镢镣镤镥镦镧镨镩镪镫镬镭镮镯镰镱镲镳镴镶长门闩闪闫闬闭问闯闰闱闲闳间闵闶闷闸闹闺闻闼闽闾闿阀阁阂阃阄阅阆阇阈阉阊阋阌阍阎阏阐阑阒阓阔阕阖阗阘阙阚阛队阳阴阵阶际陆陇陈陉陕陧陨险随隐隶隽难雏雠雳雾霁霉霭靓静靥鞑鞒鞯鞴韦韧韨韩韪韫韬韵页顶顷顸项顺须顼顽顾顿颀颁颂颃预颅领颇颈颉颊颋颌颍颎颏颐频颒颓颔颕颖颗题颙颚颛颜额颞颟颠颡颢颣颤颥颦颧风飏飐飑飒飓飔飕飖飗飘飙飚飞飨餍饤饥饦饧饨饩饪饫饬饭饮饯饰饱饲饳饴饵饶饷饸饹饺饻饼饽饾饿馀馁馂馃馄馅馆馇馈馉馊馋馌馍馎馏馐馑馒馓馔馕马驭驮驯驰驱驲驳驴驵驶驷驸驹驺驻驼驽驾驿骀骁骂骃骄骅骆骇骈骉骊骋验骍骎骏骐骑骒骓骔骕骖骗骘骙骚骛骜骝骞骟骠骡骢骣骤骥骦骧髅髋髌鬓魇魉鱼鱽鱾鱿鲀鲁鲂鲄鲅鲆鲇鲈鲉鲊鲋鲌鲍鲎鲏鲐鲑鲒鲓鲔鲕鲖鲗鲘鲙鲚鲛鲜鲝鲞鲟鲠鲡鲢鲣鲤鲥鲦鲧鲨鲩鲪鲫鲬鲭鲮鲯鲰鲱鲲鲳鲴鲵鲶鲷鲸鲹鲺鲻鲼鲽鲾鲿鳀鳁鳂鳃鳄鳅鳆鳇鳈鳉鳊鳋鳌鳍鳎鳏鳐鳑鳒鳓鳔鳕鳖鳗鳘鳙鳛鳜鳝鳞鳟鳠鳡鳢鳣鸟鸠鸡鸢鸣鸤鸥鸦鸧鸨鸩鸪鸫鸬鸭鸮鸯鸰鸱鸲鸳鸴鸵鸶鸷鸸鸹鸺鸻鸼鸽鸾鸿鹀鹁鹂鹃鹄鹅鹆鹇鹈鹉鹊鹋鹌鹍鹎鹏鹐鹑鹒鹓鹔鹕鹖鹗鹘鹚鹛鹜鹝鹞鹟鹠鹡鹢鹣鹤鹥鹦鹧鹨鹩鹪鹫鹬鹭鹯鹰鹱鹲鹳鹴鹾麦麸黄黉黡黩黪黾龙历志制一台皋准复猛钟注范签' + const FTPYStr = () => '萬與醜專業叢東絲丟兩嚴喪個爿豐臨為麗舉麼義烏樂喬習鄉書買亂爭於虧雲亙亞產畝親褻嚲億僅從侖倉儀們價眾優夥會傴傘偉傳傷倀倫傖偽佇體餘傭僉俠侶僥偵側僑儈儕儂俁儔儼倆儷儉債傾傯僂僨償儻儐儲儺兒兌兗黨蘭關興茲養獸囅內岡冊寫軍農塚馮衝決況凍淨淒涼淩減湊凜幾鳳鳧憑凱擊氹鑿芻劃劉則剛創刪別剗剄劊劌剴劑剮劍剝劇勸辦務勱動勵勁勞勢勳猛勩勻匭匱區醫華協單賣盧鹵臥衛卻巹廠廳曆厲壓厭厙廁廂厴廈廚廄廝縣參靉靆雙發變敘疊葉號歎嘰籲後嚇呂嗎唚噸聽啟吳嘸囈嘔嚦唄員咼嗆嗚詠哢嚨嚀噝吒噅鹹呱響啞噠嘵嗶噦嘩噲嚌噥喲嘜嗊嘮啢嗩唕喚呼嘖嗇囀齧囉嘽嘯噴嘍嚳囁嗬噯噓嚶囑嚕劈囂謔團園囪圍圇國圖圓聖壙場阪壞塊堅壇壢壩塢墳墜壟壟壚壘墾坰堊墊埡墶壋塏堖塒塤堝墊垵塹墮壪牆壯聲殼壺壼處備複夠頭誇夾奪奩奐奮獎奧妝婦媽嫵嫗媯姍薑婁婭嬈嬌孌娛媧嫻嫿嬰嬋嬸媼嬡嬪嬙嬤孫學孿寧寶實寵審憲宮寬賓寢對尋導壽將爾塵堯尷屍盡層屭屜屆屬屢屨嶼歲豈嶇崗峴嶴嵐島嶺嶽崠巋嶨嶧峽嶢嶠崢巒嶗崍嶮嶄嶸嶔崳嶁脊巔鞏巰幣帥師幃帳簾幟帶幀幫幬幘幗冪襆幹並廣莊慶廬廡庫應廟龐廢廎廩開異棄張彌弳彎彈強歸當錄彠彥徹徑徠禦憶懺憂愾懷態慫憮慪悵愴憐總懟懌戀懇惡慟懨愷惻惱惲悅愨懸慳憫驚懼慘懲憊愜慚憚慣湣慍憤憒願懾憖怵懣懶懍戇戔戲戧戰戩戶紮撲扡執擴捫掃揚擾撫拋摶摳掄搶護報擔擬攏揀擁攔擰撥擇掛摯攣掗撾撻挾撓擋撟掙擠揮撏撈損撿換搗據撚擄摑擲撣摻摜摣攬撳攙擱摟攪攜攝攄擺搖擯攤攖撐攆擷擼攛擻攢敵斂數齋斕鬥斬斷無舊時曠暘曇晝曨顯晉曬曉曄暈暉暫曖劄術樸機殺雜權條來楊榪傑極構樅樞棗櫪梘棖槍楓梟櫃檸檉梔柵標棧櫛櫳棟櫨櫟欄樹棲樣欒棬椏橈楨檔榿橋樺檜槳樁夢檮棶檢欞槨櫝槧欏橢樓欖櫬櫚櫸檟檻檳櫧橫檣櫻櫫櫥櫓櫞簷檁歡歟歐殲歿殤殘殞殮殫殯毆毀轂畢斃氈毿氌氣氫氬氳彙漢汙湯洶遝溝沒灃漚瀝淪滄渢溈滬濔濘淚澩瀧瀘濼瀉潑澤涇潔灑窪浹淺漿澆湞溮濁測澮濟瀏滻渾滸濃潯濜塗湧濤澇淶漣潿渦溳渙滌潤澗漲澀澱淵淥漬瀆漸澠漁瀋滲溫遊灣濕潰濺漵漊潷滾滯灩灄滿瀅濾濫灤濱灘澦濫瀠瀟瀲濰潛瀦瀾瀨瀕灝滅燈靈災燦煬爐燉煒熗點煉熾爍爛烴燭煙煩燒燁燴燙燼熱煥燜燾煆糊溜愛爺牘犛牽犧犢強狀獷獁猶狽麅獮獰獨狹獅獪猙獄猻獫獵獼玀豬貓蝟獻獺璣璵瑒瑪瑋環現瑲璽瑉玨琺瓏璫琿璡璉瑣瓊瑤璦璿瓔瓚甕甌電畫暢佘疇癤療瘧癘瘍鬁瘡瘋皰屙癰痙癢瘂癆瘓癇癡癉瘮瘞瘺癟癱癮癭癩癬癲臒皚皺皸盞鹽監蓋盜盤瞘眥矓著睜睞瞼瞞矚矯磯礬礦碭碼磚硨硯碸礪礱礫礎硜矽碩硤磽磑礄確鹼礙磧磣堿镟滾禮禕禰禎禱禍稟祿禪離禿稈種積稱穢穠穭稅穌穩穡窮竊竅窯竄窩窺竇窶豎競篤筍筆筧箋籠籩築篳篩簹箏籌簽簡籙簀篋籜籮簞簫簣簍籃籬籪籟糴類秈糶糲粵糞糧糝餱緊縶糸糾紆紅紂纖紇約級紈纊紀紉緯紜紘純紕紗綱納紝縱綸紛紙紋紡紵紖紐紓線紺絏紱練組紳細織終縐絆紼絀紹繹經紿綁絨結絝繞絰絎繪給絢絳絡絕絞統綆綃絹繡綌綏絛繼綈績緒綾緓續綺緋綽緔緄繩維綿綬繃綢綯綹綣綜綻綰綠綴緇緙緗緘緬纜緹緲緝縕繢緦綞緞緶線緱縋緩締縷編緡緣縉縛縟縝縫縗縞纏縭縊縑繽縹縵縲纓縮繆繅纈繚繕繒韁繾繰繯繳纘罌網羅罰罷羆羈羥羨翹翽翬耮耬聳恥聶聾職聹聯聵聰肅腸膚膁腎腫脹脅膽勝朧腖臚脛膠脈膾髒臍腦膿臠腳脫腡臉臘醃膕齶膩靦膃騰臏臢輿艤艦艙艫艱豔艸藝節羋薌蕪蘆蓯葦藶莧萇蒼苧蘇檾蘋莖蘢蔦塋煢繭荊薦薘莢蕘蓽蕎薈薺蕩榮葷滎犖熒蕁藎蓀蔭蕒葒葤藥蒞蓧萊蓮蒔萵薟獲蕕瑩鶯蓴蘀蘿螢營縈蕭薩蔥蕆蕢蔣蔞藍薊蘺蕷鎣驀薔蘞藺藹蘄蘊藪槁蘚虜慮虛蟲虯蟣雖蝦蠆蝕蟻螞蠶蠔蜆蠱蠣蟶蠻蟄蛺蟯螄蠐蛻蝸蠟蠅蟈蟬蠍螻蠑螿蟎蠨釁銜補襯袞襖嫋褘襪襲襏裝襠褌褳襝褲襇褸襤繈襴見觀覎規覓視覘覽覺覬覡覿覥覦覯覲覷觴觸觶讋譽謄訁計訂訃認譏訐訌討讓訕訖訓議訊記訒講諱謳詎訝訥許訛論訩訟諷設訪訣證詁訶評詛識詗詐訴診詆謅詞詘詔詖譯詒誆誄試詿詩詰詼誠誅詵話誕詬詮詭詢詣諍該詳詫諢詡譸誡誣語誚誤誥誘誨誑說誦誒請諸諏諾讀諑誹課諉諛誰諗調諂諒諄誶談誼謀諶諜謊諫諧謔謁謂諤諭諼讒諮諳諺諦謎諞諝謨讜謖謝謠謗諡謙謐謹謾謫譾謬譚譖譙讕譜譎讞譴譫讖穀豶貝貞負貟貢財責賢敗賬貨質販貪貧貶購貯貫貳賤賁貰貼貴貺貸貿費賀貽賊贄賈賄貲賃賂贓資賅贐賕賑賚賒賦賭齎贖賞賜贔賙賡賠賧賴賵贅賻賺賽賾贗讚贇贈贍贏贛赬趙趕趨趲躉躍蹌蹠躒踐躂蹺蹕躚躋踴躊蹤躓躑躡蹣躕躥躪躦軀車軋軌軒軑軔轉軛輪軟轟軲軻轤軸軹軼軤軫轢軺輕軾載輊轎輈輇輅較輒輔輛輦輩輝輥輞輬輟輜輳輻輯轀輸轡轅轄輾轆轍轔辭辯辮邊遼達遷過邁運還這進遠違連遲邇逕跡適選遜遞邐邏遺遙鄧鄺鄔郵鄒鄴鄰鬱郤郟鄶鄭鄆酈鄖鄲醞醱醬釅釃釀釋裏钜鑒鑾鏨釓釔針釘釗釙釕釷釺釧釤鈒釩釣鍆釹鍚釵鈃鈣鈈鈦鈍鈔鍾鈉鋇鋼鈑鈐鑰欽鈞鎢鉤鈧鈁鈥鈄鈕鈀鈺錢鉦鉗鈷缽鈳鉕鈽鈸鉞鑽鉬鉭鉀鈿鈾鐵鉑鈴鑠鉛鉚鈰鉉鉈鉍鈹鐸鉶銬銠鉺銪鋏鋣鐃銍鐺銅鋁銱銦鎧鍘銖銑鋌銩銛鏵銓鉿銚鉻銘錚銫鉸銥鏟銃鐋銨銀銣鑄鐒鋪鋙錸鋱鏈鏗銷鎖鋰鋥鋤鍋鋯鋨鏽銼鋝鋒鋅鋶鐦鐧銳銻鋃鋟鋦錒錆鍺錯錨錡錁錕錩錫錮鑼錘錐錦鍁錈錇錟錠鍵鋸錳錙鍥鍈鍇鏘鍶鍔鍤鍬鍾鍛鎪鍠鍰鎄鍍鎂鏤鎡鏌鎮鎛鎘鑷鐫鎳鎿鎦鎬鎊鎰鎔鏢鏜鏍鏰鏞鏡鏑鏃鏇鏐鐔钁鐐鏷鑥鐓鑭鐠鑹鏹鐙鑊鐳鐶鐲鐮鐿鑔鑣鑞鑲長門閂閃閆閈閉問闖閏闈閑閎間閔閌悶閘鬧閨聞闥閩閭闓閥閣閡閫鬮閱閬闍閾閹閶鬩閿閽閻閼闡闌闃闠闊闋闔闐闒闕闞闤隊陽陰陣階際陸隴陳陘陝隉隕險隨隱隸雋難雛讎靂霧霽黴靄靚靜靨韃鞽韉韝韋韌韍韓韙韞韜韻頁頂頃頇項順須頊頑顧頓頎頒頌頏預顱領頗頸頡頰頲頜潁熲頦頤頻頮頹頷頴穎顆題顒顎顓顏額顳顢顛顙顥纇顫顬顰顴風颺颭颮颯颶颸颼颻飀飄飆飆飛饗饜飣饑飥餳飩餼飪飫飭飯飲餞飾飽飼飿飴餌饒餉餄餎餃餏餅餑餖餓餘餒餕餜餛餡館餷饋餶餿饞饁饃餺餾饈饉饅饊饌饢馬馭馱馴馳驅馹駁驢駔駛駟駙駒騶駐駝駑駕驛駘驍罵駰驕驊駱駭駢驫驪騁驗騂駸駿騏騎騍騅騌驌驂騙騭騤騷騖驁騮騫騸驃騾驄驏驟驥驦驤髏髖髕鬢魘魎魚魛魢魷魨魯魴魺鮁鮃鯰鱸鮋鮓鮒鮊鮑鱟鮍鮐鮭鮚鮳鮪鮞鮦鰂鮜鱠鱭鮫鮮鮺鯗鱘鯁鱺鰱鰹鯉鰣鰷鯀鯊鯇鮶鯽鯒鯖鯪鯕鯫鯡鯤鯧鯝鯢鯰鯛鯨鯵鯴鯔鱝鰈鰏鱨鯷鰮鰃鰓鱷鰍鰒鰉鰁鱂鯿鰠鼇鰭鰨鰥鰩鰟鰜鰳鰾鱈鱉鰻鰵鱅鰼鱖鱔鱗鱒鱯鱤鱧鱣鳥鳩雞鳶鳴鳲鷗鴉鶬鴇鴆鴣鶇鸕鴨鴞鴦鴒鴟鴝鴛鴬鴕鷥鷙鴯鴰鵂鴴鵃鴿鸞鴻鵐鵓鸝鵑鵠鵝鵒鷳鵜鵡鵲鶓鵪鶤鵯鵬鵮鶉鶊鵷鷫鶘鶡鶚鶻鶿鶥鶩鷊鷂鶲鶹鶺鷁鶼鶴鷖鸚鷓鷚鷯鷦鷲鷸鷺鸇鷹鸌鸏鸛鸘鹺麥麩黃黌黶黷黲黽龍歷誌製壹臺臯準復勐鐘註範籤' + + const Traditionalized = (cc) => { + let str = '' + const ss = JTPYStr() + const tt = FTPYStr() + for (let i = 0; i < cc.length; i++) { + if (cc.charCodeAt(i) > 10000 && ss.indexOf(cc.charAt(i)) !== -1) { + str += tt.charAt(ss.indexOf(cc.charAt(i))) + } else str += cc.charAt(i) + } + return str + } + + const Simplized = (cc) => { + let str = '' + const ss = JTPYStr() + const tt = FTPYStr() + for (let i = 0; i < cc.length; i++) { + if (cc.charCodeAt(i) > 10000 && tt.indexOf(cc.charAt(i)) !== -1) { + str += ss.charAt(tt.indexOf(cc.charAt(i))) + } else str += cc.charAt(i) + } + return str + } + + const translateInitialization = () => { + if (translateButtonObject) { + if (currentEncoding !== targetEncoding) { + translateButtonObject.textContent = + targetEncoding === 1 + ? msgToSimplifiedChinese + : msgToTraditionalChinese + setLang() + setTimeout(translateBody, translateDelay) + } + } + } + + window.translateFn = { + translatePage, + Traditionalized, + Simplized, + translateInitialization + } + + translateInitialization() + btf.addGlobalFn('pjaxComplete', translateInitialization, 'translateInitialization') +}) diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 00000000..48d8306d --- /dev/null +++ b/js/utils.js @@ -0,0 +1,313 @@ +(() => { + const btfFn = { + debounce: (func, wait = 0, immediate = false) => { + let timeout + return (...args) => { + const later = () => { + timeout = null + if (!immediate) func(...args) + } + const callNow = immediate && !timeout + clearTimeout(timeout) + timeout = setTimeout(later, wait) + if (callNow) func(...args) + } + }, + + throttle: function (func, wait, options = {}) { + let timeout, context, args + let previous = 0 + + const later = () => { + previous = options.leading === false ? 0 : new Date().getTime() + timeout = null + func.apply(context, args) + if (!timeout) context = args = null + } + + const throttled = (...params) => { + const now = new Date().getTime() + if (!previous && options.leading === false) previous = now + const remaining = wait - (now - previous) + context = this + args = params + if (remaining <= 0 || remaining > wait) { + if (timeout) { + clearTimeout(timeout) + timeout = null + } + previous = now + func.apply(context, args) + if (!timeout) context = args = null + } else if (!timeout && options.trailing !== false) { + timeout = setTimeout(later, remaining) + } + } + + return throttled + }, + + overflowPaddingR: { + add: () => { + const paddingRight = window.innerWidth - document.body.clientWidth + + if (paddingRight > 0) { + document.body.style.paddingRight = `${paddingRight}px` + document.body.style.overflow = 'hidden' + const menuElement = document.querySelector('#page-header.nav-fixed #menus') + if (menuElement) { + menuElement.style.paddingRight = `${paddingRight}px` + } + } + }, + remove: () => { + document.body.style.paddingRight = '' + document.body.style.overflow = '' + const menuElement = document.querySelector('#page-header.nav-fixed #menus') + if (menuElement) { + menuElement.style.paddingRight = '' + } + } + }, + + snackbarShow: (text, showAction = false, duration = 2000) => { + const { position, bgLight, bgDark } = GLOBAL_CONFIG.Snackbar + const bg = document.documentElement.getAttribute('data-theme') === 'light' ? bgLight : bgDark + Snackbar.show({ + text, + backgroundColor: bg, + showAction, + duration, + pos: position, + customClass: 'snackbar-css' + }) + }, + + diffDate: (inputDate, more = false) => { + const dateNow = new Date() + const datePost = new Date(inputDate) + const diffMs = dateNow - datePost + const diffSec = diffMs / 1000 + const diffMin = diffSec / 60 + const diffHour = diffMin / 60 + const diffDay = diffHour / 24 + const diffMonth = diffDay / 30 + const { dateSuffix } = GLOBAL_CONFIG + + if (!more) return Math.floor(diffDay) + + if (diffMonth > 12) return datePost.toISOString().slice(0, 10) + if (diffMonth >= 1) return `${Math.floor(diffMonth)} ${dateSuffix.month}` + if (diffDay >= 1) return `${Math.floor(diffDay)} ${dateSuffix.day}` + if (diffHour >= 1) return `${Math.floor(diffHour)} ${dateSuffix.hour}` + if (diffMin >= 1) return `${Math.floor(diffMin)} ${dateSuffix.min}` + return dateSuffix.just + }, + + loadComment: (dom, callback) => { + if ('IntersectionObserver' in window) { + const observerItem = new IntersectionObserver((entries) => { + if (entries[0].isIntersecting) { + callback() + observerItem.disconnect() + } + }, { threshold: [0] }) + observerItem.observe(dom) + } else { + callback() + } + }, + + scrollToDest: (pos, time = 500) => { + const currentPos = window.scrollY + const isNavFixed = document.getElementById('page-header').classList.contains('fixed') + if (currentPos > pos || isNavFixed) pos = pos - 70 + + if ('scrollBehavior' in document.documentElement.style) { + window.scrollTo({ + top: pos, + behavior: 'smooth' + }) + return + } + + const startTime = performance.now() + const animate = currentTime => { + const timeElapsed = currentTime - startTime + const progress = Math.min(timeElapsed / time, 1) + window.scrollTo(0, currentPos + (pos - currentPos) * progress) + if (progress < 1) { + requestAnimationFrame(animate) + } + } + requestAnimationFrame(animate) + }, + + animateIn: (ele, animation) => { + ele.style.display = 'block' + ele.style.animation = animation + }, + + animateOut: (ele, animation) => { + const handleAnimationEnd = () => { + ele.style.display = '' + ele.style.animation = '' + ele.removeEventListener('animationend', handleAnimationEnd) + } + ele.addEventListener('animationend', handleAnimationEnd) + ele.style.animation = animation + }, + + wrap: (selector, eleType, options) => { + const createEle = document.createElement(eleType) + for (const [key, value] of Object.entries(options)) { + createEle.setAttribute(key, value) + } + selector.parentNode.insertBefore(createEle, selector) + createEle.appendChild(selector) + }, + + isHidden: ele => ele.offsetHeight === 0 && ele.offsetWidth === 0, + + getEleTop: ele => { + let actualTop = ele.offsetTop + let current = ele.offsetParent + + while (current !== null) { + actualTop += current.offsetTop + current = current.offsetParent + } + + return actualTop + }, + + loadLightbox: ele => { + const service = GLOBAL_CONFIG.lightbox + + if (service === 'medium_zoom') { + mediumZoom(ele, { background: 'var(--zoom-bg)' }) + } + + if (service === 'fancybox') { + Array.from(ele).forEach(i => { + if (i.parentNode.tagName !== 'A') { + const dataSrc = i.dataset.lazySrc || i.src + const dataCaption = i.title || i.alt || '' + btf.wrap(i, 'a', { href: dataSrc, 'data-fancybox': 'gallery', 'data-caption': dataCaption, 'data-thumb': dataSrc }) + } + }) + + if (!window.fancyboxRun) { + Fancybox.bind('[data-fancybox]', { + Hash: false, + Thumbs: { + showOnStart: false + }, + Images: { + Panzoom: { + maxScale: 4 + } + }, + Carousel: { + transition: 'slide' + }, + Toolbar: { + display: { + left: ['infobar'], + middle: [ + 'zoomIn', + 'zoomOut', + 'toggle1to1', + 'rotateCCW', + 'rotateCW', + 'flipX', + 'flipY' + ], + right: ['slideshow', 'thumbs', 'close'] + } + } + }) + window.fancyboxRun = true + } + } + }, + + setLoading: { + add: ele => { + const html = ` +
    +
    +
    +
    +
    + ` + ele.insertAdjacentHTML('afterend', html) + }, + remove: ele => { + ele.nextElementSibling.remove() + } + }, + + updateAnchor: anchor => { + if (anchor !== window.location.hash) { + if (!anchor) anchor = location.pathname + const title = GLOBAL_CONFIG_SITE.title + window.history.replaceState({ + url: location.href, + title + }, title, anchor) + } + }, + + getScrollPercent: (() => { + let docHeight, winHeight, headerHeight, contentMath + + return (currentTop, ele) => { + if (!docHeight || ele.clientHeight !== docHeight) { + docHeight = ele.clientHeight + winHeight = window.innerHeight + headerHeight = ele.offsetTop + contentMath = Math.max(docHeight - winHeight, document.documentElement.scrollHeight - winHeight) + } + + const scrollPercent = (currentTop - headerHeight) / contentMath + return Math.max(0, Math.min(100, Math.round(scrollPercent * 100))) + } + })(), + + addEventListenerPjax: (ele, event, fn, option = false) => { + ele.addEventListener(event, fn, option) + btf.addGlobalFn('pjaxSendOnce', () => { + ele.removeEventListener(event, fn, option) + }) + }, + + removeGlobalFnEvent: (key, parent = window) => { + const globalFn = parent.globalFn || {} + const keyObj = globalFn[key] + if (!keyObj) return + + Object.keys(keyObj).forEach(i => keyObj[i]()) + + delete globalFn[key] + }, + + switchComments: (el = document, path) => { + const switchBtn = el.querySelector('#switch-btn') + if (!switchBtn) return + + let switchDone = false + const postComment = el.querySelector('#post-comment') + const handleSwitchBtn = () => { + postComment.classList.toggle('move') + if (!switchDone && typeof loadOtherComment === 'function') { + switchDone = true + loadOtherComment(el, path) + } + } + btf.addEventListenerPjax(switchBtn, 'click', handleSwitchBtn) + } + } + + window.btf = { ...window.btf, ...btfFn } +})() diff --git a/link/index.html b/link/index.html new file mode 100644 index 00000000..cdbc18a0 --- /dev/null +++ b/link/index.html @@ -0,0 +1,407 @@ +友情链接 | JCAlways + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 00000000..91c4eb6e --- /dev/null +++ b/manifest.json @@ -0,0 +1 @@ +{"name":"JCAlways","short_name":"JCAlways","theme_color":"#ffffff","background_color":"#ffffff","display":"standalone","scope":"/","start_url":"/","icons":[{"sizes":"192x192","src":"https://www.zhangsifan.com/android-chrome-192x192.png","type":"image/png"},{"sizes":"512x512","src":"https://www.zhangsifan.com/android-chrome-512x512.png","type":"image/png"}],"splash_pages":null} \ No newline at end of file diff --git a/movies/index.html b/movies/index.html new file mode 100644 index 00000000..5ae83891 --- /dev/null +++ b/movies/index.html @@ -0,0 +1,283 @@ +我看过的电影 | JCAlways + + + + + + + + + +
    我看过的电影
    + + + +
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/2/index.html b/page/2/index.html new file mode 100644 index 00000000..4401aa7b --- /dev/null +++ b/page/2/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    Vue3 El-upload 开启上传文件夹功能
    vue.draggable vue3-拖拽排序组件
    Vue3使用高德地图
    Nprogress使用教程
    Fingerprintjs使用教程
    TS基础
    React基础
    Uni-App开发项目
    微信小程序基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/3/index.html b/page/3/index.html new file mode 100644 index 00000000..bd6ab87c --- /dev/null +++ b/page/3/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    VueX
    Vue的钩子函数
    Vue-Cli的使用
    Vue基础
    MVVM原理
    Jquery的常见问题
    Promise
    Node操作MySQL
    24道JavaScript算法题
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/4/index.html b/page/4/index.html new file mode 100644 index 00000000..b50c0551 --- /dev/null +++ b/page/4/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    JavaScript常见问题
    Node中的会话技术
    Node中的跨域
    ArrayBuffer
    Class 的基本语法
    Class 的继承
    ECMAScript 6 简介
    Generator 函数的异步应用
    Generator 函数的语法
    Mixin
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/5/index.html b/page/5/index.html new file mode 100644 index 00000000..bd9ece5b --- /dev/null +++ b/page/5/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    Iterator 和 for...of 循环
    Module 的加载实现
    Module 的语法
    Promise 对象
    Proxy
    Reflect
    SIMD
    Set 和 Map 数据结构
    Symbol
    async 函数
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/6/index.html b/page/6/index.html new file mode 100644 index 00000000..86533889 --- /dev/null +++ b/page/6/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    let 和 const 命令
    修饰器
    函数式编程
    函数的扩展
    变量的解构赋值
    参考链接
    字符串的扩展
    对象的扩展
    数值的扩展
    数组的扩展
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/7/index.html b/page/7/index.html new file mode 100644 index 00000000..b289171e --- /dev/null +++ b/page/7/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    最新提案
    正则的扩展
    编程风格
    读懂 ECMAScript 规格
    Express框架
    Node基础
    Hexo博客搭建流程
    JS高级
    JQuery基础
    Web Apis
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/page/8/index.html b/page/8/index.html new file mode 100644 index 00000000..801212fe --- /dev/null +++ b/page/8/index.html @@ -0,0 +1,279 @@ +JCAlways + + + + + + + + + + + +
    JS基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20220903nr/index.html b/posts/20220903nr/index.html new file mode 100644 index 00000000..595d0df2 --- /dev/null +++ b/posts/20220903nr/index.html @@ -0,0 +1,214 @@ +Nprogress使用教程 | JCAlways + + + + + + + + + + + + + +

    Nprogress使用教程

    官方文档

    +

    安装

    1
    npm install nprogress
    + +

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import "nprogress/nprogress.css";
    import NProgress from "nprogress";
    NProgress.configure({ showSpinner: false });
    // 显示进度条
    NProgress.start();
    // 隐藏进度条
    NProgress.done();

    NProgress.set(0.0); // Sorta same as .start()
    NProgress.set(0.4);
    NProgress.set(1.0); // Sorta same as .done()
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20220903nr/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20220903sg/index.html b/posts/20220903sg/index.html new file mode 100644 index 00000000..db12824f --- /dev/null +++ b/posts/20220903sg/index.html @@ -0,0 +1,217 @@ +Fingerprintjs使用教程 | JCAlways + + + + + + + + + + + + + +

    Fingerprintjs使用教程

    官方网站

    +

    安装

    1
    npm i @fingerprintjs/fingerprintjs
    + +

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    import FingerprintJS from "@fingerprintjs/fingerprintjs";

    const getFingerPrintID = async () => {
    const fpPromise = await FingerprintJS.load();
    const result = await fpPromise.get();
    return result.visitorId;
    },

    setTimeout(() => {
    console.log(await getFingerPrintID())
    }, 1000);
    + +

    示例

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20220903sg/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20220905ap/index.html b/posts/20220905ap/index.html new file mode 100644 index 00000000..86d8c111 --- /dev/null +++ b/posts/20220905ap/index.html @@ -0,0 +1,214 @@ +Vue3使用高德地图 | JCAlways + + + + + + + + + + + + + +

    Vue3使用高德地图

    官方文档

    +

    安装 Loader

    1
    npm i @amap/amap-jsapi-loader
    + +

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
     <!-- MyMap.vue -->
    <template>
    <div :id="state.id"></div>
    </template>
    <script setup lang="ts">
    import { onMounted, reactive } from "vue";
    import AMapLoader from "@amap/amap-jsapi-loader";
    const props = defineProps({
    modelValue: null, // 坐标
    });
    const state = reactive({
    id: "",
    map: null as any,
    });
    const initMap = () => {
    state.id =
    Math.random().toString(36).substring(7) + Math.floor(Math.random() * 100);
    AMapLoader.load({
    key: "", // 申请好的Web端开发者Key,首次调用 load 时必填
    version: "2.0", // 指定要加载的 JSAPI 的版本
    plugins: ["AMap.Scale", "AMap.Marker"], // 需要使用的的插件列表
    })
    .then((AMap) => {
    state.map = new AMap.Map(state.id, {
    //设置地图容器id
    viewMode: "3D", //是否为3D地图模式
    zoom: 18, //初始化地图级别
    center: props.modelValue, //初始化地图中心点位置
    doubleClickZoom: false, // 禁止双击放大地图
    layers: [],
    });
    if (state.map) {
    // 比例尺
    const scale = new AMap.Scale({
    visible: true,
    });
    state.map.addControl(scale);
    // 点
    const marker = new AMap.Marker({
    position: props.modelValue,
    });
    state.map.add(marker);
    }
    })
    .catch((e) => {
    console.log(e);
    });
    };
    onMounted(() => {
    initMap();
    });
    </script>
    <style lang="less" scoped>
    #container {
    padding: 0px;
    margin: 0px;
    width: 100%;
    height: 800px;
    }
    </style>
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20220905ap/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20220909px/index.html b/posts/20220909px/index.html new file mode 100644 index 00000000..f314f16a --- /dev/null +++ b/posts/20220909px/index.html @@ -0,0 +1,222 @@ +vue.draggable vue3-拖拽排序组件 | JCAlways + + + + + + + + + + + + + + +

    vue.draggable vue3-拖拽排序组件

    官方文档

    +

    安装

    1
    2
    3
    yarn add vuedraggable@next

    npm i -S vuedraggable@next
    + +

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    <template>
    <draggable
    :list="state.form_list"
    ghost-class="ghost"
    :force-fallback="true"
    chosen-class="chosenClass"
    animation="200"
    @start="onStart"
    @end="onEnd"
    >
    <template #item="{ element, index }">
    <div class="item_box">{{ element }}{{ index }}</div>
    </template>
    </draggable>
    </template>
    <script setup lang="ts">
    import draggable from "vuedraggable";
    import { reactive, onMounted } from "vue";

    const state = reactive({
    form_list: [{ title: "肯德基" }, { title: "麦当劳" }],
    });
    //开始拖拽事件
    const onStart = () => {};
    //结束拖拽事件
    const onEnd = () => {};
    onMounted(() => {});
    </script>
    <style lang="less" scoped></style>
    + +

    示例

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20220909px/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20220915up/index.html b/posts/20220915up/index.html new file mode 100644 index 00000000..86032b73 --- /dev/null +++ b/posts/20220915up/index.html @@ -0,0 +1,218 @@ +Vue3 El-upload 开启上传文件夹功能 | JCAlways + + + + + + + + + + + + + +

    Vue3 El-upload 开启上传文件夹功能

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    <template>
    <el-upload
    multiple
    action="http://192.168.1.8:3030/file/upload"
    :auto-upload="false"
    :show-file-list="false"
    :on-change="handleChange"
    >
    <template #trigger>
    <el-button type="primary" @click="folderMode(false)">上传文件</el-button>
    <el-button type="primary" @click="folderMode(true)">上传文件夹</el-button>
    </template>
    </el-upload>
    </template>
    <script setup lang="ts">
    import { reactive, onMounted, nextTick } from "vue";
    import type { UploadFile, UploadFiles } from "element-plus";
    const state = reactive({
    uploadEle: null as Element | null,
    uploadList: [],
    });
    const folderMode = (type: boolean) => {
    if (state.uploadEle) {
    state.uploadEle.webkitdirectory = type;
    }
    };
    const handleChange = async (
    uploadFile: UploadFile,
    uploadFiles: UploadFiles
    ) => {
    console.log(uploadFile, uploadFiles);
    };
    onMounted(() => {
    nextTick(() => {
    state.uploadEle = document.querySelector(".el-upload__input");
    });
    });
    </script>
    <style lang="less" scoped></style>
    + +

    示例

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20220915up/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20221010bg/index.html b/posts/20221010bg/index.html new file mode 100644 index 00000000..6503cf8b --- /dev/null +++ b/posts/20221010bg/index.html @@ -0,0 +1,232 @@ +WPS WEB Office 前端使用教程 | JCAlways + + + + + + + + + + + + + +

    WPS WEB Office 前端使用教程

    相关链接

    +

    下载 JS-SDK

    在使用之前,请先下载最新版本的 js-sdk 代码。

    +

    引用 JS-SDK

      +
    • 非模块化
    • +
    +
    1
    <script src="web-office-sdk.umd.js"></script>
    + +
      +
    • CommonJS 规范
    • +
    +
    1
    let WebOfficeSDK = require("./web-office-sdk.cjs.js");
    + +
      +
    • 非模块化
    • +
    +
    1
    import WebOfficeSDK from "./web-office-sdk.es.js";
    + +

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <template>
    <div class="wps"><div class="custom-mount"></div></div>
    </template>
    <script setup lang="ts">
    import { onMounted } from "vue";
    import WebOfficeSDK from './web-office-sdk.es.js';
    const init = async () => {
    const url = ''; // 后端提供的链接
    const token = "";
    const wps = WebOfficeSDK.config({
    url: url,
    mount: document.querySelector(".custom-mount")!,
    });
    wps.setToken({
    token: token,
    timeout: 10000,
    });
    };
    onMounted(() => {
    init();
    });
    </script>
    <style lang="less" scoped>
    .wps {
    width: 100%;
    height: 900px;
    background-color: #66ccff;
    .custom-mount {
    width: 100%;
    height: 100%;
    }
    }
    </style>
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20221010bg/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20221021ov/index.html b/posts/20221021ov/index.html new file mode 100644 index 00000000..7c8e5a85 --- /dev/null +++ b/posts/20221021ov/index.html @@ -0,0 +1,217 @@ +利用 kkFileView 实现在线预览文件 | JCAlways + + + + + + + + + + + + + + +

    利用 kkFileView 实现在线预览文件

    官方文档
    KK 开源社区

    +

    Docker 容器环境运行

    1
    2
    3
    4
    5
    # 拉取镜像
    docker pull keking/kkfileview
    # 运行
    docker run -it -p 8012:8012 keking/kkfileview
    # 浏览器访问容器8012端口 http://172.0.0.1:8012 即可看到项目演示用首页
    + +

    前端使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <script
    type="text/javascript"
    src="https://gcore.jsdelivr.net/npm/js-base64@3.6.0/base64.min.js"
    ></script>;

    const previewUrl = "http://127.0.0.1:8080/file/test.txt"; //要预览文件的访问地址
    window.open(
    "http://127.0.0.1:8012/onlinePreview?url=" +
    encodeURIComponent(Base64.encode(previewUrl))
    );
    + +

    示例

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20221021ov/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20230329gt/index.html b/posts/20230329gt/index.html new file mode 100644 index 00000000..f3f8de07 --- /dev/null +++ b/posts/20230329gt/index.html @@ -0,0 +1,263 @@ +GeeTest4.0使用教程 | JCAlways + + + + + + + + + + + + + +

    GeeTest4.0使用教程

    官方网站

    +

    示例

    +
    + + +

    前端

    +

    准备工作:确保已经在极验用户后台获取到了 captchaId

    +
    +

    配置参数

    +

    1.引入初始化函数

    1
    <script src="https://static.geetest.com/v4/gt4.js"></script>
    + +

    2.初始化

    1
    <div id="captcha"></div>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    initGeetest4(
    {
    captchaId: "您的captchaId",
    nativeButton: {
    width: "300px",
    height: "40px",
    }, // 极验按钮样式设置
    userInfo: "user@geetest.com", // 用户信息
    },
    function (captcha) {
    // captcha为验证码实例
    captcha.appendTo("#captcha"); // 调用appendTo将验证码插入到页的某一个元素中,这个元素用户可以自定义
    }
    );
    + +

    3.二次验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    initGeetest4(
    {
    // 省略配置参数
    },
    function (captchaObj) {
    // 省略其他方法的调用

    // 这里调用了 onSuccess 方法,该方法介绍见下文
    captchaObj.onSuccess(function () {
    var result = captchaObj.getValidate();

    // ajax 伪代码
    $.ajax({
    url: "服务端",
    data: result,
    dataType: "json",
    success: function (res) {
    console.log(res.result);
    },
    });
    });
    }
    );
    + +

    重置

    1
    captchaObj.reset();
    + +

    后端

    +

    后端使用 Nodejs + Express

    +
    +

    官方 Demo

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    var express = require("express");
    var querystring = require("querystring");
    const crypto = require("crypto");
    var axios = require("axios");
    var router = express.Router();

    // geetest 公钥
    // geetest public key
    const CAPTCHA_ID = "";

    // geetest 密钥
    // geetest secret key
    const CAPTCHA_KEY = "";

    // geetest 服务地址
    // geetest server url
    const API_SERVER = "http://gcaptcha4.geetest.com";

    // geetest 验证接口
    // geetest server interface
    const API_URL = API_SERVER + "/validate" + "?captcha_id=" + CAPTCHA_ID;

    /* GET home page. */
    router.get("/", function (req, res, next) {
    res.render("index");
    });

    router.get("/login", function (req, res, next) {
    req.query = querystring.parse(req.url.split("?")[1]);
    // 前端参数
    // web parameter
    var lot_number = req.query["lot_number"];
    var captcha_output = req.query["captcha_output"];
    var pass_token = req.query["pass_token"];
    var gen_time = req.query["gen_time"];

    // 生成签名, 使用标准的hmac算法,使用用户当前完成验证的流水号lot_number作为原始消息message,使用客户验证私钥作为key
    // 采用sha256散列算法将message和key进行单向散列生成最终的 “sign_token” 签名
    // use lot_number + CAPTCHA_KEY, generate the signature
    var sign_token = hmac_sha256_encode(lot_number, CAPTCHA_KEY);

    // 向极验转发前端数据 + “sign_token” 签名
    // send web parameter and “sign_token” to geetest server
    var datas = {
    lot_number: lot_number,
    captcha_output: captcha_output,
    pass_token: pass_token,
    gen_time: gen_time,
    sign_token: sign_token,
    };

    // post request
    // 根据极验返回的用户验证状态, 网站主进行自己的业务逻辑
    // According to the user authentication status returned by the geetest, the website owner carries out his own business logic
    post_form(datas, API_URL)
    .then((result) => {
    if (result["result"] == "success") {
    console.log("validate success");
    res.send("success");
    } else {
    console.log("validate fail:" + result["reason"]);
    res.send("fail");
    }
    })
    .catch((err) => {
    // 当请求Geetest服务接口出现异常,应放行通过,以免阻塞正常业务。
    // When the request geetest service interface is abnormal, it shall be released to avoid blocking normal business.
    console.log("Geetest server error:" + err);
    res.send("success");
    });
    });

    // 生成签名
    // Generate signature
    function hmac_sha256_encode(value, key) {
    var hash = crypto
    .createHmac("sha256", key)
    .update(value, "utf8")
    .digest("hex");
    return hash;
    }

    // 发送post请求, 响应json数据如:{"result": "success", "reason": "", "captcha_args": {}}
    // Send a post request and respond to JSON data, such as: {result ":" success "," reason ":" "," captcha_args ": {}}
    async function post_form(datas, url) {
    var options = {
    url: url,
    method: "POST",
    params: datas,
    timeout: 5000,
    };

    var result = await axios(options);

    if (result.status != 200) {
    // geetest服务响应异常
    // geetest service response exception
    console.log("Geetest Response Error, StatusCode:" + result.status);
    throw new Error("Geetest Response Error");
    }
    return result.data;
    }

    module.exports = router;
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20230329gt/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20230330gt/index.html b/posts/20230330gt/index.html new file mode 100644 index 00000000..8454db06 --- /dev/null +++ b/posts/20230330gt/index.html @@ -0,0 +1,265 @@ +GeeTest3.0使用教程 | JCAlways + + + + + + + + + + + + + +

    GeeTest3.0使用教程

    官方网站

    +

    示例

    +
    + + +

    前端

    +

    准备工作:确保已经在极验用户后台获取到了 captchaId

    +
    +

    配置参数

    +

    1.引入初始化函数

    1
    <script src="https://static.geetest.com/static/js/gt.0.4.9.js"></script>
    + +

    2.初始化

    1
    <div id="captcha"></div>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    ajax({
    url: "API1接口(详见服务端部署)",
    type: "get",
    dataType: "json",
    success: function (data) {
    //请检测data的数据结构, 保证data.gt, data.challenge, data.success有值
    initGeetest(
    {
    // 以下配置参数来自服务端 SDK
    gt: data.gt,
    challenge: data.challenge,
    offline: !data.success,
    new_captcha: true,
    },
    function (captchaObj) {
    captchaObj.appendTo("#captcha");
    }
    );
    },
    });
    + +

    3.二次验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    initGeetest(
    {
    // 省略配置参数
    },
    function (captchaObj) {
    // 省略其他方法的调用
    captchaObj.onSuccess(function () {
    var result = captchaObj.getValidate();
    // ajax 伪代码
    $.ajax({
    url: "服务端",
    data: result,
    dataType: "json",
    success: function (res) {
    console.log(res.result);
    },
    });
    });
    }
    );
    + +

    重置

    1
    captchaObj.reset();
    + +

    后端

    +

    后端使用 Nodejs + Express

    +
    +

    官方 Demo

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20230330gt/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20230505tr/index.html b/posts/20230505tr/index.html new file mode 100644 index 00000000..f26c734e --- /dev/null +++ b/posts/20230505tr/index.html @@ -0,0 +1,236 @@ +Tauri使用教程 | JCAlways + + + + + + + + + + + + + +

    Tauri使用教程

    官方网站

    +

    Tauri 是什么?

    Tauri 是一个构建适用于所有主流桌面和移动平台的轻快二进制文件的框架。开发者们可以集成任何用于创建用户界面的可以被编译成 HTML、JavaScript 和 CSS 的前端框架,同时可以在必要时使用 Rust、Swift 和 Kotlin 等语言编写后端逻辑。

    +

    前置要求

    系统依赖项

    Windows

    Tauri 使用 Microsoft C++ 构建工具进行开发以及 Microsoft Edge WebView2。这些都是在 Windows 上进行开发所必需的。

    +

    按照以下步骤安装所需的依赖项。

    +

    Microsoft C++ 构建工具

      +
    • 下载 Microsoft C++ 构建工具 安装程序并打开它以开始安装。
    • +
    • 在安装过程中,选中“使用 C++ 进行桌面开发”选项。
    • +
    +

    Visual Studio C++ 构建工具 安装程序 截图

    +

    WebView2

    +

    WebView 2 已安装在 Windows 10(从版本 1803 开始)和更高版本的 Windows 上。如果你正在这些版本之一上进行开发,则可以跳过此步骤并直接转到 下载并安装 Rust。

    +
    +

    Tauri 使用 Microsoft Edge WebView2 在 Windows 上呈现内容。

    +

    通过访问 WebView2 Runtime 下载区 安装 WebView2。下载“常青版独立安装程序(Evergreen Boostrapper)”并安装它。

    +

    MacOS

    Tauri 使用 Xcode 以及各种 macOS 和 iOS 开发依赖项。

    +

    从以下位置之一下载并安装 Xcode:

    + +

    请务必在安装后启动 Xcode,以使它完成设置。

    +

    下载并安装 Rust

    Tauri 使用 Rust 构建并需要它进行开发。使用以下方法之一安装 Rust。你可以在 https://www.rust-lang.org/tools/install 查看更多安装方法。

    +

    Linux/MacOs

    1
    curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh
    + +

    Windows

    前往 https://www.rust-lang.org/tools/install 下载 rustup。

    +

    创建项目

    使用 create-tauri-app

    1
    2
    3
    4
    5
    6
    npm create tauri-app@latest


    cd tauri-app
    npm install
    npm run tauri dev
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20230505tr/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/20230806pa/index.html b/posts/20230806pa/index.html new file mode 100644 index 00000000..55214aa1 --- /dev/null +++ b/posts/20230806pa/index.html @@ -0,0 +1,228 @@ +通行密钥开发 Passkey | JCAlways + + + + + + + + + + + + + +

    通行密钥开发 Passkey

    使用文档

    +
    +

    网站通过使用通行密钥代替密码,可提高用户帐号的安全性并简化用户帐号的管理和使用。借助通行密钥,用户可以使用设备的屏幕锁定功能(例如指纹锁、人脸识别锁或设备 PIN 码)来登录网站或应用。必须先创建通行密钥、将其与用户帐号关联,并将其公钥存储在服务器上,之后用户才能使用该通行密钥进行登录。

    +
    +

    示例

    +

    示例网站

    +
    + + +

    代码实现

    前端代码

    安装依赖库

    1
    npm install @simplewebauthn/browser
    + +

    后端需实现的接口

    注册验证器

    1
    2
    3
    从依赖方(您的服务器)获取注册选项 (/passkey/generate-registration-options)
    将身份验证者的回复提交给依赖方进行验证 (/passkey/verify-registration)

    + +

    使用验证器

    1
    2
    从依赖方(您的服务器)获取身份验证选项 (/passkey/generate-authentication-options)
    将身份验证者的回复提交给依赖方进行验证 (/passkey/verify-authentication)
    + +

    示例代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    <template>
    <div class="container">
    <el-input
    v-model.trim="state.uniid"
    style="width: 300px"
    placeholder="请输入用户唯一标识符/邮箱账号"
    />
    <el-button @click="init" :disabled="!state.uniid">创建通行密钥</el-button>
    <el-button @click="login">通过通行密钥登录</el-button>
    </div>
    </template>
    <script setup lang="ts">
    import {
    reactive,
    onMounted,
    getCurrentInstance,
    type ComponentInternalInstance,
    } from "vue";
    import {
    startRegistration,
    startAuthentication,
    } from "@simplewebauthn/browser";
    import { ElMessage } from "element-plus";
    const currentInstance = getCurrentInstance() as ComponentInternalInstance;
    const { $UtilsHttp } = currentInstance.appContext.config.globalProperties;
    const state = reactive({
    uniid: "",
    });
    const init = async () => {
    try {
    const { result } = await $UtilsHttp(
    "/passkey/generate-registration-options",
    "get",
    {
    uniid: state.uniid,
    }
    );
    const challenge = result.challenge;
    let attResp;
    try {
    attResp = await startRegistration(result);
    } catch (error) {
    if (error.name === "InvalidStateError") {
    ElMessage.error(
    "Authenticator was probably already registered by user"
    );
    } else {
    ElMessage.error(error.toString());
    }
    throw error;
    }
    try {
    const { result } = await $UtilsHttp(
    "/passkey/verify-registration",
    "post",
    {
    attResp,
    challenge,
    }
    );
    if (result) ElMessage.success("已成功添加通行密钥(PassKey)!");
    } catch (error) {
    ElMessage.error(error.response.data.error);
    }
    } catch (error) {
    ElMessage.error(error.response.data.message);
    }
    };
    const login = async () => {
    const { result } = await $UtilsHttp(
    "/passkey/generate-authentication-options",
    "get"
    );
    const challenge = result.challenge;
    let asseResp;
    try {
    asseResp = await startAuthentication(result);
    } catch (error) {
    ElMessage.error(error.toString());
    throw error;
    }
    try {
    const {
    result: { flag, token },
    } = await $UtilsHttp("/passkey/verify-authentication", "post", {
    asseResp,
    challenge,
    });
    if (flag) ElMessage.success("使用通行密钥(PassKey)登录成功!!");
    } catch (error) {
    ElMessage.error(error.response.data.message);
    }
    };
    onMounted(() => {});
    </script>
    <style lang="less" scoped></style>

    + +

    后端代码

    安装依赖库

    1
    npm install @simplewebauthn/server
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20230806pa/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/20240417re/index.html b/posts/20240417re/index.html new file mode 100644 index 00000000..e1e2d185 --- /dev/null +++ b/posts/20240417re/index.html @@ -0,0 +1,295 @@ +Google reCAPTCHA使用教程 | JCAlways + + + + + + + + + + + + + +

    Google reCAPTCHA使用教程

    官方网站

    +

    基于得分 (v3)

    示例

    + +
    正在自动验证中...
    + + +

    以编程方式调用验证方式

    +

    如果您希望更好地控制 reCAPTCHA 的运行时间,可以在 grecaptcha 对象中使用 execute 方法。为此,您需要在 reCAPTCHA 脚本加载中添加 render 参数。

    +
    +
      +
    • 使用您的网站密钥加载 JavaScript API。

      +
      1
      2
      3
      <script src="https://www.google.com/recaptcha/api.js?render=reCAPTCHA_site_key"></script>
      // 如无法连接 Google 服务器,请使用以下地址。
      <script src="https://recaptcha.net/recaptcha/api.js?render=reCAPTCHA_site_key"></script>
    • +
    • 针对您要保护的每项操作调用 grecaptcha.execute。

      +
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      function onClick(e) {
      e.preventDefault();
      grecaptcha.ready(function () {
      grecaptcha
      .execute("reCAPTCHA_site_key", { action: "submit" })
      .then(function (token) {
      // Add your logic to submit to your backend server here.
      });
      });
      }
      +
    • +
    • 在您的后端验证 reCAPTCHA 令牌。

      +
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      const {
      "success": true|false, // whether this request was a valid reCAPTCHA token for your site
      "score": number // the score for this request (0.0 - 1.0)
      "action": string // the action name for this request (important to verify)
      "challenge_ts": timestamp, // timestamp of the challenge load (ISO format yyyy-MM-dd'T'HH:mm:ssZZ)
      "hostname": string, // the hostname of the site where the reCAPTCHA was solved
      "error-codes": [...] // optional
      } = await fetch("https://www.google.com/recaptcha/api/siteverify", {
      method: "POST",
      headers: {
      "Content-Type": "application/x-www-form-urlencoded; charset=utf-8",
      },
      body: `secret=your_secret&response=${token}`,
      })
    • +
    +

    基于验证方式 (v2)

    示例

    + +
    + + +

    代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    <html>
    <head>
    <title>reCAPTCHA</title>
    <script
    src="https://www.recaptcha.net/recaptcha/api.js"
    async
    defer
    ></script>
    </head>
    <body>
    <div id="html_element"></div>
    <script>
    function open() {
    grecaptcha.render("html_element", {
    sitekey: "6Lf2irwpAAAAAIfBI_Bjo7TAccpnUAPsiI01rF7x",
    callback: async function (response) {
    await fetch("https://api.zhangsifan.com/google/getReCAPTCHA", {
    method: "POST",
    body: JSON.stringify({
    token: response,
    id: "6Lf2irwpAAAAAIfBI_Bjo7TAccpnUAPsiI01rF7x",
    }),
    headers: {
    "Content-Type": "application/json",
    },
    }).then(async (response) => {
    const { result } = await response.json();
    if (result.success) {
    if (window.confirm("验证成功,是否重置验证器?")) {
    grecaptcha.reset();
    }
    } else {
    window.alert("验证失败,是否重置验证器?");
    grecaptcha.reset();
    }
    });
    },
    });
    }
    setTimeout(() => open(), 3000);
    </script>
    </body>
    </html>
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20240417re/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/20240924wg/index.html b/posts/20240924wg/index.html new file mode 100644 index 00000000..c39e197c --- /dev/null +++ b/posts/20240924wg/index.html @@ -0,0 +1,215 @@ +风灵月影下载 | JCAlways + + + + + + + + + + + + + +

    风灵月影下载

    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20240924wg/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/20241017dv/index.html b/posts/20241017dv/index.html new file mode 100644 index 00000000..7f7eb55d --- /dev/null +++ b/posts/20241017dv/index.html @@ -0,0 +1,274 @@ +PageSpy使用教程 | JCAlways + + + + + + + + + + + + + +

    PageSpy使用教程

    官方网站

    +

    什么是 PageSpy?

    PageSpy 是一款兼容 Web / 小程序 / React Native / 鸿蒙 App 等平台项目的开源调试平台。基于对原生 API 的封装,它将调用原生方法时的参数进行过滤、转化,整理成一定格式的消息供调试端消费;调试端收到消息数据后,提供类似本地控制台的功能界面将数据呈现出来。

    +

    为什么是 PageSpy?

    +

    一图胜千言。

    +
    +

    +

    何时使用?

    任何无法在本地使用控制台调试的场景,都是 PageSpy 可以大显身手的时候! 一起来看下面的几个场景案例:

    +
      +
    • 本地调试 H5、Webview 应用:以往有些产品提供了可以在 H5 上查看信息的面板,但移动端屏幕太小操作不便、显示不友好,以及信息被截断等问题;
    • +
    • 远程办公、跨地区协同:传统沟通方式如邮件、电话、视频会议等,沟通效率不高、故障信息不全面,容易误解误判;
    • +
    • 用户终端上出现白屏问题:传统定位问题的方式包括数据监控、日志分析等,这些方式依赖排障人员要理解业务需求场景、技术实现;
    • +
    +

    PageSpy 的目标,就是为包括以上场景的人员提供帮助。

    +

    服务部署

    使用 Docker 部署

    1
    docker run -d --restart=always -v ./log:/app/log -v ./data:/app/data -p 6752:6752 --name="pageSpy" ghcr.io/huolalatech/page-spy-web:latest
    + +

    视频学习

    + +

    客户端部署

    浏览器

    导入 SDK

    1
    2
    3
    4
    5
    6
    <!-- PageSpy SDK -->
    <script crossorigin="anonymous" src="https://<your-pagespy-host>/page-spy/index.min.js"></script>

    <!-- 插件(非必须,但建议使用) -->
    <script crossorigin="anonymous" src="https://<your-pagespy-host>/plugin/data-harbor/index.min.js"></script>
    <script crossorigin="anonymous" src="https://<your-pagespy-host>/plugin/rrweb/index.min.js"></script>
    + +

    初始化 PageSpy 和插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    window.$harbor = new DataHarborPlugin();
    window.$rrweb = new RRWebPlugin();

    [window.$harbor, window.$rrweb].forEach((p) => {
    PageSpy.registerPlugin(p);
    });

    window.$pageSpy = new PageSpy({
    // 配置项
    api?: string;
    clientOrigin?: string;

    // "project" is an aggregation of information that can be searched in the room list on the debug side.
    // default: 'default'
    project?: string;

    // "title" is a user-defined parameter that can be used to distinguish the current debugging client,
    // and the corresponding information is displayed under the "device id" in each debugging connection panel.
    // default: '--'
    title?: string;

    // Indicates whether the SDK will automatically render the "Circle with Logo on White Background"
    // control in the bottom left corner of the client when initiation is complete. If set to false,
    // you can call window.$pageSpy.render() to render it manually.
    // default: true
    autoRender?: boolean;

    // Manually specify the scheme of the PageSpy service.
    // This works if the SDK can't correctly analyse the scheme, e.g. if PageSpy's browser plugin
    // is introduced into the SDK via chrome-extension://xxx/sdk/index.min.js, which will be
    // be parsed by the SDK as an invalid "chrome-extension://" and fallback to ["http://", "ws://"].
    // - (Default) Pass the value undefined or null: the SDK will parse it automatically;
    // - Pass boolean value:
    // - true: the SDK will access the PageSpy service via ["https://", "wss://"].
    // - false: the SDK will access the PageSpy service via ["http://", "wss://"]
    enableSSL?: boolean | null;

    // All internal plugins are carried with PageSpy by default out of the box.
    // You can disable some plugins as needed.
    disabledPlugins?: (InternalPlugins | string)[];

    // After adding support for offline replay in PageSpy@1.7.4, the client-integrated SDK can work without
    // establishing a connection with the debugger.
    // Default value is false, when users set it to other values will enters "offline mode", where PageSpy
    // will not create rooms or establish WebSocket connections.
    offline?: boolean;

    // Customize logo source url.
    logo?: string;

    // Customize logo style.
    logoStyle?: Object;
    });
    + +

    小程序

    导入 SDK

    在项目中安装依赖。我们提供了几种小程序平台的 SDK,请根据需要安装:

    +
      +
    • 微信小程序
    • +
    +
    1
    yarn add @huolala-tech/page-spy-wechat@latest
    + +
      +
    • 支付宝小程序
    • +
    +
    1
    yarn add @huolala-tech/page-spy-alipay@latest
    + +
      +
    • UniAPP
    • +
    +
    1
    yarn add @huolala-tech/page-spy-uniapp@latest
    + +
      +
    • Taro
    • +
    +
    1
    yarn add @huolala-tech/page-spy-taro@latest
    + +

    配置白名单

    将 PageSpy 服务域名填入小程序的 http、websocket 请求白名单中。注意除了开发环境,小程序强制要求使用 https 和 wss 协议:

    +
    1
    2
    3
    https://<your-pagespy-host>
    wss://<your-pagespy-host></your-pagespy-host
    ></your-pagespy-host>
    + +

    初始化 PageSpy 和插件

    在入口文件中引入 SDK 并实例化,初始化参数提供了可选的 配置项 用于自定义 SDK 的行为:

    +
    1
    2
    3
    4
    5
    import PageSpy from "@huolala-tech/page-spy-wechat";

    const $pageSpy = new PageSpy({
    api: "<your-pagespy-host>",
    });
    + +

    鸿蒙 App

    导入 SDK

    在待调试 HAP 目录下安装依赖:

    +
    1
    2
    3
    4
    5
    # API 9
    ohpm install @huolala/page-spy-harmony@^1.0.0

    # API 11
    ohpm install @huolala/page-spy-harmony@^2
    + +

    初始化 PageSpy 和插件

    在合适的位置引入 SDK 并初始化,这里以 EntryAbility 为例。初始化参数提供了可选的 配置项 用于自定义 SDK 的行为:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { PageSpy } from "@huolala/page-spy-harmony";
    import axiosInstance from "path/to/your/axios-instance";

    export default class EntryAbility extends UIAbility {
    onWindowStageCreate(windowStage: window.WindowStage) {
    new PageSpy({
    context: this.context,
    api: "<your-pagespy-host>",
    enableSSL: true,
    axios: axiosInstance,
    });
    }
    }
    + +

    常用 API

    更新初始化参数

    PageSpy 提供了 Device ID 用于识别设备,同时还提供了 project / title 供开发者在初始化时自定义信息,用于辅助识别客户端。但你可能希望在初始化之后更新这些参数信息,操作方式如下:

    +
    1
    2
    // 调用 updateRoomInfo 可以更新 project / title
    window.$pageSpy.updateRoomInfo({ project: "xxx", title: "xxx" });
    + +

    显示隐藏按钮

    1
    2
    3
    4
    window.$pageSpy = new PageSpy({
    // ... 其他配置参数
    autoRender: false,
    });
    + +

    显示

    1
    window.$pageSpy.render();
    + +

    隐藏/销毁

    1
    window.$pageSpy.abort();
    + +

    常见问题

    预览页面 CORS

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    server
    {
    # 设置CORS
    # 指定响应资源是否允许与给定的 origin 共享
    add_header Access-Control-Allow-Origin '*';
    add_header Access-Control-Allow-Credentials 'true';
    # 配置允许跨域的请求方法
    add_header Access-Control-Allow-Methods 'GET,POST,PUT,DELETE,Options';
    # 配置允许跨域的请求头
    # add_header Access-Control-Allow-Headers 'Authorization,Content-Type,Accept,Origin,User-Agent,Cache-Control,X-Mx-ReqToken,X-Requested-With';
    }
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/20241017dv/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/DOC-24\351\201\223JavaScript\347\256\227\346\263\225/index.html" "b/posts/DOC-24\351\201\223JavaScript\347\256\227\346\263\225/index.html" new file mode 100644 index 00000000..81a4b1d0 --- /dev/null +++ "b/posts/DOC-24\351\201\223JavaScript\347\256\227\346\263\225/index.html" @@ -0,0 +1,279 @@ +24道JavaScript算法题 | JCAlways + + + + + + + + + + + + + +

    24道JavaScript算法题

    标准排序

    第一种

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var arr = [5, 8, 3, 6, 9];
    for (var i = 0; i < arr.length; i++) {
    for (var j = i + 1; j < arr.length; j++) {
    var temp;
    if (arr[i] > arr[j]) {
    temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    }
    }
    }
    console.log(arr);
    + +

    第二种:冒泡排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var arr = [5, 4, 6, 1, 3, 2];
    for (var i = 0; i < arr.length; i++) {
    for (var j = 0; j < arr.length - i + 1; j++) {
    var temp;
    if (arr[j] > arr[j + 1]) {
    temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }
    console.log(arr);
    + +

    字符串出现最多个数(2 种)

    第一种

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //str=”abcdefgaaass”找字符中出现最多的;第一种
    var str = "abcdefgaaass";
    var newarr = str.split("");
    var max = 0,
    val = "";
    function fn(arr, a) {
    var count = 0;
    for (var i = 0; i < arr.length; i++) {
    if (arr[i] == a) {
    count++;
    }
    }
    return count;
    }
    for (var i = 0; i < newarr.length; i++) {
    var ind = fn(newarr, newarr[i]);
    if (ind > max) {
    max = ind;
    val = newarr[i];
    }
    }
    console.log("出现最多的字符是" + val + "出现的次数是" + max);
    + +

    第二种

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var strr = "aaasssssbbbcccccccccccccccccccccc";
    function change(arr) {
    for (var j = 0, len = 0, str1 = ""; j < arr.length; j++) {
    var x = arr.substr(j, 1);
    var y = arr.split(x);
    if (y.length - 1 > len) {
    len = y.length - 1;
    str1 = x + "," + len;
    }
    }
    return str1;
    }
    console.log(change(strr));
    + +

    编程实现,往数组里插入一个元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var arr = ["1411A", "44", "34305", "djg", "pic"];
    function fun3(ar, index, date) {
    for (var i = ar.length - 1; i >= index; i--) {
    ar[i + 1] = ar[i];
    }
    ar[index] = date;
    }
    fun3(arr, 2, "hello");
    console.log(arr);
    + +

    编程实现 IndexOf 方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var brr = [5, 9, 6, 3, 2, 5];
    function indexfun(arr, val) {
    for (var i = 0; i < arr.length; i++) {
    if (arr[i] == val) {
    return i;
    }
    }
    return -1;
    }
    console.log(indexfun(brr, 13));
    + +

    求数组中最大值和最小值

    1
    2
    3
    4
    5
    6
    7
    8
    var arr = [2, 41, 3, 1, 8];
    var temp = arr[0];
    for (var i = 0; i < arr.length; i++) {
    if (arr[i] < temp) {
    temp = arr[i];
    }
    }
    console.log(temp);
    + +

    将字符串转换为驼峰形式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var str = "border-bottom-color";
    function isstr(str) {
    var arr = str.split("-");
    var one = arr[0];
    for (var i = 1; i < arr.length; i++) {
    one += arr[i].charAt(0).toUpperCase() + arr[i].substring(1);
    }
    return one;
    }
    console.log(isstr(str));
    + +

    var str=’a2b4admin3’字符的每个数字都乘 2 ,成为’a4b8admin6’

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var url = "a2b4admin3";
    var str11 = "";
    for (var i = 0; i < url.length; i++) {
    if (!isNaN(url[i])) {
    str11 = str11 + url[i] * 2;
    } else {
    str11 = str11 + url[i];
    }
    }
    console.log(str11);
    + +

    var str=’a2b4admin3’将字符串中的数字用中括号括起来 ‘a[2]b[4]admin[3]’

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function num(str) {
    var arr = str.split("");
    for (var i = 0; i < arr.length; i++) {
    if (!isNaN(arr[i])) {
    arr[i] = "[" + arr[i] + "]";
    }
    }
    return arr.join("");
    }
    console.log(num(url));
    + +

    首先判断在数组中哪个数最大,然后让数组中的第一个数与最大的数相乘,返回结果

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function maxn(arr) {
    var big = arr[0];
    for (var i = 0; i < arr.length; i++) {
    if (arr[i] > big) {
    big = arr[i];
    }
    }
    return big * arr[0];
    }
    console.log(maxn([2, 1, 80, 6, 10]));
    + +

    var str=’abc’ 让字符串重复 3 遍,成为 str=’abcabcabc’

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var str = "abc";
    function xun(a, b) {
    var newstr = "";
    for (var i = 1; i <= b; i++) {
    newstr += a;
    }
    return newstr;
    }
    console.log(xun(str, 3));
    + +

    10 到 100 的十位随机数并排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    for (var i = 1; i <= 10; i++) {
    var ran = Math.floor(Math.random() * 91 + 10);
    arr.push(ran);
    }
    console.log(
    arr.sort(function (a, b) {
    return a - b;
    })
    );
    + +

    写出一个段脚本,输出当前日期 5 天之后是星期几,三种方法

    第一种

    1
    2
    3
    4
    5
    var now = new Date();
    var day = now.getDate();
    var week = ["日", "一", "二", "三", "四", "五", "六"];
    now.setDate(day + 5);
    console.log("星期" + week[now.getDay()]);
    + +

    第二种

    1
    2
    3
    4
    5
    6
    7
    var now = new Date();
    var year = now.getFullYear();
    var month = now.getMonth();
    var date = now.getDate();
    var week = ["日", "一", "二", "三", "四", "五", "六"];
    var fulture = new Date(year, month, date + 5);
    console.log("5天之后是星期" + week[fulture.getDay()]);
    + +

    第三种

    1
    2
    3
    4
    5
    6
    7
    var now=new Date();
    var time=now.getTime();
    var fulture=new Date();
    var ftime=time+24*60*60*1000*5;
    var week=['日','一','二','三','四','五','六'];
    fulture.setTime(ftime);
    console.log('5天之后是星期'+week[fulture.getDay()])*/
    + +

    封装一个 n 天之后是星期几的函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function getday(n) {
    n = typeof n === "undefined" ? 0 : n;
    var now = new Date();
    var time = now.getTime();
    var fulture = new Date();
    var ftime = time + 24 * 60 * 60 * 1000 * n;
    fulture.setTime(ftime);
    return fulture.getDay();
    }
    console.log(getday());
    + +

    “wellcome to beijing”把第一个变大写返回’WellcomeToBeijing’

    1
    //和上题相同。驼峰
    + +

    封装一个方法,实现求任意多个数的平均值。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function f1() {
    //用一个变量接收和
    var temp = 0;
    //循环每个参数
    for (var i = 0; i < arguments.length; i++) {
    temp += arguments[i];
    }
    //返回平均数
    return temp / arguments.length;
    }
    console.log(f1(1, 2, 3, 4, 9));
    + +

    判断字符串是否对称

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var str1 = "abch6g5g6hcba";
    aba;
    function isduic(str) {
    for (var i = 0, len = str1.length; i < len / 2; i++) {
    alert(str[i]);
    if (str[i] != str[len - 1 - i]) {
    str.charAt(i) != str.charAt(len - 1 - i);
    return "不对称";
    }
    }
    return "对称";
    }
    console.log(isduic(str1));
    + +

    千分符

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var str2=46548978911;
    function qianff(str){
    var strn=str.toString(),
    len=strn.length,
    ind=len%3,
    console.log(strn);
    newstr=strn.substr(0,ind);
    console.log(newstr);
    if(ind==0){
    newstr=strn.substr(0,3);
    ind=3;
    console.log(newstr);
    }
    for(i=ind;i<len;i=i+3){
    newstr+=","+strn.substr(i,3);
    }
    return newstr;
    }
    console.log(qianff(str2))
    + +

    随机生成十六进制的颜色值 var arr=[“0”,”2”,”3”,”4”,”5”,”6”,”7”,”8”,”9”,”a”,”b”,”c”,”d”]

    1
    2
    3
    4
    5
    6
    7
    8
    var arrHex = ["0", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d"];
    var fuhao = "#";
    for (var i = 0; i < 6; i++) {
    var color = arrHex[Math.floor(Math.random() * arrHex.length)];
    fuhao = fuhao + color;
    str = str + arrHex[a];
    }
    console.log(fuhao);
    + +

    随机从数组中取出三个不同的值。var arr = [1,2,3,4,5,6,7,8,9];

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
    var brr = [];
    for (var i = 0; brr.length < 3; i++) {
    var a = Math.floor(Math.random() * arr.length);
    var b = arr[a];
    if (brr.indexOf(b) == -1) {
    brr.push(b);
    }
    }
    console.log(brr);
    + +

    求数组中字符串的个数

    1
    2
    3
    4
    5
    var str=[0,1,2,3,4,5,"a","b","c","b"],sum=0;
    function strn(arr){
    for(var i=0;i<arr.length;i++){
    if(typeof(arr[i])=="string"){
    sum+
    + +

    求数组中字符串的个数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var str = [0, 1, 2, 3, 4, 5, "a", "b", "c", "b"],
    sum = 0;
    function strn(arr) {
    for (var i = 0; i < arr.length; i++) {
    if (typeof arr[i] == "string") {
    sum++;
    }
    }
    return sum;
    }
    console.log(strn(str));
    + +

    删除数组中指定的值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var drr = ["1411A", "44", "34305", "djg", "pic", 100, 35, 28];
    function fun5(rr, num) {
    for (var i = 0; i < rr.length; i++) {
    if (rr[i] == num) {
    rr.splice(i, 1);
    break;
    }
    }
    var newrr = rr.splice(num, 1);
    return rr;
    }
    console.log(fun5(drr, 1));
    + +

    把重复的值放入一个新的数组中 这中方法过于繁琐,可以使用 indexOf 方法,等于-1 判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var hrr = ["55", "55", "2", "1"];
    function fun7(rra, num) {
    var a = 0;
    for (var i = 0; i < rra.length; i++) {
    if (rra[i] == num) {
    a++;
    }
    }
    return a;
    }
    var rrb = [];
    for (var i = 0; i < hrr.length; i++) {
    var length = fun7(hrr, hrr[i]);
    if (length > 1) {
    rrb.push(hrr[i]);
    }
    }
    console.log(rrb);
    + +

    截取字符串的 var s=”abcdefg”;截取完成后实现反转功能:g,f,e 其实等同于数组翻转

    第一种

    1
    2
    3
    4
    var str = "hello";
    var arr = str.split("");
    var rts = arr.reverse();
    console.log(rts);
    + +

    第二种

    1
    2
    3
    4
    5
    6
    var str = "hello";
    var strn = "";
    for (var i = str.length; i >= 0; i--) {
    strn += str[i];
    }
    console.log(strn);
    + +

    第三种

    1
    2
    3
    4
    5
    6
    var str = "hello",
    arr = [];
    for (var i = 0; i < str.length; i++) {
    arr.unshift(str[i]);
    }
    console.log(arr);
    + +

    拓展一个方法,用于删除数组中的指定值。同 21 题一样

    编程实现数组中有几个字符串元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var brr = ["1411A", "44", "34305", "djg", "pic", 100, 35, 28];
    function fun4(crr) {
    var count = 0;
    for (var i = 0; i < crr.length; i++) {
    if (typeof crr[i] == "string") {
    count++;
    }
    }
    return count;
    }
    fun4(brr);
    + +

    数组反转

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var s = "abcdefghiklmn";
    function fun6(str) {
    var ss = s.splice("");
    var ss = s.split("");
    var ssa = [];
    for (var i = 0; i < ss.length; i++) {
    ssa.unshift(ss[i]);
    }
    return ssa.join("");
    }
    fun6(s);
    + +

    将所有数组内所有重复的值取出来放到一个新的数组中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var st = "123abcdefgaaaasssssssssssssss123",
    srn = "",
    cou = 0;
    var str = st.split("");
    function stn(srr, strn) {
    var count = 0;
    for (var i = 0; i < srr.length; i++) {
    if (srr[i] == strn) {
    count++;
    }
    }
    return count;
    }
    for (var i = 0; i < str.length; i++) {
    var anr = stn(str, str[i]);
    if (anr > cou) {
    cou = anr;
    srn = str[i];
    }
    }
    console.log("出现最多的字母是" + srn + "次数" + cou);
    + +

    求出一组数中的最大值和最小值并返回俩数的积

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var arr = [45, 234, 24, 34, 35, 4, 6, 57, 65];
    function maxValue(arr) {
    //假设一个最大值
    var max = 0,
    min = 999999999999;
    //遍历数组
    for (var i = 0; i < arr.length; i++) {
    //判断当前值是否大于max
    if (max < arr[i]) {
    max = arr[i]; //234
    }
    //判断当前值是否小min
    if (min > arr[i]) {
    min = arr[i]; //4
    }
    }
    return max * min;
    }
    console.log(maxValue(arr));
    + +

    10 到 100 的十位随机数并升序排序(不允许重复)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function fn() {
    //定义新数组
    var arr = [];
    while (arr.length < 10) {
    //随机获取10-100之间的随机数
    var val = Math.floor(Math.random() * 91 + 10);
    //判断获取的随机数 是否出现在新数组中
    if (arr.indexOf(val) == -1) {
    //将不重复的值添加到新数组中
    arr.push(val);
    }
    }
    //返回升序排序的数组
    return arr.sort(function (x, y) {
    return x - y;
    });
    }
    console.log(fn());
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/DOC-24%E9%81%93JavaScript%E7%AE%97%E6%B3%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    目录
    1. 1. 标准排序
      1. 1.1. 第一种
      2. 1.2. 第二种:冒泡排序
    2. 2. 字符串出现最多个数(2 种)
      1. 2.1. 第一种
      2. 2.2. 第二种
    3. 3. 编程实现,往数组里插入一个元素
    4. 4. 编程实现 IndexOf 方法
    5. 5. 求数组中最大值和最小值
    6. 6. 将字符串转换为驼峰形式
    7. 7. var str=’a2b4admin3’字符的每个数字都乘 2 ,成为’a4b8admin6’
    8. 8. var str=’a2b4admin3’将字符串中的数字用中括号括起来 ‘a[2]b[4]admin[3]’
    9. 9. 首先判断在数组中哪个数最大,然后让数组中的第一个数与最大的数相乘,返回结果
    10. 10. var str=’abc’ 让字符串重复 3 遍,成为 str=’abcabcabc’
    11. 11. 10 到 100 的十位随机数并排序
    12. 12. 写出一个段脚本,输出当前日期 5 天之后是星期几,三种方法
      1. 12.1. 第一种
      2. 12.2. 第二种
      3. 12.3. 第三种
    13. 13. 封装一个 n 天之后是星期几的函数
    14. 14. “wellcome to beijing”把第一个变大写返回’WellcomeToBeijing’
    15. 15. 封装一个方法,实现求任意多个数的平均值。
    16. 16. 判断字符串是否对称
    17. 17. 千分符
    18. 18. 随机生成十六进制的颜色值 var arr=[“0”,”2”,”3”,”4”,”5”,”6”,”7”,”8”,”9”,”a”,”b”,”c”,”d”]
    19. 19. 随机从数组中取出三个不同的值。var arr = [1,2,3,4,5,6,7,8,9];
    20. 20. 求数组中字符串的个数
    21. 21. 求数组中字符串的个数
    22. 22. 删除数组中指定的值
    23. 23. 把重复的值放入一个新的数组中 这中方法过于繁琐,可以使用 indexOf 方法,等于-1 判断
    24. 24. 截取字符串的 var s=”abcdefg”;截取完成后实现反转功能:g,f,e 其实等同于数组翻转
      1. 24.1. 第一种
      2. 24.2. 第二种
      3. 24.3. 第三种
    25. 25. 拓展一个方法,用于删除数组中的指定值。同 21 题一样
    26. 26. 编程实现数组中有几个字符串元素
    27. 27. 数组反转
    28. 28. 将所有数组内所有重复的值取出来放到一个新的数组中
    29. 29. 求出一组数中的最大值和最小值并返回俩数的积
    30. 30. 10 到 100 的十位随机数并升序排序(不允许重复)
    最新文章
    \ No newline at end of file diff --git a/posts/DOC-ESLint/index.html b/posts/DOC-ESLint/index.html new file mode 100644 index 00000000..6cd4b8c6 --- /dev/null +++ b/posts/DOC-ESLint/index.html @@ -0,0 +1,210 @@ +ESLint | JCAlways + + + + + + + + + + + + + +

    ESLint

    vscode中的vscode的配置代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
    "language": "vue",
    "autoFix": true
    }
    ],
    "eslint.autoFixOnSave": true,
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/DOC-ESLint/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/DOC-Hexo\345\215\232\345\256\242\346\220\255\345\273\272\346\265\201\347\250\213/index.html" "b/posts/DOC-Hexo\345\215\232\345\256\242\346\220\255\345\273\272\346\265\201\347\250\213/index.html" new file mode 100644 index 00000000..87c29263 --- /dev/null +++ "b/posts/DOC-Hexo\345\215\232\345\256\242\346\220\255\345\273\272\346\265\201\347\250\213/index.html" @@ -0,0 +1,269 @@ +Hexo博客搭建流程 | JCAlways + + + + + + + + + + + + + +

    Hexo博客搭建流程

    安装 hexo

    1
    npm install hexo -g
    + +

    hexo 常用指令

    1
    2
    3
    4
    hexo clean # 清空hexo,主要删除Hexo根目录下的public文件夹
    hexo g # 重新成功public文件夹内容
    hexo s # 启动本地hexo服务
    hexo d # 发布到远程仓库
    + +

    初始化 hexo

    1
    hexo init
    + +

    安装 deploter-git 插件

    1
    npm install hexo-deployer-git
    + +

    Hexo 发布到 github 的时候,会丢失 reanme 和 CNAME

    1
    npm install hexo-generator-cname
    + +

    安装主题 Butterfly

    +

    官方网站

    +
    +

    在博客根目录安装主题

    +
    1
    git clone -b master https://github.com/jerryc127/hexo-theme-butterfly.git themes/Butterfly
    + +

    安装 cheerio

    +
    1
    npm install cheerio@0.22.0 --save
    + +

    安装渲染器

    +
    1
    npm install hexo-renderer-pug hexo-renderer-stylus
    + +

    修改 hexo 配置文件_config.yml,把主题改为Butterfly

    +
    1
    theme: Butterfly
    + +

    其余配置请参考主题的配置文档

    +

    常用插件

    本地搜索

    安装插件

    +
    1
    npm install hexo-generator-search --save
    + +

    _config.yml配置插件

    +
    1
    2
    3
    4
    search:
    path: search.xml
    field: post
    content: true
    + +

    文章顶置

    1
    2
    npm uninstall hexo-generator-index --save
    npm install hexo-generator-index-pin-top --save
    + +
    1
    2
    3
    ---
    top: True
    ---
    + +

    note 标签

    1
    <div class="note default"><p>default</p></div>
    + +

    default

    +
    1
    <div class="note primary"><p>primary</p></div> 
    +

    primary

    +
    1
    <div class="note success"><p>success</p></div>
    +

    success

    +
    1
    <div class="note info"><p>info</p></div> 
    +

    info

    +
    1
    <div class="note warning"><p>warning</p></div> 
    +

    warning

    +
    1
    <div class="note danger"><p>danger</p></div>
    +

    danger

    +  + +

    RSS

    安装插件

    +
    1
    npm install hexo-generator-feed --save
    + +

    添加萌宠

    1
    2
    npm install hexo-helper-live2d --save
    # 参考文章:https://vonsdite.cn/posts/fbd1f97f.html
    + +

    安装 sitmap 插件

    1
    2
    npm install hexo-generator-sitemap --save
    npm install hexo-generator-baidu-sitemap --save
    + +

    卸载 hexo

    1
    npm uninstall hexo
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/DOC-Hexo%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E6%B5%81%E7%A8%8B/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/DOC-JavaScript\345\270\270\350\247\201\351\227\256\351\242\230/index.html" "b/posts/DOC-JavaScript\345\270\270\350\247\201\351\227\256\351\242\230/index.html" new file mode 100644 index 00000000..3108d295 --- /dev/null +++ "b/posts/DOC-JavaScript\345\270\270\350\247\201\351\227\256\351\242\230/index.html" @@ -0,0 +1,478 @@ +JavaScript常见问题 | JCAlways + + + + + + + + + + + + + +

    JavaScript常见问题

    javascript 的 typeof 返回哪些数据类型

    +

    string,boolean,number,undefined,function,object

    +
    +

    例举 3 种强制类型转换和 2 种隐式类型转换?

    +

    强制(parseInt,parseFloat,number
    隐式(== ===)

    +
    +

    Split 和 join 的区别

    +

    Split是将字符串切割成数组的形式
    join是将数组转换成字符串

    +
    +

    数组方法 pop() push() unshift() shift()

    +

    push()尾部添加
    pop()尾部删除
    unshift()头部添加
    shift()头部删除

    +
    +

    IE 和标准下有哪些兼容性的写法

    1
    2
    3
    var ev = ev || window.event
    document.documentElement.clientWidth || document.body.clientWidth
    Var target = ev.srcElement||ev.target
    + +

    ajax 请求的时候 get 和 post 方式的区别

    +

    get请求值在 url 后面
    post放在虚拟载体里面

    +
    +

    get 有大小限制(只能提交少量参数)
    安全问题
    应用不同 ,请求数据和提交数据

    + + + + + + + + + + + + + + + + + + + + + + + + +
    请求类型存放处提交数量大小限制应用场景安全性
    get请求值在 url 后面请求数据不安全
    post请求值在虚拟载体里面提交数据安全
    +

    call 和 apply 的区别

    +

    Object.call(this,obj1,obj2,obj3)
    Object.apply(this,arguments)

    +
    +

    ajax 请求时,如何解析 json 数据

    +

    使用 JSON.parse

    +
    +

    什么是事件委托

    +

    利用事件冒泡的原理,让自己的所触发的事件,让他的父元素代替执行!

    +
    +

    如何阻止事件冒泡

    +

    ie:阻止冒泡 ev.cancelBubble = true;非 IE ev.stopPropagation();

    +
    +

    如何阻止默认事件

    +

    return false;
    ev.preventDefault();

    +
    +

    添加 删除 替换 插入到某个节点的方法

    +

    创建新节点

    +
    1
    2
    createElement(); //创建一个具体的元素
    createTextNode(); //创建一个文本节点
    + +

    添加移除替换插入

    +
    1
    2
    3
    4
    appendChild(); //添加
    removeChild(); //移除
    replaceChild(); //替换
    insertBefore(); //插入
    + +

    查找

    +
    1
    2
    3
    getElementsByTagName(); //通过标签名称
    getElementsByName(); //通过元素的Name属性的值
    getElementById(); //通过元素Id,唯一性
    +
    +

    Javascript 的事件流模型都有什么?

    +

    “事件冒泡”:事件开始由最具体的元素接受,然后逐级向上传播

    +

    “事件捕捉”:事件由最不具体的节点先接收,然后逐级向下,一直到最具体的

    +

    “DOM 事件流”:三个阶段:事件捕捉,目标阶段,事件冒泡

    +
    +

    null 和 undefined 的区别?

    +

    null 是一个表示”无”的对象,转为数值时为 0;undefined 是一个表示”无”的原始值,转为数值时为 NaN。

    +

    当声明的变量还未被初始化时,变量的默认值为 undefined。 null 用来表示尚未存在的对象

    +

    undefined 表示”缺少值”,就是此处应该有一个值,但是还没有定义。典型用法是:

    +

    (1)变量被声明了,但没有赋值时,就等于 undefined。

    +

    (2)调用函数时,应该提供的参数没有提供,该参数等于 undefined。

    +

    (3)对象没有赋值的属性,该属性的值为 undefined。

    +

    (4)函数没有返回值时,默认返回 undefined。

    +

    null 表示”没有对象”,即该处不应该有值。典型用法是:

    +

    (1) 作为函数的参数,表示该函数的参数不是对象。

    +

    (2) 作为对象原型链的终点。

    +
    +

    new 操作符具体干了什么呢?

    +

    1、创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。

    +

    2、属性和方法被加入到 this 引用的对象中。

    +

    3、新创建的对象由 this 所引用,并且最后隐式的返回 this 。

    +
    +

    js 延迟加载的方式有哪些?

    +

    defer 和 async、动态创建 DOM 方式(创建 script,插入到 DOM 中,加载完毕后 callBack)、按需异步载入 js

    +
    +

    如何获取 javascript 三个数中的最大值和最小值?

    +
    1
    2
    Math.max(a, b, c); //最大值
    Math.min(a, b, c); //最小值
    +
    +

    form 中的 input 可以设置为 readonly 和 disable,请问 2 者有什么区别?

    +

    readonly不可编辑,但可以选择和复制;值可以传递到后台
    disabled不能编辑,不能复制,不能选择;值不可以传递到后台

    +
    +

    Ajax 原理:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    (1)创建对象
    var xhr = new XMLHttpRequest();
    (2)打开请求
    xhr.open('GET', 'example.txt', true);
    (3)发送请求
    xhr.send(); 发送请求到服务器
    (4)接收响应
    xhr.onreadystatechange =function(){}
    (1)当readystate值从一个值变为另一个值时,都会触发readystatechange事件。
    (2)当readystate==4时,表示已经接收到全部响应数据。
    (3)当status ==200时,表示服务器成功返回页面和数据。
    (4)如果(2)和(3)内容同时满足,则可以通过xhr.responseText,获得服务器返回的内容。
    +
    +

    解释什么是 Json:

    +

    JSON 是一种轻量级的数据交换格式。

    +

    JSON 独立于语言和平台,JSON 解析器和 JSON 库支持许多不同的编程语言。

    +

    JSON 的语法表示三种类型值,简单值(字符串,数值,布尔值,null),数组,对象

    +
    +

    浏览器的滚动距离:

    +

    可视区域距离页面顶部的距离

    +
    1
    scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
    +
    +

    可视区的大小:

    +

    (1)innerXXX(不兼容 ie)

    +

    window.innerHeight 可视区高度,包含滚动条宽度

    +

    window.innerWidth 可视区宽度,包含滚动条宽度

    +

    (2)document.documentElement.clientXXX(兼容 ie)

    +

    document.documentElement.clientWidth 可视区宽度,不包含滚动条宽度

    +

    document.documentElement.clientHeight 可视区高度,不包含滚动条宽度

    +
    +

    节点的种类有几种,分别是什么?

    +
    1
    2
    3
    4
    5
    (1)元素节点:nodeType ===1;

    (2)文本节点:nodeType ===3;

    (3)属性节点:nodeType ===2;
    +
    +

    innerHTML 和 outerHTML 的区别

    +

    innerHTML(元素内包含的内容)

    +

    outerHTML(自己以及元素内的内容)

    +
    +

    offsetWidth offsetHeight 和 clientWidth clientHeight 的区别

    +

    (1)offsetWidth (content 宽度+padding 宽度+border 宽度)

    +

    (2)offsetHeight(content 高度+padding 高度+border 高度)

    +

    (3)clientWidth(content 宽度+padding 宽度)

    +

    (4)clientHeight(content 高度+padding 高度)

    +
    +

    闭包的好处

    +

    (1)希望一个变量长期驻扎在内存当中(不被垃圾回收机制回收)

    +

    (2)避免全局变量的污染

    +

    (3)私有成员的存在

    +

    (4)安全性提高

    +
    +

    dom 事件委托有什么原理,有什么优缺点?

    +

    事件委托原理:事件冒泡机制

    +

    优点:

    +

    1.可以大量节省内存占用,减少事件注册。比如 ul 上代理所有 li 的 click 事件就很不错。 2.可以实现当新增子对象时,无需再对其进行事件绑定,对于动态内容部分尤为合适

    +

    缺点:

    +

    事件代理的常用应用应该仅限于上述需求,如果把所有事件都用事件代理,可能会出现事件误判。即本不该被触发的事件被绑定上了事件。

    +
    +

    dom 选择器优先级是什么,以及权重值计算

    +

    1.行内样式 1000
    2.id 0100 3.类选择器、伪类选择器、属性选择器[type=”text”] 0010 4.标签选择器、伪元素选择器(::first-line) 0001 5.通配符*、子选择器、相邻选择器 0000

    +
    +

    重排和重绘

    +

    部分渲染树(或者整个渲染树)需要重新分析并且节点尺寸需要重新计算。这被称为重排。注意这里至少会有一次重排-初始化页面布局。
    由于节点的几何属性发生改变或者由于样式发生改变,例如改变元素背景色时,屏幕上的部分内容需要更新。这样的更新被称为重绘。

    +
    +

    什么情况会触发重排和重绘

    +

    添加、删除、更新 DOM 节点
    通过 display: none 隐藏一个 DOM 节点-触发重排和重绘
    通过 visibility: hidden 隐藏一个 DOM 节点-只触发重绘,因为没有几何变化
    移动或者给页面中的 DOM 节点添加动画
    添加一个样式表,调整样式属性
    用户行为,例如调整窗口大小,改变字号,或者滚动

    +
    +

    什么是 JavaScript?

    +

    我们可以从几个方面去说 JavaScript 是什么:

    +

    <1 基于对象

    +

    javaScript 中内置了许多对象供我们使用【String、Date、Array】等等

    +

    javaScript 也允许我们自己自定义对象

    +

    <2 事件驱动

    +

    当用户触发执行某些动作的时候【鼠标单机、鼠标移动】,javaScript 提供了监听这些事件的机制。当用户触发的时候,就执行我们自己写的代码。

    +

    <3 解释性语言

    +

    [x] javaScript 代码是由浏览器解析的,并不需要编译。

    +

    <4 基于浏览器的动态交互技术

    +

    既然 javaScript 是由浏览器解析的,那么它肯定要基于浏览器。 javaScript 让网页变得更加“灵活”“

    +

    <5 弱类型

    +

    [x] 像 java、c++等编译型语言,要先定义变量,后使用。javaScript 能够直接使用,不需要先定义

    +
    +

    es6 中的箭头函数和普通函数有什么区别?

    +

    <1 普通函数中的 this 总是指向调用它的那个对象,

    +

    <2 箭头函数没有自己的 this,他的 this 永远指向其定义环境,任何方法都改变不了其指向,如 call()、bind()、apply()。(正是因为它没有 this,所以也就不能用作构造函数,也没有原型对象)

    +

    箭头函数不能当作构造函数,也就是说,不能使用 new 命令,否则会报错。

    +

    箭头函数没有原型属性。

    +

    箭头函数不可以使用 yield 命令,因此箭头函数不能用作 Generator 函数。

    +

    箭头函数不能使用 arguments 对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

    +

    变量提升:由于 js 的内存机制,function 的级别最高,而用箭头函数定义函数的时候,需要 var(let、const)关键字,而 var 所定义的变量不能得到变量提升。故箭头函数一定要定义于调用之前。

    +

    拓展:this 的指向问题?

    +

    1、普通函数中,this 指向其函数的直接调用者;

    +

    2、箭头函数中,this 指向其定义环境,任何方法都改变不了其指向,如 call( )、bind()等;

    +

    3、构造函数中,如果不使用 new,则 this 指向 window,

    +

    如果使用 new 创建了一个实例,则 this 指向该实例。

    +

    4、window 内置函数中,如 setInterval,setTimeout 等,其内部的 this 指向 Window。

    +

    5、匿名函数的 this 指向 Window。

    +

    6、apply()、call()、bind()可以改变 this 的指向

    +
    +

    请指出 JavaScript 宿主对象和原生对象的区别?

    +

    宿主对象是指 DOM 和 BOM。
    原生对象是 Object、Function、Array、String、Boolean、Number、Date、RegExp、Error、Math 等对象

    +
    +

    请尽可能详尽的解释 Ajax 的工作原理。以及使用 Ajax 都有哪些优劣?

    +

    Ajax 是无需刷新页面就能从服务器取得数据的一种方法。

    +

    Ajax 通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用 javascript 来操作 DOM 更新页面。

    +

    过程:

    +

    <1 创建 XMLHttpRequest 对象。

    +

    <2 设置响应 HTTP 请求的回调函数。

    +

    <3 创建一个 HTTP 请求,指定相应的请求方法、url 等。

    +

    <4 发送 HTTP 请求。

    +

    <5 获取服务器端返回的数据。

    +

    <6 使用 JavaScript 操作 DOM 更新页面。

    +

    缺点:

    +

    <1 对搜索引擎不友好

    +

    <2 要实现 Ajax 下的前后退功能成本较大

    +

    <3 跨域问题限制

    +
    +

    请解释变量声明提升。

    +

    变量的声明前置就是把变量的声明提升到当前作用域的最前面。
    函数的声明前置就是把整个函数提升到当前作用域的最前面(位于前置的变量声明后面)。

    +

    案例:

    +

    //变量的声明前置:

    +

    console.log(num); // undefined

    +

    var num=1;

    +

    等价于:

    +

    //变量的声明前置

    +

    var num;

    +

    console.log(num); //undefined

    +

    num=1;

    +
    +

    请描述事件冒泡机制。

    +

    事件冒泡,事件最开始时由触发的那个元素身上发生,然后沿着 DOM 树向上传播,直到 document 对象。如果想阻止事件起泡,可以使用 e.stopPropagation()。

    +
    +

    请解释 JSONP 的工作原理,以及它为什么不是真正的 Ajax。

    +

    JSONP(JSON with Padding)是一种非官方跨域数据交互协议,它允许在服务器端集成< script >标签返回至客户端,通过 javascript 回调的形式实现跨域访问。

    +

    因为同源策略的原因,我们不能使用 XMLHttpRequest 与外部服务器进行通信,但是< script >可以访问外部资源,所以通过 JSON 与< script >相结合的办法,可以绕过同源策略从外部服务器直接取得可执行的 JavaScript 函数。

    +

    原理:

    +

    客户端定义一个函数,比如 jsonpCallback,然后创建< script >,src 为 url + ?jsonp=jsonpCallback 这样的形式,之后服务器会生成一个和传递过来 jsonpCallback 一样名字的参数,并把需要传递的数据当做参数传入,比如 jsonpCallback(json),然后返回给客户端,此时客户端就执行了这个服务器端返回的 jsonpCallback(json)回调。

    +

    通俗的说,就是客户端定义一个函数然后请求,服务器端返回的 javascript 内容就是调用这个函数,需要的数据都当做参数传入这个函数了。

    +

    优点:兼容性好,简单易用,支持浏览器与服务器双向通信
    缺点:只支持 GET 请求;存在脚本注入以及跨站请求伪造等安全问题

    +

    补充一点,JSONP 不使用 XMLHttpRequest 对象加载资源,不属于真正意义上的 AJAX。

    +
    +

    请举出一个匿名函数的典型用例?

    +

    定义回调函数,立即执行函数,作为返回值的函数,使用方法 var foo = function() {}定义的函数。

    +
    +

    描述以下变量的区别:null,undefined 或 undeclared?该如何检测它们?

    +

    未定义的属性、定义未赋值的为 undefined,JavaScript 访问不会报错;null 是一种特殊的 object;NaN 是一种特殊的 number;undeclared 是未声明也未赋值的变量,JavaScript 访问会报错

    +
    +

    请解释同步和异步函数的区别。

    +

    同步调用,在发起一个函数或方法调用时,没有得到结果之前,该调用就不返回,直到返回结果;

    +

    异步调用的概念和同步相对,在一个异步调用发起后,被调用者立即返回给调用者,但调用者不能立刻得到结果,被调用者在实际处理这个调用的请求完成后,通过状态、通知或回调等方式来通知调用者请求处理的结果。

    +

    简单地说,同步就是发出一个请求后什么事都不做,一直等待请求返回后才会继续做事;异步就是发出请求后继续去做其他事,这个请求处理完成后会通知你,这时候就可以处理这个回应了

    +
    +

    你使用哪些工具和技术来调试 JavaScript 代码?

    +

    \1. javascript 的 debugger 语句
    需要调试 js 的时候,我们可以给需要调试的地方通过 debugger 打断点,代码执行到断点就会暂定,这时候通过单步调试等方式就可以调试 js 代码

    +

    if(waldo){

    +

    debugger;

    +

    }

    +

    这时候打开 console 面板,就可以调试了

    +

    2.DOM 断点
    DOM 断点是一个 Firebug 和 chrome DevTools 提供的功能,当 js 需要操作打了断点的 DOM 时,会自动暂停,类似 debugger 调试。
    使用 DOM 断点步骤:
    <1 选择你要打断点的 DOM 节点
    <2 右键选择 Break on..
    <3 选择断点类型

    +

    另外的调试方法例如 alert, console.log,查看元素等

    +
    +

    使用 Promises 而非回调 (callbacks) 优缺点是什么?

    +

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

    +

    所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

    +

    有了 Promise 对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise 对象提供统一的接口,使得控制异步操作更加容易。

    +

    Promise 也有一些缺点。

    +

    首先,无法取消 Promise,一旦新建它就会立即执行,无法中途取消。
    其次,如果不设置回调函数,Promise 内部抛出的错误,不会反应到外部。
    第三,当处于 Pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

    +
    +

    原生重点:

    +

    字符串的方法

    +

    数组的方法

    +

    面向对象

    +

    闭包

    +

    作用域

    +

    作用域链 需要重点理解概念性的问题

    +

    原型链

    +

    原型

    +

    继承

    +

    This 指向的问题

    +

    24 道基础算法题

    +

    高级算法(比如斐波那契数列,二叉树等等)

    +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/DOC-JavaScript%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    目录
    1. 1. javascript 的 typeof 返回哪些数据类型
    2. 2. 例举 3 种强制类型转换和 2 种隐式类型转换?
    3. 3. Split 和 join 的区别
    4. 4. 数组方法 pop() push() unshift() shift()
    5. 5. IE 和标准下有哪些兼容性的写法
    6. 6. ajax 请求的时候 get 和 post 方式的区别
    7. 7. call 和 apply 的区别
    8. 8. ajax 请求时,如何解析 json 数据
    9. 9. 什么是事件委托
    10. 10. 如何阻止事件冒泡
    11. 11. 如何阻止默认事件
    12. 12. 添加 删除 替换 插入到某个节点的方法
    13. 13. Javascript 的事件流模型都有什么?
    14. 14. null 和 undefined 的区别?
    15. 15. new 操作符具体干了什么呢?
    16. 16. js 延迟加载的方式有哪些?
    17. 17. 如何获取 javascript 三个数中的最大值和最小值?
    18. 18. form 中的 input 可以设置为 readonly 和 disable,请问 2 者有什么区别?
    19. 19. Ajax 原理:
    20. 20. 解释什么是 Json:
    21. 21. 浏览器的滚动距离:
    22. 22. 可视区的大小:
    23. 23. 节点的种类有几种,分别是什么?
    24. 24. innerHTML 和 outerHTML 的区别
    25. 25. offsetWidth offsetHeight 和 clientWidth clientHeight 的区别
    26. 26. 闭包的好处
    27. 27. dom 事件委托有什么原理,有什么优缺点?
    28. 28. dom 选择器优先级是什么,以及权重值计算
    29. 29. 重排和重绘
    30. 30. 什么情况会触发重排和重绘
    31. 31. 什么是 JavaScript?
    32. 32. es6 中的箭头函数和普通函数有什么区别?
    33. 33. 请指出 JavaScript 宿主对象和原生对象的区别?
    34. 34. 请尽可能详尽的解释 Ajax 的工作原理。以及使用 Ajax 都有哪些优劣?
    35. 35. 请解释变量声明提升。
    36. 36. 请描述事件冒泡机制。
    37. 37. 请解释 JSONP 的工作原理,以及它为什么不是真正的 Ajax。
    38. 38. 请举出一个匿名函数的典型用例?
    39. 39. 描述以下变量的区别:null,undefined 或 undeclared?该如何检测它们?
    40. 40. 请解释同步和异步函数的区别。
    41. 41. 你使用哪些工具和技术来调试 JavaScript 代码?
    42. 42. 使用 Promises 而非回调 (callbacks) 优缺点是什么?
    43. 43. 原生重点:
    最新文章
    \ No newline at end of file diff --git "a/posts/DOC-Jquery\347\232\204\345\270\270\350\247\201\351\227\256\351\242\230/index.html" "b/posts/DOC-Jquery\347\232\204\345\270\270\350\247\201\351\227\256\351\242\230/index.html" new file mode 100644 index 00000000..2f26d49a --- /dev/null +++ "b/posts/DOC-Jquery\347\232\204\345\270\270\350\247\201\351\227\256\351\242\230/index.html" @@ -0,0 +1,303 @@ +Jquery的常见问题 | JCAlways + + + + + + + + + + + + + +

    Jquery的常见问题

    你在公司是怎么使用 jquery 的?

    +

    在项目中是怎么用的,就是面试官想考核你是否具备实际的项目开发经验,这个时候可以结合项目中的实际情况来解答(比如用过的选择器,复选框,表单,ajax,事件等)
    配置 jquery 环境,下载 jquery 类库,在 jsp 页面引入 jquery 类库即可

    +
    1
    <script type="text/javascript" src="jquery/jquery-1.7.2.min.js" />
    + +

    接下来通过在

    +
    1
    <script> $(function(){}); </script>
    +
    +

    你为什么要使用 jquery?(或者是这样问的:你认为 jquery 有哪些好处?)

    +

    因为 jQuery 是轻量级的框架,大小不到 30kb,它有强大的选择器,出色的 DOM 操作的封装,有可靠的事件处理机制(jQuery 在处理事件绑定的时候相当的可靠),完善的 ajax(它的 ajax 封装的非常的好,不需要考虑复杂浏览器的兼容性和 XMLHttpRequest 对象的创建和使用的问题。) 出色的浏览器的兼容性。 而且支持链式操作,隐式迭代。行为层和结构层的分离,还支持丰富的插件,jquery 的文档也非常的丰富。

    +
    +

    你知道 jquery 中的选择器吗,请讲一下有哪些选择器?

    +

    jQuery 中的选择器大致分为:基本选择器,层次选择器,过滤选择器,表单选择器

    +
    +

    jquery 中的选择器 和 css 中的选择器有区别吗?

    +

    jQuery 选择器支持 CSS 里的选择器,jQuery 选择器可用来添加样式和添加相应的行为 CSS 中的选择器是只能添加相应的样式

    +
    +

    觉得 jquery 中的选择器有什么优势?

    +

    简单的写法 $(‘ID’) 来代替 document.getElementById()函数
    支持 CSS1 到 CSS3 选择器完善的处理机制(就算写错了 id 也不会报错)

    +
    +

    你在使用选择器的时候有有没有什么觉得要注意的地方?

    +
      +
    • 选择器中含有”.”,”#”,”[“ 等特殊字符的时候需要进行转译
    • +
    • 属性选择器的引号问题
    • +
    • 选择器中含有空格的注意事
    • +
    +
    +

    jquery 对象和 dom 对象是怎样转换的?

    +

    jquery 转 DOM 对象:jQuery 对象是一个数组对象,可以通过[index]的丰富得到相应的 DOM 对象还可以通过 get[index]去得到相应的 DOM 对象。
    DOM 对象转 jQuery 对象:$(DOM 对象)

    +
    +

    你是如何使用 jquery 中的 ajax 的?

    +

    如果是一些常规的 ajax 程序的话,使用 load(),$.get(),$.post(),就可以搞定了,一般我会使用的是$.post() 方法。如果需要设定beforeSend(提交前回调函数),error(失败后处理),success(成功后处理)及complete(请求完成后处理)回调函数等,这个时候我会使用$.ajax()

    +
    +

    你觉得 jquery 中的 ajax 好用吗,为什么?

    +

    好用。 因为 jQuery 提供了一些日常开发中的快捷操作,例如 load,ajax,get,post 等等,所以使用 jQuery 开发 ajax 将变得极其简单,我们就可以集中精力在业务和用户的体验上,不需要去理会那些繁琐的 XMLHttpRequest 对象了

    +
    +

    jquery 中$.get()提交和$.post()提交有区别吗?

    +
      +
    • $.get() 方法使用GET方法来进行异步请求的。$.post() 方法使用 POST 方法来进行异步请求的。
    • +
    • get 请求会将参数跟在 URL 后进行传递,而 POST 请求则是作为 HTTP 消息的实体内容发送给 Web 服务器的,这种传递是对用户不可见的。
    • +
    • get 方式传输的数据大小不能超过 2KB 而 POST 要大的多
    • +
    • GET 方式请求的数据会被浏览器缓存起来,因此有安全问题。
    • +
    +
    +

    jquery 中的 load 方法一般怎么用的?

    +

    load 方法一般在 载入远程 HTML 代码并插入到 DOM 中的时候用,通常用来从 Web 服务器上获取静态的数据文件。如果要传递参数的话,可以使用$.get() 或 $.post()。

    +
    +

    在 jquery 中你是如何去操作样式的?

    +

    addClass() 来追加样式 ,removeClass() 来删除样式,toggle() 来切换样式

    +
    +

    简单的讲叙一下 jquery 是怎么处理事件的,你用过哪些事件?

    +

    首先去装载文档,在页面家在完毕后,浏览器会通过 javascript 为 DOM 元素添加事件。

    +
    +

    你使用过 jquery 中的动画吗,是怎样用的?

    +

    hide() 和 show() 同时修改多个样式属性。像高度,宽度,不透明度。 fadeIn() 和 fadeOut() fadeTo() 只改变不透明度
    slideUp() 和 slideDown() slideToggle() 只改变高度
    animate() 属于自定义动画的方法.
    在此说一个重点,就是 css3 动画和 animate 动画的区别是什么?重点看一下

    +
    +

    你一般用什么去提交数据,为什么?

    答案:
    一般我会使用的是$.post() 方法。
    如果需要设定beforeSend(提交前回调函数),error(失败后处理),success(成功后处理及complete(请求完成后处理)回调函数等,这个时候我会使用$.ajax()

    +

    在 jquery 中引入 css 有几种方式?

    答案:
    四种 :行内式,内嵌式,导入式,链接式

    +

    你在 jquery 中使用过哪些插入节点的方法,它们的区别是什么?

    答案:
    append(),appendTo(),prepend(),prependTo(),after(),insertAfter(),before(),insertBefore() 大致可以分为内部追加和外部追加 append() 表示向每个元素内部追加内容。appendTo()表示将所有的元素追加到指定的元素中。例$(A)appendTo(B) 是将 A 追加到 B 中下面的方法解释类似

    +

    你使用过包裹节点的方法吗,包裹节点有方法有什么好处?

    答案:
    wrapAll(),wrap(), wrapInner() 需要在文档中插入额外的结构化标记的时候可以使用这些包裹的方法因为它不会改变原始文档的语义

    +

    jquery 中如何来获取或和设置属性?

    答案:
    jQuery 中可以用 attr()方法来获取和设置元素属性 removeAttr() 方法来删除元素属性

    +

    如何来设置和获取 HTML 和文本的值?

    答案:
    html()方法类似于 innerHTML 属性 可以用来读取或者设置某个元素中的 HTML 内容
    注意:html() 可以用于 xhtml 文档不能用于 xml 文档,text() 类似于 innerText 属性 可以用来读取或设置某个元素中文本内容。val() 可以用来设置和获取元素的值

    +

    你 jquery 中有哪些方法可以遍历节点?

    答案 :
    children() 取得匹配元素的子元素集合,只考虑子元素不考虑后代元素
    next() 取得匹配元素后面紧邻的同辈元素
    prev() 取得匹配元素前面紧邻的同辈元素
    siblings() 取得匹配元素前后的所有同辈元素
    closest() 取得最近的匹配元素
    find() 取得匹配元素中的元素集合 包括子代和后代

    +

    子元素选择器 和后代选择器元素有什么区别?

    答案:
    子代元素是找子节点下的所有元素,后代元素是找子节点或子节点的子节点中的元素

    +

    你觉得 beforeSend 方法有什么用?

    答案:
    发送请求前可以修改 XMLHttpRequest 对象的函数,在 beforeSend 中如果返回 false 可以取消本次的 Ajax 请求。XMLHttpRequest 对象是唯一的参数所以在这个方法里可以做验证

    +

    siblings() 方法 和 $(‘prev~div’)选择器是一样的吗?

    +

    $(‘prev~div’) 只能选择’#prev’元素后面的同辈

    元素而 siblings()方法与前后的文职无关,只要是同辈节点就都能匹配

    +
    +

    你在 ajax 中使用过 JSON 吗,你是如何用的?

    使用过,在$.getJSON() 方法的时候就是。
    因为 $.getJSON() 就是用于加载 JSON 文件的

    +

    有哪些查询节点的选择器?

    答案:
    我在公司使用过 :first 查询第一个,:last 查询最后一个,:odd 查询奇数但是索引从 0 开始:even 查询偶数,:eq(index)查询相等的 ,:gt(index)查询大于 index 的 ,:lt 查询小于 index:header 选取所有的标题等

    +
      +
    1. nextAll() 能 替代$(‘prevsiblindgs’)选择器吗?
      答案:
      能。 使用nextAll() 和使用$(‘prev
      siblindgs’) 是一样的
    2. +
    3. jQuery 中有几种方法可以来设置和获取样式
      答案:
      addClass() 方法,attr() 方法
    4. +
    5. $(document).ready()方法和window.onload有什么区别?
      答案: 两个方法有相似的功能,但是在实行时机方面是有区别的。
      window.onload方法是在网页中所有的元素(包括元素的所有关联文件)完全加载到浏览器后才执行的。
      $(document).ready() 方法可以在 DOM 载入就绪时就对其进行操纵,并调用执行绑定的函数
    6. +
    7. jQuery 是如何处理缓存的?
      答案:
      要处理缓存就是禁用缓存.
      1 通过$.post() 方法来获取数据,那么默认就是禁用缓存的。
      2 通过$.get()方法 来获取数据,可以通过设置时间戳来避免缓存。可以在 URL 后面加上+(+new Date)例 $.get(‘ajax.xml?’+(+new Date),function () { //内容 });
      3 通过$.ajax 方法来获取数据,只要设置 cache:false 即可
    8. +
    9. $.getScript()方法 和 $.getJson() 方法有什么区别?
      答案:
      1 $.getScript() 方法可以直接加载.js 文件,并且不需要对 javascript 文件进行处理,javascript 文件会自动执行。
      2 $.getJson() 是用于加载 JSON 文件的 ,用法和$.getScript()
    10. +
    11. 你读过有关于 jQuery 的书吗?
      《jquery 基础教程》 《jquery 实战》《锋利的 jquery》 《巧用 jquery》 《jQuery 用户界面库学习指南》等
      重点:其中《锋利的 jquery》这本书可以看一下
    12. +
    13. jQuery 能做什么?
      答案:
      1 获取页面的元素
      2 修改页面的外观
      3 改变页面大的内容
      4 响应用户的页面操作
      5 为页面添加动态效果
      6 无需刷新页面,即可以从服务器获取信息
      7 简化常见的 javascript 任务
    14. +
    15. 在 ajax 中 data 主要有几种方式?
      答案:
      三种,html 拼接的,json 数组,form 表单经 serialize()序列化的
    16. +
    17. jQuery 中的 hover()和 toggle()有什么区别?
      答案:
      hover()和 toggle()都是 jQuery 中两个合成事件。
      hover()方法用于模拟光标悬停事件。
      toggle()方法是连续点击事件
    18. +
    19. 你知道 jQuery 中的事件冒泡吗,它是怎么执行的,何如来停止冒泡事件?
      答案:
      知道,事件冒泡是从里面的往外面开始触发。在 jQuery 中提供了 stopPropagation()方法可以停止冒泡
    20. +
    21. jquery 表单提交前有几种校验方法?分别为?
      a) formData:返回一个数组,可以通过循环调用来校验
      b) jaForm:返回一个 jQuery 对象,所有需要先转换成 dom 对象
      c) fieldValue:返回一个数组 beforeSend()
    22. +
    23. jQuery 的实现原理?
      答案:
      (function(window, undefined) {})(window);
      jQuery 利用 JS 函数作用域的特性,采用立即调用表达式包裹了自身,解决命名空间和变量污染问题
      window.jQuery = window.$ = jQuery;
      在闭包当中将 jQuery 和 $ 绑定到 window 上,从而将 jQuery 和 $ 暴露为全局变量
    24. +
    25. jQuery.fn 的 init 方法返回的 this 指的是什么对象? 为什么要返回 this?
      答案:
      jQuery.fn 的 init 方法 返回的 this 就是 jQuery 对象
      用户使用 jQuery() 或 $() 即可初始化 jQuery 对象,不需要动态的去调用 init 方法
    26. +
    27. jQuery.extend 与 jQuery.fn.extend 的区别?
      答案:
      $.fn.extend() 和 $.extend() 是 jQuery 为扩展插件提拱了两个方法
      $.extend(object); // 为 jQuery 添加“静态方法”(工具方法)
    28. +
    29. jQuery 的属性拷贝(extend)的实现原理是什么,如何实现深拷贝?
      答案:
      浅拷贝(只复制一份原始对象的引用) var newObject = $.extend({}, oldObject);
      深拷贝(对原始对象属性所引用的对象进行进行递归拷贝) var newObject = $.extend(true, {}, oldObject);
    30. +
    31. jQuery 中的 bind(), live(), delegate(), on()的区别?
      答案:
      bind 直接绑定在目标元素上
      live 通过冒泡传播事件,默认 document 上,支持动态数据
      delegate 更精确的小范围使用事件代理,性能优于 live
      on 是最新的 1.9 版本整合了之前的三种方式的新事件绑定机制
    32. +
    33. 针对 jQuery 的优化方法?
      答案:
      缓存频繁操作 DOM 对象
      尽量使用 id 选择器代替 class 选择器
      总是从#id 选择器来继承
      尽量使用链式操作
      使用事件委托 on 绑定事件
      采用 jQuery 的内部函数 data()来存储数据
      使用最新版本的 jQuery
    34. +
    35. jQuery 的 slideUp 动画,当鼠标快速连续触发, 动画会滞后反复执行,该如何处理呢?
      答案:
      在触发元素上的事件设置为延迟处理:使用 JS 原生 setTimeout 方法
      在触发元素的事件时预先停止所有的动画,再执行相应的动画事件:$(‘.tab’).stop().slideUp();
    36. +
    37. jQuery 与 jQuery UI、jQuery Mobile 区别?
      答案:
      jQuery 是 JS 库,兼容各种 PC 浏览器,主要用作更方便地处理 DOM、事件、动画、AJAX
      jQuery UI 是建立在 jQuery 库上的一组用户界面交互、特效、小部件及主题
      jQuery Mobile 以 jQuery 为基础,用于创建“移动 Web 应用”的框架
    38. +
    39. jQuery 和 Zepto 的区别? 各自的使用场景?
      答案:
      jQuery 主要目标是 PC 的网页中,兼容全部主流浏览器。在移动设备方面,单独推出 jQuery Mobile
      Zepto 从一开始就定位移动设备,相对更轻量级。它的 API 基本兼容 jQuery,但对 PC 浏览器兼容不理想
    40. +
    41. jQuery 库中的 $() 是什么?
      答案:
      $() 函数是 jQuery() 函数的别称,$() 函数用于将任何对象包裹成 jQuery 对象,接着就被允许调用定义在 jQuery 对象上的多个不同方法。甚至可以将一个选择器字符串传入 $() 函数,它会返回一个包含所有匹配的 DOM 元素数组的 jQuery 对象。
    42. +
    43. jQuery 中 detach() 和 remove() 方法的区别是什么?
      答案:
      detach() 和 remove() 方法都被用来移除一个 DOM 元素, 两者之间的主要不同在于 detach() 会保持对过去被解除元素的跟踪, 因此它可以被取消解除, 而 remove() 方法则会保持过去被移除对象的引用. 你还可以看看 用来向 DOM 中添加元素的 appendTo() 方法
    44. +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/DOC-Jquery%E7%9A%84%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    目录
    1. 1. 你在公司是怎么使用 jquery 的?
    2. 2. 你为什么要使用 jquery?(或者是这样问的:你认为 jquery 有哪些好处?)
    3. 3. 你知道 jquery 中的选择器吗,请讲一下有哪些选择器?
    4. 4. jquery 中的选择器 和 css 中的选择器有区别吗?
    5. 5. 觉得 jquery 中的选择器有什么优势?
    6. 6. 你在使用选择器的时候有有没有什么觉得要注意的地方?
    7. 7. jquery 对象和 dom 对象是怎样转换的?
    8. 8. 你是如何使用 jquery 中的 ajax 的?
    9. 9. 你觉得 jquery 中的 ajax 好用吗,为什么?
    10. 10. jquery 中$.get()提交和$.post()提交有区别吗?
    11. 11. jquery 中的 load 方法一般怎么用的?
    12. 12. 在 jquery 中你是如何去操作样式的?
    13. 13. 简单的讲叙一下 jquery 是怎么处理事件的,你用过哪些事件?
    14. 14. 你使用过 jquery 中的动画吗,是怎样用的?
    15. 15. 你一般用什么去提交数据,为什么?
    16. 16. 在 jquery 中引入 css 有几种方式?
    17. 17. 你在 jquery 中使用过哪些插入节点的方法,它们的区别是什么?
    18. 18. 你使用过包裹节点的方法吗,包裹节点有方法有什么好处?
    19. 19. jquery 中如何来获取或和设置属性?
    20. 20. 如何来设置和获取 HTML 和文本的值?
    21. 21. 你 jquery 中有哪些方法可以遍历节点?
    22. 22. 子元素选择器 和后代选择器元素有什么区别?
    23. 23. 你觉得 beforeSend 方法有什么用?
    24. 24. siblings() 方法 和 $(‘prev~div’)选择器是一样的吗?
    25. 25. 你在 ajax 中使用过 JSON 吗,你是如何用的?
    26. 26. 有哪些查询节点的选择器?
    最新文章
    \ No newline at end of file diff --git "a/posts/DOC-Xmind\347\254\224\350\256\260\346\261\207\346\200\273/index.html" "b/posts/DOC-Xmind\347\254\224\350\256\260\346\261\207\346\200\273/index.html" new file mode 100644 index 00000000..0d02aa81 --- /dev/null +++ "b/posts/DOC-Xmind\347\254\224\350\256\260\346\261\207\346\200\273/index.html" @@ -0,0 +1,4767 @@ +Xmind笔记汇总 | JCAlways + + + + + + + + + + + + + +

    Xmind笔记汇总

    前端与移动开发

    HTML阶段

    HTML标签

      +
    • 超链接标签

      +
        +
      • a
      • +
      +
    • +
    • 标题标签

      +
        +
      • h
      • +
      +
    • +
    • 段落标签

      +
        +
      • p
      • +
      +
    • +
    • 字体样式

      +
        +
      • font
      • +
      +
    • +
    • 图片标签

      +
        +
      • img
      • +
      +
    • +
    • 换行标签

      +
        +
      • br
      • +
      +
    • +
    • 横线标签

      +
        +
      • hr
      • +
      +
    • +
    • 媒体标签

      +
        +
      • 音频标签

        +
          +
        • audio
        • +
        +
      • +
      • 视屏标签

        +
          +
        • video
        • +
        +
      • +
      +
    • +
    • 列表标签

      +
        +
      • 有序列表

        +
          +
        • ol

          +
            +
          • li
          • +
          +
        • +
        +
      • +
      • 无序列表

        +
          +
        • ul

          +
            +
          • li
          • +
          +
        • +
        +
      • +
      • 自定义列表

        +
          +
        • dl

          +
            +
          • dt

            +
              +
            • dd
            • +
            +
          • +
          +
        • +
        +
      • +
      • 下拉列表

        +
          +
        • select

          +
            +
          • option
          • +
          +
        • +
        +
      • +
      +
    • +
    • 表格标签

      +
        +
      • table

        +
          +
        • tr

          +
            +
          • td
          • +
          +
        • +
        +
      • +
      +
    • +
    • 表单标签

      +
        +
      • form
      • +
      +
    • +
    • 输入框

      +
        +
      • 文本框

        +
          +
        • text
        • +
        +
      • +
      • 密码输入框

        +
          +
        • password
        • +
        +
      • +
      • 单选框

        +
          +
        • radio
        • +
        +
      • +
      • 多选框

        +
          +
        • checkbox
        • +
        +
      • +
      • 文本域

        +
          +
        • textarea
        • +
        +
      • +
      • 邮箱

        +
          +
        • email
        • +
        +
      • +
      • 电话

        +
          +
        • tel
        • +
        +
      • +
      • 数字

        +
          +
        • number
        • +
        +
      • +
      • URL

        +
          +
        • url
        • +
        +
      • +
      • 搜索

        +
          +
        • search
        • +
        +
      • +
      • 区域

        +
          +
        • range
        • +
        +
      • +
      • 时间

        +
          +
        • 年月日

          +
            +
          • date
          • +
          +
        • +
        • 时分

          +
            +
          • time
          • +
          +
        • +
        +
      • +
      • 颜色

        +
          +
        • color
        • +
        +
      • +
      +
    • +
    • 按钮

      +
        +
      • 普通按钮

        +
          +
        • button
        • +
        +
      • +
      • 提交按钮

        +
          +
        • submit
        • +
        +
      • +
      • 重置按钮

        +
          +
        • reset
        • +
        +
      • +
      • 图象按钮

        +
          +
        • image
        • +
        +
      • +
      • 文件按钮

        +
          +
        • file
        • +
        +
      • +
      +
    • +
    • 文字最小化

      +
        +
      • small
      • +
      +
    • +
    +

    属性

      +
    • 长度

      +
        +
      • width 宽度
      • +
      • height 高度
      • +
      +
    • +
    • 字体 font

      +
        +
      • ins 下划线
      • +
      • em 斜体
      • +
      • strong 粗体
      • +
      • del 删除线
      • +
      • color 颜色
      • +
      • size 文本大小
      • +
      +
    • +
    • 图片

      +
        +
      • src 路径
      • +
      • title 悬停时的文本提示
      • +
      • ait 图片文字描述
      • +
      +
    • +
    • 表格

      +
        +
      • acption 标题
      • +
      • border 边框
      • +
      • cellspacing 单元格间距
      • +
      • cellpadding 文字与边框的距离
      • +
      • colspan 合并列
      • +
      • rowspan 合并行
      • +
      +
    • +
    • 对齐 align

      +
        +
      • left 左对齐
      • +
      • center 居中
      • +
      • right 右对齐
      • +
      +
    • +
    • 媒体

      +
        +
      • autoplay 自动播放
      • +
      • controls 控件
      • +
      +
    • +
    • 共用属性

      +
        +
      • name 名称
      • +
      • id id
      • +
      • value 值
      • +
      • checked 默认选中(选择框)
      • +
      • selected 默认选中(下拉列表)
      • +
      • multiple 单选变多选
      • +
      • readonly 只读
      • +
      • disabled 禁用
      • +
      • maxlenght 最大长度
      • +
      • placeholder 提示信息
      • +
      +
    • +
    +

    CSS阶段

    选择器

      +
    • 标签选择器
    • +
    • 类选择器(最常用)
    • +
    • ID选择器
    • +
    • 通配符选择器
    • +
    • 后代选择器
    • +
    • 子代选择器
    • +
    • 标签指定式选择器
    • +
    • 并集选择器
    • +
    +

    伪类选择器

      +
    • :hover 设置鼠标悬停样式
    • +
    • :link {} 设置连接样式
    • +
    • :active{} 标签被激活的样式
    • +
    • :visited {} 标签被访问过的样式
    • +
    • :focus {} 设置当获取鼠标焦点时候的样式
    • +
    • :checked 选中的样式
    • +
    +

    结构伪类选择器

      +
    • :first-child {} 选中父元素第一个元素
    • +
    • :last-child {} 选中父元素最后一个元素
    • +
    • :nth-child(n) {} 选中父元素中第n个子元素
    • +
    +

    属性

      +
    • 字体样式

      +
        +
      • font-size 文字大小

        +
      • +
      • font-family 字体样式

        +
          +
        • 使用汉字
        • +
        • 使用英文
        • +
        • 使用unicode
        • +
        +
      • +
      • font-width 字体粗体

        +
          +
        • 700(bold)【加粗】 | 400(normal) 【正常显示】
        • +
        +
      • +
      • font-style 文字斜体

        +
          +
        • italic(斜体) | normal (正常显示)
        • +
        +
      • +
      • line-height 行高

        +
      • +
      • text-indent 文本缩进 首行缩进

        +
      • +
      • text-align 文本对齐

        +
          +
        • left 左对齐
        • +
        • center 居中
        • +
        • right 右对齐
        • +
        +
      • +
      • text-decoration 文本修饰

        +
      • +
      • 合写

        +
          +
        • font: 700 italic 50px / 60px ‘宋体’;
        • +
        +
      • +
      +
    • +
    • 颜色表示方法

      +
        +
      • 使用单词表示颜色
      • +
      • 使用十六进制表示颜色
      • +
      • 使用RGB表示颜色
      • +
      • 使用RGBA表示颜色(透明度)
      • +
      +
    • +
    • 元素显示方式

      +
        +
      • display: block

        +
          +
        • 块级元素
        • +
        +
      • +
      • display:inline-block

        +
          +
        • 行内块元素
        • +
        +
      • +
      • display:inline

        +
          +
        • 行内元素
        • +
        +
      • +
      +
    • +
    • 背景

      +
        +
      • 背景图

        +
          +
        • background-image: url(‘1.jpg’);
        • +
        +
      • +
      • 显示方式

        +
          +
        • background-repeat:

          +
            +
          • repeat 平铺
          • +
          • no-repeat 不平铺
          • +
          • repeat-x
          • +
          • repeat-y
          • +
          +
        • +
        +
      • +
      • 图片位置

        +
      • +
      • 背景颜色

        +
          +
        • background-color
        • +
        +
      • +
      • 图片大小

        +
          +
        • background-size:

          +
            +
          • +
          • cover 图片盒子一样大
          • +
          • contain 比例
          • +
          +
        • +
        +
      • +
      • 背景合写方式

        +
          +
        • background: red url(‘1.jpg’) no-repeat center;
        • +
        +
      • +
      +
    • +
    • a标签

      +
        +
      • 去除下划线

        +
          +
        • text-decoration: none;
        • +
        +
      • +
      +
    • +
    • 列表样式

      +
        +
      • 去除li的小圆点

        +
          +
        • list-style:none;
        • +
        +
      • +
      +
    • +
    • 文本域

      +
        +
      • 禁止文本域拖动

        +
          +
        • resize:none;
        • +
        +
      • +
      +
    • +
    • 盒子模型

      +
        +
      • 边框

        +
          +
        • 边框颜色

          +
        • +
        • 边框样式

          +
            +
          • none 没有边框
          • +
          • solid 实线边框
          • +
          • dashed 虚线边框
          • +
          • dotted 点线边框
          • +
          +
        • +
        • 边框宽度

          +
        • +
        • 边框合写

          +
            +
          • boder:颜色 样式 宽度
          • +
          +
        • +
        • 单独给边框设置样式

          +
            +
          • border-top 上边框
          • +
          • border-left 左边框
          • +
          • border-right 右边框
          • +
          • border-buttom 下边框
          • +
          +
        • +
        +
      • +
      • 内边距

        +
          +
        • padding-top 上边距

          +
        • +
        • padding-left 左边距

          +
        • +
        • padding-right 右边距

          +
        • +
        • padding-buttom 下边距

          +
        • +
        • 简写

          +
            +
          • padding:10px 上 右 下 左都为10px
          • +
          • padding:10px 20px 上下10px 左右20px
          • +
          • padding:10px 20px 30px 上10px 左右20px 下30px
          • +
          • padding:10px 20px 30px 40px 上10px 右20px 下30px 左40px
          • +
          +
        • +
        +
      • +
      • 外边距

        +
          +
        • margin-top 上边距

          +
        • +
        • margin-left 左边距

          +
        • +
        • margin-right 右边距

          +
        • +
        • margin-buttom 下边距

          +
        • +
        • margin:0 auto 使盒子居中

          +
        • +
        • 简写

          +
            +
          • margin:10px 上 右 下 左都为10px
          • +
          • margin:10px 20px 上下10px 左右20px
          • +
          • margin:10px 20px 30px 上10px 左右20px 下30px
          • +
          • margin:10px 20px 30px 40px 上10px 右20px 下30px 左40px
          • +
          +
        • +
        +
      • +
      • 边框圆角

        +
          +
        • border-radius
        • +
        +
      • +
      • 盒子阴影

        +
          +
        • box-shadow:
        • +
        +
      • +
      • 文字阴影

        +
          +
        • text-shadow:
        • +
        +
      • +
      • 盒子大小

        +
          +
        • box-sizing

          +
            +
          • border-box; 实际大小
          • +
          • content-box; 内容区域大小
          • +
          +
        • +
        +
      • +
      +
    • +
    • 浮动

      +
        +
      • float:left 左浮动

        +
      • +
      • float:right 右浮动

        +
      • +
      • 清除浮动

        +
          +
        • clear属性

          +

          在浮动元素后面添加一个空的div

          +

          选中当前div设置属性 clear: both | left | right;

          +
        • +
        • 使用伪元素清除浮动

          +

          clearfix::after {
          content: “”;
          display: block;
          clear: both;
          height: 0;
          line-height: 0;
          visibility: hidden;
          }
          .clearfix {
          /* 为了兼容IE浏览器 */
          zoom: 1;
          }

          +
        • +
        • 给父元素设置 overflow:hidden;

          +
        • +
        +
      • +
      +
    • +
    • 伪元素

      +
        +
      • ::before {}
      • +
      • ::after {}
      • +
      +
    • +
    • 定位

      +
        +
      • 静态定位

        +
          +
        • position: static;
        • +
        +
      • +
      • 绝对定位

        +
          +
        • position:abso
        • +
        +
      • +
      • 相对定位

        +
          +
        • position: relative;
        • +
        +
      • +
      • 固定定位

        +
          +
        • position: fixed;
        • +
        +
      • +
      • 层级

        +
          +
        • z-index
        • +
        +
      • +
      +
    • +
    • 动画

      +
        +
      • animation动画

        +
          +
        • 1.定义动画集

          +

          @keyframes 自定义动画集名称 {

          +
                          from {
          +                     开始状态的代码
          +                }
          +
          +                to {
          +                    结束状态的代码
          +                }
          +          }
          +
          +
        • +
        • 2.调用动画集

          +
            +
          • animation-name
          • +
          +
        • +
        • 3.设置动画执行时间

          +
            +
          • animation-duration
          • +
          +
        • +
        • 4.设置动画执行次数

          +
            +
          • animation-iteration-count: infinite;
          • +
          +
        • +
        • 5.设置动画的速度

          +
            +
          • animation-timing-function
          • +
          +
        • +
        • 6.设置动画逆播

          +
            +
          • animation-direction: alternate;
          • +
          +
        • +
        • 7.设置动画在结束位置停止

          +
            +
          • animation-fill-mode: forwards;
          • +
          +
        • +
        • 8.设置动画延时

          +
            +
          • animation-delay
          • +
          +
        • +
        • 合写

          +
        • +
        +
      • +
      • 补间动画

        +
          +
        • transition

          +
            +
          • 补间对象

            +
          • +
          • 补间时间

            +
          • +
          • 动画速度类型

            +
              +
            • ease 速度由快到慢
            • +
            • linear 匀速的
            • +
            • ease-in 加速效果
            • +
            • ease-out 减速
            • +
            • ease-in-out 先加速再减速
            • +
            +
          • +
          +
        • +
        +
      • +
      • 2D转换

        +
          +
        • 位移

          +
            +
          • transform: translate(x, y);

            +
          • +
          • 绝对定位盒子居中

            +

            通过位移的方式实现绝对定位的盒子居中显示:
            position: absolute;
            父元素宽度的一半
            left: 50%;
            元素自己宽度的一半
            transform: translate(-50%);

            +
          • +
          +
        • +
        • 旋转

          +
            +
          • transform: rotate()

            +
          • +
          • 单位是 ‘deg’

            +
          • +
          • 修改旋转位置

            +
              +
            • transform-origin

              +
                +
              • left | right | top | bottom | center
              • +
              +
            • +
            +
          • +
          +
        • +
        • 缩放

          +
            +
          • transform: scale(x, y)
          • +
          • 1 大小不变
          • +
          +
        • +
        +
      • +
      • 3D转换

        +
          +
        • 位移

          +
            +
          • transform: translateX() translateY() translateZ();
          • +
          +
        • +
        • 透视

          +
            +
          • perspective: 1000px;
          • +
          +
        • +
        • 旋转

          +
            +
          • transform: rotateX() rotateY() rotateZ();
          • +
          +
        • +
        • 缩放

          +
            +
          • transform: scaleX() scaleY() scaleZ();
          • +
          +
        • +
        • 平面图形转3D

          +
            +
          • transform-style: preserve-3d;
          • +
          +
        • +
        +
      • +
      +
    • +
    • 私有前缀

      +
        +
      • -webkit-

        +
          +
        • 让webkit内核浏览器支持对应的属性
        • +
        +
      • +
      • -moz-

        +
          +
        • 让火狐浏览器支持对应的属性
        • +
        +
      • +
      • -o-

        +
          +
        • 让欧朋浏览器支持对应的属性
        • +
        +
      • +
      • -ms-

        +
          +
        • 让IE浏览器支持对应的属性
        • +
        +
      • +
      +
    • +
    • 渐变

      +
        +
      • 线性渐变 background-image: linear-gradient()

        +
          +
        • 渐变方向

          +
            +
          • 方位

            +
              +
            • to top
            • +
            • to bottom
            • +
            • to left
            • +
            • to right
            • +
            +
          • +
          • 角度

            +
              +
            • 0deg

              +
                +
              • 从下向上
              • +
              +
            • +
            • 90deg

              +
                +
              • 从左向右
              • +
              +
            • +
            +
          • +
          +
        • +
        • 颜色

          +
            +
          • 开始颜色

            +
              +
              • +
              • +
              +
                +
              • 渐变范围 百分比表示(默认是宽度)

                +
                  +
                • 如果设置了background-size
                • +
                +
              • +
              +
            • +
            +
          • +
          • 结束颜色

            +
          • +
          +
        • +
        +
      • +
      • 径向渐变 background-image: radial-gradient()

        +
          +
        • 范围

          +
            +
          • 半径范围

            +
              +
            • at

              +
                +
              • 圆点位置
              • +
              +
            • +
            +
          • +
          +
        • +
        • 颜色

          +
            +
          • 开始颜色
          • +
          • 结束颜色
          • +
          +
        • +
        +
      • +
      +
    • +
    • 多背景

      +
    • +
    • 元素隐藏

      +
        +
      • display

        +
          +
        • display:none 隐藏元素,不占位置
        • +
        • display: block 显示元素
        • +
        +
      • +
      • visibility:hidden 隐藏元素,占位置

        +
      • +
      • overflow

        +
          +
        • visible 可见的
        • +
        • hidden; 溢出隐藏
        • +
        • auto 超出添加滚动条,不超出不添加
        • +
        • scroll 设置滚动条
        • +
        +
      • +
      +
    • +
    • 文本修饰

      +
        +
      • 文字换行

        +
          +
        • word-break: break-all;

          +
            +
          • 让内容在末尾处换行
          • +
          +
        • +
        • word-break: keep-all;

          +
            +
          • 遇到空格就换行
          • +
          +
        • +
        +
      • +
      • 文字溢出显示省略号

        +
          +
        • overflow: hidden;

          +
            +
          • 子主题 1
          • +
          +
        • +
        • text-overflow: ellipsis;

          +
            +
          • 超出容器出现省略号
          • +
          +
        • +
        • display: -webkit-box;

          +
            +
          • 设置为伸缩盒子
          • +
          +
        • +
        • -webkit-line-clamp: 2;

          +
            +
          • 设置文字要显示几行
          • +
          +
        • +
        • -webkit-box-orient: vertical;

          +
            +
          • 设置文字的显示为垂直
          • +
          +
        • +
        +
      • +
      +
    • +
    • 去除图片底边三像素

      +
        +
      • dispaly:blcok; 设置为行内块

        +
      • +
      • vertical-align

        +
          +
        • top
        • +
        • middle
        • +
        • bottom
        • +
        • baseline
        • +
        +
      • +
      • overflow:hidden

        +
      • +
      +
    • +
    • 鼠标样式

      +
        +
      • cursor

        +
          +
        • help 帮助指针
        • +
        • pointer 手
        • +
        • move 移动
        • +
        • 帮助链接
        • +
        +
      • +
      +
    • +
    +

    Less

      +
    • 用来维护或管理CSS代码的工具

      +
    • +
    • 使用方法

      +
        +
      • 定义变量

        +

        @自定义变量名称 :值;

        +

        div{
        font-size:@自定义变量名称;
        }

        +
      • +
      • 嵌套写法

        +

        选择器 {
        选择器{
        }
        }

        +
      • +
      • 公共样式设置(函数)

        +

        .public(@自定义变量名称){
        color:red;
        }

        +

        p{
        .public(值);
        }

        +
      • +
      • 支持数学运算

        +
      • +
      +
    • +
    • 引用方法

      +
        +
      • link标签引用less文件

        +
          +
        • +
        +
      • +
      • 引用less.js

        +
          +
        • +
        +
      • +
      • 需要放到服务器下

        +
      • +
      +
    • +
    +

    移动WEB阶段

    注意

      +
    • 页面水平方向不能出现水平条
    • +
    • 页面不能出现缩放效果
    • +
    +

    分辨率

      +
    • 屏幕分辨率
    • +
    • 物理分辨率
    • +
    +

    像素密度(PPI)

      +
    • 每英寸面积中所能容纳像素点个数
    • +
    +

    设备独立像素(DPR)

    页面适配方法

      +
    • 流式布局(百分比布局)+视口设置(meta标签)

      +
        +
      • 宽度

        +
          +
        • max-width:768px;

          +
            +
          • 最大宽度
          • +
          +
        • +
        • min-width:320px;

          +
            +
          • 最小宽度
          • +
          +
        • +
        +
      • +
      +
    • +
    • 伸缩布局

      +
        +
      • display:flex

        +
          +
        • 伸缩盒子

          +
            +
          • 两条轴

            +
              +
            • 主轴

              +
                +
              • 子元素都会默认在主轴方向显示
              • +
              +
            • +
            • 侧轴

              +
                +
              • 垂直于主轴
              • +
              +
            • +
            +
          • +
          • 设置主轴方向

            +
              +
            • flex-direction:

              +
                +
              • row—在一行上
              • +
              • column—独占一行
              • +
              +
            • +
            +
          • +
          • 元素的对其方式

            +
              +
            • 主轴的对其方式

              +
                +
              • justify-content:

                +
                  +
                • flex-start

                  +
                    +
                  • 在主轴的开始位置显示
                  • +
                  +
                • +
                • flex-end

                  +
                    +
                  • 在主轴的结束位置
                  • +
                  +
                • +
                • center

                  +
                    +
                  • 居中
                  • +
                  +
                • +
                • space-between

                  +
                    +
                  • 两端对其 中间自适应
                  • +
                  +
                • +
                • space-around

                  +
                    +
                  • 围绕式
                  • +
                  +
                • +
                +
              • +
              +
            • +
            • 侧轴的对其方式

              +
                +
              • align-items:

                +
                  +
                • flex-start

                  +
                    +
                  • 在侧轴的开始位置
                  • +
                  +
                • +
                • flex-end

                  +
                    +
                  • 在侧轴的结束位置
                  • +
                  +
                • +
                • center

                  +
                    +
                  • 居中
                  • +
                  +
                • +
                • stretch

                  +
                    +
                  • 默认值 拉伸
                  • +
                  +
                • +
                +
              • +
              +
            • +
            • 设置换行后的对其方式

              +
                +
              • align-content:

                +
                  +
                • flex-start

                  +
                    +
                  • 在主轴的开始位置显示
                  • +
                  +
                • +
                • flex-end

                  +
                    +
                  • 在主轴的结束位置
                  • +
                  +
                • +
                • center

                  +
                    +
                  • 居中
                  • +
                  +
                • +
                • space-between

                  +
                    +
                  • 两端对其 中间自适应
                  • +
                  +
                • +
                • space-around

                  +
                    +
                  • 围绕式
                  • +
                  +
                • +
                • stretch

                  +
                    +
                  • 默认值 拉伸
                  • +
                  +
                • +
                +
              • +
              +
            • +
            • 单独设置对其方式

              +
                +
              • align-self:
              • +
              +
            • +
            +
          • +
          • 设置子元素占父元素的几份

            +
              +
            • flex:
            • +
            • 给子元素设置
            • +
            +
          • +
          • 排序

            +
              +
            • order

              +
                +
              • 值越小 越靠前
              • +
              +
            • +
            +
          • +
          • 换行

            +
              +
            • flex-wrap:

              +
                +
              • nowrap; 不换行
              • +
              • wrap; 换行
              • +
              +
            • +
            +
          • +
          +
        • +
        +
      • +
      +
    • +
    • rem+媒体查询

      +
        +
      • rem 相对单位

        +
      • +
      • 媒体查询

        +
          +
        • 媒体类型

          +
            +
          • all 所有设备
          • +
          • print 打印设备
          • +
          • screen 带有屏幕的设备
          • +
          +
        • +
        • 媒体特性

          +
            +
          • width

            +
              +
            • min-width
            • +
            • max-width
            • +
            +
          • +
          • height

            +
              +
            • min-height
            • +
            • max-height
            • +
            +
          • +
          • orientation 屏幕方向

            +
              +
            • portrait 竖屏
            • +
            • landscape 横屏
            • +
            +
          • +
          • 语法

            +

            @media only screen and (width :320px) {
            设置CSS代码
            }

            +
          • +
          +
        • +
        +
      • +
      +
    • +
    • 响应式布局 媒体查询 + 流式布局(伸缩布局)

      +
    • +
    +

    框架

      +
    • 本质:就是一个CSS文件

      +
    • +
    • Bootstrap

      +
        +
      • 下载Bootstrap CSS文件

        +
      • +
      • 初始化结构

        +
          +
        • 添加 解决IE浏览器支持媒体查询兼容性问题

          + +
        • +
        • 添加 解决IE低版本兼容H5标签

          +
        • +
        +
      • +
      • 基本的排版

        +
      • +
      • 状态类

        +
      • +
      • 栅格系统

        +
          +
        • 引用bootstrap.min.css文件

          +
        • +
        • 给父元素设置一个固定的类名

          +
            +
          • .container 带版心
          • +
          • +
          • .container-fluid 通栏显示
          • +
          +
        • +
        • 实现响应式布局(栅格参数)

          +
            +
          • col-lg-值 适配大设备
          • +
          • col-md-值 适配中等设备
          • +
          • col-sm-值 适配小设备
          • +
          • col-xs-值 适配手机设配
          • +
          • 取值范围1-12
          • +
          +
        • +
        +
      • +
      • 响应式工具

        +
          +
        • 控制元素的隐藏和显示
        • +
        +
      • +
      +
    • +
    • Amazeui

      +
    • +
    • Framework7

      +
    • +
    +

    JavaScript阶段

    学习线路

      +
    • JS的基本组成

      +
        +
      • js的基本语法ECMA SCRIPT

        +
          +
        • JS书写位置

          +
            +
          • 外联式

            +
              +
            • 通过script标签引入

              +
                +
              • +
              +
            • +
            +
          • +
          • 内嵌式

            +
              +
            • 在script标签中写入js代码
            • +
            +
          • +
          • 行内式

            +
              +
            • 将js代码写入到标签中

              +
                +
              • +
              +
            • +
            +
          • +
          +
        • +
        • 关键字

          +
            +
          • var 声明变量

            +
          • +
          • break 跳出

            +
          • +
          • this 谁调用,就指向谁

            +
          • +
          • instanceof 判断当前对象是不是通过指定的构造函数创建出来的(在继承中使用)

            +

            对象 instanceof 构造函数

            +
          • +
          • typeof 获取变量的数据类型

            +

            typeof 值

            +
          • +
          +
        • +
        • 通过JS网页中输出消息

          +
            +
          • 弹窗

            +

            alert(“你好世界”)

            +
          • +
          • 在网页输出文字或标签

            +

            document.write(‘你好’);

            +
          • +
          • 打印log

            +

            console.log(‘nihao’);

            +
          • +
          • 弹窗输入框

            +

            prompt(“请输入Password”);

            +
          • +
          • 确定取消框

            +

            confirm(“是否确认”);

            +
          • +
          +
        • +
        • 变量(容器)

          +
            +
          • 定义变量

            +
              +
            • var 自定义变量名称;
            • +
            +
          • +
          • 给变量赋值

            +
              +
            • 变量名称 = 值;
            • +
            +
          • +
          • 命名规范

            +
              +
            • 不能以数字开头
            • +
            • 不能以特殊符号开头
            • +
            • 不推荐使用汉字
            • +
            • 不能是关键字
            • +
            • 不能是保留字
            • +
            • 不能出现空格
            • +
            +
          • +
          +
        • +
        • 数据类形

          +
            +
          • 简单数据类型

            +
              +
            • 字符串类型(string)

              +
            • +
            • 数字类型(number)

              +
                +
              • 最大值

                +
                  +
                • Number.MAX_VALUE;
                • +
                +
              • +
              • 最小值

                +
                  +
                • Number.MIN_VALUE;
                • +
                +
              • +
              +
            • +
            • 布尔类型(boolean)

              +
            • +
            • 未定义(undefined)

              +
            • +
            +
          • +
          • 复杂数据类型

            +
          • +
          +
        • +
        • 数据类型转换

          +
            +
          • 强制类型转换

            +
              +
            • 转换为数字

              +
                +
              • Number(变量);
              • +
              • parseInt(变量); 只保留整数
              • +
              • parseFloat(变量); 会保留小数
              • +
              +
            • +
            • 转换为字符串

              +
                +
              • 变量.toString();
              • +
              • String(变量);
              • +
              +
            • +
            • 转换为布尔类型

              +
                +
              • Boolean();
              • +
              +
            • +
            +
          • +
          • 影式类型转换

            +
          • +
          +
        • +
        • 判断一个值是不是数字

          +
            +
          • isNaN(值);

            +
              +
            • true 不是数字
            • +
            • false 是数字
            • +
            +
          • +
          +
        • +
        • 运算符

          +
            +
          • 算数运算符

            +
              +
              • +
                • +
                  • +
                  • / % () 优先级()
                  • +
                  +
                • +
                +
              • +
              +
            • +
            +
          • +
          • 赋值运算符

            +
              +
            • = 把右侧的结果赋值给左边的变量

              +
            • +
            • a +=b

              +
                +
              • a = a + b;
              • +
              +
            • +
            • a -=b

              +
            • +
            • a *=b

              +
            • +
            • a /=b

              +
            • +
            +
          • +
          • 一元运算符

            +
              +
            • ++

              +
                +
              • ++a

                +
                  +
                • 加1后在赋值
                • +
                +
              • +
              • a++

                +
                  +
                • 赋值后在加1
                • +
                +
              • +
              +
            • +
            +
            +
              - --a
            +  - a--
            +
            +
          • +
          • 比较运算符

            +
              +
            • +
              +
            • +
            • <
            • +
            • +

              = 大于或等于

              +
              +
            • +
            • <= 小于或等于
            • +
            • == 判断值是否相等 不考虑数据类型
            • +
            • === 全等于 考虑数据类型
            • +
            • != 不等于
            • +
            • !== 全不等于
            • +
            +
          • +
          • 逻辑运算符

            +
              +
            • && 且
            • +
            • || 或
            • +
            • ! 飞
            • +
            +
          • +
          +
        • +
        • 判断语句

          +
            +
          • if语句

            +

            if(条件){

            +
                }else{
            +
            +

            }

            +
          • +
          • 三元表达式

            +

            条件表达式 ? 逻辑代码1:逻辑代码2

            +
          • +
          • 多条件判断

            +

            if(条件){

            +
                }else if{
            +
            +

            }

            +
          • +
          • switch语句

            +

            switch (变量) {
            case ‘1’:
            // 代码
            break;
            default:
            // 代码
            break;
            }

            +
          • +
          +
        • +
        • 循环

          +
            +
          • while循环
          • +
          • do while循环
          • +
          • for 循环
          • +
          +
        • +
        • 数组(容器)

          +
            +
          • 保存变量的容器,一次可以保存多个值

            +
          • +
          • 定义数组(ary)

            +
              +
            • 通过字面量的方式

              +

              var 自定义数组名称 = [];

              +
            • +
            +
          • +
          • 赋值

            +
              +
            • 定义数组赋值(初始化)

              +

              var ary = [1,2,3,’A’,’B’,’C’,true,false];

              +
            • +
            • 数组名称[索引值]=值;

              +

              ary[0]=1;

              +
            • +
            +
          • +
          • 从数组中取值

            +

            ary[3];

            +
          • +
          • 数组的遍历

            +
              +
            • 通过循环的方式找数组中的值

              +
            • +
            • 获取数组的长度

              +

              ary.length

              +
            • +
            +
          • +
          • 冒泡排序

            +
              +
            • 升序排序

              +

              var ary = [1, 2, 3, 4, 5];
              for (var i = 0; i < ary.length - 1; i++) {
              for (var j = 0; j < ary.length - 1 - i; j++) {
              if (ary[j] > ary[j + 1]) {
              var temp = ary[j];
              ary[j] = ary[j + 1];
              ary[j + 1] = temp
              }
              }
              }
              alert(ary);

              +
            • +
            • 降序排序

              +

              var ary = [1, 2, 3, 4, 5];
              for (var i = 0; i < ary.length - 1; i++) {
              for (var j = 0; j < ary.length - 1 - i; j++) {
              if (ary[j] < ary[j + 1]) {
              var temp = ary[j];
              ary[j] = ary[j + 1];
              ary[j + 1] = temp
              }
              }
              }
              alert(ary);

              +
            • +
            +
          • +
          +
        • +
        • 函数

          +
            +
          • 将程序中重复出现的代码封装成一个公共的代码

            +
          • +
          • 定义函数

            +
              +
            • 通过function定义函数

              +

              function 自定义函数名称 (){
              逻辑代码
              }

              +
            • +
            • 通过表达式定义函数

              +

              var 函数名称= function(){}

              +
            • +
            • 命名规范

              +
                +
              • 以动词开始
              • +
              • 小驼峰命名法(单词首字母小写,第二个单词首字母大写)
              • +
              +
            • +
            • 函数后面的’()’不能丢

              +
            • +
            • 需要调用函数才会执行

              +
            • +
            +
          • +
          • 调用函数

            +
              +
            • 函数名称();
            • +
            +
          • +
          • 函数中的参数

            +
              +
            • 形式参数(形参)

              +
                +
              • 在定义函数时候,函数名后面()中的变量
              • +
              +
            • +
            • 实际参数(实参)

              +
                +
              • 在调用函数的时候,函数名后面()中的值
              • +
              +
            • +
            • 总结

              +
                +
              1. 如果函数有参数,那么该函数就叫有参函数
              2. +
              3. 如果函数没有参数,那么该函数就叫无参函数
              4. +
              5. 函数中,形参的个数是根据需求中设计的。
              6. +
              7. 形参的名字叫什么都可以。
              8. +
              9. 形参变量的值来自于实参的赋值结果
              10. +
              11. 形参和实参是一一对应的(赋值结果,个数)
              12. +
              +
            • +
            +
          • +
          • 函数中的返回值

            +
              +
            • 将函数里面的值返回给函数外部
            • +
            • 函数内部的值无法在外部使用(作用域的原因)
            • +
            • return后面是什么,返回的就是什么
            • +
            • return后面的代码不在执行
            • +
            • 如果只有return 那么返回的就是undefined
            • +
            +
          • +
          • 作用域

            +
              +
            • 程序中代码能够起作用的区域

              +
            • +
            • 全局作用域(全局变量)

              +
                +
              • 函数外部形成的区域,在全局定义的变量,就是全局变量
              • +
              • 可以在任何区域使用
              • +
              +
            • +
            • 局部作用域(局部变量)

              +
                +
              • 函数内部形成的区域,在局部定义的变量,就是局部变量
              • +
              +
            • +
            +
          • +
          • 作用域链

            +
              +
            • 多个作用域形成的链条状
            • +
            • 在作用域链中,如果没有值,就会沿着链条向上查找
            • +
            +
          • +
          • 匿名函数

            +
              +
            • 函数没有名字

              +

              function () {
              }

              +
            • +
            +
          • +
          • 命名函数

            +
              +
            • 函数有名字

              +

              function name() {
              }
              name();

              +
            • +
            +
          • +
          • 自调用函数(自执行函数)

            +
              +
            • 函数自己调用自己

              +

              (function () {
              alert(‘1’);
              })();

              +
            • +
            +
          • +
          • arguments

            +
              +
            • 在函数中用来保存实参信息的容器

              +
            • +
            • 实参的个数

              +
                +
              • length(保存的就是实参的个数)
              • +
              +
            • +
            • 实参中的具体值

              +
                +
              • 值是以数组的方式保存在arguments中
              • +
              +
            • +
            • 总结

              +
                +
              • 如果函数中的形参个数是确定的,推荐使用形参获取即可
              • +
              • 如果函数中的形参个数不确定,那么使用arguments的方式获取
              • +
              +
            • +
            • 例子

              +

              alert(getMax(1, 311, 9, 2));
              function getMax() {
              var max = arguments[0];
              for (var i = 1; i < arguments.length; i++) {
              if (arguments[i] > max) {
              max = arguments[i];
              }
              }
              return max;
              }

              +
            • +
            +
          • +
          +
        • +
        • 对象(容器)

          +
            +
          • 在程序中对具体事物的一个抽象描述(特征,功能)

            +
          • +
          • 定义/创建对象

            +
              +
            • 通过字面量的方式

              +

              var 自定义对象名称 = {}

              +
            • +
            • 通过构造函数创建对象

              +

              var 自定义对象名称 = new Object();

              +
            • +
            • 通过工厂的方式创建对象

              +

              function creatrObject(xm, sjh) {
              var dx = new Object();
              dx.username = xm;
              dx.phone = sjh;
              return dx;
              }
              var zs = creatrObject(“张三”, 123);
              var ls = creatrObject(“李四”, 123);

              +
                +
              • 将构造函数创建对象封装成一个函数
              • +
              +
            • +
            • 通过自定义构造函数的方式创建对象

              +

              function People(xm, sjh) {
              this.username = xm;
              this.phone = sjh;
              this.sing = function () {
              alert(‘在唱歌’);
              }
              }
              var zs = new People(“01”,1);
              var ls = new People(“02”,2);

              +
                +
              • 先准备一个自定义构造函数

                +
              • +
              • 通过自定义构造函数创建对象

                +
              • +
              • 注意

                +
                  +
                • 命名规范:帕斯卡命名法(大驼峰法)每个单词首字母大写
                • +
                • 构造函数以名词开始
                • +
                +
              • +
              +
            • +
            +
          • +
          • 赋值

            +
              +
            • 定义对象赋值(初始化)

              +

              var mc = {
              username: “张三”,
              height: 180,
              weight: 75,
              }

              +
            • +
            • 键值对赋值

              +

              mc[“username”] = “张三”;
              mc[“sing”] = function(){
              alert(“张三正在唱歌”);
              };

              +
            • +
            • 通过”对象.”的方式赋值

              +

              mc.username = “张三”;
              mc.sing = function (){
              alert(“张三正在唱歌”);
              }

              +
            • +
            • 注意

              +
                +
              • 对象中的值是以 键/值 对的形式出现

                +

                例如:
                username: “张三”,
                height: 180,
                weight:75,

                +
              • +
              • 键 就是自定义变量

                +
              • +
              • 在对象的函数叫方法

                +
              • +
              • 在对象中 用来描述对象特征的键称之为对象的属性

                +
              • +
              +
            • +
            +
          • +
          • 从对象中取值

            +
              +
            • 键值对的方式取值

              +

              mc[‘username’]
              mc“sing”;

              +
            • +
            • “对象.”的方式取值

              +

              mc.username
              mc.sing()

              +
            • +
            +
          • +
          • 对象的遍历

            +

            for (key in 对象) {
            console.log(obj[key]);
            }

            +
          • +
          • 简单数据类型(栈区)

            +
          • +
          • 复杂数据类型(堆区)

            +
              +
            • 在堆区保存结果
            • +
            • 在赋值的过程中,将内存地址拷贝赋值给另外一个变量
            • +
            +
          • +
          +
        • +
        • 内置对象

          +
            +
          • 在js中,已经写好的对象

            +
          • +
          • 如何学 自学查文档

            +
          • +
          • Math

            +
              +
            • Math.PI

              +
                +
              • 圆周率
              • +
              +
            • +
            • Math.pow(x,y)

              +
                +
              • x的y次方
              • +
              +
            • +
            • Math.abs(x)

              +
                +
              • x的绝对值
              • +
              +
            • +
            • Math.round(x)

              +
                +
              • x四舍五入
              • +
              +
            • +
            • Math.max()

              +
                +
              • 获取一组数字的最大值
              • +
              +
            • +
            • Math.min()

              +
                +
              • 获取一组数字的最小值
              • +
              +
            • +
            • Math.sin(a)

              +
                +
              • 获取角的正弦值
              • +
              +
            • +
            +
          • +
          • Date

            +
          • +
          • Array

            +
              +
            • 取值

              +
                +
              • 数组名.pop();

                +
                  +
                • 在数组的结尾处取值
                • +
                +
              • +
              • 数组名.shift();

                +
                  +
                • 在数组的开始处取值
                • +
                +
              • +
              +
            • +
            • 赋值

              +
                +
              • 数组名.push();

                +
                  +
                • 在数组的结尾处添加值
                • +
                +
              • +
              • 数组名.unshift();

                +
                  +
                • 在数组的开始出添加值
                • +
                +
              • +
              +
            • +
            • 筛选

              +
                +
              • 数组名.indexOf();

                +
                  +
                • 获取数组中对应值得索引位置(从前往后)
                • +
                +
              • +
              • 数组名.lastIndexOf();

                +
                  +
                • 获取数组中对应值的索引位置(从后往前)
                • +
                +
              • +
              +
            • +
            • 翻转

              +
                +
              • 数组名.reverse();
              • +
              +
            • +
            • 拼接字符串

              +
                +
              • 数组名.join(“|”);
              • +
              +
            • +
            +
          • +
          • 字符串

            +
              +
            • 分割字符串

              +
                +
              • 字符串.split
              • +
              +
            • +
            • 截取字符串

              +
                +
              • 字符串.slice(开始位置,结束位置);

                +
                  +
                • 不包括结束位置
                • +
                • 返回的是字符串
                • +
                • 没有设置结束位置截取到最后
                • +
                • 如果是负数,从最后做减法
                • +
                +
              • +
              • 字符串.substring(开始位置,结束位置);

                +
              • +
              • 字符串.substr(开始位置,个数);

                +
              • +
              +
            • +
            • 去空白

              +
                +
              • 字符串.trim();

                +
                  +
                • 去掉首尾空格
                • +
                +
              • +
              +
            • +
            • 替换

              +
                +
              • 字符串.replace(x,y)

                +
                  +
                • x替换成y
                • +
                +
              • +
              +
            • +
            • 拼接字符串

              +
                +
              • 字符串.concat();
              • +
              +
            • +
            • 字符串.charAt(index)

              +
            • +
            • 字符串.str[index]

              +
            • +
            +
          • +
          +
        • +
        • 代码预解析

          +
            +
          • 在代码开始执行之前,将代码进行一个预先编译的过程

            +
          • +
          • 变量的提升

            +
              +
            • 当程序中出现变量,那么就会将变量声明提升到当前作用域的开始位置.不包括变量的赋值
            • +
            +
          • +
          • 函数的提升

            +
              +
            • 当程序中出现函数,就会将函数的声明提升到当前作用域的开始位置,不包括函数的调用
            • +
            +
          • +
          +
        • +
        • 方法

          +
            +
          • 字符串中转义字符

            +
              +
            • \
            • +
            • \n \r 回车换行
            • +
            +
          • +
          • 分割字符串

            +
              +
            • 字符串.split(); 得到的是数组
            • +
            +
          • +
          • 获取系统时间

            +
              +
            • 定义一个日期对象

              +
                +
              • var d = new Date();
              • +
              +
            • +
            • 获取年

              +
                +
              • getFullYear();
              • +
              +
            • +
            • 获取月

              +
                +
              • getMonth()+1;
              • +
              +
            • +
            • 获取日

              +
                +
              • getDate();
              • +
              +
            • +
            • 获取星期

              +
                +
              • getDay();
              • +
              +
            • +
            • +
                +
              • getHours();
              • +
              +
            • +
            • +
                +
              • getMinutes();
              • +
              +
            • +
            • +
                +
              • getSeconds();
              • +
              +
            • +
            +
          • +
          • 数学

            +
              +
            • Math.floor(值);

              +
                +
              • 地板函数:得到的都是整数(小于当前数并与它最接近的一个整数)
              • +
              +
            • +
            • Math.ceil(值);

              +
                +
              • 天花板函数:得到的是整数(小于当前数并与它最接近的一个整数)
              • +
              +
            • +
            • Math.random();

              +
                +
              • 随机数函数:[0,1)
              • +
              +
            • +
            • Math.floor(Math.random()*(max-min+1)+min)

              +
                +
              • 随机数公式
              • +
              +
            • +
            +
          • +
          +
        • +
        +
      • +
      • WebAPI

        +
          +
        • DOM操作([文档对象模型]通过js的方式操作页面的标签)document

          +
            +
          • 记住各种API+逻辑思维====>>网页效果

            +
          • +
          • 选中标签

            +
              +
            • 通过标签的名字获取标签

              +

              document.getElementsByName(“标签名字”);

              +
                +
              • 返回的是一个伪数组
              • +
              • 如果要获取每个具体标签对象,通过循环遍历的方式
              • +
              +
            • +
            • 通过CSS选择器获取标签

              +

              document.querySelector(“CSS选择器”);

              +
                +
              • 智能选中瞒住条件的一个标签
              • +
              +
            • +
            • 通过CSS选择器获取全部标签

              +

              document.querySelectorAll(“css选择器”);

              +
                +
              • 返回的是一个伪数组
              • +
              +
            • +
            • 通过标签ID获取标签

              +

              document.getElementById(“标签ID”);

              +
                +
              • 只能获取一个标签
              • +
              +
            • +
            +
          • +
          • 事件

            +
              +
            • 给标签绑定事件

              +
                +
              • 事件源

                +
                  +
                • 要绑定事件的标签
                • +
                +
              • +
              • 事件类型

                +
                  +
                • on的方式

                  +
                    +
                  • 鼠标

                    +
                      +
                    • 单击事件onclick
                    • +
                    • 双击ondblclick
                    • +
                    • 获取焦点onfocus
                    • +
                    • 失去焦点onblur
                    • +
                    • onmouseenter鼠标进入
                    • +
                    • onmouseleave鼠标离开
                    • +
                    • onmouseover鼠标悬停
                    • +
                    • onmouseout鼠标悬停离开
                    • +
                    • onmousemove鼠标移动事件
                    • +
                    • onmouseup鼠标弹起事件
                    • +
                    • onmousedown鼠标按下事件
                    • +
                    • onscroll滚动条事件
                    • +
                    +
                  • +
                  • 键盘

                    +
                      +
                    • oninput输入事件
                    • +
                    • onkeydown键盘按下事件(可以获取所有按键)
                    • +
                    • onkeyup键盘松开事件
                    • +
                    • onkeypress键盘按下事件(不能获取系统按键)
                    • +
                    +
                  • +
                  • onchange 发生改变

                    +
                  • +
                  • 通过on的方式给元素注册事件的时候,注册用一个事件,那么最后的事件会覆盖前面的事件

                    +
                  • +
                  +
                • +
                • 事件监听的方式(多个人使用)

                  +
                    +
                  • 事件源.addEventListener(事件类型,处理程序,参数3);

                    +
                      +
                    • 参数1–>事件类型(不能加on除自带on)
                    • +
                    • 参数2–>处理程序
                    • +
                    • 参数3–>true捕获效果 false冒泡效果
                    • +
                    +
                  • +
                  • 移动端事件

                    +
                      +
                    • 按下事件

                      +

                      div.addEventListener(“touchstart”, function () {
                      console.log(“按下事件”);
                      });

                      +
                    • +
                    • 抬起事件

                      +

                      div.addEventListener(“touchend”, function () {
                      console.log(“抬起事件”);
                      });

                      +
                    • +
                    • 移动事件

                      +

                      div.addEventListener(“touchmove”, function () {
                      console.log(“移动事件”);
                      });

                      +
                    • +
                    • 封装移动端的点击事件

                      +
                        +
                      • 点击事件:判断;手指按下的位置和手指离开的距离(通过事件对象参数)

                        +
                      • +
                      • 手指信息 手指位置和个数

                        +
                      • +
                      • 移动端的事件对象参数

                        +
                          +
                        • e.touches获取位于移动设备的屏幕上的手指信息(伪数组)

                          +
                            +
                          • clientX —>获取距离视口的位置
                          • +
                          • pageX —>获取距离页面的坐标
                          • +
                          • screenX —>获取距离整个屏幕的坐标
                          • +
                          +
                        • +
                        • e.targetTouches获取元素身上的手指信息

                          +
                        • +
                        • e.changedTouches 离开屏幕的信息

                          +
                        • +
                        • 封装案例

                          +
                            +
                          • index.html
                          • +
                          +
                        • +
                        +
                      • +
                      +
                    • +
                    +
                  • +
                  +
                • +
                • ie低版本浏览器

                  +
                    +
                  • 事件源.attachEvent(参数1,参数2)

                    +
                      +
                    • 参数1–>事件类型(需要加on)
                    • +
                    • 参数2–>处理程序
                    • +
                    +
                  • +
                  • 腻子程序

                    +
                  • +
                  +
                • +
                +
              • +
              • 处理程序

                +
                  +
                • 实现功能的一个函数
                • +
                +
              • +
              +
            • +
            • 给元素移除事件

              +
                +
              • on的方式

                +
                  +
                • 事件源.事件类型= null;
                • +
                +
              • +
              • 事件监听的方式

                +
                  +
                • 事件源.removeEventListener(参数1,参数2)

                  +
                    +
                  • 参数1–>要被移除的事件类型
                  • +
                  • 参数2–>要被移除的处理程序(命名函数)
                  • +
                  +
                • +
                +
              • +
              • ie低版本浏览器

                +
                  +
                • 事件源.detachEvent(参数1,参数2)
                • +
                +
              • +
              +
            • +
            • 获取标签中的值

              +
                +
              • 对象名.innerHTML

                +
                  +
                • 同时可以获取文本或标签,特殊符号
                • +
                +
              • +
              • 对象名.innerText

                +
                  +
                • 获取标签中的纯文本
                • +
                +
              • +
              +
            • +
            • 给标签动态的赋值

              +
                +
              • 对象名.innerHTML = 值;

                +
                  +
                • 遇到HTML标签会把标签进行解析
                • +
                +
              • +
              • 对象名.innerText = 值;

                +
                  +
                • 纯文本
                • +
                +
              • +
              +
            • +
            • 阻止a标签跳转

              +
                +
              • 在a标签的事件中设置return false

                +

                a.onclick = function () { return false }

                +
              • +
              • 在a标签的href中设置JavaScript:;

                +
              • +
              +
            • +
            +
          • +
          • 事件流(事件执行过程)

            +
              +
            • 1.事件捕获

              +
            • +
            • 2.事件执行

              +
            • +
            • 3.事件冒泡

              +
                +
              • 可以阻止
              • +
              +
            • +
            +
          • +
          • 委托(事件对象参数)

            +
              +
            • 自己应该完成的事情委托别人完成

              +
            • +
            • 事件对象参数

              +
                +
              • 当用户在执行某个事件的时候,会将执行过程中的信息保存起来

                +
              • +
              • e.target—>获取真正执行事件的事件源

                +
              • +
              • e.target—>获取正在执行事件的类型

                +
              • +
              • e.key—>键的名字

                +
              • +
              • e.keyCode—>键的值

                +
              • +
              • 鼠标在页面中的位置

                +
                  +
                • e.clientX—>从HTML的可视区域左上角开始计算
                • +
                • e.clientY—>
                • +
                • e.offsetX—>从当前元素的左上边开始计算
                • +
                • e.offsetY
                • +
                • e.pageX—>从页面的可视区域左上角开始计算
                • +
                • e.pageY
                • +
                • e.screenX—>从整个屏幕的左上角开始计算
                • +
                • e.screenX
                • +
                +
              • +
              +
            • +
            +
          • +
          • 改变样式

            +
              +
            • 给标签添加类样式

              +
                +
              • 先定义好一个类样式
              • +
              • 对象名.className = “类名”;
              • +
              • 添加多个类名 使用多个空格隔开
              • +
              • 属性多的时候可以使用
              • +
              +
            • +
            • 行内添加样式

              +
                +
              • style属性实现

                +
              • +
              • 对象名.style.css属性名字=值

                +
              • +
              • 案例

                +
                哈哈
                + +
              • +
              • 就是给标签动态添加了一个style属性

                +
              • +
              • 属性比较少的时候可以使用

                +
              • +
              +
            • +
            • H5的方式操作样式

              +
                +
              • 通过H5的方式给标签添加类样式

                +
              • +
              • 添加类名

                +
                  +
                • 对象名.classList.add(“类名”,”类名”);
                • +
                +
              • +
              • 删除类名

                +
                  +
                • 对象名.classList.remove(“类名”);
                • +
                +
              • +
              • 是否包含

                +
                  +
                • 对象名.classList.contains(“类名”);
                • +
                +
              • +
              • 有删无加

                +
                  +
                • 对象名.classList.toggle(“类名”);
                • +
                +
              • +
              • 案例

                +
                + + +
              • +
              • 为函数开启

                +

                function fn() {
                “use strict”
                var n = 2;
                console.log(n)
                }
                fn();

                +
              • +
              +
            • +
            • 变量规定

              +
                +
              • 声明必须加var
              • +
              • 不能删除已定义的变量
              • +
              +
            • +
            • this指向

              +
                +
              • 普通函数this是undefined
              • +
              +
            • +
            • 函数变化

              +
                +
              • 参数不能重名
              • +
              • 函数必须声明在顶层
              • +
              +
            • +
            • 更多严格模式要求参考

              +
            • +
            +
          • +
          +
        • +
        • 高阶函数

          +
            +
          • 对其他函数进行操作的函数,它接收函数作为参数或将函数作为返回值输出。
          • +
          +
        • +
        • 闭包

          +
            +
          • 一个作用域可以访问另外一个函数内部的局部变量

            +
          • +
          • 变量作用域

            +
              +
            • 函数内部可以使用全局变量
            • +
            • 函数外部不可以使用局部变量
            • +
            • 当函数执行完毕,本作用域内的局部变量会销毁
            • +
            +
          • +
          +
        • +
        • 递归

          +
            +
          • 函数内部自己调用自己
          • +
          • 退出条件 return
          • +
          +
        • +
        • 拷贝

          +
            +
          • 浅拷贝

            +
              +
            • 只拷贝最外面一层

              +
            • +
            • for in方法拷贝

              +

              var obj = {
              name : ‘张三丰’,
              age : 22
              };

              var newObj = {};
              for (key in obj) {
              newObj[key] = obj[key];
              }

              console.log(newObj);

              +
            • +
            • Object.assign(新, 旧)

              +

              Object.assign(target, sources);

              +

              console.log(newObj);

              +
            • +
            +
          • +
          • 深拷贝

            +
              +
            • 使用递归

              +

              var obj = {
              name : ‘1张三丰’,
              age : 22,
              messige : {
              sex : ‘男’,
              score : 16
              },
              color : [‘red’,’purple’,’qing’]

              }

              var newObj = {};


              function kaobei (newObj,obj) {

              for (key in obj) {

              if (obj[key] instanceof Array) {
              newObj[key] = [];
              kaobei(newObj[key],obj[key]);
              } else if (obj[key] instanceof Object) {
              newObj[key] = {};
              kaobei(newObj[key],obj[key])
              } else {
              newObj[key] = obj[key];
              }

              }

              }
              obj.messige.sex = 99;
              kaobei(newObj,obj);
              console.log(newObj);

              +
            • +
            +
          • +
          +
        • +
        • 正则表达式

          +
            +
          • 使用

            +
              +
            • 实例化对象

              +

              var regexp = new RegExp(/123/);
              console.log(regexp);

              +
            • +
            • 字面量创建

              +

              var rg = /abc/;

              +
            • +
            +
          • +
          • 测试正则表达式

            +
              +
            • 表达式名.test(str)
            • +
            +
          • +
          • 表达式

            +
              +
            • 简单字符
            • +
            • 特殊字符
            • +
            +
          • +
          • 边界符

            +
              +
            • ^ 以谁开始

              +

              var reg = /^abc/;
              console.log(reg.test(‘abca’));

              +
            • +
            • $ 以谁结束

              +

              var q = /abc$/;
              console.log(q.test(‘aabc’));

              +
            • +
            • /^ $/ 精准匹配

              +

              var q = /^abc$/;
              console.log(q.test(‘abc’));

              +
            • +
            +
          • +
          • 字符类

            +
              +
            • [] 方括号

              +
                +
              • /[abc]/ 包含其中一个
              • +
              • /[a|b|c]/ 包含其中一个
              • +
              • /^[a-zA-Z0-9]$/ 包含
              • +
              • /^[^a-zA-Z0-9]$/ 取反
              • +
              +
            • +
            • 量词符

              +
                +
                • +
                • 重复0次或更多次
                • +
                +
              • +
                • +
                • 重复1次或更多次
                • +
                +
              • +
              • ? 重复0次或1次
              • +
              • {n} 重复n次
              • +
              • {n,} 重复n次或更多次
              • +
              • {n,m} 重复n到m次
              • +
              +
            • +
            • 预定义类

              +
                +
              • \d 匹配0-9之间任意数字
              • +
              • \D 匹配0-9以外任意字符
              • +
              • \w 匹配任意的字母 数字 下划线
              • +
              • \W 除所有字母 数字 下划线以外的字符
              • +
              • \s 匹配空格
              • +
              • \S 匹配非空格的字符
              • +
              +
            • +
            +
          • +
          +
        • +
        +
      • +
      • AJAX编程

        +
          +
        • 个人整理

          +
            +
          • 什么是AJAX?

            +
              +
            • async javascript and xml

              +
                +
              • 异步的JavaScript和XML
              • +
              +
            • +
            • 什么是异步?

              +
                +
              • 不阻塞
              • +
              +
            • +
            • XML 数据格式

              +
                +
              • 子主题 1
              • +
              +
            • +
            • 优点

              +
                +
              • 局部更新,用户体验好
              • +
              • 分离,有利于前后端分工合作
              • +
              +
            • +
            • 什么时候使用AJAX?

              +
                +
              • 在不更新页面的情况下,浏览器从web服务器新数据以更新界面
              • +
              +
            • +
            +
          • +
          • 请求与响应

            +
              +
            • request 请求:浏览器向web服务器请求地址(文件,接口)
            • +
            • response 响应:web服务器处理请求,返回结果(结果在响应体中)
            • +
            +
          • +
          • 服务器与客户端

            +
              +
            • 服务器==高配置+特殊软件
            • +
            • 客户端==低配置硬件 + 一般软件
            • +
            +
          • +
          • 本地虚拟服务器–小黑窗的使用

            +
              +
            • 自己访问网页
            • +
            • 让其它同学访问网页
            • +
            • 访问内置接口
            • +
            +
          • +
          • 使用jQuery调用

            +
              +
            • $.ajax({})

              +
                +
              • async 是否需要异步
              • +
              • cache 缓存
              • +
              • url 接口地址
              • +
              • type 请求方式
              • +
              • success 成功执行函数
              • +
              • error 错误执行函数
              • +
              +
            • +
            +
          • +
          • 使用原生调用

            +
              +
            • get

              +
                +
              • 创建对象

                +
                  +
                • var xhr = new XMLHttpRequest();
                • +
                +
              • +
              • 设置请求

                +
                  +
                • xhr.open(“get”, “url”+?参数=值&);
                • +
                +
              • +
              • 设置回调函数

                +
                  +
                • xhr.onload=function () { console.log(xhr.responseText); };
                • +
                • xhr.onreadystatechange = function(){}兼容ie
                • +
                +
              • +
              • 发送

                +
                  +
                • xhr.send();
                • +
                +
              • +
              +
            • +
            • post

              +
                +
              • 创建对象

                +
                  +
                • var xhr = new XMLHttpRequest();
                • +
                +
              • +
              • 设置请求

                +
                  +
                • xhr.open(“post”, “url”);
                • +
                +
              • +
              • 设置请求头

                +
                  +
                • xhr.setRequestHeader()
                • +
                +
              • +
              • 设置回调函数

                +
                  +
                • xhr.onload=function () { console.log(xhr.responseText); };
                • +
                • xhr.onreadystatechange = function(){}兼容ie
                • +
                +
              • +
              • 发送

                +
                  +
                • xhr.send(参数);
                • +
                +
              • +
              +
            • +
            +
          • +
          • 服务器返回的数据

            +
              +
            • JSON字符串

              +
                +
              • var arr = JSON.parse(str); 字符串转数组
              • +
              • var str = JSON.stringify(arr); 数组转字符串
              • +
              +
            • +
            • XML字符串

              +
            • +
            +
          • +
          • 同步与异步

            +
              +
            • xml.open(类型,地址,是否异步)
            • +
            • 建议使用异步
            • +
            +
          • +
          • 接口

            +
              +
            • 后端写好的函数,前端直接调用
            • +
            • 小黑窗服务器已经内置几个接口
            • +
            • 是一段代码
            • +
            +
          • +
          • http协议

            +
              +
            • 浏览器与web服务器的约定

              +
            • +
            • 内容

              +
                +
              • 请求报文

                +
                  +
                • +
                    +
                  • 请求方式 路径 协议及版本
                  • +
                  +
                • +
                • +
                    +
                  • 浏览器在向服务器发送请求的时候自动携带的信息
                  • +
                  +
                • +
                • +
                    +
                  • post的传入的参数
                  • +
                  +
                • +
                +
              • +
              • 响应报文

                +
                  +
                • +
                    +
                  • Status 状态码

                    +
                      +
                    • 200 成功
                    • +
                    • 302 重定向
                    • +
                    • 304 读取缓存
                    • +
                    • 404 资源不存在
                    • +
                    • 500 服务器内部错误
                    • +
                    +
                  • +
                  +
                • +
                • +
                    +
                  • 服务器返回响应的时候携带了附加信息
                  • +
                  • 由键值对组成
                  • +
                  • 每个请求的响应头可能都不一样:它完全取决于服务器;
                  • +
                  +
                • +
                • +
                    +
                  • 响应内容,最重要的。
                  • +
                  +
                • +
                +
              • +
              +
            • +
            +
          • +
          +
        • +
        • 讲师整理

          +
            +
          • $.ajax()

            +
              +
            • $.ajax({url, type, data,success:function(res){} })
            • +
            +
          • +
          • 接口

            +
              +
            • 后端写好的函数,前端直接调用
            • +
            • 小黑窗服务器已经内置几个接口
            • +
            • 是一段代码
            • +
            +
          • +
          • http协议

            +
              +
            • 浏览器与web服务器的约定

              +
            • +
            • 内容

              +
                +
              • 请求报文

                +
                  +
                • +
                    +
                  • 路径
                  • +
                  +
                • +
                • +
                • +
                • +
                • +
                +
              • +
              • 响应报文

                +
                  +
                • +
                    +
                  • 状态码

                    +
                      +
                    • 记得5个
                    • +
                    +
                  • +
                  +
                • +
                • +
                • +
                • +
                    +
                  • 响应内容,最重要的。
                  • +
                  +
                • +
                +
              • +
              +
            • +
            +
          • +
          • 服务器与客户端

            +
              +
            • 服务器:高配置+特殊软件
            • +
            +
          • +
          • 本地虚拟服务器–小黑窗的使用

            +
              +
            • 自己访问网页
            • +
            • 让其它同学访问网页
            • +
            • 访问内置接口
            • +
            +
          • +
          • 请求与响应

            +
              +
            • 请求:浏览器向web服务器请求地址(文件,接口)
            • +
            • 响应:web服务器处理请求,返回结果(结果在响应体中)
            • +
            +
          • +
          • ajax

            +
              +
            • 场景

              +
                +
              • 局部更新
              • +
              +
            • +
            • 是什么

              +
                +
              • async javascript and xml
              • +
              • 异步
              • +
              • XML
              • +
              +
            • +
            • 优点

              +
                +
              • 局部更新,用户体验好
              • +
              • 分离,有利于前后端分工合作
              • +
              +
            • +
            +
          • +
          • 留言板案例

            +
          • +
          • 原生ajax

            +
              +
            • get-带参数

              +
                +
              • var xhr = new XMLHttpRequest()
              • +
              • xhr.open(‘get’,’url?参数1=值1&参数2=值2&参数3=值3’)
              • +
              • xhr.onload=function(){ xhr.responseText ;// 响应体}
              • +
              • xhr.send()
              • +
              +
            • +
            • post-带参数

              +
                +
              • var xhr = new XMLHttpRequest()
              • +
              • xhr.open(‘post’,’url’)
              • +
              • xhr.setRequestHeader(‘Content-type’,’application/x-www-form-urlencoded’)
              • +
              • xhr.onload=function(){ xhr.responseText ;// 响应体}
              • +
              • xhr.send(参数1=值1&参数2=值2&参数3=值3)
              • +
              +
            • +
            • 服务器返回的数据

              +
                +
              • JSON字符串

                +
                  +
                • 把JSON字符串转对象

                  +
                    +
                  • JSON.parse()
                  • +
                  +
                • +
                • 把js数据转成JSON字符串

                  +
                    +
                  • JSON.stringify()
                  • +
                  +
                • +
                +
              • +
              • XML字符串

                +
              • +
              +
            • +
            • ajax同步与异步

              +
                +
              • xhr.open(类型,地址,是否异步)
              • +
              • 建议使用异步。
              • +
              +
            • +
            • ie 的问题

              +
                +
              • 兼容性 onload 用 onreadystatechange

                +
              • +
              • ie get请求有缓存

                +
                  +
                • 解决:url?_=时间戳
                • +
                +
              • +
              +
            • +
            • 解决ajax的异常

              +
                +
              • 500,404错误
              • +
              • xhr.status 表示http响应状态码。
              • +
              +
            • +
            • FormData

              +
                +
              • 1.上传文件

                +
                  +
                • 进度条
                • +
                • xhr.upload.onprogress
                • +
                +
              • +
              • 2.快速获取表单元素的值

                +
                  +
                • var fd = new FormData(表单的dom元素);xhr.send(fd)
                • +
                • 不需要额外设置请求头
                • +
                +
              • +
              +
            • +
            • 封装

              +
                +
              • $.ajax({url,data,type,success})
              • +
              +
            • +
            +
          • +
          • postman

            +
              +
            • 测试接口
            • +
            +
          • +
          • ajax库

            +
              +
            • $.ajax()其它参数

              +
                +
              • $.get
              • +
              • $.post
              • +
              • $.getJSON
              • +
              +
            • +
            • axios

              +
                +
              • 会用get
              • +
              • 会用post
              • +
              +
            • +
            +
          • +
          • [案例]会员管理系统

            +
          • +
          +
        • +
        +
      • +
      • 服务端程序

        +
      • +
      +

      js效果

        +
      • 制作游戏(手机端)
      • +
      • 通过JS实现地理定位
      • +
      • 实现服务端应用(bodejs)
      • +
      +

      Git阶段

      个人整理

        +
      • 基本指令

        +
          +
        • mkdir 目录名 新建目录
        • +
        • cd 目录名 进入目录
        • +
        • cd .. 返回上级
        • +
        • ls 查看目录
        • +
        • la -a 查看隐藏的目录
        • +
        • touch 文件名 新建文件
        • +
        • rm 文件名 删除文件
        • +
        • clear 清屏
        • +
        • cat 文件名 查看文件内容
        • +
        • less 文件名 查看文件内容 按q退出
        • +
        +
      • +
      • 常见命令

        +
          +
        • git init 初始化

          +
        • +
        • git add . 添加文件

          +
        • +
        • git commit -m “” 添加备注

          +
        • +
        • git log 查看日志

          +
        • +
        • git status 查看状态

          +
            +
          • untracked 未跟踪
          • +
          • staged 已暂存
          • +
          • committed 已提交
          • +
          • modified 已修改
          • +
          +
        • +
        • git checkout . 放弃工作区的修改

          +
        • +
        +
      • +
      +

      讲师整理

        +
      • 作用

        +
          +
        • 对代码进行版本管理
        • +
        +
      • +
      • 工作流程

        +
          +
        • 初始git

          +
            +
          • 一个空目录
          • +
          • git init
          • +
          +
        • +
        • 向这个目录添加文件

          +
        • +
        • git add

          +
        • +
        • git commit

          +
        • +
        • 修改文件

          +
        • +
        • git commit -a -m “”

          +
        • +
        +
      • +
      • 三个区域

        +
          +
        • 工作区

          +
        • +
        • 暂存区

          +
            +
          • add
          • +
          +
        • +
        • 仓库

          +
            +
          • commit
          • +
          +
        • +
        +
      • +
      • 文件四种状态

        +
          +
        • 未跟踪

          +
        • +
        • 已跟踪

          +
            +
          • 已修改
          • +
          • 已提交
          • +
          • 已暂存
          • +
          +
        • +
        +
      • +
      • 还原代码

        +
          +
        • 从工作区还原

          +
            +
          • git checkout – 文件名
          • +
          • git checkout .
          • +
          +
        • +
        • 从暂存区还原

          +
            +
          • 某个文件

            +
              +
            • git reset HEAD 文件名
            • +
            • git checkout – 文件名
            • +
            +
          • +
          • 全部

            +
              +
            • git reset HEAD
            • +
            • git checkout .
            • +
            +
          • +
          +
        • +
        • 从仓库中还原

          +
            +
          • 找出commitID

            +
              +
            • git log
            • +
            • git log –oneline
            • +
            +
          • +
          • 整体还原

            +
              +
            • git reset –hard commitID
            • +
            +
          • +
          • 还原某个文件

            +
              +
            • git checkout commitID 文件名
            • +
            +
          • +
          +
        • +
        +
      • +
      • 分支

        +
          +
        • 为什么要建分支

          +
            +
          • 为了不影响原来的代码。
          • +
          +
        • +
        • 命令

          +
            +
          • 创建

            +
              +
            • git branch 分支名
            • +
            +
          • +
          • 切换

            +
              +
            • git checkout 分支名
            • +
            +
          • +
          • 创建并切换

            +
              +
            • git checkout -b 分支名
            • +
            +
          • +
          • 删除

            +
              +
            • git brand -d 分支名
            • +
            +
          • +
          +
        • +
        • 合并分支

          +
            +
          • 为什么要合并
          • +
          • git merge 分支名
          • +
          +
        • +
        • 冲突

          +
            +
          • 手动解决
          • +
          • add ,commit
          • +
          +
        • +
        +
      • +
      • 远程仓库

        +
          +
        • github

          +
            +
          • git clone

            +
              +
            • 第一天上班,down代码
            • +
            • 找代码管理员要权限
            • +
            +
          • +
          • git pull

            +
              +
            • 每天上班第一件事,拉最新的代码
            • +
            +
          • +
          • git push

            +
              +
            • 下班最后一件班,提本地最新的代码
            • +
            +
          • +
          +
        • +
        +
      • +
      +

      大事件

      操作

        +
      • 建立远程仓库 github

        +
      • +
      • git clone 到本地

        +
      • +
      • 加入初始的代码

        +
          +
        • apiserver

          +
        • +
        • web_back

          +
            +
          • 管理员使用的页面
          • +
          +
        • +
        • web_front

          +
            +
          • 游客使用的页面
          • +
          +
        • +
        +
      • +
      • 本地提交一次,形成第一个版本

        +
      • +
      • git push 到远程

        +
      • +
      • 在本地开发:

        +
          +
        • 拉分支

          +
            +
          • git checkout -b dev
          • +
          +
        • +
        +
      • +
      +

      开发具体功能

        +
      • 测接口是否正常工作

        +
          +
        • postman
        • +
        +
      • +
      • 发ajax请求。。。。。。

        +
      • +
      • 开发完成之后

        +
          +
        • git 提交新版本
        • +
        • vscode -git-提交版本
        • +
        +
      • +
      +

      ajax操作的封装

        +
      • js/utils

        +
          +
        • config.js

          +
            +
          • 把项目中要用到的配置信息,单独放置。包括每个接口的url地址
          • +
          +
        • +
        • user.js

          +
            +
          • 把所有涉及用户操作的代码,全写在一起。
          • +
          +
        • +
        +
      • +
      +

      具体功能

        +
      • 管理员

        +
          +
        • 用户的登陆

          +
            +
          • bootstrap的模态框
          • +
          +
        • +
        • 用户的退出

          +
        • +
        • 用户获取个人信息

          +
        • +
        • 修改个人信息

          +
        • +
        +
      • +
      • 文章类型管理

        +
          +
        • 添加
        • +
        • 修改
        • +
        • 删除
        • +
        • 查询-显示
        • +
        +
      • +
      • 文章管理

        +
          +
        • 显示,筛选
        • +
        • 分页
        • +
        • 添加
        • +
        • 删除
        • +
        • 编辑
        • +
        +
      • +
      • 插件

        +
          +
        • 分页
        • +
        • 日历
        • +
        • 富文本框
        • +
        +
      • +
      • 游客

        +
          +
        • 主页

          +
        • +
        • 详情页

          +
            +
          • 添加
          • +
          • 查看评论
          • +
          +
        • +
        +
      • +
      +

      排错

        +
      • 观察在控制台中,是否出现了错误
      • +
      +

      arttemplate

        +
      • 模板引擎

        +
          +
        • 作用:把数据快速转换成html字符串
        • +
        +
      • +
      • 步骤

        +
          +
        • 引入 .js文件

          +
        • +
        • 准备数据 res

          +
            +
          • 一定是一个对象 { name: “XXX” }
          • +
          +
        • +
        • 准备模板

          +
        • +
        +
      • +
      +
      1
      2
      3
      4
      <script id="" type="text/html">{{name}}</script>
      {{each res对象中的属性名-数组 item index}}

      var htmlStr = template(模板id,res)
      + + +

      iframe

        +
      • 在一个网页中嵌入另一个网页

        +
      • +
      • 典型结构

        +
          +
        • a href=”” target=”iframe的name”
        • +
        +
      • +
      +

      达成的目标

        +
      • 初步建立pc端网站模块化的开发思想

        +
      • +
      • 熟悉常见的操作流程

        +
          +
        • 添加

          +
            +
          • 收集信息

            +
          • +
          • 检测信息的有效性

            +
              +
            • 用户名不能为空
            • +
            • 长度不能太小
            • +
            • email格式
            • +
            • ……
            • +
            +
          • +
          • 调用接口

            +
              +
            • 成功

              +
            • +
            • 失败

              +
                +
              • 模态框提示
              • +
              +
            • +
            +
          • +
          +
        • +
        • 删除

          +
            +
          • 提示是否删除

            +
          • +
          • 收集信息

            +
              +
            • 掌握从地址栏中传值到另一个页面
            • +
            +
          • +
          • 检测信息的有效性

            +
              +
            • 用户名不能为空
            • +
            • 长度不能太小
            • +
            • email格式
            • +
            • ……
            • +
            +
          • +
          • 调用接口

            +
              +
            • 成功
            • +
            • 失败
            • +
            +
          • +
          +
        • +
        • 修改

          +
            +
          • 获取出详情数据

            +
          • +
          • 显示在页面上

            +
          • +
          • 收集信息

            +
          • +
          • 检测信息的有效性

            +
              +
            • 用户名不能为空
            • +
            • 长度不能太小
            • +
            • email格式
            • +
            • ……
            • +
            +
          • +
          • 调用接口

            +
              +
            • 成功
            • +
            • 失败
            • +
            +
          • +
          +
        • +
        • 查询

          +
            +
          • 收集信息

            +
          • +
          • 检测信息的有效性

            +
              +
            • 用户名不能为空
            • +
            • 长度不能太小
            • +
            • email格式
            • +
            • ……
            • +
            +
          • +
          • 组装查询条件

            +
          • +
          • 调用接口

            +
              +
            • 成功

              +
                +
              • 使用模板引擎进行数据渲染
              • +
              +
            • +
            • 失败

              +
            • +
            +
          • +
          +
        • +
        +
      • +
      • 解决错误的基本方法

        +
          +
        • alert()

          +
        • +
        • console.log()

          +
        • +
        • 加断点

          +
            +
          • debugger
          • +
          +
        • +
        +
      • +
      • ajax使用

        +
          +
        • 在network面板中观察

          +
            +
          • 请求行

            +
              +
            • 有接口的地址
            • +
            +
          • +
          • 参数

            +
              +
            • 格式
            • +
            • 个数
            • +
            +
          • +
          • 响应结果

            +
          • +
          +
        • +
        +
      • +
      • 代码量的训练

        +
      • +
      +

      XMind: ZEN - Trial Version

      +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/DOC-Xmind%E7%AC%94%E8%AE%B0%E6%B1%87%E6%80%BB/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/ES6-ArrayBuffer/index.html b/posts/ES6-ArrayBuffer/index.html new file mode 100644 index 00000000..5b2d9465 --- /dev/null +++ b/posts/ES6-ArrayBuffer/index.html @@ -0,0 +1,732 @@ +ArrayBuffer | JCAlways + + + + + + + + + + + + + +

    ArrayBuffer

    ArrayBuffer

    ArrayBuffer对象、TypedArray视图和DataView`视图是 JavaScript 操作二进制数据的一个接口。这些对象早就存在,属于独立的规格(2011 年 2 月发布),ES6 将它们纳入了 ECMAScript 规格,并且增加了新的方法。它们都是以数组的语法处理二进制数据,所以统称为二进制数组。

    +

    这个接口的原始设计目的,与 WebGL 项目有关。所谓 WebGL,就是指浏览器与显卡之间的通信接口,为了满足 JavaScript 与显卡之间大量的、实时的数据交换,它们之间的数据通信必须是二进制的,而不能是传统的文本格式。文本格式传递一个 32 位整数,两端的 JavaScript 脚本与显卡都要进行格式转化,将非常耗时。这时要是存在一种机制,可以像 C 语言那样,直接操作字节,将 4 个字节的 32 位整数,以二进制形式原封不动地送入显卡,脚本的性能就会大幅提升。

    +

    二进制数组就是在这种背景下诞生的。它很像 C 语言的数组,允许开发者以数组下标的形式,直接操作内存,大大增强了 JavaScript 处理二进制数据的能力,使得开发者有可能通过 JavaScript 与操作系统的原生接口进行二进制通信。

    +

    二进制数组由三类对象组成。

    +

    (1)ArrayBuffer对象:代表内存之中的一段二进制数据,可以通过“视图”进行操作。“视图”部署了数组接口,这意味着,可以用数组的方法操作内存。

    +

    (2)TypedArray视图:共包括 9 种类型的视图,比如Uint8Array(无符号 8 位整数)数组视图, Int16Array(16 位整数)数组视图, Float32Array(32 位浮点数)数组视图等等。

    +

    (3)DataView视图:可以自定义复合格式的视图,比如第一个字节是 Uint8(无符号 8 位整数)、第二、三个字节是 Int16(16 位整数)、第四个字节开始是 Float32(32 位浮点数)等等,此外还可以自定义字节序。

    +

    简单说,ArrayBuffer对象代表原始的二进制数据,TypedArray 视图用来读写简单类型的二进制数据,DataView视图用来读写复杂类型的二进制数据。

    +

    TypedArray 视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    数据类型字节长度含义对应的 C 语言类型
    Int818 位带符号整数signed char
    Uint818 位不带符号整数unsigned char
    Uint8C18 位不带符号整数(自动过滤溢出)unsigned char
    Int16216 位带符号整数short
    Uint16216 位不带符号整数unsigned short
    Int32432 位带符号整数int
    Uint32432 位不带符号的整数unsigned int
    Float32432 位浮点数float
    Float64864 位浮点数double
    +

    注意,二进制数组并不是真正的数组,而是类似数组的对象。

    +

    很多浏览器操作的 API,用到了二进制数组操作二进制数据,下面是其中的几个。

    +
      +
    • File API
    • +
    • XMLHttpRequest
    • +
    • Fetch API
    • +
    • Canvas
    • +
    • WebSockets
    • +
    +

    ArrayBuffer 对象

    概述

    ArrayBuffer对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray视图和DataView视图)来读写,视图的作用是以指定格式解读二进制数据。

    +

    ArrayBuffer也是一个构造函数,可以分配一段可以存放数据的连续内存区域。

    +
    1
    const buf = new ArrayBuffer(32);
    + +

    上面代码生成了一段 32 字节的内存区域,每个字节的值默认都是 0。可以看到,ArrayBuffer构造函数的参数是所需要的内存大小(单位字节)。

    +

    为了读写这段内容,需要为它指定视图。DataView视图的创建,需要提供ArrayBuffer对象实例作为参数。

    +
    1
    2
    3
    const buf = new ArrayBuffer(32);
    const dataView = new DataView(buf);
    dataView.getUint8(0); // 0
    + +

    上面代码对一段 32 字节的内存,建立DataView视图,然后以不带符号的 8 位整数格式,从头读取 8 位二进制数据,结果得到 0,因为原始内存的ArrayBuffer对象,默认所有位都是 0。

    +

    另一种 TypedArray 视图,与DataView视图的一个区别是,它不是一个构造函数,而是一组构造函数,代表不同的数据格式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const buffer = new ArrayBuffer(12);

    const x1 = new Int32Array(buffer);
    x1[0] = 1;
    const x2 = new Uint8Array(buffer);
    x2[0] = 2;

    x1[0]; // 2
    + +

    上面代码对同一段内存,分别建立两种视图:32 位带符号整数(Int32Array构造函数)和 8 位不带符号整数(Uint8Array构造函数)。由于两个视图对应的是同一段内存,一个视图修改底层内存,会影响到另一个视图。

    +

    TypedArray 视图的构造函数,除了接受ArrayBuffer实例作为参数,还可以接受普通数组作为参数,直接分配内存生成底层的ArrayBuffer实例,并同时完成对这段内存的赋值。

    +
    1
    2
    3
    4
    5
    const typedArray = new Uint8Array([0, 1, 2]);
    typedArray.length; // 3

    typedArray[0] = 5;
    typedArray; // [5, 1, 2]
    + +

    上面代码使用 TypedArray 视图的Uint8Array构造函数,新建一个不带符号的 8 位整数视图。可以看到,Uint8Array直接使用普通数组作为参数,对底层内存的赋值同时完成。

    +

    ArrayBuffer.prototype.byteLength

    ArrayBuffer实例的byteLength属性,返回所分配的内存区域的字节长度。

    +
    1
    2
    3
    const buffer = new ArrayBuffer(32);
    buffer.byteLength;
    // 32
    + +

    如果要分配的内存区域很大,有可能分配失败(因为没有那么多的连续空余内存),所以有必要检查是否分配成功。

    +
    1
    2
    3
    4
    5
    if (buffer.byteLength === n) {
    // 成功
    } else {
    // 失败
    }
    + +

    ArrayBuffer.prototype.slice()

    ArrayBuffer实例有一个slice方法,允许将内存区域的一部分,拷贝生成一个新的ArrayBuffer对象。

    +
    1
    2
    const buffer = new ArrayBuffer(8);
    const newBuffer = buffer.slice(0, 3);
    + +

    上面代码拷贝buffer对象的前 3 个字节(从 0 开始,到第 3 个字节前面结束),生成一个新的ArrayBuffer对象。slice方法其实包含两步,第一步是先分配一段新内存,第二步是将原来那个ArrayBuffer对象拷贝过去。

    +

    slice方法接受两个参数,第一个参数表示拷贝开始的字节序号(含该字节),第二个参数表示拷贝截止的字节序号(不含该字节)。如果省略第二个参数,则默认到原ArrayBuffer对象的结尾。

    +

    除了slice方法,ArrayBuffer对象不提供任何直接读写内存的方法,只允许在其上方建立视图,然后通过视图读写。

    +

    ArrayBuffer.isView()

    ArrayBuffer有一个静态方法isView,返回一个布尔值,表示参数是否为ArrayBuffer的视图实例。这个方法大致相当于判断参数,是否为 TypedArray 实例或DataView实例。

    +
    1
    2
    3
    4
    5
    const buffer = new ArrayBuffer(8);
    ArrayBuffer.isView(buffer); // false

    const v = new Int32Array(buffer);
    ArrayBuffer.isView(v); // true
    + +

    TypedArray 视图

    概述

    ArrayBuffer对象作为内存区域,可以存放多种类型的数据。同一段内存,不同数据有不同的解读方式,这就叫做“视图”(view)。ArrayBuffer有两种视图,一种是 TypedArray 视图,另一种是DataView视图。前者的数组成员都是同一个数据类型,后者的数组成员可以是不同的数据类型。

    +

    目前,TypedArray 视图一共包括 9 种类型,每一种视图都是一种构造函数。

    +
      +
    • **Int8Array**:8 位有符号整数,长度 1 个字节。
    • +
    • **Uint8Array**:8 位无符号整数,长度 1 个字节。
    • +
    • **Uint8ClampedArray**:8 位无符号整数,长度 1 个字节,溢出处理不同。
    • +
    • **Int16Array**:16 位有符号整数,长度 2 个字节。
    • +
    • **Uint16Array**:16 位无符号整数,长度 2 个字节。
    • +
    • **Int32Array**:32 位有符号整数,长度 4 个字节。
    • +
    • **Uint32Array**:32 位无符号整数,长度 4 个字节。
    • +
    • **Float32Array**:32 位浮点数,长度 4 个字节。
    • +
    • **Float64Array**:64 位浮点数,长度 8 个字节。
    • +
    +

    这 9 个构造函数生成的数组,统称为 TypedArray 视图。它们很像普通数组,都有length属性,都能用方括号运算符([])获取单个元素,所有数组的方法,在它们上面都能使用。普通数组与 TypedArray 数组的差异主要在以下方面。

    +
      +
    • TypedArray 数组的所有成员,都是同一种类型。
    • +
    • TypedArray 数组的成员是连续的,不会有空位。
    • +
    • TypedArray 数组成员的默认值为 0。比如,new Array(10)返回一个普通数组,里面没有任何成员,只是 10 个空位;new Uint8Array(10)返回一个 TypedArray 数组,里面 10 个成员都是 0。
    • +
    • TypedArray 数组只是一层视图,本身不储存数据,它的数据都储存在底层的ArrayBuffer对象之中,要获取底层对象必须使用buffer属性。
    • +
    +

    构造函数

    TypedArray 数组提供 9 种构造函数,用来生成相应类型的数组实例。

    +

    构造函数有多种用法。

    +

    (1)TypedArray(buffer, byteOffset=0, length?)

    +

    同一个ArrayBuffer对象之上,可以根据不同的数据类型,建立多个视图。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 创建一个8字节的ArrayBuffer
    const b = new ArrayBuffer(8);

    // 创建一个指向b的Int32视图,开始于字节0,直到缓冲区的末尾
    const v1 = new Int32Array(b);

    // 创建一个指向b的Uint8视图,开始于字节2,直到缓冲区的末尾
    const v2 = new Uint8Array(b, 2);

    // 创建一个指向b的Int16视图,开始于字节2,长度为2
    const v3 = new Int16Array(b, 2, 2);
    + +

    上面代码在一段长度为 8 个字节的内存(b)之上,生成了三个视图:v1v2v3

    +

    视图的构造函数可以接受三个参数:

    +
      +
    • 第一个参数(必需):视图对应的底层ArrayBuffer对象。
    • +
    • 第二个参数(可选):视图开始的字节序号,默认从 0 开始。
    • +
    • 第三个参数(可选):视图包含的数据个数,默认直到本段内存区域结束。
    • +
    +

    因此,v1v2v3是重叠的:v1[0]是一个 32 位整数,指向字节 0 ~字节 3;v2[0]是一个 8 位无符号整数,指向字节 2;v3[0]是一个 16 位整数,指向字节 2 ~字节 3。只要任何一个视图对内存有所修改,就会在另外两个视图上反应出来。

    +

    注意,byteOffset必须与所要建立的数据类型一致,否则会报错。

    +
    1
    2
    3
    const buffer = new ArrayBuffer(8);
    const i16 = new Int16Array(buffer, 1);
    // Uncaught RangeError: start offset of Int16Array should be a multiple of 2
    + +

    上面代码中,新生成一个 8 个字节的ArrayBuffer对象,然后在这个对象的第一个字节,建立带符号的 16 位整数视图,结果报错。因为,带符号的 16 位整数需要两个字节,所以byteOffset参数必须能够被 2 整除。

    +

    如果想从任意字节开始解读ArrayBuffer对象,必须使用DataView视图,因为 TypedArray 视图只提供 9 种固定的解读格式。

    +

    (2)TypedArray(length)

    +

    视图还可以不通过ArrayBuffer对象,直接分配内存而生成。

    +
    1
    2
    3
    4
    const f64a = new Float64Array(8);
    f64a[0] = 10;
    f64a[1] = 20;
    f64a[2] = f64a[0] + f64a[1];
    + +

    上面代码生成一个 8 个成员的Float64Array数组(共 64 字节),然后依次对每个成员赋值。这时,视图构造函数的参数就是成员的个数。可以看到,视图数组的赋值操作与普通数组的操作毫无两样。

    +

    (3)TypedArray(typedArray)

    +

    TypedArray 数组的构造函数,可以接受另一个 TypedArray 实例作为参数。

    +
    1
    const typedArray = new Int8Array(new Uint8Array(4));
    + +

    上面代码中,Int8Array构造函数接受一个Uint8Array实例作为参数。

    +

    注意,此时生成的新数组,只是复制了参数数组的值,对应的底层内存是不一样的。新数组会开辟一段新的内存储存数据,不会在原数组的内存之上建立视图。

    +
    1
    2
    3
    4
    5
    6
    7
    const x = new Int8Array([1, 1]);
    const y = new Int8Array(x);
    x[0]; // 1
    y[0]; // 1

    x[0] = 2;
    y[0]; // 1
    + +

    上面代码中,数组y是以数组x为模板而生成的,当x变动的时候,y并没有变动。

    +

    如果想基于同一段内存,构造不同的视图,可以采用下面的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    const x = new Int8Array([1, 1]);
    const y = new Int8Array(x.buffer);
    x[0]; // 1
    y[0]; // 1

    x[0] = 2;
    y[0]; // 2
    + +

    (4)TypedArray(arrayLikeObject)

    +

    构造函数的参数也可以是一个普通数组,然后直接生成 TypedArray 实例。

    +
    1
    const typedArray = new Uint8Array([1, 2, 3, 4]);
    + +

    注意,这时 TypedArray 视图会重新开辟内存,不会在原数组的内存上建立视图。

    +

    上面代码从一个普通的数组,生成一个 8 位无符号整数的 TypedArray 实例。

    +

    TypedArray 数组也可以转换回普通数组。

    +
    1
    2
    3
    4
    5
    const normalArray = [...typedArray];
    // or
    const normalArray = Array.from(typedArray);
    // or
    const normalArray = Array.prototype.slice.call(typedArray);
    + +

    数组方法

    普通数组的操作方法和属性,对 TypedArray 数组完全适用。

    +
      +
    • TypedArray.prototype.copyWithin(target, start[, end = this.length])
    • +
    • TypedArray.prototype.entries()
    • +
    • TypedArray.prototype.every(callbackfn, thisArg?)
    • +
    • TypedArray.prototype.fill(value, start=0, end=this.length)
    • +
    • TypedArray.prototype.filter(callbackfn, thisArg?)
    • +
    • TypedArray.prototype.find(predicate, thisArg?)
    • +
    • TypedArray.prototype.findIndex(predicate, thisArg?)
    • +
    • TypedArray.prototype.forEach(callbackfn, thisArg?)
    • +
    • TypedArray.prototype.indexOf(searchElement, fromIndex=0)
    • +
    • TypedArray.prototype.join(separator)
    • +
    • TypedArray.prototype.keys()
    • +
    • TypedArray.prototype.lastIndexOf(searchElement, fromIndex?)
    • +
    • TypedArray.prototype.map(callbackfn, thisArg?)
    • +
    • TypedArray.prototype.reduce(callbackfn, initialValue?)
    • +
    • TypedArray.prototype.reduceRight(callbackfn, initialValue?)
    • +
    • TypedArray.prototype.reverse()
    • +
    • TypedArray.prototype.slice(start=0, end=this.length)
    • +
    • TypedArray.prototype.some(callbackfn, thisArg?)
    • +
    • TypedArray.prototype.sort(comparefn)
    • +
    • TypedArray.prototype.toLocaleString(reserved1?, reserved2?)
    • +
    • TypedArray.prototype.toString()
    • +
    • TypedArray.prototype.values()
    • +
    +

    上面所有方法的用法,请参阅数组方法的介绍,这里不再重复了。

    +

    注意,TypedArray 数组没有concat方法。如果想要合并多个 TypedArray 数组,可以用下面这个函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function concatenate(resultConstructor, ...arrays) {
    let totalLength = 0;
    for (let arr of arrays) {
    totalLength += arr.length;
    }
    let result = new resultConstructor(totalLength);
    let offset = 0;
    for (let arr of arrays) {
    result.set(arr, offset);
    offset += arr.length;
    }
    return result;
    }

    concatenate(Uint8Array, Uint8Array.of(1, 2), Uint8Array.of(3, 4));
    // Uint8Array [1, 2, 3, 4]
    + +

    另外,TypedArray 数组与普通数组一样,部署了 Iterator 接口,所以可以被遍历。

    +
    1
    2
    3
    4
    5
    6
    7
    let ui8 = Uint8Array.of(0, 1, 2);
    for (let byte of ui8) {
    console.log(byte);
    }
    // 0
    // 1
    // 2
    + +

    字节序

    字节序指的是数值在内存中的表示方式。

    +
    1
    2
    3
    4
    5
    6
    const buffer = new ArrayBuffer(16);
    const int32View = new Int32Array(buffer);

    for (let i = 0; i < int32View.length; i++) {
    int32View[i] = i * 2;
    }
    + +

    上面代码生成一个 16 字节的ArrayBuffer对象,然后在它的基础上,建立了一个 32 位整数的视图。由于每个 32 位整数占据 4 个字节,所以一共可以写入 4 个整数,依次为 0,2,4,6。

    +

    如果在这段数据上接着建立一个 16 位整数的视图,则可以读出完全不一样的结果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const int16View = new Int16Array(buffer);

    for (let i = 0; i < int16View.length; i++) {
    console.log("Entry " + i + ": " + int16View[i]);
    }
    // Entry 0: 0
    // Entry 1: 0
    // Entry 2: 2
    // Entry 3: 0
    // Entry 4: 4
    // Entry 5: 0
    // Entry 6: 6
    // Entry 7: 0
    + +

    由于每个 16 位整数占据 2 个字节,所以整个ArrayBuffer对象现在分成 8 段。然后,由于 x86 体系的计算机都采用小端字节序(little endian),相对重要的字节排在后面的内存地址,相对不重要字节排在前面的内存地址,所以就得到了上面的结果。

    +

    比如,一个占据四个字节的 16 进制数0x12345678,决定其大小的最重要的字节是“12”,最不重要的是“78”。小端字节序将最不重要的字节排在前面,储存顺序就是78563412;大端字节序则完全相反,将最重要的字节排在前面,储存顺序就是12345678。目前,所有个人电脑几乎都是小端字节序,所以 TypedArray 数组内部也采用小端字节序读写数据,或者更准确的说,按照本机操作系统设定的字节序读写数据。

    +

    这并不意味大端字节序不重要,事实上,很多网络设备和特定的操作系统采用的是大端字节序。这就带来一个严重的问题:如果一段数据是大端字节序,TypedArray 数组将无法正确解析,因为它只能处理小端字节序!为了解决这个问题,JavaScript 引入DataView对象,可以设定字节序,下文会详细介绍。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    // 假定某段buffer包含如下字节 [0x02, 0x01, 0x03, 0x07]
    const buffer = new ArrayBuffer(4);
    const v1 = new Uint8Array(buffer);
    v1[0] = 2;
    v1[1] = 1;
    v1[2] = 3;
    v1[3] = 7;

    const uInt16View = new Uint16Array(buffer);

    // 计算机采用小端字节序
    // 所以头两个字节等于258
    if (uInt16View[0] === 258) {
    console.log("OK"); // "OK"
    }

    // 赋值运算
    uInt16View[0] = 255; // 字节变为[0xFF, 0x00, 0x03, 0x07]
    uInt16View[0] = 0xff05; // 字节变为[0x05, 0xFF, 0x03, 0x07]
    uInt16View[1] = 0x0210; // 字节变为[0x05, 0xFF, 0x10, 0x02]
    + +

    下面的函数可以用来判断,当前视图是小端字节序,还是大端字节序。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const BIG_ENDIAN = Symbol("BIG_ENDIAN");
    const LITTLE_ENDIAN = Symbol("LITTLE_ENDIAN");

    function getPlatformEndianness() {
    let arr32 = Uint32Array.of(0x12345678);
    let arr8 = new Uint8Array(arr32.buffer);
    switch (arr8[0] * 0x1000000 + arr8[1] * 0x10000 + arr8[2] * 0x100 + arr8[3]) {
    case 0x12345678:
    return BIG_ENDIAN;
    case 0x78563412:
    return LITTLE_ENDIAN;
    default:
    throw new Error("Unknown endianness");
    }
    }
    + +

    总之,与普通数组相比,TypedArray 数组的最大优点就是可以直接操作内存,不需要数据类型转换,所以速度快得多。

    +

    BYTES_PER_ELEMENT 属性

    每一种视图的构造函数,都有一个BYTES_PER_ELEMENT属性,表示这种数据类型占据的字节数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Int8Array.BYTES_PER_ELEMENT; // 1
    Uint8Array.BYTES_PER_ELEMENT; // 1
    Int16Array.BYTES_PER_ELEMENT; // 2
    Uint16Array.BYTES_PER_ELEMENT; // 2
    Int32Array.BYTES_PER_ELEMENT; // 4
    Uint32Array.BYTES_PER_ELEMENT; // 4
    Float32Array.BYTES_PER_ELEMENT; // 4
    Float64Array.BYTES_PER_ELEMENT; // 8
    + +

    这个属性在 TypedArray 实例上也能获取,即有TypedArray.prototype.BYTES_PER_ELEMENT

    +

    ArrayBuffer 与字符串的互相转换

    ArrayBuffer转为字符串,或者字符串转为ArrayBuffer,有一个前提,即字符串的编码方法是确定的。假定字符串采用 UTF-16 编码(JavaScript 的内部编码方式),可以自己编写转换函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // ArrayBuffer 转为字符串,参数为 ArrayBuffer 对象
    function ab2str(buf) {
    // 注意,如果是大型二进制数组,为了避免溢出,
    // 必须一个一个字符地转
    if (buf && buf.byteLength < 1024) {
    return String.fromCharCode.apply(null, new Uint16Array(buf));
    }

    const bufView = new Uint16Array(buf);
    const len = bufView.length;
    const bstr = new Array(len);
    for (let i = 0; i < len; i++) {
    bstr[i] = String.fromCharCode.call(null, bufView[i]);
    }
    return bstr.join("");
    }

    // 字符串转为 ArrayBuffer 对象,参数为字符串
    function str2ab(str) {
    const buf = new ArrayBuffer(str.length * 2); // 每个字符占用2个字节
    const bufView = new Uint16Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
    bufView[i] = str.charCodeAt(i);
    }
    return buf;
    }
    + +

    溢出

    不同的视图类型,所能容纳的数值范围是确定的。超出这个范围,就会出现溢出。比如,8 位视图只能容纳一个 8 位的二进制值,如果放入一个 9 位的值,就会溢出。

    +

    TypedArray 数组的溢出处理规则,简单来说,就是抛弃溢出的位,然后按照视图类型进行解释。

    +
    1
    2
    3
    4
    5
    6
    7
    const uint8 = new Uint8Array(1);

    uint8[0] = 256;
    uint8[0]; // 0

    uint8[0] = -1;
    uint8[0]; // 255
    + +

    上面代码中,uint8是一个 8 位视图,而 256 的二进制形式是一个 9 位的值100000000,这时就会发生溢出。根据规则,只会保留后 8 位,即00000000uint8视图的解释规则是无符号的 8 位整数,所以00000000就是0

    +

    负数在计算机内部采用“2 的补码”表示,也就是说,将对应的正数值进行否运算,然后加1。比如,-1对应的正值是1,进行否运算以后,得到11111110,再加上1就是补码形式11111111uint8按照无符号的 8 位整数解释11111111,返回结果就是255

    +

    一个简单转换规则,可以这样表示。

    +
      +
    • 正向溢出(overflow):当输入值大于当前数据类型的最大值,结果等于当前数据类型的最小值加上余值,再减去 1。
    • +
    • 负向溢出(underflow):当输入值小于当前数据类型的最小值,结果等于当前数据类型的最大值减去余值的绝对值,再加上 1。
    • +
    +

    上面的“余值”就是模运算的结果,即 JavaScript 里面的%运算符的结果。

    +
    1
    2
    12 % 4; // 0
    12 % 5; // 2
    + +

    上面代码中,12 除以 4 是没有余值的,而除以 5 会得到余值 2。

    +

    请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    const int8 = new Int8Array(1);

    int8[0] = 128;
    int8[0]; // -128

    int8[0] = -129;
    int8[0]; // 127
    + +

    上面例子中,int8是一个带符号的 8 位整数视图,它的最大值是 127,最小值是-128。输入值为128时,相当于正向溢出1,根据“最小值加上余值(128 除以 127 的余值是 1),再减去 1”的规则,就会返回-128;输入值为-129时,相当于负向溢出1,根据“最大值减去余值的绝对值(-129 除以-128 的余值的绝对值是 1),再加上 1”的规则,就会返回127

    +

    Uint8ClampedArray视图的溢出规则,与上面的规则不同。它规定,凡是发生正向溢出,该值一律等于当前数据类型的最大值,即 255;如果发生负向溢出,该值一律等于当前数据类型的最小值,即 0。

    +
    1
    2
    3
    4
    5
    6
    7
    const uint8c = new Uint8ClampedArray(1);

    uint8c[0] = 256;
    uint8c[0]; // 255

    uint8c[0] = -1;
    uint8c[0]; // 0
    + +

    上面例子中,uint8C是一个Uint8ClampedArray视图,正向溢出时都返回 255,负向溢出都返回 0。

    +

    TypedArray.prototype.buffer

    TypedArray 实例的buffer属性,返回整段内存区域对应的ArrayBuffer对象。该属性为只读属性。

    +
    1
    2
    const a = new Float32Array(64);
    const b = new Uint8Array(a.buffer);
    + +

    上面代码的a视图对象和b视图对象,对应同一个ArrayBuffer对象,即同一段内存。

    +

    TypedArray.prototype.byteLength,TypedArray.prototype.byteOffset

    byteLength属性返回 TypedArray 数组占据的内存长度,单位为字节。byteOffset属性返回 TypedArray 数组从底层ArrayBuffer对象的哪个字节开始。这两个属性都是只读属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const b = new ArrayBuffer(8);

    const v1 = new Int32Array(b);
    const v2 = new Uint8Array(b, 2);
    const v3 = new Int16Array(b, 2, 2);

    v1.byteLength; // 8
    v2.byteLength; // 6
    v3.byteLength; // 4

    v1.byteOffset; // 0
    v2.byteOffset; // 2
    v3.byteOffset; // 2
    + +

    TypedArray.prototype.length

    length属性表示 TypedArray 数组含有多少个成员。注意将byteLength属性和length属性区分,前者是字节长度,后者是成员长度。

    +
    1
    2
    3
    4
    const a = new Int16Array(8);

    a.length; // 8
    a.byteLength; // 16
    + +

    TypedArray.prototype.set()

    TypedArray 数组的set方法用于复制数组(普通数组或 TypedArray 数组),也就是将一段内容完全复制到另一段内存。

    +
    1
    2
    3
    4
    const a = new Uint8Array(8);
    const b = new Uint8Array(8);

    b.set(a);
    + +

    上面代码复制a数组的内容到b数组,它是整段内存的复制,比一个个拷贝成员的那种复制快得多。

    +

    set方法还可以接受第二个参数,表示从b对象的哪一个成员开始复制a对象。

    +
    1
    2
    3
    4
    const a = new Uint16Array(8);
    const b = new Uint16Array(10);

    b.set(a, 2);
    + +

    上面代码的b数组比a数组多两个成员,所以从b[2]开始复制。

    +

    TypedArray.prototype.subarray()

    subarray方法是对于 TypedArray 数组的一部分,再建立一个新的视图。

    +
    1
    2
    3
    4
    5
    const a = new Uint16Array(8);
    const b = a.subarray(2, 3);

    a.byteLength; // 16
    b.byteLength; // 2
    + +

    subarray方法的第一个参数是起始的成员序号,第二个参数是结束的成员序号(不含该成员),如果省略则包含剩余的全部成员。所以,上面代码的a.subarray(2,3),意味着 b 只包含a[2]一个成员,字节长度为 2。

    +

    TypedArray.prototype.slice()

    TypeArray 实例的slice方法,可以返回一个指定位置的新的 TypedArray 实例。

    +
    1
    2
    3
    let ui8 = Uint8Array.of(0, 1, 2);
    ui8.slice(-1);
    // Uint8Array [ 2 ]
    + +

    上面代码中,ui8是 8 位无符号整数数组视图的一个实例。它的slice方法可以从当前视图之中,返回一个新的视图实例。

    +

    slice方法的参数,表示原数组的具体位置,开始生成新数组。负值表示逆向的位置,即-1 为倒数第一个位置,-2 表示倒数第二个位置,以此类推。

    +

    TypedArray.of()

    TypedArray 数组的所有构造函数,都有一个静态方法of,用于将参数转为一个 TypedArray 实例。

    +
    1
    2
    Float32Array.of(0.151, -8, 3.7);
    // Float32Array [ 0.151, -8, 3.7 ]
    + +

    下面三种方法都会生成同样一个 TypedArray 数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 方法一
    let tarr = new Uint8Array([1, 2, 3]);

    // 方法二
    let tarr = Uint8Array.of(1, 2, 3);

    // 方法三
    let tarr = new Uint8Array(3);
    tarr[0] = 1;
    tarr[1] = 2;
    tarr[2] = 3;
    + +

    TypedArray.from()

    静态方法from接受一个可遍历的数据结构(比如数组)作为参数,返回一个基于这个结构的 TypedArray 实例。

    +
    1
    2
    Uint16Array.from([0, 1, 2]);
    // Uint16Array [ 0, 1, 2 ]
    + +

    这个方法还可以将一种 TypedArray 实例,转为另一种。

    +
    1
    2
    const ui16 = Uint16Array.from(Uint8Array.of(0, 1, 2));
    ui16 instanceof Uint16Array; // true
    + +

    from方法还可以接受一个函数,作为第二个参数,用来对每个元素进行遍历,功能类似map方法。

    +
    1
    2
    3
    4
    5
    Int8Array.of(127, 126, 125).map((x) => 2 * x);
    // Int8Array [ -2, -4, -6 ]

    Int16Array.from(Int8Array.of(127, 126, 125), (x) => 2 * x);
    // Int16Array [ 254, 252, 250 ]
    + +

    上面的例子中,from方法没有发生溢出,这说明遍历不是针对原来的 8 位整数数组。也就是说,from会将第一个参数指定的 TypedArray 数组,拷贝到另一段内存之中,处理之后再将结果转成指定的数组格式。

    +

    复合视图

    由于视图的构造函数可以指定起始位置和长度,所以在同一段内存之中,可以依次存放不同类型的数据,这叫做“复合视图”。

    +
    1
    2
    3
    4
    5
    const buffer = new ArrayBuffer(24);

    const idView = new Uint32Array(buffer, 0, 1);
    const usernameView = new Uint8Array(buffer, 4, 16);
    const amountDueView = new Float32Array(buffer, 20, 1);
    + +

    上面代码将一个 24 字节长度的ArrayBuffer对象,分成三个部分:

    +
      +
    • 字节 0 到字节 3:1 个 32 位无符号整数
    • +
    • 字节 4 到字节 19:16 个 8 位整数
    • +
    • 字节 20 到字节 23:1 个 32 位浮点数
    • +
    +

    这种数据结构可以用如下的 C 语言描述:

    +
    1
    2
    3
    4
    5
    struct someStruct {
    unsigned long id;
    char username[16];
    float amountDue;
    };
    + +

    DataView 视图

    如果一段数据包括多种类型(比如服务器传来的 HTTP 数据),这时除了建立ArrayBuffer对象的复合视图以外,还可以通过DataView视图进行操作。

    +

    DataView视图提供更多操作选项,而且支持设定字节序。本来,在设计目的上,ArrayBuffer对象的各种 TypedArray 视图,是用来向网卡、声卡之类的本机设备传送数据,所以使用本机的字节序就可以了;而DataView视图的设计目的,是用来处理网络设备传来的数据,所以大端字节序或小端字节序是可以自行设定的。

    +

    DataView视图本身也是构造函数,接受一个ArrayBuffer对象作为参数,生成视图。

    +
    1
    DataView(ArrayBuffer buffer [, 字节起始位置 [, 长度]]);
    + +

    下面是一个例子。

    +
    1
    2
    const buffer = new ArrayBuffer(24);
    const dv = new DataView(buffer);
    + +

    DataView实例有以下属性,含义与 TypedArray 实例的同名方法相同。

    +
      +
    • DataView.prototype.buffer:返回对应的 ArrayBuffer 对象
    • +
    • DataView.prototype.byteLength:返回占据的内存字节长度
    • +
    • DataView.prototype.byteOffset:返回当前视图从对应的 ArrayBuffer 对象的哪个字节开始
    • +
    +

    DataView实例提供 8 个方法读取内存。

    +
      +
    • **getInt8**:读取 1 个字节,返回一个 8 位整数。
    • +
    • **getUint8**:读取 1 个字节,返回一个无符号的 8 位整数。
    • +
    • **getInt16**:读取 2 个字节,返回一个 16 位整数。
    • +
    • **getUint16**:读取 2 个字节,返回一个无符号的 16 位整数。
    • +
    • **getInt32**:读取 4 个字节,返回一个 32 位整数。
    • +
    • **getUint32**:读取 4 个字节,返回一个无符号的 32 位整数。
    • +
    • **getFloat32**:读取 4 个字节,返回一个 32 位浮点数。
    • +
    • **getFloat64**:读取 8 个字节,返回一个 64 位浮点数。
    • +
    +

    这一系列get方法的参数都是一个字节序号(不能是负数,否则会报错),表示从哪个字节开始读取。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const buffer = new ArrayBuffer(24);
    const dv = new DataView(buffer);

    // 从第1个字节读取一个8位无符号整数
    const v1 = dv.getUint8(0);

    // 从第2个字节读取一个16位无符号整数
    const v2 = dv.getUint16(1);

    // 从第4个字节读取一个16位无符号整数
    const v3 = dv.getUint16(3);
    + +

    上面代码读取了ArrayBuffer对象的前 5 个字节,其中有一个 8 位整数和两个十六位整数。

    +

    如果一次读取两个或两个以上字节,就必须明确数据的存储方式,到底是小端字节序还是大端字节序。默认情况下,DataViewget方法使用大端字节序解读数据,如果需要使用小端字节序解读,必须在get方法的第二个参数指定true

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 小端字节序
    const v1 = dv.getUint16(1, true);

    // 大端字节序
    const v2 = dv.getUint16(3, false);

    // 大端字节序
    const v3 = dv.getUint16(3);
    + +

    DataView 视图提供 8 个方法写入内存。

    +
      +
    • **setInt8**:写入 1 个字节的 8 位整数。
    • +
    • **setUint8**:写入 1 个字节的 8 位无符号整数。
    • +
    • **setInt16**:写入 2 个字节的 16 位整数。
    • +
    • **setUint16**:写入 2 个字节的 16 位无符号整数。
    • +
    • **setInt32**:写入 4 个字节的 32 位整数。
    • +
    • **setUint32**:写入 4 个字节的 32 位无符号整数。
    • +
    • **setFloat32**:写入 4 个字节的 32 位浮点数。
    • +
    • **setFloat64**:写入 8 个字节的 64 位浮点数。
    • +
    +

    这一系列set方法,接受两个参数,第一个参数是字节序号,表示从哪个字节开始写入,第二个参数为写入的数据。对于那些写入两个或两个以上字节的方法,需要指定第三个参数,false或者undefined表示使用大端字节序写入,true表示使用小端字节序写入。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 在第1个字节,以大端字节序写入值为25的32位整数
    dv.setInt32(0, 25, false);

    // 在第5个字节,以大端字节序写入值为25的32位整数
    dv.setInt32(4, 25);

    // 在第9个字节,以小端字节序写入值为2.5的32位浮点数
    dv.setFloat32(8, 2.5, true);
    + +

    如果不确定正在使用的计算机的字节序,可以采用下面的判断方式。

    +
    1
    2
    3
    4
    5
    const littleEndian = (function () {
    const buffer = new ArrayBuffer(2);
    new DataView(buffer).setInt16(0, 256, true);
    return new Int16Array(buffer)[0] === 256;
    })();
    + +

    如果返回true,就是小端字节序;如果返回false,就是大端字节序。

    +

    二进制数组的应用

    大量的 Web API 用到了ArrayBuffer对象和它的视图对象。

    +

    AJAX

    传统上,服务器通过 AJAX 操作只能返回文本数据,即responseType属性默认为textXMLHttpRequest第二版XHR2允许服务器返回二进制数据,这时分成两种情况。如果明确知道返回的二进制数据类型,可以把返回类型(responseType)设为arraybuffer;如果不知道,就设为blob

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let xhr = new XMLHttpRequest();
    xhr.open("GET", someUrl);
    xhr.responseType = "arraybuffer";

    xhr.onload = function () {
    let arrayBuffer = xhr.response;
    // ···
    };

    xhr.send();
    + +

    如果知道传回来的是 32 位整数,可以像下面这样处理。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    xhr.onreadystatechange = function () {
    if (req.readyState === 4) {
    const arrayResponse = xhr.response;
    const dataView = new DataView(arrayResponse);
    const ints = new Uint32Array(dataView.byteLength / 4);

    xhrDiv.style.backgroundColor = "#00FF00";
    xhrDiv.innerText = "Array is " + ints.length + "uints long";
    }
    };
    + +

    Canvas

    网页Canvas元素输出的二进制像素数据,就是 TypedArray 数组。

    +
    1
    2
    3
    4
    5
    const canvas = document.getElementById("myCanvas");
    const ctx = canvas.getContext("2d");

    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const uint8ClampedArray = imageData.data;
    + +

    需要注意的是,上面代码的uint8ClampedArray虽然是一个 TypedArray 数组,但是它的视图类型是一种针对Canvas元素的专有类型Uint8ClampedArray。这个视图类型的特点,就是专门针对颜色,把每个字节解读为无符号的 8 位整数,即只能取值 0 ~ 255,而且发生运算的时候自动过滤高位溢出。这为图像处理带来了巨大的方便。

    +

    举例来说,如果把像素的颜色值设为Uint8Array类型,那么乘以一个 gamma 值的时候,就必须这样计算:

    +
    1
    u8[i] = Math.min(255, Math.max(0, u8[i] * gamma));
    + +

    因为Uint8Array类型对于大于 255 的运算结果(比如0xFF+1),会自动变为0x00,所以图像处理必须要像上面这样算。这样做很麻烦,而且影响性能。如果将颜色值设为Uint8ClampedArray类型,计算就简化许多。

    +
    1
    pixels[i] *= gamma;
    + +

    Uint8ClampedArray类型确保将小于 0 的值设为 0,将大于 255 的值设为 255。注意,IE 10 不支持该类型。

    +

    WebSocket

    WebSocket可以通过ArrayBuffer,发送或接收二进制数据。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let socket = new WebSocket("ws://127.0.0.1:8081");
    socket.binaryType = "arraybuffer";

    // Wait until socket is open
    socket.addEventListener("open", function (event) {
    // Send binary data
    const typedArray = new Uint8Array(4);
    socket.send(typedArray.buffer);
    });

    // Receive binary data
    socket.addEventListener("message", function (event) {
    const arrayBuffer = event.data;
    // ···
    });
    + +

    Fetch API

    Fetch API 取回的数据,就是ArrayBuffer对象。

    +
    1
    2
    3
    4
    5
    6
    7
    fetch(url)
    .then(function (response) {
    return response.arrayBuffer();
    })
    .then(function (arrayBuffer) {
    // ...
    });
    + +

    File API

    如果知道一个文件的二进制数据类型,也可以将这个文件读取为ArrayBuffer对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const fileInput = document.getElementById("fileInput");
    const file = fileInput.files[0];
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = function () {
    const arrayBuffer = reader.result;
    // ···
    };
    + +

    下面以处理 bmp 文件为例。假定file变量是一个指向 bmp 文件的文件对象,首先读取文件。

    +
    1
    2
    3
    const reader = new FileReader();
    reader.addEventListener("load", processimage, false);
    reader.readAsArrayBuffer(file);
    + +

    然后,定义处理图像的回调函数:先在二进制数据之上建立一个DataView视图,再建立一个bitmap对象,用于存放处理后的数据,最后将图像展示在Canvas元素之中。

    +
    1
    2
    3
    4
    5
    6
    function processimage(e) {
    const buffer = e.target.result;
    const datav = new DataView(buffer);
    const bitmap = {};
    // 具体的处理步骤
    }
    + +

    具体处理图像数据时,先处理 bmp 的文件头。具体每个文件头的格式和定义,请参阅有关资料。

    +
    1
    2
    3
    4
    5
    6
    bitmap.fileheader = {};
    bitmap.fileheader.bfType = datav.getUint16(0, true);
    bitmap.fileheader.bfSize = datav.getUint32(2, true);
    bitmap.fileheader.bfReserved1 = datav.getUint16(6, true);
    bitmap.fileheader.bfReserved2 = datav.getUint16(8, true);
    bitmap.fileheader.bfOffBits = datav.getUint32(10, true);
    + +

    接着处理图像元信息部分。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bitmap.infoheader = {};
    bitmap.infoheader.biSize = datav.getUint32(14, true);
    bitmap.infoheader.biWidth = datav.getUint32(18, true);
    bitmap.infoheader.biHeight = datav.getUint32(22, true);
    bitmap.infoheader.biPlanes = datav.getUint16(26, true);
    bitmap.infoheader.biBitCount = datav.getUint16(28, true);
    bitmap.infoheader.biCompression = datav.getUint32(30, true);
    bitmap.infoheader.biSizeImage = datav.getUint32(34, true);
    bitmap.infoheader.biXPelsPerMeter = datav.getUint32(38, true);
    bitmap.infoheader.biYPelsPerMeter = datav.getUint32(42, true);
    bitmap.infoheader.biClrUsed = datav.getUint32(46, true);
    bitmap.infoheader.biClrImportant = datav.getUint32(50, true);
    + +

    最后处理图像本身的像素信息。

    +
    1
    2
    const start = bitmap.fileheader.bfOffBits;
    bitmap.pixels = new Uint8Array(buffer, start);
    + +

    至此,图像文件的数据全部处理完成。下一步,可以根据需要,进行图像变形,或者转换格式,或者展示在Canvas网页元素之中。

    +

    SharedArrayBuffer

    JavaScript 是单线程的,Web worker 引入了多线程:主线程用来与用户互动,Worker 线程用来承担计算任务。每个线程的数据都是隔离的,通过postMessage()通信。下面是一个例子。

    +
    1
    2
    // 主线程
    const w = new Worker("myworker.js");
    + +

    上面代码中,主线程新建了一个 Worker 线程。该线程与主线程之间会有一个通信渠道,主线程通过w.postMessage向 Worker 线程发消息,同时通过message事件监听 Worker 线程的回应。

    +
    1
    2
    3
    4
    5
    // 主线程
    w.postMessage("hi");
    w.onmessage = function (ev) {
    console.log(ev.data);
    };
    + +

    上面代码中,主线程先发一个消息hi,然后在监听到 Worker 线程的回应后,就将其打印出来。

    +

    Worker 线程也是通过监听message事件,来获取主线程发来的消息,并作出反应。

    +
    1
    2
    3
    4
    5
    // Worker 线程
    onmessage = function (ev) {
    console.log(ev.data);
    postMessage("ho");
    };
    + +

    线程之间的数据交换可以是各种格式,不仅仅是字符串,也可以是二进制数据。这种交换采用的是复制机制,即一个进程将需要分享的数据复制一份,通过postMessage方法交给另一个进程。如果数据量比较大,这种通信的效率显然比较低。很容易想到,这时可以留出一块内存区域,由主线程与 Worker 线程共享,两方都可以读写,那么就会大大提高效率,协作起来也会比较简单(不像postMessage那么麻烦)。

    +

    ES2017 引入SharedArrayBuffer,允许 Worker 线程与主线程共享同一块内存。SharedArrayBuffer的 API 与ArrayBuffer一模一样,唯一的区别是后者无法共享数据。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 主线程

    // 新建 1KB 共享内存
    const sharedBuffer = new SharedArrayBuffer(1024);

    // 主线程将共享内存的地址发送出去
    w.postMessage(sharedBuffer);

    // 在共享内存上建立视图,供写入数据
    const sharedArray = new Int32Array(sharedBuffer);
    + +

    上面代码中,postMessage方法的参数是SharedArrayBuffer对象。

    +

    Worker 线程从事件的data属性上面取到数据。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // Worker 线程
    onmessage = function (ev) {
    // 主线程共享的数据,就是 1KB 的共享内存
    const sharedBuffer = ev.data;

    // 在共享内存上建立视图,方便读写
    const sharedArray = new Int32Array(sharedBuffer);

    // ...
    };
    + +

    共享内存也可以在 Worker 线程创建,发给主线程。

    +

    SharedArrayBufferArrayBuffer一样,本身是无法读写的,必须在上面建立视图,然后通过视图读写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 分配 10 万个 32 位整数占据的内存空间
    const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);

    // 建立 32 位整数视图
    const ia = new Int32Array(sab); // ia.length == 100000

    // 新建一个质数生成器
    const primes = new PrimeGenerator();

    // 将 10 万个质数,写入这段内存空间
    for (let i = 0; i < ia.length; i++) ia[i] = primes.next();

    // 向 Worker 线程发送这段共享内存
    w.postMessage(ia);
    + +

    Worker 线程收到数据后的处理如下。

    +
    1
    2
    3
    4
    5
    6
    7
    // Worker 线程
    let ia;
    onmessage = function (ev) {
    ia = ev.data;
    console.log(ia.length); // 100000
    console.log(ia[37]); // 输出 163,因为这是第38个质数
    };
    + +

    Atomics 对象

    多线程共享内存,最大的问题就是如何防止两个线程同时修改某个地址,或者说,当一个线程修改共享内存以后,必须有一个机制让其他线程同步。SharedArrayBuffer API 提供Atomics对象,保证所有共享内存的操作都是“原子性”的,并且可以在所有线程内同步。

    +

    什么叫“原子性操作”呢?现代编程语言中,一条普通的命令被编译器处理以后,会变成多条机器指令。如果是单线程运行,这是没有问题的;多线程环境并且共享内存时,就会出问题,因为这一组机器指令的运行期间,可能会插入其他线程的指令,从而导致运行结果出错。请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 主线程
    ia[42] = 314159; // 原先的值 191
    ia[37] = 123456; // 原先的值 163

    // Worker 线程
    console.log(ia[37]);
    console.log(ia[42]);
    // 可能的结果
    // 123456
    // 191
    + +

    上面代码中,主线程的原始顺序是先对 42 号位置赋值,再对 37 号位置赋值。但是,编译器和 CPU 为了优化,可能会改变这两个操作的执行顺序(因为它们之间互不依赖),先对 37 号位置赋值,再对 42 号位置赋值。而执行到一半的时候,Worker 线程可能就会来读取数据,导致打印出123456191

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 主线程
    const sab = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT * 100000);
    const ia = new Int32Array(sab);

    for (let i = 0; i < ia.length; i++) {
    ia[i] = primes.next(); // 将质数放入 ia
    }

    // worker 线程
    ia[112]++; // 错误
    Atomics.add(ia, 112, 1); // 正确
    + +

    上面代码中,Worker 线程直接改写共享内存ia[112]++是不正确的。因为这行语句会被编译成多条机器指令,这些指令之间无法保证不会插入其他进程的指令。请设想如果两个线程同时ia[112]++,很可能它们得到的结果都是不正确的。

    +

    Atomics对象就是为了解决这个问题而提出,它可以保证一个操作所对应的多条机器指令,一定是作为一个整体运行的,中间不会被打断。也就是说,它所涉及的操作都可以看作是原子性的单操作,这可以避免线程竞争,提高多线程共享内存时的操作安全。所以,ia[112]++要改写成Atomics.add(ia, 112, 1)

    +

    Atomics对象提供多种方法。

    +

    (1)Atomics.store(),Atomics.load()

    +

    store()方法用来向共享内存写入数据,load()方法用来从共享内存读出数据。比起直接的读写操作,它们的好处是保证了读写操作的原子性。

    +

    此外,它们还用来解决一个问题:多个线程使用共享内存的某个位置作为开关(flag),一旦该位置的值变了,就执行特定操作。这时,必须保证该位置的赋值操作,一定是在它前面的所有可能会改写内存的操作结束后执行;而该位置的取值操作,一定是在它后面所有可能会读取该位置的操作开始之前执行。store方法和load方法就能做到这一点,编译器不会为了优化,而打乱机器指令的执行顺序。

    +
    1
    2
    Atomics.load(array, index);
    Atomics.store(array, index, value);
    + +

    store方法接受三个参数:SharedBuffer 的视图、位置索引和值,返回sharedArray[index]的值。load方法只接受两个参数:SharedBuffer 的视图和位置索引,也是返回sharedArray[index]的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 主线程 main.js
    ia[42] = 314159; // 原先的值 191
    Atomics.store(ia, 37, 123456); // 原先的值是 163

    // Worker 线程 worker.js
    while (Atomics.load(ia, 37) == 163);
    console.log(ia[37]); // 123456
    console.log(ia[42]); // 314159
    + +

    上面代码中,主线程的Atomics.store向 42 号位置的赋值,一定是早于 37 位置的赋值。只要 37 号位置等于 163,Worker 线程就不会终止循环,而对 37 号位置和 42 号位置的取值,一定是在Atomics.load操作之后。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 主线程
    const worker = new Worker("worker.js");
    const length = 10;
    const size = Int32Array.BYTES_PER_ELEMENT * length;
    // 新建一段共享内存
    const sharedBuffer = new SharedArrayBuffer(size);
    const sharedArray = new Int32Array(sharedBuffer);
    for (let i = 0; i < 10; i++) {
    // 向共享内存写入 10 个整数
    Atomics.store(sharedArray, i, 0);
    }
    worker.postMessage(sharedBuffer);
    + +

    上面代码中,主线程用Atomics.store()方法写入数据。下面是 Worker 线程用Atomics.load()方法读取数据。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // worker.js
    self.addEventListener(
    "message",
    (event) => {
    const sharedArray = new Int32Array(event.data);
    for (let i = 0; i < 10; i++) {
    const arrayValue = Atomics.load(sharedArray, i);
    console.log(`The item at array index ${i} is ${arrayValue}`);
    }
    },
    false
    );
    + +

    (2)Atomics.exchange()

    +

    Worker 线程如果要写入数据,可以使用上面的Atomics.store()方法,也可以使用Atomics.exchange()方法。它们的区别是,Atomics.store()返回写入的值,而Atomics.exchange()返回被替换的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // Worker 线程
    self.addEventListener(
    "message",
    (event) => {
    const sharedArray = new Int32Array(event.data);
    for (let i = 0; i < 10; i++) {
    if (i % 2 === 0) {
    const storedValue = Atomics.store(sharedArray, i, 1);
    console.log(`The item at array index ${i} is now ${storedValue}`);
    } else {
    const exchangedValue = Atomics.exchange(sharedArray, i, 2);
    console.log(
    `The item at array index ${i} was ${exchangedValue}, now 2`
    );
    }
    }
    },
    false
    );
    + +

    上面代码将共享内存的偶数位置的值改成1,奇数位置的值改成2

    +

    (3)Atomics.wait(),Atomics.wake()

    +

    使用while循环等待主线程的通知,不是很高效,如果用在主线程,就会造成卡顿,Atomics对象提供了wait()wake()两个方法用于等待通知。这两个方法相当于锁内存,即在一个线程进行操作时,让其他线程休眠(建立锁),等到操作结束,再唤醒那些休眠的线程(解除锁)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // Worker 线程
    self.addEventListener(
    "message",
    (event) => {
    const sharedArray = new Int32Array(event.data);
    const arrayIndex = 0;
    const expectedStoredValue = 50;
    Atomics.wait(sharedArray, arrayIndex, expectedStoredValue);
    console.log(Atomics.load(sharedArray, arrayIndex));
    },
    false
    );
    + +

    上面代码中,Atomics.wait()方法等同于告诉 Worker 线程,只要满足给定条件(sharedArray0号位置等于50),就在这一行 Worker 线程进入休眠。

    +

    主线程一旦更改了指定位置的值,就可以唤醒 Worker 线程。

    +
    1
    2
    3
    4
    5
    6
    // 主线程
    const newArrayValue = 100;
    Atomics.store(sharedArray, 0, newArrayValue);
    const arrayIndex = 0;
    const queuePos = 1;
    Atomics.wake(sharedArray, arrayIndex, queuePos);
    + +

    上面代码中,sharedArray0号位置改为100,然后就执行Atomics.wake()方法,唤醒在sharedArray0号位置休眠队列里的一个线程。

    +

    Atomics.wait()方法的使用格式如下。

    +
    1
    Atomics.wait(sharedArray, index, value, timeout);
    + +

    它的四个参数含义如下。

    +
      +
    • sharedArray:共享内存的视图数组。
    • +
    • index:视图数据的位置(从 0 开始)。
    • +
    • value:该位置的预期值。一旦实际值等于预期值,就进入休眠。
    • +
    • timeout:整数,表示过了这个时间以后,就自动唤醒,单位毫秒。该参数可选,默认值是Infinity,即无限期的休眠,只有通过Atomics.wake()方法才能唤醒。
    • +
    +

    Atomics.wait()的返回值是一个字符串,共有三种可能的值。如果sharedArray[index]不等于value,就返回字符串not-equal,否则就进入休眠。如果Atomics.wake()方法唤醒,就返回字符串ok;如果因为超时唤醒,就返回字符串timed-out

    +

    Atomics.wake()方法的使用格式如下。

    +
    1
    Atomics.wake(sharedArray, index, count);
    + +

    它的三个参数含义如下。

    +
      +
    • sharedArray:共享内存的视图数组。
    • +
    • index:视图数据的位置(从 0 开始)。
    • +
    • count:需要唤醒的 Worker 线程的数量,默认为Infinity
    • +
    +

    Atomics.wake()方法一旦唤醒休眠的 Worker 线程,就会让它继续往下运行。

    +

    请看一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 主线程
    console.log(ia[37]); // 163
    Atomics.store(ia, 37, 123456);
    Atomics.wake(ia, 37, 1);

    // Worker 线程
    Atomics.wait(ia, 37, 163);
    console.log(ia[37]); // 123456
    + +

    上面代码中,视图数组ia的第 37 号位置,原来的值是163。Worker 线程使用Atomics.wait()方法,指定只要ia[37]等于163,就进入休眠状态。主线程使用Atomics.store()方法,将123456写入ia[37],然后使用Atomics.wake()方法唤醒 Worker 线程。

    +

    另外,基于waitwake这两个方法的锁内存实现,可以看 Lars T Hansen 的 js-lock-and-condition 这个库。

    +

    注意,浏览器的主线程不宜设置休眠,这会导致用户失去响应。而且,主线程实际上会拒绝进入休眠。

    +

    (4)运算方法

    +

    共享内存上面的某些运算是不能被打断的,即不能在运算过程中,让其他线程改写内存上面的值。Atomics 对象提供了一些运算方法,防止数据被改写。

    +
    1
    Atomics.add(sharedArray, index, value);
    + +

    Atomics.add用于将value加到sharedArray[index],返回sharedArray[index]旧的值。

    +
    1
    Atomics.sub(sharedArray, index, value);
    + +

    Atomics.sub用于将valuesharedArray[index]减去,返回sharedArray[index]旧的值。

    +
    1
    Atomics.and(sharedArray, index, value);
    + +

    Atomics.and用于将valuesharedArray[index]进行位运算and,放入sharedArray[index],并返回旧的值。

    +
    1
    Atomics.or(sharedArray, index, value);
    + +

    Atomics.or用于将valuesharedArray[index]进行位运算or,放入sharedArray[index],并返回旧的值。

    +
    1
    Atomics.xor(sharedArray, index, value);
    + +

    Atomic.xor用于将vaulesharedArray[index]进行位运算xor,放入sharedArray[index],并返回旧的值。

    +

    (5)其他方法

    +

    Atomics对象还有以下方法。

    +
      +
    • Atomics.compareExchange(sharedArray, index, oldval, newval):如果sharedArray[index]等于oldval,就写入newval,返回oldval
    • +
    • Atomics.isLockFree(size):返回一个布尔值,表示Atomics对象是否可以处理某个size的内存锁定。如果返回false,应用程序就需要自己来实现锁定。
    • +
    +

    Atomics.compareExchange的一个用途是,从 SharedArrayBuffer 读取一个值,然后对该值进行某个操作,操作结束以后,检查一下 SharedArrayBuffer 里面原来那个值是否发生变化(即被其他线程改写过)。如果没有改写过,就将它写回原来的位置,否则读取新的值,再重头进行一次操作。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-ArrayBuffer/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Class \347\232\204\345\237\272\346\234\254\350\257\255\346\263\225/index.html" "b/posts/ES6-Class \347\232\204\345\237\272\346\234\254\350\257\255\346\263\225/index.html" new file mode 100644 index 00000000..de3e68c9 --- /dev/null +++ "b/posts/ES6-Class \347\232\204\345\237\272\346\234\254\350\257\255\346\263\225/index.html" @@ -0,0 +1,432 @@ +Class 的基本语法 | JCAlways + + + + + + + + + + + + + +

    Class 的基本语法

    Class 的基本语法

    简介

    JavaScript 语言中,生成实例对象的传统方法是通过构造函数。下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function Point(x, y) {
    this.x = x;
    this.y = y;
    }

    Point.prototype.toString = function () {
    return "(" + this.x + ", " + this.y + ")";
    };

    var p = new Point(1, 2);
    + +

    上面这种写法跟传统的面向对象语言(比如 C++ 和 Java)差异很大,很容易让新学习这门语言的程序员感到困惑。

    +

    ES6 提供了更接近传统语言的写法,引入了 Class(类)这个概念,作为对象的模板。通过class关键字,可以定义类。

    +

    基本上,ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5 都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。上面的代码用 ES6 的class改写,就是下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //定义类
    class Point {
    constructor(x, y) {
    this.x = x;
    this.y = y;
    }

    toString() {
    return "(" + this.x + ", " + this.y + ")";
    }
    }
    + +

    上面代码定义了一个“类”,可以看到里面有一个constructor方法,这就是构造方法,而this关键字则代表实例对象。也就是说,ES5 的构造函数Point,对应 ES6 的Point类的构造方法。

    +

    Point类除了构造方法,还定义了一个toString方法。注意,定义“类”的方法的时候,前面不需要加上function这个关键字,直接把函数定义放进去了就可以了。另外,方法之间不需要逗号分隔,加了会报错。

    +

    ES6 的类,完全可以看作构造函数的另一种写法。

    +
    1
    2
    3
    4
    5
    6
    class Point {
    // ...
    }

    typeof Point; // "function"
    Point === Point.prototype.constructor; // true
    + +

    上面代码表明,类的数据类型就是函数,类本身就指向构造函数。

    +

    使用的时候,也是直接对类使用new命令,跟构造函数的用法完全一致。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class Bar {
    doStuff() {
    console.log("stuff");
    }
    }

    var b = new Bar();
    b.doStuff(); // "stuff"
    + +

    构造函数的prototype属性,在 ES6 的“类”上面继续存在。事实上,类的所有方法都定义在类的prototype属性上面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class Point {
    constructor() {
    // ...
    }

    toString() {
    // ...
    }

    toValue() {
    // ...
    }
    }

    // 等同于

    Point.prototype = {
    constructor() {},
    toString() {},
    toValue() {},
    };
    + +

    在类的实例上面调用方法,其实就是调用原型上的方法。

    +
    1
    2
    3
    4
    class B {}
    let b = new B();

    b.constructor === B.prototype.constructor; // true
    + +

    上面代码中,bB类的实例,它的constructor方法就是B类原型的constructor方法。

    +

    由于类的方法都定义在prototype对象上面,所以类的新方法可以添加在prototype对象上面。Object.assign方法可以很方便地一次向类添加多个方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Point {
    constructor() {
    // ...
    }
    }

    Object.assign(Point.prototype, {
    toString() {},
    toValue() {},
    });
    + +

    prototype对象的constructor属性,直接指向“类”的本身,这与 ES5 的行为是一致的。

    +
    1
    Point.prototype.constructor === Point; // true
    + +

    另外,类的内部所有定义的方法,都是不可枚举的(non-enumerable)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Point {
    constructor(x, y) {
    // ...
    }

    toString() {
    // ...
    }
    }

    Object.keys(Point.prototype);
    // []
    Object.getOwnPropertyNames(Point.prototype);
    // ["constructor","toString"]
    + +

    上面代码中,toString方法是Point类内部定义的方法,它是不可枚举的。这一点与 ES5 的行为不一致。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var Point = function (x, y) {
    // ...
    };

    Point.prototype.toString = function () {
    // ...
    };

    Object.keys(Point.prototype);
    // ["toString"]
    Object.getOwnPropertyNames(Point.prototype);
    // ["constructor","toString"]
    + +

    上面代码采用 ES5 的写法,toString方法就是可枚举的。

    +

    类的属性名,可以采用表达式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let methodName = "getArea";

    class Square {
    constructor(length) {
    // ...
    }

    [methodName]() {
    // ...
    }
    }
    + +

    上面代码中,Square类的方法名getArea,是从表达式得到的。

    +

    严格模式

    类和模块的内部,默认就是严格模式,所以不需要使用use strict指定运行模式。只要你的代码写在类或模块之中,就只有严格模式可用。

    +

    考虑到未来所有的代码,其实都是运行在模块之中,所以 ES6 实际上把整个语言升级到了严格模式。

    +

    constructor 方法

    constructor方法是类的默认方法,通过new命令生成对象实例时,自动调用该方法。一个类必须有constructor方法,如果没有显式定义,一个空的constructor方法会被默认添加。

    +
    1
    2
    3
    4
    5
    6
    class Point {}

    // 等同于
    class Point {
    constructor() {}
    }
    + +

    上面代码中,定义了一个空的类Point,JavaScript 引擎会自动为它添加一个空的constructor方法。

    +

    constructor方法默认返回实例对象(即this),完全可以指定返回另外一个对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class Foo {
    constructor() {
    return Object.create(null);
    }
    }

    new Foo() instanceof Foo;
    // false
    + +

    上面代码中,constructor函数返回一个全新的对象,结果导致实例对象不是Foo类的实例。

    +

    类必须使用new调用,否则会报错。这是它跟普通构造函数的一个主要区别,后者不用new也可以执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class Foo {
    constructor() {
    return Object.create(null);
    }
    }

    Foo();
    // TypeError: Class constructor Foo cannot be invoked without 'new'
    + +

    类的实例对象

    生成类的实例对象的写法,与 ES5 完全一样,也是使用new命令。前面说过,如果忘记加上new,像函数那样调用Class,将会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Point {
    // ...
    }

    // 报错
    var point = Point(2, 3);

    // 正确
    var point = new Point(2, 3);
    + +

    与 ES5 一样,实例的属性除非显式定义在其本身(即定义在this对象上),否则都是定义在原型上(即定义在class上)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    //定义类
    class Point {
    constructor(x, y) {
    this.x = x;
    this.y = y;
    }

    toString() {
    return "(" + this.x + ", " + this.y + ")";
    }
    }

    var point = new Point(2, 3);

    point.toString(); // (2, 3)

    point.hasOwnProperty("x"); // true
    point.hasOwnProperty("y"); // true
    point.hasOwnProperty("toString"); // false
    point.__proto__.hasOwnProperty("toString"); // true
    + +

    上面代码中,xy都是实例对象point自身的属性(因为定义在this变量上),所以hasOwnProperty方法返回true,而toString是原型对象的属性(因为定义在Point类上),所以hasOwnProperty方法返回false。这些都与 ES5 的行为保持一致。

    +

    与 ES5 一样,类的所有实例共享一个原型对象。

    +
    1
    2
    3
    4
    5
    var p1 = new Point(2, 3);
    var p2 = new Point(3, 2);

    p1.__proto__ === p2.__proto__;
    //true
    + +

    上面代码中,p1p2都是Point的实例,它们的原型都是Point.prototype,所以__proto__属性是相等的。

    +

    这也意味着,可以通过实例的__proto__属性为“类”添加方法。

    +
    +

    __proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var p1 = new Point(2, 3);
    var p2 = new Point(3, 2);

    p1.__proto__.printName = function () {
    return "Oops";
    };

    p1.printName(); // "Oops"
    p2.printName(); // "Oops"

    var p3 = new Point(4, 2);
    p3.printName(); // "Oops"
    + +

    上面代码在p1的原型上添加了一个printName方法,由于p1的原型就是p2的原型,因此p2也可以调用这个方法。而且,此后新建的实例p3也可以调用这个方法。这意味着,使用实例的__proto__属性改写原型,必须相当谨慎,不推荐使用,因为这会改变“类”的原始定义,影响到所有实例。

    +

    Class 表达式

    与函数一样,类也可以使用表达式的形式定义。

    +
    1
    2
    3
    4
    5
    const MyClass = class Me {
    getClassName() {
    return Me.name;
    }
    };
    + +

    上面代码使用表达式定义了一个类。需要注意的是,这个类的名字是MyClass而不是MeMe只在 Class 的内部代码可用,指代当前类。

    +
    1
    2
    3
    let inst = new MyClass();
    inst.getClassName(); // Me
    Me.name; // ReferenceError: Me is not defined
    + +

    上面代码表示,Me只在 Class 内部有定义。

    +

    如果类的内部没用到的话,可以省略Me,也就是可以写成下面的形式。

    +
    1
    2
    3
    const MyClass = class {
    /* ... */
    };
    + +

    采用 Class 表达式,可以写出立即执行的 Class。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let person = new (class {
    constructor(name) {
    this.name = name;
    }

    sayName() {
    console.log(this.name);
    }
    })("张三");

    person.sayName(); // "张三"
    + +

    上面代码中,person是一个立即执行的类的实例。

    +

    不存在变量提升

    类不存在变量提升(hoist),这一点与 ES5 完全不同。

    +
    1
    2
    new Foo(); // ReferenceError
    class Foo {}
    + +

    上面代码中,Foo类使用在前,定义在后,这样会报错,因为 ES6 不会把类的声明提升到代码头部。这种规定的原因与下文要提到的继承有关,必须保证子类在父类之后定义。

    +
    1
    2
    3
    4
    {
    let Foo = class {};
    class Bar extends Foo {}
    }
    + +

    上面的代码不会报错,因为Bar继承Foo的时候,Foo已经有定义了。但是,如果存在class的提升,上面代码就会报错,因为class会被提升到代码头部,而let命令是不提升的,所以导致Bar继承Foo的时候,Foo还没有定义。

    +

    私有方法和私有属性

    现有的方法

    私有方法是常见需求,但 ES6 不提供,只能通过变通方法模拟实现。

    +

    一种做法是在命名上加以区别。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Widget {
    // 公有方法
    foo(baz) {
    this._bar(baz);
    }

    // 私有方法
    _bar(baz) {
    return (this.snaf = baz);
    }

    // ...
    }
    + +

    上面代码中,_bar方法前面的下划线,表示这是一个只限于内部使用的私有方法。但是,这种命名是不保险的,在类的外部,还是可以调用到这个方法。

    +

    另一种方法就是索性将私有方法移出模块,因为模块内部的所有方法都是对外可见的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Widget {
    foo(baz) {
    bar.call(this, baz);
    }

    // ...
    }

    function bar(baz) {
    return (this.snaf = baz);
    }
    + +

    上面代码中,foo是公有方法,内部调用了bar.call(this, baz)。这使得bar实际上成为了当前模块的私有方法。

    +

    还有一种方法是利用Symbol值的唯一性,将私有方法的名字命名为一个Symbol值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const bar = Symbol("bar");
    const snaf = Symbol("snaf");

    export default class myClass {
    // 公有方法
    foo(baz) {
    this[bar](baz);
    }

    // 私有方法
    [bar](baz) {
    return (this[snaf] = baz);
    }

    // ...
    }
    + +

    上面代码中,barsnaf都是Symbol值,导致第三方无法获取到它们,因此达到了私有方法和私有属性的效果。

    +

    私有属性的提案

    目前,有一个提案,为class加了私有属性。方法是在属性名之前,使用#表示。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class Point {
    #x;

    constructor(x = 0) {
    #x = +x; // 写成 this.#x 亦可
    }

    get x() { return #x }
    set x(value) { #x = +value }
    }
    + +

    上面代码中,#x就是私有属性,在Point类之外是读取不到这个属性的。由于井号#是属性名的一部分,使用时必须带有#一起使用,所以#xx是两个不同的属性。

    +

    私有属性可以指定初始值,在构造函数执行时进行初始化。

    +
    1
    2
    3
    4
    5
    6
    class Point {
    #x = 0;
    constructor() {
    #x; // 0
    }
    }
    + +

    之所以要引入一个新的前缀#表示私有属性,而没有采用private关键字,是因为 JavaScript 是一门动态语言,没有类型声明,使用独立的符号似乎是唯一的比较方便可靠的方法,能够准确地区分一种属性是否为私有属性。另外,Ruby 语言使用@表示私有属性,ES6 没有用这个符号而使用#,是因为@已经被留给了 Decorator。

    +

    这种写法不仅可以写私有属性,还可以用来写私有方法。

    +
    1
    2
    3
    4
    5
    6
    7
    class Foo {
    #a;
    #b;
    #sum() { return #a + #b; }
    printSum() { console.log(#sum()); }
    constructor(a, b) { #a = a; #b = b; }
    }
    + +

    上面代码中,#sum()就是一个私有方法。

    +

    另外,私有属性也可以设置 getter 和 setter 方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Counter {
    #xValue = 0;

    get #x() { return #xValue; }
    set #x(value) {
    this.#xValue = value;
    }

    constructor() {
    super();
    // ...
    }
    }
    + +

    上面代码中,#x是一个私有属性,它的读写都通过get #x()set #x()来完成。

    +

    私有属性不限于从this引用,类的实例也可以引用私有属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class Foo {
    #privateValue = 42;
    static getPrivateValue(foo) {
    return foo.#privateValue;
    }
    }

    Foo.getPrivateValue(new Foo()); // 42
    + +

    上面代码允许从实例foo上面引用私有属性。

    +

    但是,直接从实例上引用私有属性是不可以的,只能在类的定义中引用。

    +
    1
    2
    3
    4
    5
    class Foo {
    #bar;
    }
    let foo = new Foo();
    foo.#bar; // 报错
    + +

    上面代码直接从实例引用私有属性,导致报错。

    +

    this 的指向

    类的方法内部如果含有this,它默认指向类的实例。但是,必须非常小心,一旦单独使用该方法,很可能报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Logger {
    printName(name = "there") {
    this.print(`Hello ${name}`);
    }

    print(text) {
    console.log(text);
    }
    }

    const logger = new Logger();
    const { printName } = logger;
    printName(); // TypeError: Cannot read property 'print' of undefined
    + +

    上面代码中,printName方法中的this,默认指向Logger类的实例。但是,如果将这个方法提取出来单独使用,this会指向该方法运行时所在的环境,因为找不到print方法而导致报错。

    +

    一个比较简单的解决方法是,在构造方法中绑定this,这样就不会找不到print方法了。

    +
    1
    2
    3
    4
    5
    6
    7
    class Logger {
    constructor() {
    this.printName = this.printName.bind(this);
    }

    // ...
    }
    + +

    另一种解决方法是使用箭头函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Logger {
    constructor() {
    this.printName = (name = "there") => {
    this.print(`Hello ${name}`);
    };
    }

    // ...
    }
    + +

    还有一种解决方法是使用Proxy,获取方法的时候,自动绑定this

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function selfish(target) {
    const cache = new WeakMap();
    const handler = {
    get(target, key) {
    const value = Reflect.get(target, key);
    if (typeof value !== "function") {
    return value;
    }
    if (!cache.has(value)) {
    cache.set(value, value.bind(target));
    }
    return cache.get(value);
    },
    };
    const proxy = new Proxy(target, handler);
    return proxy;
    }

    const logger = selfish(new Logger());
    + +

    name 属性

    由于本质上,ES6 的类只是 ES5 的构造函数的一层包装,所以函数的许多特性都被Class继承,包括name属性。

    +
    1
    2
    class Point {}
    Point.name; // "Point"
    + +

    name属性总是返回紧跟在class关键字后面的类名。

    +

    Class 的取值函数(getter)和存值函数(setter)

    与 ES5 一样,在“类”的内部可以使用getset关键字,对某个属性设置存值函数和取值函数,拦截该属性的存取行为。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class MyClass {
    constructor() {
    // ...
    }
    get prop() {
    return "getter";
    }
    set prop(value) {
    console.log("setter: " + value);
    }
    }

    let inst = new MyClass();

    inst.prop = 123;
    // setter: 123

    inst.prop;
    // 'getter'
    + +

    上面代码中,prop属性有对应的存值函数和取值函数,因此赋值和读取行为都被自定义了。

    +

    存值函数和取值函数是设置在属性的 Descriptor 对象上的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class CustomHTMLElement {
    constructor(element) {
    this.element = element;
    }

    get html() {
    return this.element.innerHTML;
    }

    set html(value) {
    this.element.innerHTML = value;
    }
    }

    var descriptor = Object.getOwnPropertyDescriptor(
    CustomHTMLElement.prototype,
    "html"
    );

    "get" in descriptor; // true
    "set" in descriptor; // true
    + +

    上面代码中,存值函数和取值函数是定义在html属性的描述对象上面,这与 ES5 完全一致。

    +

    Class 的 Generator 方法

    如果某个方法之前加上星号(*),就表示该方法是一个 Generator 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Foo {
    constructor(...args) {
    this.args = args;
    }
    *[Symbol.iterator]() {
    for (let arg of this.args) {
    yield arg;
    }
    }
    }

    for (let x of new Foo("hello", "world")) {
    console.log(x);
    }
    // hello
    // world
    + +

    上面代码中,Foo类的Symbol.iterator方法前有一个星号,表示该方法是一个 Generator 函数。Symbol.iterator方法返回一个Foo类的默认遍历器,for...of循环会自动调用这个遍历器。

    +

    Class 的静态方法

    类相当于实例的原型,所有在类中定义的方法,都会被实例继承。如果在一个方法前,加上static关键字,就表示该方法不会被实例继承,而是直接通过类来调用,这就称为“静态方法”。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Foo {
    static classMethod() {
    return "hello";
    }
    }

    Foo.classMethod(); // 'hello'

    var foo = new Foo();
    foo.classMethod();
    // TypeError: foo.classMethod is not a function
    + +

    上面代码中,Foo类的classMethod方法前有static关键字,表明该方法是一个静态方法,可以直接在Foo类上调用(Foo.classMethod()),而不是在Foo类的实例上调用。如果在实例上调用静态方法,会抛出一个错误,表示不存在该方法。

    +

    注意,如果静态方法包含this关键字,这个this指的是类,而不是实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Foo {
    static bar() {
    this.baz();
    }
    static baz() {
    console.log("hello");
    }
    baz() {
    console.log("world");
    }
    }

    Foo.bar(); // hello
    + +

    上面代码中,静态方法bar调用了this.baz,这里的this指的是Foo类,而不是Foo的实例,等同于调用Foo.baz。另外,从这个例子还可以看出,静态方法可以与非静态方法重名。

    +

    父类的静态方法,可以被子类继承。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Foo {
    static classMethod() {
    return "hello";
    }
    }

    class Bar extends Foo {}

    Bar.classMethod(); // 'hello'
    + +

    上面代码中,父类Foo有一个静态方法,子类Bar可以调用这个方法。

    +

    静态方法也是可以从super对象上调用的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Foo {
    static classMethod() {
    return "hello";
    }
    }

    class Bar extends Foo {
    static classMethod() {
    return super.classMethod() + ", too";
    }
    }

    Bar.classMethod(); // "hello, too"
    + +

    Class 的静态属性和实例属性

    静态属性指的是 Class 本身的属性,即Class.propName,而不是定义在实例对象(this)上的属性。

    +
    1
    2
    3
    4
    class Foo {}

    Foo.prop = 1;
    Foo.prop; // 1
    + +

    上面的写法为Foo类定义了一个静态属性prop

    +

    目前,只有这种写法可行,因为 ES6 明确规定,Class 内部只有静态方法,没有静态属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 以下两种写法都无效
    class Foo {
    // 写法一
    prop: 2;

    // 写法二
    static prop: 2;
    }

    Foo.prop; // undefined
    + +

    目前有一个静态属性的提案,对实例属性和静态属性都规定了新的写法。

    +

    (1)类的实例属性

    +

    类的实例属性可以用等式,写入类的定义之中。

    +
    1
    2
    3
    4
    5
    6
    7
    class MyClass {
    myProp = 42;

    constructor() {
    console.log(this.myProp); // 42
    }
    }
    + +

    上面代码中,myProp就是MyClass的实例属性。在MyClass的实例上,可以读取这个属性。

    +

    以前,我们定义实例属性,只能写在类的constructor方法里面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class ReactCounter extends React.Component {
    constructor(props) {
    super(props);
    this.state = {
    count: 0,
    };
    }
    }
    + +

    上面代码中,构造方法constructor里面,定义了this.state属性。

    +

    有了新的写法以后,可以不在constructor方法里面定义。

    +
    1
    2
    3
    4
    5
    class ReactCounter extends React.Component {
    state = {
    count: 0,
    };
    }
    + +

    这种写法比以前更清晰。

    +

    为了可读性的目的,对于那些在constructor里面已经定义的实例属性,新写法允许直接列出。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class ReactCounter extends React.Component {
    state;
    constructor(props) {
    super(props);
    this.state = {
    count: 0,
    };
    }
    }
    + +

    (2)类的静态属性

    +

    类的静态属性只要在上面的实例属性写法前面,加上static关键字就可以了。

    +
    1
    2
    3
    4
    5
    6
    7
    class MyClass {
    static myStaticProp = 42;

    constructor() {
    console.log(MyClass.myStaticProp); // 42
    }
    }
    + +

    同样的,这个新写法大大方便了静态属性的表达。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 老写法
    class Foo {
    // ...
    }
    Foo.prop = 1;

    // 新写法
    class Foo {
    static prop = 1;
    }
    + +

    上面代码中,老写法的静态属性定义在类的外部。整个类生成以后,再生成静态属性。这样让人很容易忽略这个静态属性,也不符合相关代码应该放在一起的代码组织原则。另外,新写法是显式声明(declarative),而不是赋值处理,语义更好。

    +

    new.target 属性

    new是从构造函数生成实例对象的命令。ES6 为new命令引入了一个new.target属性,该属性一般用在构造函数之中,返回new命令作用于的那个构造函数。如果构造函数不是通过new命令调用的,new.target会返回undefined,因此这个属性可以用来确定构造函数是怎么调用的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function Person(name) {
    if (new.target !== undefined) {
    this.name = name;
    } else {
    throw new Error("必须使用 new 命令生成实例");
    }
    }

    // 另一种写法
    function Person(name) {
    if (new.target === Person) {
    this.name = name;
    } else {
    throw new Error("必须使用 new 命令生成实例");
    }
    }

    var person = new Person("张三"); // 正确
    var notAPerson = Person.call(person, "张三"); // 报错
    + +

    上面代码确保构造函数只能通过new命令调用。

    +

    Class 内部调用new.target,返回当前 Class。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Rectangle {
    constructor(length, width) {
    console.log(new.target === Rectangle);
    this.length = length;
    this.width = width;
    }
    }

    var obj = new Rectangle(3, 4); // 输出 true
    + +

    需要注意的是,子类继承父类时,new.target会返回子类。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Rectangle {
    constructor(length, width) {
    console.log(new.target === Rectangle);
    // ...
    }
    }

    class Square extends Rectangle {
    constructor(length) {
    super(length, length);
    }
    }

    var obj = new Square(3); // 输出 false
    + +

    上面代码中,new.target会返回子类。

    +

    利用这个特点,可以写出不能独立使用、必须继承后才能使用的类。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Shape {
    constructor() {
    if (new.target === Shape) {
    throw new Error("本类不能实例化");
    }
    }
    }

    class Rectangle extends Shape {
    constructor(length, width) {
    super();
    // ...
    }
    }

    var x = new Shape(); // 报错
    var y = new Rectangle(3, 4); // 正确
    + +

    上面代码中,Shape类不能被实例化,只能用于继承。

    +

    注意,在函数外部,使用new.target会报错。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Class%20%E7%9A%84%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Class \347\232\204\347\273\247\346\211\277/index.html" "b/posts/ES6-Class \347\232\204\347\273\247\346\211\277/index.html" new file mode 100644 index 00000000..412a0b87 --- /dev/null +++ "b/posts/ES6-Class \347\232\204\347\273\247\346\211\277/index.html" @@ -0,0 +1,378 @@ +Class 的继承 | JCAlways + + + + + + + + + + + + + +

    Class 的继承

    Class 的继承

    简介

    Class 可以通过extends关键字实现继承,这比 ES5 的通过修改原型链实现继承,要清晰和方便很多。

    +
    1
    2
    3
    class Point {}

    class ColorPoint extends Point {}
    + +

    上面代码定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。但是由于没有部署任何代码,所以这两个类完全一样,等于复制了一个Point类。下面,我们在ColorPoint内部加上代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class ColorPoint extends Point {
    constructor(x, y, color) {
    super(x, y); // 调用父类的constructor(x, y)
    this.color = color;
    }

    toString() {
    return this.color + " " + super.toString(); // 调用父类的toString()
    }
    }
    + +

    上面代码中,constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。

    +

    子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类自己的this对象,必须先通过父类的构造函数完成塑造,得到与父类同样的实例属性和方法,然后再对其进行加工,加上子类自己的实例属性和方法。如果不调用super方法,子类就得不到this对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Point {
    /* ... */
    }

    class ColorPoint extends Point {
    constructor() {}
    }

    let cp = new ColorPoint(); // ReferenceError
    + +

    上面代码中,ColorPoint继承了父类Point,但是它的构造函数没有调用super方法,导致新建实例时报错。

    +

    ES5 的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面(Parent.apply(this))。ES6 的继承机制完全不同,实质是先将父类实例对象的属性和方法,加到this上面(所以必须先调用super方法),然后再用子类的构造函数修改this

    +

    如果子类没有定义constructor方法,这个方法会被默认添加,代码如下。也就是说,不管有没有显式定义,任何一个子类都有constructor方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class ColorPoint extends Point {}

    // 等同于
    class ColorPoint extends Point {
    constructor(...args) {
    super(...args);
    }
    }
    + +

    另一个需要注意的地方是,在子类的构造函数中,只有调用super之后,才可以使用this关键字,否则会报错。这是因为子类实例的构建,基于父类实例,只有super方法才能调用父类实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Point {
    constructor(x, y) {
    this.x = x;
    this.y = y;
    }
    }

    class ColorPoint extends Point {
    constructor(x, y, color) {
    this.color = color; // ReferenceError
    super(x, y);
    this.color = color; // 正确
    }
    }
    + +

    上面代码中,子类的constructor方法没有调用super之前,就使用this关键字,结果报错,而放在super方法之后就是正确的。

    +

    下面是生成子类实例的代码。

    +
    1
    2
    3
    4
    let cp = new ColorPoint(25, 8, "green");

    cp instanceof ColorPoint; // true
    cp instanceof Point; // true
    + +

    上面代码中,实例对象cp同时是ColorPointPoint两个类的实例,这与 ES5 的行为完全一致。

    +

    最后,父类的静态方法,也会被子类继承。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class A {
    static hello() {
    console.log("hello world");
    }
    }

    class B extends A {}

    B.hello(); // hello world
    + +

    上面代码中,hello()A类的静态方法,B继承A,也继承了A的静态方法。

    +

    Object.getPrototypeOf()

    Object.getPrototypeOf方法可以用来从子类上获取父类。

    +
    1
    2
    Object.getPrototypeOf(ColorPoint) === Point;
    // true
    + +

    因此,可以使用这个方法判断,一个类是否继承了另一个类。

    +

    super 关键字

    super这个关键字,既可以当作函数使用,也可以当作对象使用。在这两种情况下,它的用法完全不同。

    +

    第一种情况,super作为函数调用时,代表父类的构造函数。ES6 要求,子类的构造函数必须执行一次super函数。

    +
    1
    2
    3
    4
    5
    6
    7
    class A {}

    class B extends A {
    constructor() {
    super();
    }
    }
    + +

    上面代码中,子类B的构造函数之中的super(),代表调用父类的构造函数。这是必须的,否则 JavaScript 引擎会报错。

    +

    注意,super虽然代表了父类A的构造函数,但是返回的是子类B的实例,即super内部的this指的是B,因此super()在这里相当于A.prototype.constructor.call(this)

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class A {
    constructor() {
    console.log(new.target.name);
    }
    }
    class B extends A {
    constructor() {
    super();
    }
    }
    new A(); // A
    new B(); // B
    + +

    上面代码中,new.target指向当前正在执行的函数。可以看到,在super()执行时,它指向的是子类B的构造函数,而不是父类A的构造函数。也就是说,super()内部的this指向的是B

    +

    作为函数时,super()只能用在子类的构造函数之中,用在其他地方就会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    class A {}

    class B extends A {
    m() {
    super(); // 报错
    }
    }
    + +

    上面代码中,super()用在B类的m方法之中,就会造成句法错误。

    +

    第二种情况,super作为对象时,在普通方法中,指向父类的原型对象;在静态方法中,指向父类。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class A {
    p() {
    return 2;
    }
    }

    class B extends A {
    constructor() {
    super();
    console.log(super.p()); // 2
    }
    }

    let b = new B();
    + +

    上面代码中,子类B当中的super.p(),就是将super当作一个对象使用。这时,super在普通方法之中,指向A.prototype,所以super.p()就相当于A.prototype.p()

    +

    这里需要注意,由于super指向父类的原型对象,所以定义在父类实例上的方法或属性,是无法通过super调用的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class A {
    constructor() {
    this.p = 2;
    }
    }

    class B extends A {
    get m() {
    return super.p;
    }
    }

    let b = new B();
    b.m; // undefined
    + +

    上面代码中,p是父类A实例的属性,super.p就引用不到它。

    +

    如果属性定义在父类的原型对象上,super就可以取到。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {}
    A.prototype.x = 2;

    class B extends A {
    constructor() {
    super();
    console.log(super.x); // 2
    }
    }

    let b = new B();
    + +

    上面代码中,属性x是定义在A.prototype上面的,所以super.x可以取到它的值。

    +

    ES6 规定,在子类普通方法中通过super调用父类的方法时,方法内部的this指向当前的子类实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class A {
    constructor() {
    this.x = 1;
    }
    print() {
    console.log(this.x);
    }
    }

    class B extends A {
    constructor() {
    super();
    this.x = 2;
    }
    m() {
    super.print();
    }
    }

    let b = new B();
    b.m(); // 2
    + +

    上面代码中,super.print()虽然调用的是A.prototype.print(),但是A.prototype.print()内部的this指向子类B的实例,导致输出的是2,而不是1。也就是说,实际上执行的是super.print.call(this)

    +

    由于this指向子类实例,所以如果通过super对某个属性赋值,这时super就是this,赋值的属性会变成子类实例的属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class A {
    constructor() {
    this.x = 1;
    }
    }

    class B extends A {
    constructor() {
    super();
    this.x = 2;
    super.x = 3;
    console.log(super.x); // undefined
    console.log(this.x); // 3
    }
    }

    let b = new B();
    + +

    上面代码中,super.x赋值为3,这时等同于对this.x赋值为3。而当读取super.x的时候,读的是A.prototype.x,所以返回undefined

    +

    如果super作为对象,用在静态方法之中,这时super将指向父类,而不是父类的原型对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    class Parent {
    static myMethod(msg) {
    console.log("static", msg);
    }

    myMethod(msg) {
    console.log("instance", msg);
    }
    }

    class Child extends Parent {
    static myMethod(msg) {
    super.myMethod(msg);
    }

    myMethod(msg) {
    super.myMethod(msg);
    }
    }

    Child.myMethod(1); // static 1

    var child = new Child();
    child.myMethod(2); // instance 2
    + +

    上面代码中,super在静态方法之中指向父类,在普通方法之中指向父类的原型对象。

    +

    另外,在子类的静态方法中通过super调用父类的方法时,方法内部的this指向当前的子类,而不是子类的实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class A {
    constructor() {
    this.x = 1;
    }
    static print() {
    console.log(this.x);
    }
    }

    class B extends A {
    constructor() {
    super();
    this.x = 2;
    }
    static m() {
    super.print();
    }
    }

    B.x = 3;
    B.m(); // 3
    + +

    上面代码中,静态方法B.m里面,super.print指向父类的静态方法。这个方法里面的this指向的是B,而不是B的实例。

    +

    注意,使用super的时候,必须显式指定是作为函数、还是作为对象使用,否则会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class A {}

    class B extends A {
    constructor() {
    super();
    console.log(super); // 报错
    }
    }
    + +

    上面代码中,console.log(super)当中的super,无法看出是作为函数使用,还是作为对象使用,所以 JavaScript 引擎解析代码的时候就会报错。这时,如果能清晰地表明super的数据类型,就不会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class A {}

    class B extends A {
    constructor() {
    super();
    console.log(super.valueOf() instanceof B); // true
    }
    }

    let b = new B();
    + +

    上面代码中,super.valueOf()表明super是一个对象,因此就不会报错。同时,由于super使得this指向B的实例,所以super.valueOf()返回的是一个B的实例。

    +

    最后,由于对象总是继承其他对象的,所以可以在任意一个对象中,使用super关键字。

    +
    1
    2
    3
    4
    5
    6
    7
    var obj = {
    toString() {
    return "MyObject: " + super.toString();
    },
    };

    obj.toString(); // MyObject: [object Object]
    + +

    类的 prototype 属性和__proto__属性

    大多数浏览器的 ES5 实现之中,每一个对象都有__proto__属性,指向对应的构造函数的prototype属性。Class 作为构造函数的语法糖,同时有prototype属性和__proto__属性,因此同时存在两条继承链。

    +

    (1)子类的__proto__属性,表示构造函数的继承,总是指向父类。

    +

    (2)子类prototype属性的__proto__属性,表示方法的继承,总是指向父类的prototype属性。

    +
    1
    2
    3
    4
    5
    6
    class A {}

    class B extends A {}

    B.__proto__ === A; // true
    B.prototype.__proto__ === A.prototype; // true
    + +

    上面代码中,子类B__proto__属性指向父类A,子类Bprototype属性的__proto__属性指向父类Aprototype属性。

    +

    这样的结果是因为,类的继承是按照下面的模式实现的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class A {}

    class B {}

    // B 的实例继承 A 的实例
    Object.setPrototypeOf(B.prototype, A.prototype);

    // B 继承 A 的静态属性
    Object.setPrototypeOf(B, A);

    const b = new B();
    + +

    《对象的扩展》一章给出过Object.setPrototypeOf方法的实现。

    +
    1
    2
    3
    4
    Object.setPrototypeOf = function (obj, proto) {
    obj.__proto__ = proto;
    return obj;
    };
    + +

    因此,就得到了上面的结果。

    +
    1
    2
    3
    4
    5
    6
    7
    Object.setPrototypeOf(B.prototype, A.prototype);
    // 等同于
    B.prototype.__proto__ = A.prototype;

    Object.setPrototypeOf(B, A);
    // 等同于
    B.__proto__ = A;
    + +

    这两条继承链,可以这样理解:作为一个对象,子类(B)的原型(__proto__属性)是父类(A);作为一个构造函数,子类(B)的原型对象(prototype属性)是父类的原型对象(prototype属性)的实例。

    +
    1
    2
    3
    Object.create(A.prototype);
    // 等同于
    B.prototype.__proto__ = A.prototype;
    + +

    extends关键字后面可以跟多种类型的值。

    +
    1
    class B extends A {}
    + +

    上面代码的A,只要是一个有prototype属性的函数,就能被B继承。由于函数都有prototype属性(除了Function.prototype函数),因此A可以是任意函数。

    +

    下面,讨论两种情况。第一种,子类继承Object类。

    +
    1
    2
    3
    4
    class A extends Object {}

    A.__proto__ === Object; // true
    A.prototype.__proto__ === Object.prototype; // true
    + +

    这种情况下,A其实就是构造函数Object的复制,A的实例就是Object的实例。

    +

    第二种情况,不存在任何继承。

    +
    1
    2
    3
    4
    class A {}

    A.__proto__ === Function.prototype; // true
    A.prototype.__proto__ === Object.prototype; // true
    + +

    这种情况下,A作为一个基类(即不存在任何继承),就是一个普通函数,所以直接继承Function.prototype。但是,A调用后返回一个空对象(即Object实例),所以A.prototype.__proto__指向构造函数(Object)的prototype属性。

    +

    实例的 __proto__ 属性

    子类实例的__proto__属性的__proto__属性,指向父类实例的__proto__属性。也就是说,子类的原型的原型,是父类的原型。

    +
    1
    2
    3
    4
    5
    var p1 = new Point(2, 3);
    var p2 = new ColorPoint(2, 3, "red");

    p2.__proto__ === p1.__proto__; // false
    p2.__proto__.__proto__ === p1.__proto__; // true
    + +

    上面代码中,ColorPoint继承了Point,导致前者原型的原型是后者的原型。

    +

    因此,通过子类实例的__proto__.__proto__属性,可以修改父类实例的行为。

    +
    1
    2
    3
    4
    5
    p2.__proto__.__proto__.printName = function () {
    console.log("Ha");
    };

    p1.printName(); // "Ha"
    + +

    上面代码在ColorPoint的实例p2上向Point类添加方法,结果影响到了Point的实例p1

    +

    原生构造函数的继承

    原生构造函数是指语言内置的构造函数,通常用来生成数据结构。ECMAScript 的原生构造函数大致有下面这些。

    +
      +
    • Boolean()
    • +
    • Number()
    • +
    • String()
    • +
    • Array()
    • +
    • Date()
    • +
    • Function()
    • +
    • RegExp()
    • +
    • Error()
    • +
    • Object()
    • +
    +

    以前,这些原生构造函数是无法继承的,比如,不能自己定义一个Array的子类。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function MyArray() {
    Array.apply(this, arguments);
    }

    MyArray.prototype = Object.create(Array.prototype, {
    constructor: {
    value: MyArray,
    writable: true,
    configurable: true,
    enumerable: true,
    },
    });
    + +

    上面代码定义了一个继承 Array 的MyArray类。但是,这个类的行为与Array完全不一致。

    +
    1
    2
    3
    4
    5
    6
    var colors = new MyArray();
    colors[0] = "red";
    colors.length; // 0

    colors.length = 0;
    colors[0]; // "red"
    + +

    之所以会发生这种情况,是因为子类无法获得原生构造函数的内部属性,通过Array.apply()或者分配给原型对象都不行。原生构造函数会忽略apply方法传入的this,也就是说,原生构造函数的this无法绑定,导致拿不到内部属性。

    +

    ES5 是先新建子类的实例对象this,再将父类的属性添加到子类上,由于父类的内部属性无法获取,导致无法继承原生的构造函数。比如,Array构造函数有一个内部属性[[DefineOwnProperty]],用来定义新属性时,更新length属性,这个内部属性无法在子类获取,导致子类的length属性行为不正常。

    +

    下面的例子中,我们想让一个普通对象继承Error对象。

    +
    1
    2
    3
    4
    5
    6
    7
    var e = {};

    Object.getOwnPropertyNames(Error.call(e));
    // [ 'stack' ]

    Object.getOwnPropertyNames(e);
    // []
    + +

    上面代码中,我们想通过Error.call(e)这种写法,让普通对象e具有Error对象的实例属性。但是,Error.call()完全忽略传入的第一个参数,而是返回一个新对象,e本身没有任何变化。这证明了Error.call(e)这种写法,无法继承原生构造函数。

    +

    ES6 允许继承原生构造函数定义子类,因为 ES6 是先新建父类的实例对象this,然后再用子类的构造函数修饰this,使得父类的所有行为都可以继承。下面是一个继承Array的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class MyArray extends Array {
    constructor(...args) {
    super(...args);
    }
    }

    var arr = new MyArray();
    arr[0] = 12;
    arr.length; // 1

    arr.length = 0;
    arr[0]; // undefined
    + +

    上面代码定义了一个MyArray类,继承了Array构造函数,因此就可以从MyArray生成数组的实例。这意味着,ES6 可以自定义原生数据结构(比如ArrayString等)的子类,这是 ES5 无法做到的。

    +

    上面这个例子也说明,extends关键字不仅可以用来继承类,还可以用来继承原生的构造函数。因此可以在原生数据结构的基础上,定义自己的数据结构。下面就是定义了一个带版本功能的数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    class VersionedArray extends Array {
    constructor() {
    super();
    this.history = [[]];
    }
    commit() {
    this.history.push(this.slice());
    }
    revert() {
    this.splice(0, this.length, ...this.history[this.history.length - 1]);
    }
    }

    var x = new VersionedArray();

    x.push(1);
    x.push(2);
    x; // [1, 2]
    x.history; // [[]]

    x.commit();
    x.history; // [[], [1, 2]]

    x.push(3);
    x; // [1, 2, 3]
    x.history; // [[], [1, 2]]

    x.revert();
    x; // [1, 2]
    + +

    上面代码中,VersionedArray会通过commit方法,将自己的当前状态生成一个版本快照,存入history属性。revert方法用来将数组重置为最新一次保存的版本。除此之外,VersionedArray依然是一个普通数组,所有原生的数组方法都可以在它上面调用。

    +

    下面是一个自定义Error子类的例子,可以用来定制报错时的行为。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    class ExtendableError extends Error {
    constructor(message) {
    super();
    this.message = message;
    this.stack = new Error().stack;
    this.name = this.constructor.name;
    }
    }

    class MyError extends ExtendableError {
    constructor(m) {
    super(m);
    }
    }

    var myerror = new MyError("ll");
    myerror.message; // "ll"
    myerror instanceof Error; // true
    myerror.name; // "MyError"
    myerror.stack;
    // Error
    // at MyError.ExtendableError
    // ...
    + +

    注意,继承Object的子类,有一个行为差异

    +
    1
    2
    3
    4
    5
    6
    7
    class NewObj extends Object {
    constructor() {
    super(...arguments);
    }
    }
    var o = new NewObj({ attr: true });
    o.attr === true; // false
    + +

    上面代码中,NewObj继承了Object,但是无法通过super方法向父类Object传参。这是因为 ES6 改变了Object构造函数的行为,一旦发现Object方法不是通过new Object()这种形式调用,ES6 规定Object构造函数会忽略参数。

    +

    Mixin 模式的实现

    Mixin 指的是多个对象合成一个新的对象,新对象具有各个组成成员的接口。它的最简单实现如下。

    +
    1
    2
    3
    4
    5
    6
    7
    const a = {
    a: "a",
    };
    const b = {
    b: "b",
    };
    const c = { ...a, ...b }; // {a: 'a', b: 'b'}
    + +

    上面代码中,c对象是a对象和b对象的合成,具有两者的接口。

    +

    下面是一个更完备的实现,将多个类的接口“混入”(mix in)另一个类。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function mix(...mixins) {
    class Mix {}

    for (let mixin of mixins) {
    copyProperties(Mix.prototype, mixin); // 拷贝实例属性
    copyProperties(Mix.prototype, Reflect.getPrototypeOf(mixin)); // 拷贝原型属性
    }

    return Mix;
    }

    function copyProperties(target, source) {
    for (let key of Reflect.ownKeys(source)) {
    if (key !== "constructor" && key !== "prototype" && key !== "name") {
    let desc = Object.getOwnPropertyDescriptor(source, key);
    Object.defineProperty(target, key, desc);
    }
    }
    }
    + +

    上面代码的mix函数,可以将多个对象合成为一个类。使用的时候,只要继承这个类即可。

    +
    1
    2
    3
    class DistributedEdit extends mix(Loggable, Serializable) {
    // ...
    }
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Class%20%E7%9A%84%E7%BB%A7%E6%89%BF/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-ECMAScript 6 \347\256\200\344\273\213/index.html" "b/posts/ES6-ECMAScript 6 \347\256\200\344\273\213/index.html" new file mode 100644 index 00000000..f772fc1d --- /dev/null +++ "b/posts/ES6-ECMAScript 6 \347\256\200\344\273\213/index.html" @@ -0,0 +1,391 @@ +ECMAScript 6 简介 | JCAlways + + + + + + + + + + + + + +

    ECMAScript 6 简介

    ECMAScript 6 简介

    ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

    +

    ECMAScript 和 JavaScript 的关系

    一个常见的问题是,ECMAScript 和 JavaScript 到底是什么关系?

    +

    要讲清楚这个问题,需要回顾历史。1996 年 11 月,JavaScript 的创造者 Netscape 公司,决定将 JavaScript 提交给标准化组织 ECMA,希望这种语言能够成为国际标准。次年,ECMA 发布 262 号标准文件(ECMA-262)的第一版,规定了浏览器脚本语言的标准,并将这种语言称为 ECMAScript,这个版本就是 1.0 版。

    +

    该标准从一开始就是针对 JavaScript 语言制定的,但是之所以不叫 JavaScript,有两个原因。一是商标,Java 是 Sun 公司的商标,根据授权协议,只有 Netscape 公司可以合法地使用 JavaScript 这个名字,且 JavaScript 本身也已经被 Netscape 公司注册为商标。二是想体现这门语言的制定者是 ECMA,不是 Netscape,这样有利于保证这门语言的开放性和中立性。

    +

    因此,ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现(另外的 ECMAScript 方言还有 Jscript 和 ActionScript)。日常场合,这两个词是可以互换的。

    +

    ES6 与 ECMAScript 2015 的关系

    ECMAScript 2015(简称 ES2015)这个词,也是经常可以看到的。它与 ES6 是什么关系呢?

    +

    2011 年,ECMAScript 5.1 版发布后,就开始制定 6.0 版了。因此,ES6 这个词的原意,就是指 JavaScript 语言的下一个版本。

    +

    但是,因为这个版本引入的语法功能太多,而且制定过程当中,还有很多组织和个人不断提交新功能。事情很快就变得清楚了,不可能在一个版本里面包括所有将要引入的功能。常规的做法是先发布 6.0 版,过一段时间再发 6.1 版,然后是 6.2 版、6.3 版等等。

    +

    但是,标准的制定者不想这样做。他们想让标准的升级成为常规流程:任何人在任何时候,都可以向标准委员会提交新语法的提案,然后标准委员会每个月开一次会,评估这些提案是否可以接受,需要哪些改进。如果经过多次会议以后,一个提案足够成熟了,就可以正式进入标准了。这就是说,标准的版本升级成为了一个不断滚动的流程,每个月都会有变动。

    +

    标准委员会最终决定,标准在每年的 6 月份正式发布一次,作为当年的正式版本。接下来的时间,就在这个版本的基础上做改动,直到下一年的 6 月份,草案就自然变成了新一年的版本。这样一来,就不需要以前的版本号了,只要用年份标记就可以了。

    +

    ES6 的第一个版本,就这样在 2015 年 6 月发布了,正式名称就是《ECMAScript 2015 标准》(简称 ES2015)。2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布,这个版本可以看作是 ES6.1 版,因为两者的差异非常小(只新增了数组实例的includes方法和指数运算符),基本上是同一个标准。根据计划,2017 年 6 月发布 ES2017 标准。

    +

    因此,ES6 既是一个历史名词,也是一个泛指,含义是 5.1 版以后的 JavaScript 的下一代标准,涵盖了 ES2015、ES2016、ES2017 等等,而 ES2015 则是正式名称,特指该年发布的正式版本的语言标准。本书中提到 ES6 的地方,一般是指 ES2015 标准,但有时也是泛指“下一代 JavaScript 语言”。

    +

    语法提案的批准流程

    任何人都可以向标准委员会(又称 TC39 委员会)提案,要求修改语言标准。

    +

    一种新的语法从提案到变成正式标准,需要经历五个阶段。每个阶段的变动都需要由 TC39 委员会批准。

    +
      +
    • Stage 0 - Strawman(展示阶段)
    • +
    • Stage 1 - Proposal(征求意见阶段)
    • +
    • Stage 2 - Draft(草案阶段)
    • +
    • Stage 3 - Candidate(候选人阶段)
    • +
    • Stage 4 - Finished(定案阶段)
    • +
    +

    一个提案只要能进入 Stage 2,就差不多肯定会包括在以后的正式标准里面。ECMAScript 当前的所有提案,可以在 TC39 的官方网站Github.com/tc39/ecma262查看。

    +

    本书的写作目标之一,是跟踪 ECMAScript 语言的最新进展,介绍 5.1 版本以后所有的新语法。对于那些明确或很有希望,将要列入标准的新语法,都将予以介绍。

    +

    ECMAScript 的历史

    ES6 从开始制定到最后发布,整整用了 15 年。

    +

    前面提到,ECMAScript 1.0 是 1997 年发布的,接下来的两年,连续发布了 ECMAScript 2.0(1998 年 6 月)和 ECMAScript 3.0(1999 年 12 月)。3.0 版是一个巨大的成功,在业界得到广泛支持,成为通行标准,奠定了 JavaScript 语言的基本语法,以后的版本完全继承。直到今天,初学者一开始学习 JavaScript,其实就是在学 3.0 版的语法。

    +

    2000 年,ECMAScript 4.0 开始酝酿。这个版本最后没有通过,但是它的大部分内容被 ES6 继承了。因此,ES6 制定的起点其实是 2000 年。

    +

    为什么 ES4 没有通过呢?因为这个版本太激进了,对 ES3 做了彻底升级,导致标准委员会的一些成员不愿意接受。ECMA 的第 39 号技术专家委员会(Technical Committee 39,简称 TC39)负责制订 ECMAScript 标准,成员包括 Microsoft、Mozilla、Google 等大公司。

    +

    2007 年 10 月,ECMAScript 4.0 版草案发布,本来预计次年 8 月发布正式版本。但是,各方对于是否通过这个标准,发生了严重分歧。以 Yahoo、Microsoft、Google 为首的大公司,反对 JavaScript 的大幅升级,主张小幅改动;以 JavaScript 创造者 Brendan Eich 为首的 Mozilla 公司,则坚持当前的草案。

    +

    2008 年 7 月,由于对于下一个版本应该包括哪些功能,各方分歧太大,争论过于激烈,ECMA 开会决定,中止 ECMAScript 4.0 的开发,将其中涉及现有功能改善的一小部分,发布为 ECMAScript 3.1,而将其他激进的设想扩大范围,放入以后的版本,由于会议的气氛,该版本的项目代号起名为 Harmony(和谐)。会后不久,ECMAScript 3.1 就改名为 ECMAScript 5。

    +

    2009 年 12 月,ECMAScript 5.0 版正式发布。Harmony 项目则一分为二,一些较为可行的设想定名为 JavaScript.next 继续开发,后来演变成 ECMAScript 6;一些不是很成熟的设想,则被视为 JavaScript.next.next,在更远的将来再考虑推出。TC39 委员会的总体考虑是,ES5 与 ES3 基本保持兼容,较大的语法修正和新功能加入,将由 JavaScript.next 完成。当时,JavaScript.next 指的是 ES6,第六版发布以后,就指 ES7。TC39 的判断是,ES5 会在 2013 年的年中成为 JavaScript 开发的主流标准,并在此后五年中一直保持这个位置。

    +

    2011 年 6 月,ECMAscript 5.1 版发布,并且成为 ISO 国际标准(ISO/IEC 16262:2011)。

    +

    2013 年 3 月,ECMAScript 6 草案冻结,不再添加新功能。新的功能设想将被放到 ECMAScript 7。

    +

    2013 年 12 月,ECMAScript 6 草案发布。然后是 12 个月的讨论期,听取各方反馈。

    +

    2015 年 6 月,ECMAScript 6 正式通过,成为国际标准。从 2000 年算起,这时已经过去了 15 年。

    +

    部署进度

    各大浏览器的最新版本,对 ES6 的支持可以查看kangax.github.io/es5-compat-table/es6/。随着时间的推移,支持度已经越来越高了,超过 90%的 ES6 语法特性都实现了。

    +

    Node 是 JavaScript 的服务器运行环境(runtime)。它对 ES6 的支持度更高。除了那些默认打开的功能,还有一些语法功能已经实现了,但是默认没有打开。使用下面的命令,可以查看 Node 已经实现的 ES6 特性。

    +
    1
    $ node --v8-options | grep harmony
    + +

    上面命令的输出结果,会因为版本的不同而有所不同。

    +

    我写了一个工具 ES-Checker,用来检查各种运行环境对 ES6 的支持情况。访问ruanyf.github.io/es-checker,可以看到您的浏览器支持 ES6 的程度。运行下面的命令,可以查看你正在使用的 Node 环境对 ES6 的支持程度。

    +
    1
    2
    3
    4
    5
    6
    7
    $ npm install -g es-checker
    $ es-checker

    =========================================
    Passes 24 feature Dectations
    Your runtime supports 57% of ECMAScript 6
    =========================================
    + +

    Babel 转码器

    Babel 是一个广泛使用的 ES6 转码器,可以将 ES6 代码转为 ES5 代码,从而在现有环境执行。这意味着,你可以用 ES6 的方式编写程序,又不用担心现有环境是否支持。下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    // 转码前
    input.map((item) => item + 1);

    // 转码后
    input.map(function (item) {
    return item + 1;
    });
    + +

    上面的原始代码用了箭头函数,Babel 将其转为普通函数,就能在不支持箭头函数的 JavaScript 环境执行了。

    +

    配置文件.babelrc

    Babel 的配置文件是.babelrc,存放在项目的根目录下。使用 Babel 的第一步,就是配置这个文件。

    +

    该文件用来设置转码规则和插件,基本格式如下。

    +
    1
    2
    3
    4
    {
    "presets": [],
    "plugins": []
    }
    + +

    presets字段设定转码规则,官方提供以下的规则集,你可以根据需要安装。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    # 最新转码规则
    $ npm install --save-dev babel-preset-latest

    # react 转码规则
    $ npm install --save-dev babel-preset-react

    # 不同阶段语法提案的转码规则(共有4个阶段),选装一个
    $ npm install --save-dev babel-preset-stage-0
    $ npm install --save-dev babel-preset-stage-1
    $ npm install --save-dev babel-preset-stage-2
    $ npm install --save-dev babel-preset-stage-3
    + +

    然后,将这些规则加入.babelrc

    +
    1
    2
    3
    4
    5
    6
    7
    8
    {
    "presets": [
    "latest",
    "react",
    "stage-2"
    ],
    "plugins": []
    }
    + +

    注意,以下所有 Babel 工具和模块的使用,都必须先写好.babelrc

    +

    命令行转码babel-cli

    Babel 提供babel-cli工具,用于命令行转码。

    +

    它的安装命令如下。

    +
    1
    $ npm install --global babel-cli
    + +

    基本用法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    # 转码结果输出到标准输出
    $ babel example.js

    # 转码结果写入一个文件
    # --out-file 或 -o 参数指定输出文件
    $ babel example.js --out-file compiled.js
    # 或者
    $ babel example.js -o compiled.js

    # 整个目录转码
    # --out-dir 或 -d 参数指定输出目录
    $ babel src --out-dir lib
    # 或者
    $ babel src -d lib

    # -s 参数生成source map文件
    $ babel src -d lib -s
    + +

    上面代码是在全局环境下,进行 Babel 转码。这意味着,如果项目要运行,全局环境必须有 Babel,也就是说项目产生了对环境的依赖。另一方面,这样做也无法支持不同项目使用不同版本的 Babel。

    +

    一个解决办法是将babel-cli安装在项目之中。

    +
    1
    2
    # 安装
    $ npm install --save-dev babel-cli
    + +

    然后,改写package.json

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    // ...
    "devDependencies": {
    "babel-cli": "^6.0.0"
    },
    "scripts": {
    "build": "babel src -d lib"
    },
    }
    + +

    转码的时候,就执行下面的命令。

    +
    1
    $ npm run build
    + +

    babel-node

    babel-cli工具自带一个babel-node命令,提供一个支持 ES6 的 REPL 环境。它支持 Node 的 REPL 环境的所有功能,而且可以直接运行 ES6 代码。

    +

    它不用单独安装,而是随babel-cli一起安装。然后,执行babel-node就进入 REPL 环境。

    +
    1
    2
    3
    $ babel-node
    > (x => x * 2)(1)
    2
    + +

    babel-node命令可以直接运行 ES6 脚本。将上面的代码放入脚本文件es6.js,然后直接运行。

    +
    1
    2
    $ babel-node es6.js
    2
    + +

    babel-node也可以安装在项目中。

    +
    1
    $ npm install --save-dev babel-cli
    + +

    然后,改写package.json

    +
    1
    2
    3
    4
    5
    {
    "scripts": {
    "script-name": "babel-node script.js"
    }
    }
    + +

    上面代码中,使用babel-node替代node,这样script.js本身就不用做任何转码处理。

    +

    babel-register

    babel-register模块改写require命令,为它加上一个钩子。此后,每当使用require加载.js.jsx.es.es6后缀名的文件,就会先用 Babel 进行转码。

    +
    1
    $ npm install --save-dev babel-register
    + +

    使用时,必须首先加载babel-register

    +
    1
    2
    require("babel-register");
    require("./index.js");
    + +

    然后,就不需要手动对index.js转码了。

    +

    需要注意的是,babel-register只会对require命令加载的文件转码,而不会对当前文件转码。另外,由于它是实时转码,所以只适合在开发环境使用。

    +

    babel-core

    如果某些代码需要调用 Babel 的 API 进行转码,就要使用babel-core模块。

    +

    安装命令如下。

    +
    1
    $ npm install babel-core --save
    + +

    然后,在项目中就可以调用babel-core

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var babel = require("babel-core");

    // 字符串转码
    babel.transform("code();", options);
    // => { code, map, ast }

    // 文件转码(异步)
    babel.transformFile("filename.js", options, function (err, result) {
    result; // => { code, map, ast }
    });

    // 文件转码(同步)
    babel.transformFileSync("filename.js", options);
    // => { code, map, ast }

    // Babel AST转码
    babel.transformFromAst(ast, code, options);
    // => { code, map, ast }
    + +

    配置对象options,可以参看官方文档http://babeljs.io/docs/usage/options/

    +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    var es6Code = "let x = n => n + 1";
    var es5Code = require("babel-core").transform(es6Code, {
    presets: ["latest"],
    }).code;
    // '"use strict";\n\nvar x = function x(n) {\n return n + 1;\n};'
    + +

    上面代码中,transform方法的第一个参数是一个字符串,表示需要被转换的 ES6 代码,第二个参数是转换的配置对象。

    +

    babel-polyfill

    Babel 默认只转换新的 JavaScript 句法(syntax),而不转换新的 API,比如IteratorGeneratorSetMapProxyReflectSymbolPromise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转码。

    +

    举例来说,ES6 在Array对象上新增了Array.from方法。Babel 就不会转码这个方法。如果想让这个方法运行,必须使用babel-polyfill,为当前环境提供一个垫片。

    +

    安装命令如下。

    +
    1
    $ npm install --save babel-polyfill
    + +

    然后,在脚本头部,加入如下一行代码。

    +
    1
    2
    3
    import "babel-polyfill";
    // 或者
    require("babel-polyfill");
    + +

    Babel 默认不转码的 API 非常多,详细清单可以查看babel-plugin-transform-runtime模块的definitions.js文件。

    +

    浏览器环境

    Babel 也可以用于浏览器环境。但是,从 Babel 6.0 开始,不再直接提供浏览器版本,而是要用构建工具构建出来。如果你没有或不想使用构建工具,可以使用babel-standalone模块提供的浏览器版本,将其插入网页。

    +
    1
    2
    3
    4
    <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-standalone/6.4.4/babel.min.js"></script>
    <script type="text/babel">
    // Your ES6 code
    </script>
    + +

    注意,网页实时将 ES6 代码转为 ES5,对性能会有影响。生产环境需要加载已经转码完成的脚本。

    +

    下面是如何将代码打包成浏览器可以使用的脚本,以Babel配合Browserify为例。首先,安装babelify模块。

    +
    1
    $ npm install --save-dev babelify babel-preset-latest
    + +

    然后,再用命令行转换 ES6 脚本。

    +
    1
    2
    $  browserify script.js -o bundle.js \
    -t [ babelify --presets [ latest ] ]
    + +

    上面代码将 ES6 脚本script.js,转为bundle.js,浏览器直接加载后者就可以了。

    +

    package.json设置下面的代码,就不用每次命令行都输入参数了。

    +
    1
    2
    3
    4
    5
    {
    "browserify": {
    "transform": [["babelify", { "presets": ["latest"] }]]
    }
    }
    + +

    在线转换

    Babel 提供一个REPL 在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。

    +

    与其他工具的配合

    许多工具需要 Babel 进行前置转码,这里举两个例子:ESLint 和 Mocha。

    +

    ESLint 用于静态检查代码的语法和风格,安装命令如下。

    +
    1
    $ npm install --save-dev eslint babel-eslint
    + +

    然后,在项目根目录下,新建一个配置文件.eslintrc,在其中加入parser字段。

    +
    1
    2
    3
    4
    5
    6
    {
    "parser": "babel-eslint",
    "rules": {
    ...
    }
    }
    + +

    再在package.json之中,加入相应的scripts脚本。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    {
    "name": "my-module",
    "scripts": {
    "lint": "eslint my-files.js"
    },
    "devDependencies": {
    "babel-eslint": "...",
    "eslint": "..."
    }
    }
    + +

    Mocha 则是一个测试框架,如果需要执行使用 ES6 语法的测试脚本,可以修改package.jsonscripts.test

    +
    1
    2
    3
    "scripts": {
    "test": "mocha --ui qunit --compilers js:babel-core/register"
    }
    + +

    上面命令中,--compilers参数指定脚本的转码器,规定后缀名为js的文件,都需要使用babel-core/register先转码。

    +

    Traceur 转码器

    Google 公司的Traceur转码器,也可以将 ES6 代码转为 ES5 代码。

    +

    直接插入网页

    Traceur 允许将 ES6 代码直接插入网页。首先,必须在网页头部加载 Traceur 库文件。

    +
    1
    2
    3
    4
    5
    6
    <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
    <script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
    <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
    <script type="module">
    import "./Greeter.js";
    </script>
    + +

    上面代码中,一共有 4 个script标签。第一个是加载 Traceur 的库文件,第二个和第三个是将这个库文件用于浏览器环境,第四个则是加载用户脚本,这个脚本里面可以使用 ES6 代码。

    +

    注意,第四个script标签的type属性的值是module,而不是text/javascript。这是 Traceur 编译器识别 ES6 代码的标志,编译器会自动将所有type=module的代码编译为 ES5,然后再交给浏览器执行。

    +

    除了引用外部 ES6 脚本,也可以直接在网页中放置 ES6 代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <script type="module">
    class Calc {
    constructor() {
    console.log('Calc constructor');
    }
    add(a, b) {
    return a + b;
    }
    }

    var c = new Calc();
    console.log(c.add(4,5));
    </script>
    + +

    正常情况下,上面代码会在控制台打印出9

    +

    如果想对 Traceur 的行为有精确控制,可以采用下面参数配置的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <script>
    // Create the System object
    window.System = new traceur.runtime.BrowserTraceurLoader();
    // Set some experimental options
    var metadata = {
    traceurOptions: {
    experimental: true,
    properTailCalls: true,
    symbols: true,
    arrayComprehension: true,
    asyncFunctions: true,
    asyncGenerators: exponentiation,
    forOn: true,
    generatorComprehension: true
    }
    };
    // Load your module
    System.import('./myModule.js', {metadata: metadata}).catch(function(ex) {
    console.error('Import failed', ex.stack || ex);
    });
    </script>
    + +

    上面代码中,首先生成 Traceur 的全局对象window.System,然后System.import方法可以用来加载 ES6。加载的时候,需要传入一个配置对象metadata,该对象的traceurOptions属性可以配置支持 ES6 功能。如果设为experimental: true,就表示除了 ES6 以外,还支持一些实验性的新功能。

    +

    在线转换

    Traceur 也提供一个在线编译器,可以在线将 ES6 代码转为 ES5 代码。转换后的代码,可以直接作为 ES5 代码插入网页运行。

    +

    上面的例子转为 ES5 代码运行,就是下面这个样子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <script src="https://google.github.io/traceur-compiler/bin/traceur.js"></script>
    <script src="https://google.github.io/traceur-compiler/bin/BrowserSystem.js"></script>
    <script src="https://google.github.io/traceur-compiler/src/bootstrap.js"></script>
    <script>
    $traceurRuntime.ModuleStore.getAnonymousModule(function() {
    "use strict";

    var Calc = function Calc() {
    console.log('Calc constructor');
    };

    ($traceurRuntime.createClass)(Calc, {add: function(a, b) {
    return a + b;
    }}, {});

    var c = new Calc();
    console.log(c.add(4, 5));
    return {};
    });
    </script>
    + +

    命令行转换

    作为命令行工具使用时,Traceur 是一个 Node 的模块,首先需要用 npm 安装。

    +
    1
    $ npm install -g traceur
    + +

    安装成功后,就可以在命令行下使用 Traceur 了。

    +

    Traceur 直接运行 ES6 脚本文件,会在标准输出显示运行结果,以前面的calc.js为例。

    +
    1
    2
    3
    $ traceur calc.js
    Calc constructor
    9
    + +

    如果要将 ES6 脚本转为 ES5 保存,要采用下面的写法。

    +
    1
    $ traceur --script calc.es6.js --out calc.es5.js
    + +

    上面代码的--script选项表示指定输入文件,--out选项表示指定输出文件。

    +

    为了防止有些特性编译不成功,最好加上--experimental选项。

    +
    1
    $ traceur --script calc.es6.js --out calc.es5.js --experimental
    + +

    命令行下转换生成的文件,就可以直接放到浏览器中运行。

    +

    Node 环境的用法

    Traceur 的 Node 用法如下(假定已安装traceur模块)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var traceur = require("traceur");
    var fs = require("fs");

    // 将 ES6 脚本转为字符串
    var contents = fs.readFileSync("es6-file.js").toString();

    var result = traceur.compile(contents, {
    filename: "es6-file.js",
    sourceMap: true,
    // 其他设置
    modules: "commonjs",
    });

    if (result.error) throw result.error;

    // result 对象的 js 属性就是转换后的 ES5 代码
    fs.writeFileSync("out.js", result.js);
    // sourceMap 属性对应 map 文件
    fs.writeFileSync("out.js.map", result.sourceMap);
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-ECMAScript%206%20%E7%AE%80%E4%BB%8B/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Generator \345\207\275\346\225\260\347\232\204\345\274\202\346\255\245\345\272\224\347\224\250/index.html" "b/posts/ES6-Generator \345\207\275\346\225\260\347\232\204\345\274\202\346\255\245\345\272\224\347\224\250/index.html" new file mode 100644 index 00000000..dd34bad1 --- /dev/null +++ "b/posts/ES6-Generator \345\207\275\346\225\260\347\232\204\345\274\202\346\255\245\345\272\224\347\224\250/index.html" @@ -0,0 +1,415 @@ +Generator 函数的异步应用 | JCAlways + + + + + + + + + + + + + +

    Generator 函数的异步应用

    Generator 函数的异步应用

    异步编程对 JavaScript 语言太重要。Javascript 语言的执行环境是“单线程”的,如果没有异步编程,根本没法用,非卡死不可。本章主要介绍 Generator 函数如何完成异步操作。

    +

    传统方法

    ES6 诞生以前,异步编程的方法,大概有下面四种。

    +
      +
    • 回调函数
    • +
    • 事件监听
    • +
    • 发布/订阅
    • +
    • Promise 对象
    • +
    +

    Generator 函数将 JavaScript 异步编程带入了一个全新的阶段。

    +

    基本概念

    异步

    所谓”异步”,简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。

    +

    比如,有一个任务是读取文件进行处理,任务的第一段是向操作系统发出请求,要求读取文件。然后,程序执行其他任务,等到操作系统返回文件,再接着执行任务的第二段(处理文件)。这种不连续的执行,就叫做异步。

    +

    相应地,连续的执行就叫做同步。由于是连续执行,不能插入其他任务,所以操作系统从硬盘读取文件的这段时间,程序只能干等着。

    +

    回调函数

    JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。回调函数的英语名字callback,直译过来就是”重新调用”。

    +

    读取文件进行处理,是这样写的。

    +
    1
    2
    3
    4
    fs.readFile("/etc/passwd", "utf-8", function (err, data) {
    if (err) throw err;
    console.log(data);
    });
    + +

    上面代码中,readFile函数的第三个参数,就是回调函数,也就是任务的第二段。等到操作系统返回了/etc/passwd这个文件以后,回调函数才会执行。

    +

    一个有趣的问题是,为什么 Node 约定,回调函数的第一个参数,必须是错误对象err(如果没有错误,该参数就是null)?

    +

    原因是执行分成两段,第一段执行完以后,任务所在的上下文环境就已经结束了。在这以后抛出的错误,原来的上下文环境已经无法捕捉,只能当作参数,传入第二段。

    +

    Promise

    回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。

    +
    1
    2
    3
    4
    5
    fs.readFile(fileA, "utf-8", function (err, data) {
    fs.readFile(fileB, "utf-8", function (err, data) {
    // ...
    });
    });
    + +

    不难想象,如果依次读取两个以上的文件,就会出现多重嵌套。代码不是纵向发展,而是横向发展,很快就会乱成一团,无法管理。因为多个异步操作形成了强耦合,只要有一个操作需要修改,它的上层回调函数和下层回调函数,可能都要跟着修改。这种情况就称为”回调函数地狱”(callback hell)。

    +

    Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件,写法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var readFile = require("fs-readfile-promise");

    readFile(fileA)
    .then(function (data) {
    console.log(data.toString());
    })
    .then(function () {
    return readFile(fileB);
    })
    .then(function (data) {
    console.log(data.toString());
    })
    .catch(function (err) {
    console.log(err);
    });
    + +

    上面代码中,我使用了fs-readfile-promise模块,它的作用就是返回一个 Promise 版本的readFile函数。Promise 提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。

    +

    可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。

    +

    Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。

    +

    那么,有没有更好的写法呢?

    +

    Generator 函数

    协程

    传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做”协程”(coroutine),意思是多个线程互相协作,完成异步任务。

    +

    协程有点像函数,又有点像线程。它的运行流程大致如下。

    +
      +
    • 第一步,协程A开始执行。
    • +
    • 第二步,协程A执行到一半,进入暂停,执行权转移到协程B
    • +
    • 第三步,(一段时间后)协程B交还执行权。
    • +
    • 第四步,协程A恢复执行。
    • +
    +

    上面流程的协程A,就是异步任务,因为它分成两段(或多段)执行。

    +

    举例来说,读取文件的协程写法如下。

    +
    1
    2
    3
    4
    5
    function* asyncJob() {
    // ...其他代码
    var f = yield readFile(fileA);
    // ...其他代码
    }
    + +

    上面代码的函数asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。

    +

    协程遇到yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。

    +

    协程的 Generator 函数实现

    Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。

    +

    整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用yield语句注明。Generator 函数的执行方法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function* gen(x) {
    var y = yield x + 2;
    return y;
    }

    var g = gen(1);
    g.next(); // { value: 3, done: false }
    g.next(); // { value: undefined, done: true }
    + +

    上面代码中,调用 Generator 函数,会返回一个内部指针(即遍历器)g。这是 Generator 函数不同于普通函数的另一个地方,即执行它不会返回结果,返回的是指针对象。调用指针gnext方法,会移动内部指针(即执行异步任务的第一段),指向第一个遇到的yield语句,上例是执行到x + 2为止。

    +

    换言之,next方法的作用是分阶段执行Generator函数。每次调用next方法,会返回一个对象,表示当前阶段的信息(value属性和done属性)。value属性是yield语句后面表达式的值,表示当前阶段的值;done属性是一个布尔值,表示 Generator 函数是否执行完毕,即是否还有下一个阶段。

    +

    Generator 函数的数据交换和错误处理

    Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。

    +

    next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function* gen(x) {
    var y = yield x + 2;
    return y;
    }

    var g = gen(1);
    g.next(); // { value: 3, done: false }
    g.next(2); // { value: 2, done: true }
    + +

    上面代码中,第一个next方法的value属性,返回表达式x + 2的值3。第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的value属性,返回的就是2(变量y的值)。

    +

    Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function* gen(x) {
    try {
    var y = yield x + 2;
    } catch (e) {
    console.log(e);
    }
    return y;
    }

    var g = gen(1);
    g.next();
    g.throw("出错了");
    // 出错了
    + +

    上面代码的最后一行,Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获。这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

    +

    异步任务的封装

    下面看看如何使用 Generator 函数,执行一个真实的异步任务。

    +
    1
    2
    3
    4
    5
    6
    7
    var fetch = require("node-fetch");

    function* gen() {
    var url = "https://api.github.com/users/github";
    var result = yield fetch(url);
    console.log(result.bio);
    }
    + +

    上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。就像前面说过的,这段代码非常像同步操作,除了加上了yield命令。

    +

    执行这段代码的方法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var g = gen();
    var result = g.next();

    result.value
    .then(function (data) {
    return data.json();
    })
    .then(function (data) {
    g.next(data);
    });
    + +

    上面代码中,首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

    +

    可以看到,虽然 Generator 函数将异步操作表示得很简洁,但是流程管理却不方便(即何时执行第一阶段、何时执行第二阶段)。

    +

    Thunk 函数

    Thunk 函数是自动执行 Generator 函数的一种方法。

    +

    参数的求值策略

    Thunk 函数早在上个世纪 60 年代就诞生了。

    +

    那时,编程语言刚刚起步,计算机学家还在研究,编译器怎么写比较好。一个争论的焦点是”求值策略”,即函数的参数到底应该何时求值。

    +
    1
    2
    3
    4
    5
    6
    7
    var x = 1;

    function f(m) {
    return m * 2;
    }

    f(x + 5);
    + +

    上面代码先定义函数f,然后向它传入表达式x + 5。请问,这个表达式应该何时求值?

    +

    一种意见是”传值调用”(call by value),即在进入函数体之前,就计算x + 5的值(等于 6),再将这个值传入函数f。C 语言就采用这种策略。

    +
    1
    2
    3
    f(x + 5);
    // 传值调用时,等同于
    f(6);
    + +

    另一种意见是“传名调用”(call by name),即直接将表达式x + 5传入函数体,只在用到它的时候求值。Haskell 语言采用这种策略。

    +
    1
    2
    3
    4
    f(x + 5)(
    // 传名调用时,等同于
    x + 5
    ) * 2;
    + +

    传值调用和传名调用,哪一种比较好?

    +

    回答是各有利弊。传值调用比较简单,但是对参数求值的时候,实际上还没用到这个参数,有可能造成性能损失。

    +
    1
    2
    3
    4
    5
    function f(a, b) {
    return b;
    }

    f(3 * x * x - 2 * x - 1, x);
    + +

    上面代码中,函数f的第一个参数是一个复杂的表达式,但是函数体内根本没用到。对这个参数求值,实际上是不必要的。因此,有一些计算机学家倾向于”传名调用”,即只在执行时求值。

    +

    Thunk 函数的含义

    编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function f(m) {
    return m * 2;
    }

    f(x + 5);

    // 等同于

    var thunk = function () {
    return x + 5;
    };

    function f(thunk) {
    return thunk() * 2;
    }
    + +

    上面代码中,函数 f 的参数x + 5被一个函数替换了。凡是用到原参数的地方,对Thunk函数求值即可。

    +

    这就是 Thunk 函数的定义,它是“传名调用”的一种实现策略,用来替换某个表达式。

    +

    JavaScript 语言的 Thunk 函数

    JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 正常版本的readFile(多参数版本)
    fs.readFile(fileName, callback);

    // Thunk版本的readFile(单参数版本)
    var Thunk = function (fileName) {
    return function (callback) {
    return fs.readFile(fileName, callback);
    };
    };

    var readFileThunk = Thunk(fileName);
    readFileThunk(callback);
    + +

    上面代码中,fs模块的readFile方法是一个多参数函数,两个参数分别为文件名和回调函数。经过转换器处理,它变成了一个单参数函数,只接受回调函数作为参数。这个单参数版本,就叫做 Thunk 函数。

    +

    任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // ES5版本
    var Thunk = function (fn) {
    return function () {
    var args = Array.prototype.slice.call(arguments);
    return function (callback) {
    args.push(callback);
    return fn.apply(this, args);
    };
    };
    };

    // ES6版本
    const Thunk = function (fn) {
    return function (...args) {
    return function (callback) {
    return fn.call(this, ...args, callback);
    };
    };
    };
    + +

    使用上面的转换器,生成fs.readFile的 Thunk 函数。

    +
    1
    2
    var readFileThunk = Thunk(fs.readFile);
    readFileThunk(fileA)(callback);
    + +

    下面是另一个完整的例子。

    +
    1
    2
    3
    4
    5
    6
    function f(a, cb) {
    cb(a);
    }
    const ft = Thunk(f);

    ft(1)(console.log); // 1
    + +

    Thunkify 模块

    生产环境的转换器,建议使用 Thunkify 模块。

    +

    首先是安装。

    +
    1
    $ npm install thunkify
    + +

    使用方式如下。

    +
    1
    2
    3
    4
    5
    6
    7
    var thunkify = require("thunkify");
    var fs = require("fs");

    var read = thunkify(fs.readFile);
    read("package.json")(function (err, str) {
    // ...
    });
    + +

    Thunkify 的源码与上一节那个简单的转换器非常像。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    function thunkify(fn) {
    return function () {
    var args = new Array(arguments.length);
    var ctx = this;

    for (var i = 0; i < args.length; ++i) {
    args[i] = arguments[i];
    }

    return function (done) {
    var called;

    args.push(function () {
    if (called) return;
    called = true;
    done.apply(null, arguments);
    });

    try {
    fn.apply(ctx, args);
    } catch (err) {
    done(err);
    }
    };
    };
    }
    + +

    它的源码主要多了一个检查机制,变量called确保回调函数只运行一次。这样的设计与下文的 Generator 函数相关。请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function f(a, b, callback) {
    var sum = a + b;
    callback(sum);
    callback(sum);
    }

    var ft = thunkify(f);
    var print = console.log.bind(console);
    ft(1, 2)(print);
    // 3
    + +

    上面代码中,由于thunkify只允许回调函数执行一次,所以只输出一行结果。

    +

    Generator 函数的流程管理

    你可能会问, Thunk 函数有什么用?回答是以前确实没什么用,但是 ES6 有了 Generator 函数,Thunk 函数现在可以用于 Generator 函数的自动流程管理。

    +

    Generator 函数可以自动执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* gen() {
    // ...
    }

    var g = gen();
    var res = g.next();

    while (!res.done) {
    console.log(res.value);
    res = g.next();
    }
    + +

    上面代码中,Generator 函数gen会自动执行完所有步骤。

    +

    但是,这不适合异步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。这时,Thunk 函数就能派上用处。以读取文件为例。下面的 Generator 函数封装了两个异步操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var fs = require("fs");
    var thunkify = require("thunkify");
    var readFileThunk = thunkify(fs.readFile);

    var gen = function* () {
    var r1 = yield readFileThunk("/etc/fstab");
    console.log(r1.toString());
    var r2 = yield readFileThunk("/etc/shells");
    console.log(r2.toString());
    };
    + +

    上面代码中,yield命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。

    +

    这种方法就是 Thunk 函数,因为它可以在回调函数里,将执行权交还给 Generator 函数。为了便于理解,我们先看如何手动执行上面这个 Generator 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var g = gen();

    var r1 = g.next();
    r1.value(function (err, data) {
    if (err) throw err;
    var r2 = g.next(data);
    r2.value(function (err, data) {
    if (err) throw err;
    g.next(data);
    });
    });
    + +

    上面代码中,变量g是 Generator 函数的内部指针,表示目前执行到哪一步。next方法负责将指针移动到下一步,并返回该步的信息(value属性和done属性)。

    +

    仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程。

    +

    Thunk 函数的自动流程管理

    Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function run(fn) {
    var gen = fn();

    function next(err, data) {
    var result = gen.next(data);
    if (result.done) return;
    result.value(next);
    }

    next();
    }

    function* g() {
    // ...
    }

    run(g);
    + +

    上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。

    +

    有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var g = function* () {
    var f1 = yield readFileThunk("fileA");
    var f2 = yield readFileThunk("fileB");
    // ...
    var fn = yield readFileThunk("fileN");
    };

    run(g);
    + +

    上面代码中,函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。

    +

    Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。

    +

    co 模块

    基本用法

    co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。

    +

    下面是一个 Generator 函数,用于依次读取两个文件。

    +
    1
    2
    3
    4
    5
    6
    var gen = function* () {
    var f1 = yield readFile("/etc/fstab");
    var f2 = yield readFile("/etc/shells");
    console.log(f1.toString());
    console.log(f2.toString());
    };
    + +

    co 模块可以让你不用编写 Generator 函数的执行器。

    +
    1
    2
    var co = require("co");
    co(gen);
    + +

    上面代码中,Generator 函数只要传入co函数,就会自动执行。

    +

    co函数返回一个Promise对象,因此可以用then方法添加回调函数。

    +
    1
    2
    3
    co(gen).then(function () {
    console.log("Generator 函数执行完成");
    });
    + +

    上面代码中,等到 Generator 函数执行结束,就会输出一行提示。

    +

    co 模块的原理

    为什么 co 可以自动执行 Generator 函数?

    +

    前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。

    +

    两种方法可以做到这一点。

    +

    (1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。

    +

    (2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。

    +

    co 模块其实就是将两种自动执行器(Thunk 函数和 Promise 对象),包装成一个模块。使用 co 的前提条件是,Generator 函数的yield命令后面,只能是 Thunk 函数或 Promise 对象。如果数组或对象的成员,全部都是 Promise 对象,也可以使用 co,详见后文的例子。

    +

    上一节已经介绍了基于 Thunk 函数的自动执行器。下面来看,基于 Promise 对象的自动执行器。这是理解 co 模块必须的。

    +

    基于 Promise 对象的自动执行

    还是沿用上面的例子。首先,把fs模块的readFile方法包装成一个 Promise 对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var fs = require("fs");

    var readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
    if (error) return reject(error);
    resolve(data);
    });
    });
    };

    var gen = function* () {
    var f1 = yield readFile("/etc/fstab");
    var f2 = yield readFile("/etc/shells");
    console.log(f1.toString());
    console.log(f2.toString());
    };
    + +

    然后,手动执行上面的 Generator 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    var g = gen();

    g.next().value.then(function (data) {
    g.next(data).value.then(function (data) {
    g.next(data);
    });
    });
    + +

    手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function run(gen) {
    var g = gen();

    function next(data) {
    var result = g.next(data);
    if (result.done) return result.value;
    result.value.then(function (data) {
    next(data);
    });
    }

    next();
    }

    run(gen);
    + +

    上面代码中,只要 Generator 函数还没执行到最后一步,next函数就调用自身,以此实现自动执行。

    +

    co 模块的源码

    co 就是上面那个自动执行器的扩展,它的源码只有几十行,非常简单。

    +

    首先,co 函数接受 Generator 函数作为参数,返回一个 Promise 对象。

    +
    1
    2
    3
    4
    5
    function co(gen) {
    var ctx = this;

    return new Promise(function (resolve, reject) {});
    }
    + +

    在返回的 Promise 对象里面,co 先检查参数gen是否为 Generator 函数。如果是,就执行该函数,得到一个内部指针对象;如果不是就返回,并将 Promise 对象的状态改为resolved

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function co(gen) {
    var ctx = this;

    return new Promise(function (resolve, reject) {
    if (typeof gen === "function") gen = gen.call(ctx);
    if (!gen || typeof gen.next !== "function") return resolve(gen);
    });
    }
    + +

    接着,co 将 Generator 函数的内部指针对象的next方法,包装成onFulfilled函数。这主要是为了能够捕捉抛出的错误。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function co(gen) {
    var ctx = this;

    return new Promise(function (resolve, reject) {
    if (typeof gen === "function") gen = gen.call(ctx);
    if (!gen || typeof gen.next !== "function") return resolve(gen);

    onFulfilled();
    function onFulfilled(res) {
    var ret;
    try {
    ret = gen.next(res);
    } catch (e) {
    return reject(e);
    }
    next(ret);
    }
    });
    }
    + +

    最后,就是关键的next函数,它会反复调用自身。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function next(ret) {
    if (ret.done) return resolve(ret.value);
    var value = toPromise.call(ctx, ret.value);
    if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
    return onRejected(
    new TypeError(
    "You may only yield a function, promise, generator, array, or object, " +
    'but the following object was passed: "' +
    String(ret.value) +
    '"'
    )
    );
    }
    + +

    上面代码中,next函数的内部代码,一共只有四行命令。

    +

    第一行,检查当前是否为 Generator 函数的最后一步,如果是就返回。

    +

    第二行,确保每一步的返回值,是 Promise 对象。

    +

    第三行,使用then方法,为返回值加上回调函数,然后通过onFulfilled函数再次调用next函数。

    +

    第四行,在参数不符合要求的情况下(参数非 Thunk 函数和 Promise 对象),将 Promise 对象的状态改为rejected,从而终止执行。

    +

    处理并发的异步操作

    co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。

    +

    这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 数组的写法
    co(function* () {
    var res = yield [Promise.resolve(1), Promise.resolve(2)];
    console.log(res);
    }).catch(onerror);

    // 对象的写法
    co(function* () {
    var res = yield {
    1: Promise.resolve(1),
    2: Promise.resolve(2),
    };
    console.log(res);
    }).catch(onerror);
    + +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    co(function* () {
    var values = [n1, n2, n3];
    yield values.map(somethingAsync);
    });

    function* somethingAsync(x) {
    // do something async
    return y;
    }
    + +

    上面的代码允许并发三个somethingAsync异步操作,等到它们全部完成,才会进行下一步。

    +

    实例:处理 Stream

    Node 提供 Stream 模式读写数据,特点是一次只处理数据的一部分,数据分成一块块依次处理,就好像“数据流”一样。这对于处理大规模数据非常有利。Stream 模式使用 EventEmitter API,会释放三个事件。

    +
      +
    • data事件:下一块数据块已经准备好了。
    • +
    • end事件:整个“数据流”处理“完了。
    • +
    • error事件:发生错误。
    • +
    +

    使用Promise.race()函数,可以判断这三个事件之中哪一个最先发生,只有当data事件最先发生时,才进入下一个数据块的处理。从而,我们可以通过一个while循环,完成所有数据的读取。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    const co = require("co");
    const fs = require("fs");

    const stream = fs.createReadStream("./les_miserables.txt");
    let valjeanCount = 0;

    co(function* () {
    while (true) {
    const res = yield Promise.race([
    new Promise((resolve) => stream.once("data", resolve)),
    new Promise((resolve) => stream.once("end", resolve)),
    new Promise((resolve, reject) => stream.once("error", reject)),
    ]);
    if (!res) {
    break;
    }
    stream.removeAllListeners("data");
    stream.removeAllListeners("end");
    stream.removeAllListeners("error");
    valjeanCount += (res.toString().match(/valjean/gi) || []).length;
    }
    console.log("count:", valjeanCount); // count: 1120
    });
    + +

    上面代码采用 Stream 模式读取《悲惨世界》的文本文件,对于每个数据块都使用stream.once方法,在dataenderror三个事件上添加一次性回调函数。变量res只有在data事件发生时才有值,然后累加每个数据块之中valjean这个词出现的次数。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Generator%20%E5%87%BD%E6%95%B0%E7%9A%84%E5%BC%82%E6%AD%A5%E5%BA%94%E7%94%A8/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Generator \345\207\275\346\225\260\347\232\204\350\257\255\346\263\225/index.html" "b/posts/ES6-Generator \345\207\275\346\225\260\347\232\204\350\257\255\346\263\225/index.html" new file mode 100644 index 00000000..df47640f --- /dev/null +++ "b/posts/ES6-Generator \345\207\275\346\225\260\347\232\204\350\257\255\346\263\225/index.html" @@ -0,0 +1,520 @@ +Generator 函数的语法 | JCAlways + + + + + + + + + + + + + +

    Generator 函数的语法

    Generator 函数的语法

    简介

    基本概念

    Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。本章详细介绍 Generator 函数的语法和 API,它的异步编程应用请看《Generator 函数的异步应用》一章。

    +

    Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。

    +

    执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

    +

    形式上,Generator 函数是一个普通函数,但是有两个特征。一是,function关键字与函数名之间有一个星号;二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

    +
    1
    2
    3
    4
    5
    6
    7
    function* helloWorldGenerator() {
    yield "hello";
    yield "world";
    return "ending";
    }

    var hw = helloWorldGenerator();
    + +

    上面代码定义了一个 Generator 函数helloWorldGenerator,它内部有两个yield表达式(helloworld),即该函数有三个状态:hello,world 和 return 语句(结束执行)。

    +

    然后,Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。

    +

    下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    hw.next();
    // { value: 'hello', done: false }

    hw.next();
    // { value: 'world', done: false }

    hw.next();
    // { value: 'ending', done: true }

    hw.next();
    // { value: undefined, done: true }
    + +

    上面代码一共调用了四次next方法。

    +

    第一次调用,Generator 函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,它的value属性就是当前yield表达式的值hellodone属性的值false,表示遍历还没有结束。

    +

    第二次调用,Generator 函数从上次yield表达式停下的地方,一直执行到下一个yield表达式。next方法返回的对象的value属性就是当前yield表达式的值worlddone属性的值false,表示遍历还没有结束。

    +

    第三次调用,Generator 函数从上次yield表达式停下的地方,一直执行到return语句(如果没有return语句,就执行到函数结束)。next方法返回的对象的value属性,就是紧跟在return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束。

    +

    第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。

    +

    总结一下,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

    +

    ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。

    +
    1
    2
    3
    4
    function * foo(x, y) { ··· }
    function *foo(x, y) { ··· }
    function* foo(x, y) { ··· }
    function*foo(x, y) { ··· }
    + +

    由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function关键字后面。本书也采用这种写法。

    +

    yield 表达式

    由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

    +

    遍历器对象的next方法的运行逻辑如下。

    +

    (1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。

    +

    (2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。

    +

    (3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。

    +

    (4)如果该函数没有return语句,则返回的对象的value属性值为undefined

    +

    需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

    +
    1
    2
    3
    function* gen() {
    yield 123 + 456;
    }
    + +

    上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值。

    +

    yield表达式与return语句既有相似之处,也有区别。相似之处在于,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return语句,但是可以执行多次(或者说多个)yield表达式。正常函数只能返回一个值,因为只能执行一次return;Generator 函数可以返回一系列的值,因为可以有任意多个yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(英语中,generator 这个词是“生成器”的意思)。

    +

    Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function* f() {
    console.log("执行了!");
    }

    var generator = f();

    setTimeout(function () {
    generator.next();
    }, 2000);
    + +

    上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。

    +

    另外需要注意,yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。

    +
    1
    2
    3
    4
    (function (){
    yield 1;
    })()
    // SyntaxError: Unexpected number
    + +

    上面代码在一个普通函数中使用yield表达式,结果产生一个句法错误。

    +

    下面是另外一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var arr = [1, [[2, 3], 4], [5, 6]];

    var flat = function* (a) {
    a.forEach(function (item) {
    if (typeof item !== 'number') {
    yield* flat(item);
    } else {
    yield item;
    }
    });
    };

    for (var f of flat(arr)){
    console.log(f);
    }
    + +

    上面代码也会产生句法错误,因为forEach方法的参数是一个普通函数,但是在里面使用了yield表达式(这个函数里面还使用了yield*表达式,详细介绍见后文)。一种修改方法是改用for循环。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var arr = [1, [[2, 3], 4], [5, 6]];

    var flat = function* (a) {
    var length = a.length;
    for (var i = 0; i < length; i++) {
    var item = a[i];
    if (typeof item !== "number") {
    yield* flat(item);
    } else {
    yield item;
    }
    }
    };

    for (var f of flat(arr)) {
    console.log(f);
    }
    // 1, 2, 3, 4, 5, 6
    + +

    另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。

    +
    1
    2
    3
    4
    5
    6
    7
    function* demo() {
    console.log('Hello' + yield); // SyntaxError
    console.log('Hello' + yield 123); // SyntaxError

    console.log('Hello' + (yield)); // OK
    console.log('Hello' + (yield 123)); // OK
    }
    + +

    yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。

    +
    1
    2
    3
    4
    function* demo() {
    foo(yield "a", yield "b"); // OK
    let input = yield; // OK
    }
    + +

    与 Iterator 接口的关系

    上一章说过,任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。

    +

    由于 Generator 函数就是遍历器生成函数,因此可以把 Generator 赋值给对象的Symbol.iterator属性,从而使得该对象具有 Iterator 接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var myIterable = {};
    myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
    };

    [...myIterable]; // [1, 2, 3]
    + +

    上面代码中,Generator 函数赋值给Symbol.iterator属性,从而使得myIterable对象具有了 Iterator 接口,可以被...运算符遍历了。

    +

    Generator 函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function* gen() {
    // some code
    }

    var g = gen();

    g[Symbol.iterator]() === g;
    // true
    + +

    上面代码中,gen是一个 Generator 函数,调用它会生成一个遍历器对象g。它的Symbol.iterator属性,也是一个遍历器对象生成函数,执行后返回它自己。

    +

    next 方法的参数

    yield表达式本身没有返回值,或者说总是返回undefinednext方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* f() {
    for (var i = 0; true; i++) {
    var reset = yield i;
    if (reset) {
    i = -1;
    }
    }
    }

    var g = f();

    g.next(); // { value: 0, done: false }
    g.next(); // { value: 1, done: false }
    g.next(true); // { value: 0, done: false }
    + +

    上面代码先定义了一个可以无限运行的 Generator 函数f,如果next方法没有参数,每次运行到yield表达式,变量reset的值总是undefined。当next方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

    +

    这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。

    +

    再看一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* foo(x) {
    var y = 2 * (yield x + 1);
    var z = yield y / 3;
    return x + y + z;
    }

    var a = foo(5);
    a.next(); // Object{value:6, done:false}
    a.next(); // Object{value:NaN, done:false}
    a.next(); // Object{value:NaN, done:true}

    var b = foo(5);
    b.next(); // { value:6, done:false }
    b.next(12); // { value:8, done:false }
    b.next(13); // { value:42, done:true }
    + +

    上面代码中,第二次运行next方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以 3 以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN

    +

    如果向next方法提供参数,返回结果就完全不一样了。上面代码第一次调用bnext方法时,返回x+1的值6;第二次调用next方法,将上一次yield表达式的值设为12,因此y等于24,返回y / 3的值8;第三次调用next方法,将上一次yield表达式的值设为13,因此z等于13,这时x等于5y等于24,所以return语句的值等于42

    +

    注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。

    +

    再看一个通过next方法的参数,向 Generator 函数内部输入值的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* dataConsumer() {
    console.log("Started");
    console.log(`1. ${yield}`);
    console.log(`2. ${yield}`);
    return "result";
    }

    let genObj = dataConsumer();
    genObj.next();
    // Started
    genObj.next("a");
    // 1. a
    genObj.next("b");
    // 2. b
    + +

    上面代码是一个很直观的例子,每次通过next方法向 Generator 函数输入值,然后打印出来。

    +

    如果想要第一次调用next方法时,就能够输入值,可以在 Generator 函数外面再包一层。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function wrapper(generatorFunction) {
    return function (...args) {
    let generatorObject = generatorFunction(...args);
    generatorObject.next();
    return generatorObject;
    };
    }

    const wrapped = wrapper(function* () {
    console.log(`First input: ${yield}`);
    return "DONE";
    });

    wrapped().next("hello!");
    // First input: hello!
    + +

    上面代码中,Generator 函数如果不用wrapper先包一层,是无法第一次调用next方法,就输入参数的。

    +

    for…of 循环

    for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function* foo() {
    yield 1;
    yield 2;
    yield 3;
    yield 4;
    yield 5;
    return 6;
    }

    for (let v of foo()) {
    console.log(v);
    }
    // 1 2 3 4 5
    + +

    上面代码使用for...of循环,依次显示 5 个yield表达式的值。这里需要注意,一旦next方法的返回对象的done属性为truefor...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中。

    +

    下面是一个利用 Generator 函数和for...of循环,实现斐波那契数列的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function* fibonacci() {
    let [prev, curr] = [0, 1];
    for (;;) {
    yield curr;
    [prev, curr] = [curr, prev + curr];
    }
    }

    for (let n of fibonacci()) {
    if (n > 1000) break;
    console.log(n);
    }
    + +

    从上面代码可见,使用for...of语句时不需要使用next方法。

    +

    利用for...of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for...of循环,通过 Generator 函数为它加上这个接口,就可以用了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* objectEntries(obj) {
    let propKeys = Reflect.ownKeys(obj);

    for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
    }
    }

    let jane = { first: "Jane", last: "Doe" };

    for (let [key, value] of objectEntries(jane)) {
    console.log(`${key}: ${value}`);
    }
    // first: Jane
    // last: Doe
    + +

    上面代码中,对象jane原生不具备 Iterator 接口,无法用for...of遍历。这时,我们通过 Generator 函数objectEntries为它加上遍历器接口,就可以用for...of遍历了。加上遍历器接口的另一种写法是,将 Generator 函数加到对象的Symbol.iterator属性上面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function* objectEntries() {
    let propKeys = Object.keys(this);

    for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
    }
    }

    let jane = { first: "Jane", last: "Doe" };

    jane[Symbol.iterator] = objectEntries;

    for (let [key, value] of jane) {
    console.log(`${key}: ${value}`);
    }
    // first: Jane
    // last: Doe
    + +

    除了for...of循环以外,扩展运算符(...)、解构赋值和Array.from方法内部调用的,都是遍历器接口。这意味着,它们都可以将 Generator 函数返回的 Iterator 对象,作为参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function* numbers() {
    yield 1;
    yield 2;
    return 3;
    yield 4;
    }

    // 扩展运算符
    [...numbers()]; // [1, 2]

    // Array.from 方法
    Array.from(numbers()); // [1, 2]

    // 解构赋值
    let [x, y] = numbers();
    x; // 1
    y; // 2

    // for...of 循环
    for (let n of numbers()) {
    console.log(n);
    }
    // 1
    // 2
    + +

    Generator.prototype.throw()

    Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    var g = function* () {
    try {
    yield;
    } catch (e) {
    console.log("内部捕获", e);
    }
    };

    var i = g();
    i.next();

    try {
    i.throw("a");
    i.throw("b");
    } catch (e) {
    console.log("外部捕获", e);
    }
    // 内部捕获 a
    // 外部捕获 b
    + +

    上面代码中,遍历器对象i连续抛出两个错误。第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。

    +

    throw方法可以接受一个参数,该参数会被catch语句接收,建议抛出Error对象的实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var g = function* () {
    try {
    yield;
    } catch (e) {
    console.log(e);
    }
    };

    var i = g();
    i.next();
    i.throw(new Error("出错了!"));
    // Error: 出错了!(…)
    + +

    注意,不要混淆遍历器对象的throw方法和全局的throw命令。上面代码的错误,是用遍历器对象的throw方法抛出的,而不是用throw命令抛出的。后者只能被函数体外的catch语句捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    var g = function* () {
    while (true) {
    try {
    yield;
    } catch (e) {
    if (e != "a") throw e;
    console.log("内部捕获", e);
    }
    }
    };

    var i = g();
    i.next();

    try {
    throw new Error("a");
    throw new Error("b");
    } catch (e) {
    console.log("外部捕获", e);
    }
    // 外部捕获 [Error: a]
    + +

    上面代码之所以只捕获了a,是因为函数体外的catch语句块,捕获了抛出的a错误以后,就不会再继续try代码块里面剩余的语句了。

    +

    如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var g = function* () {
    while (true) {
    yield;
    console.log("内部捕获", e);
    }
    };

    var i = g();
    i.next();

    try {
    i.throw("a");
    i.throw("b");
    } catch (e) {
    console.log("外部捕获", e);
    }
    // 外部捕获 a
    + +

    上面代码中,Generator 函数g内部没有部署try...catch代码块,所以抛出的错误直接被外部catch代码块捕获。

    +

    如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var gen = function* gen() {
    yield console.log("hello");
    yield console.log("world");
    };

    var g = gen();
    g.next();
    g.throw();
    // hello
    // Uncaught undefined
    + +

    上面代码中,g.throw抛出错误以后,没有任何try...catch代码块可以捕获这个错误,导致程序报错,中断执行。

    +

    throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* gen() {
    try {
    yield 1;
    } catch (e) {
    console.log("内部捕获");
    }
    }

    var g = gen();
    g.throw(1);
    // Uncaught 1
    + +

    上面代码中,g.throw(1)执行时,next方法一次都没有执行过。这时,抛出的错误不会被内部捕获,而是直接在外部抛出,导致程序出错。这种行为其实很好理解,因为第一次执行next方法,等同于启动执行 Generator 函数的内部代码,否则 Generator 函数还没有开始执行,这时throw方法抛错只可能抛出在函数外部。

    +

    throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var gen = function* gen() {
    try {
    yield console.log("a");
    } catch (e) {
    // ...
    }
    yield console.log("b");
    yield console.log("c");
    };

    var g = gen();
    g.next(); // a
    g.throw(); // b
    g.next(); // c
    + +

    上面代码中,g.throw方法被捕获以后,自动执行了一次next方法,所以会打印b。另外,也可以看到,只要 Generator 函数内部部署了try...catch代码块,那么遍历器的throw方法抛出的错误,不影响下一次遍历。

    +

    另外,throw命令与g.throw方法是无关的,两者互不影响。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var gen = function* gen() {
    yield console.log("hello");
    yield console.log("world");
    };

    var g = gen();
    g.next();

    try {
    throw new Error();
    } catch (e) {
    g.next();
    }
    // hello
    // world
    + +

    上面代码中,throw命令抛出的错误不会影响到遍历器的状态,所以两次执行next方法,都进行了正确的操作。

    +

    这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield表达式,可以只用一个try...catch代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在 Generator 函数内部写一次catch语句就可以了。

    +

    Generator 函数体外抛出的错误,可以在函数体内捕获;反过来,Generator 函数体内抛出的错误,也可以被函数体外的catch捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* foo() {
    var x = yield 3;
    var y = x.toUpperCase();
    yield y;
    }

    var it = foo();

    it.next(); // { value:3, done:false }

    try {
    it.next(42);
    } catch (err) {
    console.log(err);
    }
    + +

    上面代码中,第二个next方法向函数体内传入一个参数 42,数值是没有toUpperCase方法的,所以会抛出一个 TypeError 错误,被函数体外的catch捕获。

    +

    一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next方法,将返回一个value属性等于undefineddone属性等于true的对象,即 JavaScript 引擎认为这个 Generator 已经运行结束了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    function* g() {
    yield 1;
    console.log("throwing an exception");
    throw new Error("generator broke!");
    yield 2;
    yield 3;
    }

    function log(generator) {
    var v;
    console.log("starting generator");
    try {
    v = generator.next();
    console.log("第一次运行next方法", v);
    } catch (err) {
    console.log("捕捉错误", v);
    }
    try {
    v = generator.next();
    console.log("第二次运行next方法", v);
    } catch (err) {
    console.log("捕捉错误", v);
    }
    try {
    v = generator.next();
    console.log("第三次运行next方法", v);
    } catch (err) {
    console.log("捕捉错误", v);
    }
    console.log("caller done");
    }

    log(g());
    // starting generator
    // 第一次运行next方法 { value: 1, done: false }
    // throwing an exception
    // 捕捉错误 { value: 1, done: false }
    // 第三次运行next方法 { value: undefined, done: true }
    // caller done
    + +

    上面代码一共三次运行next方法,第二次运行的时候会抛出错误,然后第三次运行的时候,Generator 函数就已经结束了,不再执行下去了。

    +

    Generator.prototype.return()

    Generator 函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历 Generator 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* gen() {
    yield 1;
    yield 2;
    yield 3;
    }

    var g = gen();

    g.next(); // { value: 1, done: false }
    g.return("foo"); // { value: "foo", done: true }
    g.next(); // { value: undefined, done: true }
    + +

    上面代码中,遍历器对象g调用return方法后,返回值的value属性就是return方法的参数foo。并且,Generator 函数的遍历就终止了,返回值的done属性为true,以后再调用next方法,done属性总是返回true

    +

    如果return方法调用时,不提供参数,则返回值的value属性为undefined

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* gen() {
    yield 1;
    yield 2;
    yield 3;
    }

    var g = gen();

    g.next(); // { value: 1, done: false }
    g.return(); // { value: undefined, done: true }
    + +

    如果 Generator 函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function* numbers() {
    yield 1;
    try {
    yield 2;
    yield 3;
    } finally {
    yield 4;
    yield 5;
    }
    yield 6;
    }
    var g = numbers();
    g.next(); // { value: 1, done: false }
    g.next(); // { value: 2, done: false }
    g.return(7); // { value: 4, done: false }
    g.next(); // { value: 5, done: false }
    g.next(); // { value: 7, done: true }
    + +

    上面代码中,调用return方法后,就开始执行finally代码块,然后等到finally代码块执行完,再执行return方法。

    +

    next()、throw()、return() 的共同点

    next()throw()return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

    +

    next()是将yield表达式替换成一个值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const g = function* (x, y) {
    let result = yield x + y;
    return result;
    };

    const gen = g(1, 2);
    gen.next(); // Object {value: 3, done: false}

    gen.next(1); // Object {value: 1, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = 1;
    + +

    上面代码中,第二个next(1)方法就相当于将yield表达式替换成一个值1。如果next方法没有参数,就相当于替换成undefined

    +

    throw()是将yield表达式替换成一个throw语句。

    +
    1
    2
    3
    gen.throw(new Error("出错了")); // Uncaught Error: 出错了
    // 相当于将 let result = yield x + y
    // 替换成 let result = throw(new Error('出错了'));
    + +

    return()是将yield表达式替换成一个return语句。

    +
    1
    2
    3
    gen.return(2); // Object {value: 2, done: true}
    // 相当于将 let result = yield x + y
    // 替换成 let result = return 2;
    + +

    yield* 表达式

    如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function* foo() {
    yield "a";
    yield "b";
    }

    function* bar() {
    yield "x";
    foo();
    yield "y";
    }

    for (let v of bar()) {
    console.log(v);
    }
    // "x"
    // "y"
    + +

    上面代码中,foobar都是 Generator 函数,在bar里面调用foo,是不会有效果的。

    +

    这个就需要用到yield*表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    function* bar() {
    yield "x";
    yield* foo();
    yield "y";
    }

    // 等同于
    function* bar() {
    yield "x";
    yield "a";
    yield "b";
    yield "y";
    }

    // 等同于
    function* bar() {
    yield "x";
    for (let v of foo()) {
    yield v;
    }
    yield "y";
    }

    for (let v of bar()) {
    console.log(v);
    }
    // "x"
    // "a"
    // "b"
    // "y"
    + +

    再来看一个对比的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    function* inner() {
    yield "hello!";
    }

    function* outer1() {
    yield "open";
    yield inner();
    yield "close";
    }

    var gen = outer1();
    gen.next().value; // "open"
    gen.next().value; // 返回一个遍历器对象
    gen.next().value; // "close"

    function* outer2() {
    yield "open";
    yield* inner();
    yield "close";
    }

    var gen = outer2();
    gen.next().value; // "open"
    gen.next().value; // "hello!"
    gen.next().value; // "close"
    + +

    上面例子中,outer2使用了yield*outer1没使用。结果就是,outer1返回一个遍历器对象,outer2返回该遍历器对象的内部值。

    +

    从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let delegatedIterator = (function* () {
    yield "Hello!";
    yield "Bye!";
    })();

    let delegatingIterator = (function* () {
    yield "Greetings!";
    yield* delegatedIterator;
    yield "Ok, bye.";
    })();

    for (let value of delegatingIterator) {
    console.log(value);
    }
    // "Greetings!
    // "Hello!"
    // "Bye!"
    // "Ok, bye."
    + +

    上面代码中,delegatingIterator是代理者,delegatedIterator是被代理者。由于yield* delegatedIterator语句得到的值,是一个遍历器,所以要用星号表示。运行结果就是使用一个遍历器,遍历了多个 Generator 函数,有递归的效果。

    +

    yield*后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* concat(iter1, iter2) {
    yield* iter1;
    yield* iter2;
    }

    // 等同于

    function* concat(iter1, iter2) {
    for (var value of iter1) {
    yield value;
    }
    for (var value of iter2) {
    yield value;
    }
    }
    + +

    上面代码说明,yield*后面的 Generator 函数(没有return语句时),不过是for...of的一种简写形式,完全可以用后者替代前者。反之,在有return语句时,则需要用var value = yield* iterator的形式获取return语句的值。

    +

    如果yield*后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。

    +
    1
    2
    3
    4
    5
    function* gen() {
    yield* ["a", "b", "c"];
    }

    gen().next(); // { value:"a", done:false }
    + +

    上面代码中,yield命令后面如果不加星号,返回的是整个数组,加了星号就表示返回的是数组的遍历器对象。

    +

    实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

    +
    1
    2
    3
    4
    5
    6
    7
    let read = (function* () {
    yield "hello";
    yield* "hello";
    })();

    read.next().value; // "hello"
    read.next().value; // "h"
    + +

    上面代码中,yield表达式返回整个字符串,yield*语句返回单个字符。因为字符串具有 Iterator 接口,所以被yield*遍历。

    +

    如果被代理的 Generator 函数有return语句,那么就可以向代理它的 Generator 函数返回数据。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    function* foo() {
    yield 2;
    yield 3;
    return "foo";
    }

    function* bar() {
    yield 1;
    var v = yield* foo();
    console.log("v: " + v);
    yield 4;
    }

    var it = bar();

    it.next();
    // {value: 1, done: false}
    it.next();
    // {value: 2, done: false}
    it.next();
    // {value: 3, done: false}
    it.next();
    // "v: foo"
    // {value: 4, done: false}
    it.next();
    // {value: undefined, done: true}
    + +

    上面代码在第四次调用next方法的时候,屏幕上会有输出,这是因为函数fooreturn语句,向函数bar提供了返回值。

    +

    再看一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function* genFuncWithReturn() {
    yield "a";
    yield "b";
    return "The result";
    }
    function* logReturned(genObj) {
    let result = yield* genObj;
    console.log(result);
    }

    [...logReturned(genFuncWithReturn())];
    // The result
    // 值为 [ 'a', 'b' ]
    + +

    上面代码中,存在两次遍历。第一次是扩展运算符遍历函数logReturned返回的遍历器对象,第二次是yield*语句遍历函数genFuncWithReturn返回的遍历器对象。这两次遍历的效果是叠加的,最终表现为扩展运算符遍历函数genFuncWithReturn返回的遍历器对象。所以,最后的数据表达式得到的值等于[ 'a', 'b' ]。但是,函数genFuncWithReturnreturn语句的返回值The result,会返回给函数logReturned内部的result变量,因此会有终端输出。

    +

    yield*命令可以很方便地取出嵌套数组的所有成员。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function* iterTree(tree) {
    if (Array.isArray(tree)) {
    for (let i = 0; i < tree.length; i++) {
    yield* iterTree(tree[i]);
    }
    } else {
    yield tree;
    }
    }

    const tree = ["a", ["b", "c"], ["d", "e"]];

    for (let x of iterTree(tree)) {
    console.log(x);
    }
    // a
    // b
    // c
    // d
    // e
    + +

    下面是一个稍微复杂的例子,使用yield*语句遍历完全二叉树。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    // 下面是二叉树的构造函数,
    // 三个参数分别是左树、当前节点和右树
    function Tree(left, label, right) {
    this.left = left;
    this.label = label;
    this.right = right;
    }

    // 下面是中序(inorder)遍历函数。
    // 由于返回的是一个遍历器,所以要用generator函数。
    // 函数体内采用递归算法,所以左树和右树要用yield*遍历
    function* inorder(t) {
    if (t) {
    yield* inorder(t.left);
    yield t.label;
    yield* inorder(t.right);
    }
    }

    // 下面生成二叉树
    function make(array) {
    // 判断是否为叶节点
    if (array.length == 1) return new Tree(null, array[0], null);
    return new Tree(make(array[0]), array[1], make(array[2]));
    }
    let tree = make([[["a"], "b", ["c"]], "d", [["e"], "f", ["g"]]]);

    // 遍历二叉树
    var result = [];
    for (let node of inorder(tree)) {
    result.push(node);
    }

    result;
    // ['a', 'b', 'c', 'd', 'e', 'f', 'g']
    + +

    作为对象属性的 Generator 函数

    如果一个对象的属性是 Generator 函数,可以简写成下面的形式。

    +
    1
    2
    3
    4
    5
    let obj = {
    * myGeneratorMethod() {
    ···
    }
    };
    + +

    上面代码中,myGeneratorMethod属性前面有一个星号,表示这个属性是一个 Generator 函数。

    +

    它的完整形式如下,与上面的写法是等价的。

    +
    1
    2
    3
    4
    5
    let obj = {
    myGeneratorMethod: function* () {
    // ···
    },
    };
    + +

    Generator 函数的this

    Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* g() {}

    g.prototype.hello = function () {
    return "hi!";
    };

    let obj = g();

    obj instanceof g; // true
    obj.hello(); // 'hi!'
    + +

    上面代码表明,Generator 函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。

    +
    1
    2
    3
    4
    5
    6
    7
    function* g() {
    this.a = 11;
    }

    let obj = g();
    obj.next();
    obj.a; // undefined
    + +

    上面代码中,Generator 函数gthis对象上面添加了一个属性a,但是obj对象拿不到这个属性。

    +

    Generator 函数也不能跟new命令一起用,会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    function* F() {
    yield (this.x = 2);
    yield (this.y = 3);
    }

    new F();
    // TypeError: F is not a constructor
    + +

    上面代码中,new命令跟构造函数F一起使用,结果报错,因为F不是构造函数。

    +

    那么,有没有办法让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this

    +

    下面是一个变通方法。首先,生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function* F() {
    this.a = 1;
    yield (this.b = 2);
    yield (this.c = 3);
    }
    var obj = {};
    var f = F.call(obj);

    f.next(); // Object {value: 2, done: false}
    f.next(); // Object {value: 3, done: false}
    f.next(); // Object {value: undefined, done: true}

    obj.a; // 1
    obj.b; // 2
    obj.c; // 3
    + +

    上面代码中,首先是F内部的this对象绑定obj对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次next方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。

    +

    上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,有没有办法将这两个对象统一呢?

    +

    一个办法就是将obj换成F.prototype

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* F() {
    this.a = 1;
    yield (this.b = 2);
    yield (this.c = 3);
    }
    var f = F.call(F.prototype);

    f.next(); // Object {value: 2, done: false}
    f.next(); // Object {value: 3, done: false}
    f.next(); // Object {value: undefined, done: true}

    f.a; // 1
    f.b; // 2
    f.c; // 3
    + +

    再将F改成构造函数,就可以对它执行new命令了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    function* gen() {
    this.a = 1;
    yield (this.b = 2);
    yield (this.c = 3);
    }

    function F() {
    return gen.call(gen.prototype);
    }

    var f = new F();

    f.next(); // Object {value: 2, done: false}
    f.next(); // Object {value: 3, done: false}
    f.next(); // Object {value: undefined, done: true}

    f.a; // 1
    f.b; // 2
    f.c; // 3
    + +

    含义

    Generator 与状态机

    Generator 是实现状态机的最佳结构。比如,下面的clock函数就是一个状态机。

    +
    1
    2
    3
    4
    5
    6
    var ticking = true;
    var clock = function () {
    if (ticking) console.log("Tick!");
    else console.log("Tock!");
    ticking = !ticking;
    };
    + +

    上面代码的clock函数一共有两种状态(TickTock),每运行一次,就改变一次状态。这个函数如果用 Generator 实现,就是下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var clock = function* () {
    while (true) {
    console.log("Tick!");
    yield;
    console.log("Tock!");
    yield;
    }
    };
    + +

    上面的 Generator 实现与 ES5 实现对比,可以看到少了用来保存状态的外部变量ticking,这样就更简洁,更安全(状态不会被非法篡改)、更符合函数式编程的思想,在写法上也更优雅。Generator 之所以可以不用外部变量保存状态,是因为它本身就包含了一个状态信息,即目前是否处于暂停态。

    +

    Generator 与协程

    协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

    +

    (1)协程与子例程的差异

    +

    传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

    +

    从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。

    +

    (2)协程与普通线程的差异

    +

    不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

    +

    由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

    +

    Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

    +

    如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表达式交换控制权。

    +

    Generator 与上下文

    JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。

    +

    这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

    +

    Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function* gen() {
    yield 1;
    return 2;
    }

    let g = gen();

    console.log(g.next().value, g.next().value);
    + +

    上面代码中,第一次执行g.next()时,Generator 函数gen的上下文会加入堆栈,即开始运行gen内部的代码。等遇到yield 1时,gen上下文退出堆栈,内部状态冻结。第二次执行g.next()时,gen上下文重新加入堆栈,变成当前的上下文,重新恢复执行。

    +

    应用

    Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。

    +

    (1)异步操作的同步化表达

    Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* loadUI() {
    showLoadingScreen();
    yield loadUIDataAsynchronously();
    hideLoadingScreen();
    }
    var loader = loadUI();
    // 加载UI
    loader.next();

    // 卸载UI
    loader.next();
    + +

    上面代码中,第一次调用loadUI函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面(showLoadingScreen),并且异步加载数据(loadUIDataAsynchronously)。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面。可以看到,这种写法的好处是所有Loading界面的逻辑,都被封装在一个函数,按部就班非常清晰。

    +

    Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function* main() {
    var result = yield request("http://some.url");
    var resp = JSON.parse(result);
    console.log(resp.value);
    }

    function request(url) {
    makeAjaxCall(url, function (response) {
    it.next(response);
    });
    }

    var it = main();
    it.next();
    + +

    上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined

    +

    下面是另一个例子,通过 Generator 函数逐行读取文本文件。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function* numbers() {
    let file = new FileReader("numbers.txt");
    try {
    while (!file.eof) {
    yield parseInt(file.readLine(), 10);
    }
    } finally {
    file.close();
    }
    }
    + +

    上面代码打开文本文件,使用yield表达式可以手动逐行读取文件。

    +

    (2)控制流管理

    如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    step1(function (value1) {
    step2(value1, function (value2) {
    step3(value2, function (value3) {
    step4(value3, function (value4) {
    // Do something with value4
    });
    });
    });
    });
    + +

    采用 Promise 改写上面的代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Promise.resolve(step1)
    .then(step2)
    .then(step3)
    .then(step4)
    .then(
    function (value4) {
    // Do something with value4
    },
    function (error) {
    // Handle any error from step1 through step4
    }
    )
    .done();
    + +

    上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* longRunningTask(value1) {
    try {
    var value2 = yield step1(value1);
    var value3 = yield step2(value2);
    var value4 = yield step3(value3);
    var value5 = yield step4(value4);
    // Do something with value4
    } catch (e) {
    // Handle any error from step1 through step4
    }
    }
    + +

    然后,使用一个函数,按次序自动执行所有步骤。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    scheduler(longRunningTask(initialValue));

    function scheduler(task) {
    var taskObj = task.next(task.value);
    // 如果Generator函数未结束,就继续调用
    if (!taskObj.done) {
    task.value = taskObj.value;
    scheduler(task);
    }
    }
    + +

    注意,上面这种做法,只适合同步操作,即所有的task都必须是同步的,不能有异步操作。因为这里的代码一得到返回值,就继续往下执行,没有判断异步操作何时完成。如果要控制异步的操作流程,详见后面的《异步操作》一章。

    +

    下面,利用for...of循环会自动依次执行yield命令的特性,提供一种更一般的控制流管理的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let steps = [step1Func, step2Func, step3Func];

    function* iterateSteps(steps) {
    for (var i = 0; i < steps.length; i++) {
    var step = steps[i];
    yield step();
    }
    }
    + +

    上面代码中,数组steps封装了一个任务的多个步骤,Generator 函数iterateSteps则是依次为这些步骤加上yield命令。

    +

    将任务分解成步骤之后,还可以将项目分解成多个依次执行的任务。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let jobs = [job1, job2, job3];

    function* iterateJobs(jobs) {
    for (var i = 0; i < jobs.length; i++) {
    var job = jobs[i];
    yield* iterateSteps(job.steps);
    }
    }
    + +

    上面代码中,数组jobs封装了一个项目的多个任务,Generator 函数iterateJobs则是依次为这些任务加上yield*命令。

    +

    最后,就可以用for...of循环一次性依次执行所有任务的所有步骤。

    +
    1
    2
    3
    for (var step of iterateJobs(jobs)) {
    console.log(step.id);
    }
    + +

    再次提醒,上面的做法只能用于所有步骤都是同步操作的情况,不能有异步操作的步骤。如果想要依次执行异步的步骤,必须使用后面的《异步操作》一章介绍的方法。

    +

    for...of的本质是一个while循环,所以上面的代码实质上执行的是下面的逻辑。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var it = iterateJobs(jobs);
    var res = it.next();

    while (!res.done) {
    var result = res.value;
    // ...
    res = it.next();
    }
    + +

    (3)部署 Iterator 接口

    利用 Generator 函数,可以在任意对象上部署 Iterator 接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function* iterEntries(obj) {
    let keys = Object.keys(obj);
    for (let i = 0; i < keys.length; i++) {
    let key = keys[i];
    yield [key, obj[key]];
    }
    }

    let myObj = { foo: 3, bar: 7 };

    for (let [key, value] of iterEntries(myObj)) {
    console.log(key, value);
    }

    // foo 3
    // bar 7
    + +

    上述代码中,myObj是一个普通对象,通过iterEntries函数,就有了 Iterator 接口。也就是说,可以在任意对象上部署next方法。

    +

    下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function* makeSimpleGenerator(array) {
    var nextIndex = 0;

    while (nextIndex < array.length) {
    yield array[nextIndex++];
    }
    }

    var gen = makeSimpleGenerator(["yo", "ya"]);

    gen.next().value; // 'yo'
    gen.next().value; // 'ya'
    gen.next().done; // true
    + +

    (4)作为数据结构

    Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。

    +
    1
    2
    3
    4
    5
    function* doStuff() {
    yield fs.readFile.bind(null, "hello.txt");
    yield fs.readFile.bind(null, "world.txt");
    yield fs.readFile.bind(null, "and-such.txt");
    }
    + +

    上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。

    +
    1
    2
    3
    for (task of doStuff()) {
    // task是一个函数,可以像回调函数那样使用它
    }
    + +

    实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。

    +
    1
    2
    3
    4
    5
    6
    7
    function doStuff() {
    return [
    fs.readFile.bind(null, "hello.txt"),
    fs.readFile.bind(null, "world.txt"),
    fs.readFile.bind(null, "and-such.txt"),
    ];
    }
    + +

    上面的函数,可以用一模一样的for...of循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Generator%20%E5%87%BD%E6%95%B0%E7%9A%84%E8%AF%AD%E6%B3%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Iterator \345\222\214 for...of \345\276\252\347\216\257/index.html" "b/posts/ES6-Iterator \345\222\214 for...of \345\276\252\347\216\257/index.html" new file mode 100644 index 00000000..e5159f89 --- /dev/null +++ "b/posts/ES6-Iterator \345\222\214 for...of \345\276\252\347\216\257/index.html" @@ -0,0 +1,417 @@ +Iterator 和 for...of 循环 | JCAlways + + + + + + + + + + + + + +

    Iterator 和 for...of 循环

    Iterator 和 for…of 循环

    Iterator(遍历器)的概念

    JavaScript 原有的表示“集合”的数据结构,主要是数组(Array)和对象(Object),ES6 又添加了MapSet。这样就有了四种数据集合,用户还可以组合使用它们,定义自己的数据结构,比如数组的成员是MapMap的成员是对象。这样就需要一种统一的接口机制,来处理所有不同的数据结构。

    +

    遍历器(Iterator)就是这样一种机制。它是一种接口,为各种不同的数据结构提供统一的访问机制。任何数据结构只要部署 Iterator 接口,就可以完成遍历操作(即依次处理该数据结构的所有成员)。

    +

    Iterator 的作用有三个:一是为各种数据结构,提供一个统一的、简便的访问接口;二是使得数据结构的成员能够按某种次序排列;三是 ES6 创造了一种新的遍历命令for...of循环,Iterator 接口主要供for...of消费。

    +

    Iterator 的遍历过程是这样的。

    +

    (1)创建一个指针对象,指向当前数据结构的起始位置。也就是说,遍历器对象本质上,就是一个指针对象。

    +

    (2)第一次调用指针对象的next方法,可以将指针指向数据结构的第一个成员。

    +

    (3)第二次调用指针对象的next方法,指针就指向数据结构的第二个成员。

    +

    (4)不断调用指针对象的next方法,直到它指向数据结构的结束位置。

    +

    每一次调用next方法,都会返回数据结构的当前成员的信息。具体来说,就是返回一个包含valuedone两个属性的对象。其中,value属性是当前成员的值,done属性是一个布尔值,表示遍历是否结束。

    +

    下面是一个模拟next方法返回值的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var it = makeIterator(["a", "b"]);

    it.next(); // { value: "a", done: false }
    it.next(); // { value: "b", done: false }
    it.next(); // { value: undefined, done: true }

    function makeIterator(array) {
    var nextIndex = 0;
    return {
    next: function () {
    return nextIndex < array.length
    ? { value: array[nextIndex++], done: false }
    : { value: undefined, done: true };
    },
    };
    }
    + +

    上面代码定义了一个makeIterator函数,它是一个遍历器生成函数,作用就是返回一个遍历器对象。对数组['a', 'b']执行这个函数,就会返回该数组的遍历器对象(即指针对象)it

    +

    指针对象的next方法,用来移动指针。开始时,指针指向数组的开始位置。然后,每次调用next方法,指针就会指向数组的下一个成员。第一次调用,指向a;第二次调用,指向b

    +

    next方法返回一个对象,表示当前数据成员的信息。这个对象具有valuedone两个属性,value属性返回当前位置的成员,done属性是一个布尔值,表示遍历是否结束,即是否还有必要再一次调用next方法。

    +

    总之,调用指针对象的next方法,就可以遍历事先给定的数据结构。

    +

    对于遍历器对象来说,done: falsevalue: undefined属性都是可以省略的,因此上面的makeIterator函数可以简写成下面的形式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function makeIterator(array) {
    var nextIndex = 0;
    return {
    next: function () {
    return nextIndex < array.length
    ? { value: array[nextIndex++] }
    : { done: true };
    },
    };
    }
    + +

    由于 Iterator 只是把接口规格加到数据结构之上,所以,遍历器与它所遍历的那个数据结构,实际上是分开的,完全可以写出没有对应数据结构的遍历器对象,或者说用遍历器对象模拟出数据结构。下面是一个无限运行的遍历器对象的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var it = idMaker();

    it.next().value; // 0
    it.next().value; // 1
    it.next().value; // 2
    // ...

    function idMaker() {
    var index = 0;

    return {
    next: function () {
    return { value: index++, done: false };
    },
    };
    }
    + +

    上面的例子中,遍历器生成函数idMaker,返回一个遍历器对象(即指针对象)。但是并没有对应的数据结构,或者说,遍历器对象自己描述了一个数据结构出来。

    +

    如果使用 TypeScript 的写法,遍历器接口(Iterable)、指针对象(Iterator)和next方法返回值的规格可以描述如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    interface Iterable {
    [Symbol.iterator]() : Iterator,
    }

    interface Iterator {
    next(value?: any) : IterationResult,
    }

    interface IterationResult {
    value: any,
    done: boolean,
    }
    + +

    默认 Iterator 接口

    Iterator 接口的目的,就是为所有数据结构,提供了一种统一的访问机制,即for...of循环(详见下文)。当使用for...of循环遍历某种数据结构时,该循环会自动去寻找 Iterator 接口。

    +

    一种数据结构只要部署了 Iterator 接口,我们就称这种数据结构是“可遍历的”(iterable)。

    +

    ES6 规定,默认的 Iterator 接口部署在数据结构的Symbol.iterator属性,或者说,一个数据结构只要具有Symbol.iterator属性,就可以认为是“可遍历的”(iterable)。Symbol.iterator属性本身是一个函数,就是当前数据结构默认的遍历器生成函数。执行这个函数,就会返回一个遍历器。至于属性名Symbol.iterator,它是一个表达式,返回Symbol对象的iterator属性,这是一个预定义好的、类型为 Symbol 的特殊值,所以要放在方括号内(参见《Symbol》一章)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const obj = {
    [Symbol.iterator]: function () {
    return {
    next: function () {
    return {
    value: 1,
    done: true,
    };
    },
    };
    },
    };
    + +

    上面代码中,对象obj是可遍历的(iterable),因为具有Symbol.iterator属性。执行这个属性,会返回一个遍历器对象。该对象的根本特征就是具有next方法。每次调用next方法,都会返回一个代表当前成员的信息对象,具有valuedone两个属性。

    +

    ES6 的有些数据结构原生具备 Iterator 接口(比如数组),即不用任何处理,就可以被for...of循环遍历。原因在于,这些数据结构原生部署了Symbol.iterator属性(详见下文),另外一些数据结构没有(比如对象)。凡是部署了Symbol.iterator属性的数据结构,就称为部署了遍历器接口。调用这个接口,就会返回一个遍历器对象。

    +

    原生具备 Iterator 接口的数据结构如下。

    +
      +
    • Array
    • +
    • Map
    • +
    • Set
    • +
    • String
    • +
    • TypedArray
    • +
    • 函数的 arguments 对象
    • +
    • NodeList 对象
    • +
    +

    下面的例子是数组的Symbol.iterator属性。

    +
    1
    2
    3
    4
    5
    6
    7
    let arr = ["a", "b", "c"];
    let iter = arr[Symbol.iterator]();

    iter.next(); // { value: 'a', done: false }
    iter.next(); // { value: 'b', done: false }
    iter.next(); // { value: 'c', done: false }
    iter.next(); // { value: undefined, done: true }
    + +

    上面代码中,变量arr是一个数组,原生就具有遍历器接口,部署在arrSymbol.iterator属性上面。所以,调用这个属性,就得到遍历器对象。

    +

    对于原生部署 Iterator 接口的数据结构,不用自己写遍历器生成函数,for...of循环会自动遍历它们。除此之外,其他数据结构(主要是对象)的 Iterator 接口,都需要自己在Symbol.iterator属性上面部署,这样才会被for...of循环遍历。

    +

    对象(Object)之所以没有默认部署 Iterator 接口,是因为对象的哪个属性先遍历,哪个属性后遍历是不确定的,需要开发者手动指定。本质上,遍历器是一种线性处理,对于任何非线性的数据结构,部署遍历器接口,就等于部署一种线性转换。不过,严格地说,对象部署遍历器接口并不是很必要,因为这时对象实际上被当作 Map 结构使用,ES5 没有 Map 结构,而 ES6 原生提供了。

    +

    一个对象如果要具备可被for...of循环调用的 Iterator 接口,就必须在Symbol.iterator的属性上部署遍历器生成方法(原型链上的对象具有该方法也可)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    class RangeIterator {
    constructor(start, stop) {
    this.value = start;
    this.stop = stop;
    }

    [Symbol.iterator]() {
    return this;
    }

    next() {
    var value = this.value;
    if (value < this.stop) {
    this.value++;
    return { done: false, value: value };
    }
    return { done: true, value: undefined };
    }
    }

    function range(start, stop) {
    return new RangeIterator(start, stop);
    }

    for (var value of range(0, 3)) {
    console.log(value); // 0, 1, 2
    }
    + +

    上面代码是一个类部署 Iterator 接口的写法。Symbol.iterator属性对应一个函数,执行后返回当前对象的遍历器对象。

    +

    下面是通过遍历器实现指针结构的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    function Obj(value) {
    this.value = value;
    this.next = null;
    }

    Obj.prototype[Symbol.iterator] = function () {
    var iterator = { next: next };

    var current = this;

    function next() {
    if (current) {
    var value = current.value;
    current = current.next;
    return { done: false, value: value };
    } else {
    return { done: true };
    }
    }
    return iterator;
    };

    var one = new Obj(1);
    var two = new Obj(2);
    var three = new Obj(3);

    one.next = two;
    two.next = three;

    for (var i of one) {
    console.log(i); // 1, 2, 3
    }
    + +

    上面代码首先在构造函数的原型链上部署Symbol.iterator方法,调用该方法会返回遍历器对象iterator,调用该对象的next方法,在返回一个值的同时,自动将内部指针移到下一个实例。

    +

    下面是另一个为对象添加 Iterator 接口的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let obj = {
    data: ["hello", "world"],
    [Symbol.iterator]() {
    const self = this;
    let index = 0;
    return {
    next() {
    if (index < self.data.length) {
    return {
    value: self.data[index++],
    done: false,
    };
    } else {
    return { value: undefined, done: true };
    }
    },
    };
    },
    };
    + +

    对于类似数组的对象(存在数值键名和length属性),部署 Iterator 接口,有一个简便方法,就是Symbol.iterator方法直接引用数组的 Iterator 接口。

    +
    1
    2
    3
    4
    5
    NodeList.prototype[Symbol.iterator] = Array.prototype[Symbol.iterator];
    // 或者
    NodeList.prototype[Symbol.iterator] = [][Symbol.iterator];

    [...document.querySelectorAll("div")]; // 可以执行了
    + +

    NodeList 对象是类似数组的对象,本来就具有遍历接口,可以直接遍历。上面代码中,我们将它的遍历接口改成数组的Symbol.iterator属性,可以看到没有任何影响。

    +

    下面是另一个类似数组的对象调用数组的Symbol.iterator方法的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let iterable = {
    0: "a",
    1: "b",
    2: "c",
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator],
    };
    for (let item of iterable) {
    console.log(item); // 'a', 'b', 'c'
    }
    + +

    注意,普通对象部署数组的Symbol.iterator方法,并无效果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let iterable = {
    a: "a",
    b: "b",
    c: "c",
    length: 3,
    [Symbol.iterator]: Array.prototype[Symbol.iterator],
    };
    for (let item of iterable) {
    console.log(item); // undefined, undefined, undefined
    }
    + +

    如果Symbol.iterator方法对应的不是遍历器生成函数(即会返回一个遍历器对象),解释引擎将会报错。

    +
    1
    2
    3
    4
    5
    var obj = {};

    obj[Symbol.iterator] = () => 1;

    [...obj]; // TypeError: [] is not a function
    + +

    上面代码中,变量objSymbol.iterator方法对应的不是遍历器生成函数,因此报错。

    +

    有了遍历器接口,数据结构就可以用for...of循环遍历(详见下文),也可以使用while循环遍历。

    +
    1
    2
    3
    4
    5
    6
    7
    var $iterator = ITERABLE[Symbol.iterator]();
    var $result = $iterator.next();
    while (!$result.done) {
    var x = $result.value;
    // ...
    $result = $iterator.next();
    }
    + +

    上面代码中,ITERABLE代表某种可遍历的数据结构,$iterator是它的遍历器对象。遍历器对象每次移动指针(next方法),都检查一下返回值的done属性,如果遍历还没结束,就移动遍历器对象的指针到下一步(next方法),不断循环。

    +

    调用 Iterator 接口的场合

    有一些场合会默认调用 Iterator 接口(即Symbol.iterator方法),除了下文会介绍的for...of循环,还有几个别的场合。

    +

    (1)解构赋值

    +

    对数组和 Set 结构进行解构赋值时,会默认调用Symbol.iterator方法。

    +
    1
    2
    3
    4
    5
    6
    7
    let set = new Set().add("a").add("b").add("c");

    let [x, y] = set;
    // x='a'; y='b'

    let [first, ...rest] = set;
    // first='a'; rest=['b','c'];
    + +

    (2)扩展运算符

    +

    扩展运算符(…)也会调用默认的 Iterator 接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 例一
    var str = "hello";
    [...str]; // ['h','e','l','l','o']

    // 例二
    let arr = ["b", "c"];
    ["a", ...arr, "d"];
    // ['a', 'b', 'c', 'd']
    + +

    上面代码的扩展运算符内部就调用 Iterator 接口。

    +

    实际上,这提供了一种简便机制,可以将任何部署了 Iterator 接口的数据结构,转为数组。也就是说,只要某个数据结构部署了 Iterator 接口,就可以对它使用扩展运算符,将其转为数组。

    +
    1
    let arr = [...iterable];
    + +

    (3)yield*

    +

    yield*后面跟的是一个可遍历的结构,它会调用该结构的遍历器接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let generator = function* () {
    yield 1;
    yield* [2, 3, 4];
    yield 5;
    };

    var iterator = generator();

    iterator.next(); // { value: 1, done: false }
    iterator.next(); // { value: 2, done: false }
    iterator.next(); // { value: 3, done: false }
    iterator.next(); // { value: 4, done: false }
    iterator.next(); // { value: 5, done: false }
    iterator.next(); // { value: undefined, done: true }
    + +

    (4)其他场合

    +

    由于数组的遍历会调用遍历器接口,所以任何接受数组作为参数的场合,其实都调用了遍历器接口。下面是一些例子。

    +
      +
    • for…of
    • +
    • Array.from()
    • +
    • Map(), Set(), WeakMap(), WeakSet()(比如new Map([['a',1],['b',2]])
    • +
    • Promise.all()
    • +
    • Promise.race()
    • +
    +

    字符串的 Iterator 接口

    字符串是一个类似数组的对象,也原生具有 Iterator 接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var someString = "hi";
    typeof someString[Symbol.iterator];
    // "function"

    var iterator = someString[Symbol.iterator]();

    iterator.next(); // { value: "h", done: false }
    iterator.next(); // { value: "i", done: false }
    iterator.next(); // { value: undefined, done: true }
    + +

    上面代码中,调用Symbol.iterator方法返回一个遍历器对象,在这个遍历器上可以调用 next 方法,实现对于字符串的遍历。

    +

    可以覆盖原生的Symbol.iterator方法,达到修改遍历器行为的目的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var str = new String("hi");

    [...str]; // ["h", "i"]

    str[Symbol.iterator] = function () {
    return {
    next: function () {
    if (this._first) {
    this._first = false;
    return { value: "bye", done: false };
    } else {
    return { done: true };
    }
    },
    _first: true,
    };
    };

    [...str]; // ["bye"]
    str; // "hi"
    + +

    上面代码中,字符串 str 的Symbol.iterator方法被修改了,所以扩展运算符(...)返回的值变成了bye,而字符串本身还是hi

    +

    Iterator 接口与 Generator 函数

    Symbol.iterator方法的最简单实现,还是使用下一章要介绍的 Generator 函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let myIterable = {
    [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
    yield 3;
    }
    }
    [...myIterable] // [1, 2, 3]

    // 或者采用下面的简洁写法

    let obj = {
    * [Symbol.iterator]() {
    yield 'hello';
    yield 'world';
    }
    };

    for (let x of obj) {
    console.log(x);
    }
    // "hello"
    // "world"
    + +

    上面代码中,Symbol.iterator方法几乎不用部署任何代码,只要用 yield 命令给出每一步的返回值即可。

    +

    遍历器对象的 return(),throw()

    遍历器对象除了具有next方法,还可以具有return方法和throw方法。如果你自己写遍历器对象生成函数,那么next方法是必须部署的,return方法和throw方法是否部署是可选的。

    +

    return方法的使用场合是,如果for...of循环提前退出(通常是因为出错,或者有break语句),就会调用return方法。如果一个对象在完成遍历前,需要清理或释放资源,就可以部署return方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function readLinesSync(file) {
    return {
    [Symbol.iterator]() {
    return {
    next() {
    return { done: false };
    },
    return() {
    file.close();
    return { done: true };
    },
    };
    },
    };
    }
    + +

    上面代码中,函数readLinesSync接受一个文件对象作为参数,返回一个遍历器对象,其中除了next方法,还部署了return方法。下面的两种情况,都会触发执行return方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 情况一
    for (let line of readLinesSync(fileName)) {
    console.log(line);
    break;
    }

    // 情况二
    for (let line of readLinesSync(fileName)) {
    console.log(line);
    throw new Error();
    }
    + +

    上面代码中,情况一输出文件的第一行以后,就会执行return方法,关闭这个文件;情况二会在执行return方法关闭文件之后,再抛出错误。

    +

    注意,return方法必须返回一个对象,这是 Generator 规格决定的。

    +

    throw方法主要是配合 Generator 函数使用,一般的遍历器对象用不到这个方法。请参阅《Generator 函数》一章。

    +

    for…of 循环

    ES6 借鉴 C++、Java、C# 和 Python 语言,引入了for...of循环,作为遍历所有数据结构的统一的方法。

    +

    一个数据结构只要部署了Symbol.iterator属性,就被视为具有 iterator 接口,就可以用for...of循环遍历它的成员。也就是说,for...of循环内部调用的是数据结构的Symbol.iterator方法。

    +

    for...of循环可以使用的范围包括数组、Set 和 Map 结构、某些类似数组的对象(比如arguments对象、DOM NodeList 对象)、后文的 Generator 对象,以及字符串。

    +

    数组

    数组原生具备iterator接口(即默认部署了Symbol.iterator属性),for...of循环本质上就是调用这个接口产生的遍历器,可以用下面的代码证明。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const arr = ["red", "green", "blue"];

    for (let v of arr) {
    console.log(v); // red green blue
    }

    const obj = {};
    obj[Symbol.iterator] = arr[Symbol.iterator].bind(arr);

    for (let v of obj) {
    console.log(v); // red green blue
    }
    + +

    上面代码中,空对象obj部署了数组arrSymbol.iterator属性,结果objfor...of循环,产生了与arr完全一样的结果。

    +

    for...of循环可以代替数组实例的forEach方法。

    +
    1
    2
    3
    4
    5
    6
    const arr = ["red", "green", "blue"];

    arr.forEach(function (element, index) {
    console.log(element); // red green blue
    console.log(index); // 0 1 2
    });
    + +

    JavaScript 原有的for...in循环,只能获得对象的键名,不能直接获取键值。ES6 提供for...of循环,允许遍历获得键值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var arr = ["a", "b", "c", "d"];

    for (let a in arr) {
    console.log(a); // 0 1 2 3
    }

    for (let a of arr) {
    console.log(a); // a b c d
    }
    + +

    上面代码表明,for...in循环读取键名,for...of循环读取键值。如果要通过for...of循环,获取数组的索引,可以借助数组实例的entries方法和keys方法(参见《数组的扩展》一章)。

    +

    for...of循环调用遍历器接口,数组的遍历器接口只返回具有数字索引的属性。这一点跟for...in循环也不一样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let arr = [3, 5, 7];
    arr.foo = "hello";

    for (let i in arr) {
    console.log(i); // "0", "1", "2", "foo"
    }

    for (let i of arr) {
    console.log(i); // "3", "5", "7"
    }
    + +

    上面代码中,for...of循环不会返回数组arrfoo属性。

    +

    Set 和 Map 结构

    Set 和 Map 结构也原生具有 Iterator 接口,可以直接使用for...of循环。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    var engines = new Set(["Gecko", "Trident", "Webkit", "Webkit"]);
    for (var e of engines) {
    console.log(e);
    }
    // Gecko
    // Trident
    // Webkit

    var es6 = new Map();
    es6.set("edition", 6);
    es6.set("committee", "TC39");
    es6.set("standard", "ECMA-262");
    for (var [name, value] of es6) {
    console.log(name + ": " + value);
    }
    // edition: 6
    // committee: TC39
    // standard: ECMA-262
    + +

    上面代码演示了如何遍历 Set 结构和 Map 结构。值得注意的地方有两个,首先,遍历的顺序是按照各个成员被添加进数据结构的顺序。其次,Set 结构遍历时,返回的是一个值,而 Map 结构遍历时,返回的是一个数组,该数组的两个成员分别为当前 Map 成员的键名和键值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let map = new Map().set("a", 1).set("b", 2);
    for (let pair of map) {
    console.log(pair);
    }
    // ['a', 1]
    // ['b', 2]

    for (let [key, value] of map) {
    console.log(key + " : " + value);
    }
    // a : 1
    // b : 2
    + +

    计算生成的数据结构

    有些数据结构是在现有数据结构的基础上,计算生成的。比如,ES6 的数组、Set、Map 都部署了以下三个方法,调用后都返回遍历器对象。

    +
      +
    • entries() 返回一个遍历器对象,用来遍历[键名, 键值]组成的数组。对于数组,键名就是索引值;对于 Set,键名与键值相同。Map 结构的 Iterator 接口,默认就是调用entries方法。
    • +
    • keys() 返回一个遍历器对象,用来遍历所有的键名。
    • +
    • values() 返回一个遍历器对象,用来遍历所有的键值。
    • +
    +

    这三个方法调用后生成的遍历器对象,所遍历的都是计算生成的数据结构。

    +
    1
    2
    3
    4
    5
    6
    7
    let arr = ["a", "b", "c"];
    for (let pair of arr.entries()) {
    console.log(pair);
    }
    // [0, 'a']
    // [1, 'b']
    // [2, 'c']
    + +

    类似数组的对象

    类似数组的对象包括好几类。下面是for...of循环用于字符串、DOM NodeList 对象、arguments对象的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // 字符串
    let str = "hello";

    for (let s of str) {
    console.log(s); // h e l l o
    }

    // DOM NodeList对象
    let paras = document.querySelectorAll("p");

    for (let p of paras) {
    p.classList.add("test");
    }

    // arguments对象
    function printArgs() {
    for (let x of arguments) {
    console.log(x);
    }
    }
    printArgs("a", "b");
    // 'a'
    // 'b'
    + +

    对于字符串来说,for...of循环还有一个特点,就是会正确识别 32 位 UTF-16 字符。

    +
    1
    2
    3
    4
    5
    for (let x of "a\uD83D\uDC0A") {
    console.log(x);
    }
    // 'a'
    // '\uD83D\uDC0A'
    + +

    并不是所有类似数组的对象都具有 Iterator 接口,一个简便的解决方法,就是使用Array.from方法将其转为数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let arrayLike = { length: 2, 0: "a", 1: "b" };

    // 报错
    for (let x of arrayLike) {
    console.log(x);
    }

    // 正确
    for (let x of Array.from(arrayLike)) {
    console.log(x);
    }
    + +

    对象

    对于普通的对象,for...of结构不能直接使用,会报错,必须部署了 Iterator 接口后才能使用。但是,这样情况下,for...in循环依然可以用来遍历键名。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let es6 = {
    edition: 6,
    committee: "TC39",
    standard: "ECMA-262",
    };

    for (let e in es6) {
    console.log(e);
    }
    // edition
    // committee
    // standard

    for (let e of es6) {
    console.log(e);
    }
    // TypeError: es6[Symbol.iterator] is not a function
    + +

    上面代码表示,对于普通的对象,for...in循环可以遍历键名,for...of循环会报错。

    +

    一种解决方法是,使用Object.keys方法将对象的键名生成一个数组,然后遍历这个数组。

    +
    1
    2
    3
    for (var key of Object.keys(someObject)) {
    console.log(key + ": " + someObject[key]);
    }
    + +

    另一个方法是使用 Generator 函数将对象重新包装一下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function* entries(obj) {
    for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
    }
    }

    for (let [key, value] of entries(obj)) {
    console.log(key, "->", value);
    }
    // a -> 1
    // b -> 2
    // c -> 3
    + +

    与其他遍历语法的比较

    以数组为例,JavaScript 提供多种遍历语法。最原始的写法就是for循环。

    +
    1
    2
    3
    for (var index = 0; index < myArray.length; index++) {
    console.log(myArray[index]);
    }
    + +

    这种写法比较麻烦,因此数组提供内置的forEach方法。

    +
    1
    2
    3
    myArray.forEach(function (value) {
    console.log(value);
    });
    + +

    这种写法的问题在于,无法中途跳出forEach循环,break命令或return命令都不能奏效。

    +

    for...in循环可以遍历数组的键名。

    +
    1
    2
    3
    for (var index in myArray) {
    console.log(myArray[index]);
    }
    + +

    for...in循环有几个缺点。

    +
      +
    • 数组的键名是数字,但是for...in循环是以字符串作为键名“0”、“1”、“2”等等。
    • +
    • for...in循环不仅遍历数字键名,还会遍历手动添加的其他键,甚至包括原型链上的键。
    • +
    • 某些情况下,for...in循环会以任意顺序遍历键名。
    • +
    +

    总之,for...in循环主要是为遍历对象而设计的,不适用于遍历数组。

    +

    for...of循环相比上面几种做法,有一些显著的优点。

    +
    1
    2
    3
    for (let value of myArray) {
    console.log(value);
    }
    + +
      +
    • 有着同for...in一样的简洁语法,但是没有for...in那些缺点。
    • +
    • 不同于forEach方法,它可以与breakcontinuereturn配合使用。
    • +
    • 提供了遍历所有数据结构的统一操作接口。
    • +
    +

    下面是一个使用 break 语句,跳出for...of循环的例子。

    +
    1
    2
    3
    4
    for (var n of fibonacci) {
    if (n > 1000) break;
    console.log(n);
    }
    + +

    上面的例子,会输出斐波纳契数列小于等于 1000 的项。如果当前项大于 1000,就会使用break语句跳出for...of循环。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Iterator%20%E5%92%8C%20for...of%20%E5%BE%AA%E7%8E%AF/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/ES6-Mixin/index.html b/posts/ES6-Mixin/index.html new file mode 100644 index 00000000..7453d970 --- /dev/null +++ b/posts/ES6-Mixin/index.html @@ -0,0 +1,238 @@ +Mixin | JCAlways + + + + + + + + + + + + + +

    Mixin

    Mixin

    JavaScript 语言的设计是单一继承,即子类只能继承一个父类,不允许继承多个父类。这种设计保证了对象继承的层次结构是树状的,而不是复杂的网状结构

    +

    但是,这大大降低了编程的灵活性。因为实际开发中,有时不可避免,子类需要继承多个父类。举例来说,“猫”可以继承“哺乳类动物”,也可以继承“宠物”。

    +

    各种单一继承的编程语言,有不同的多重继承解决方案。比如,Java 语言也是子类只能继承一个父类,但是还允许继承多个界面(interface),这样就间接实现了多重继承。Interface 与父类一样,也是一个类,只不过它只定义接口(method signature),不定义实现,因此又被称为“抽象类”。凡是继承于 Interface 的方法,都必须自己定义实现,否则就会报错。这样就避免了多重继承的最大问题:多个父类的同名方法的碰撞(naming collision)。

    +

    JavaScript 语言没有采用 Interface 的方案,而是通过代理(delegation)实现了从其他类引入方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var Enumerable_first = function () {
    this.first = function () {
    return this[0];
    };
    };

    var list = ["foo", "bar", "baz"];
    Enumerable_first.call(list); // explicit delegation
    list.first(); // "foo"
    + +

    上面代码中,list是一个数组,本身并没有first方法。通过call方法,可以把Enumerable_first里面的方法,绑定到list,从而list就具有first方法。这就叫做“代理”(delegation),list对象代理了Enumerable_firstfirst方法。

    +

    含义

    Mixin 这个名字来自于冰淇淋,在基本口味的冰淇淋上面混入其他口味,这就叫做 Mix-in。

    +

    它允许向一个类里面注入一些代码,使得一个类的功能能够“混入”另一个类。实质上是多重继承的一种解决方案,但是避免了多重继承的复杂性,而且有利于代码复用。

    +

    Mixin 就是一个正常的类,不仅定义了接口,还定义了接口的实现。

    +

    子类通过在this对象上面绑定方法,达到多重继承的目的。

    +

    很多库提供了 Mixin 功能。下面以 Lodash 为例。

    +
    1
    2
    3
    4
    5
    6
    7
    function vowels(string) {
    return /[aeiou]/i.test(this.value);
    }

    var obj = { value: "hello" };
    _.mixin(obj, { vowels: vowels });
    obj.vowels(); // true
    + +

    上面代码通过 Lodash 库的_.mixin方法,让obj对象继承了vowels方法。

    +

    Underscore 的类似方法是_.extend

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var Person = function (fName, lName) {
    this.firstName = fName;
    this.lastName = lName;
    };

    var sam = new Person("Sam", "Lowry");

    var NameMixin = {
    fullName: function () {
    return this.firstName + " " + this.lastName;
    },
    rename: function (first, last) {
    this.firstName = first;
    this.lastName = last;
    return this;
    },
    };
    _.extend(Person.prototype, NameMixin);
    sam.rename("Samwise", "Gamgee");
    sam.fullName(); // "Samwise Gamgee"
    + +

    上面代码通过_.extend方法,在sam对象上面(准确说是它的原型对象Person.prototype上面),混入了NameMixin类。

    +

    extend方法的实现非常简单。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function extend(destination, source) {
    for (var k in source) {
    if (source.hasOwnProperty(k)) {
    destination[k] = source[k];
    }
    }
    return destination;
    }
    + +

    上面代码将source对象的所有方法,添加到destination对象。

    +

    Trait

    Trait 是另外一种多重继承的解决方案。它与 Mixin 很相似,但是有一些细微的差别。

    +
      +
    • Mixin 可以包含状态(state),Trait 不包含,即 Trait 里面的方法都是互不相干,可以线性包含的。比如,Trait1包含方法ABTrait2继承了Trait1,同时还包含一个自己的方法C,实际上就等同于直接包含方法ABC
    • +
    • 对于同名方法的碰撞,Mixin 包含了解决规则,Trait 则是报错。
    • +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Mixin/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/ES6-Module \347\232\204\345\212\240\350\275\275\345\256\236\347\216\260/index.html" "b/posts/ES6-Module \347\232\204\345\212\240\350\275\275\345\256\236\347\216\260/index.html" new file mode 100644 index 00000000..b11b6ac5 --- /dev/null +++ "b/posts/ES6-Module \347\232\204\345\212\240\350\275\275\345\256\236\347\216\260/index.html" @@ -0,0 +1,461 @@ +Module 的加载实现 | JCAlways + + + + + + + + + + + + + +

    Module 的加载实现

    Module 的加载实现

    上一章介绍了模块的语法,本章介绍如何在浏览器和 Node 之中加载 ES6 模块,以及实际开发中经常遇到的一些问题(比如循环加载)。

    +

    浏览器加载

    传统方法

    HTML 网页中,浏览器通过<script>标签加载 JavaScript 脚本。

    +
    1
    2
    3
    4
    5
    6
    7
    <!-- 页面内嵌的脚本 -->
    <script type="application/javascript">
    // module code
    </script>

    <!-- 外部脚本 -->
    <script type="application/javascript" src="path/to/myModule.js"></script>
    + +

    上面代码中,由于浏览器脚本的默认语言是 JavaScript,因此type="application/javascript"可以省略。

    +

    默认情况下,浏览器是同步加载 JavaScript 脚本,即渲染引擎遇到<script>标签就会停下来,等到执行完脚本,再继续向下渲染。如果是外部脚本,还必须加入脚本下载的时间。

    +

    如果脚本体积很大,下载和执行的时间就会很长,因此造成浏览器堵塞,用户会感觉到浏览器“卡死”了,没有任何响应。这显然是很不好的体验,所以浏览器允许脚本异步加载,下面就是两种异步加载的语法。

    +
    1
    2
    <script src="path/to/myModule.js" defer></script>
    <script src="path/to/myModule.js" async></script>
    + +

    上面代码中,<script>标签打开deferasync属性,脚本就会异步加载。渲染引擎遇到这一行命令,就会开始下载外部脚本,但不会等它下载和执行,而是直接执行后面的命令。

    +

    deferasync的区别是:defer要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),才会执行;async一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。一句话,defer是“渲染完再执行”,async是“下载完就执行”。另外,如果有多个defer脚本,会按照它们在页面出现的顺序加载,而多个async脚本是不能保证加载顺序的。

    +

    加载规则

    浏览器加载 ES6 模块,也使用<script>标签,但是要加入type="module"属性。

    +
    1
    <script type="module" src="./foo.js"></script>
    + +

    上面代码在网页中插入一个模块foo.js,由于type属性设为module,所以浏览器知道这是一个 ES6 模块。

    +

    浏览器对于带有type="module"<script>,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了<script>标签的defer属性。

    +
    1
    2
    3
    <script type="module" src="./foo.js"></script>
    <!-- 等同于 -->
    <script type="module" src="./foo.js" defer></script>
    + +

    如果网页有多个<script type="module">,它们会按照在页面出现的顺序依次执行。

    +

    <script>标签的async属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。

    +
    1
    <script type="module" src="./foo.js" async></script>
    + +

    一旦使用了async属性,<script type="module">就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。

    +

    ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。

    +
    1
    2
    3
    4
    5
    <script type="module">
    import utils from "./utils.js";

    // other code
    </script>
    + +

    对于外部的模块脚本(上例是foo.js),有几点需要注意。

    +
      +
    • 代码是在模块作用域之中运行,而不是在全局作用域运行。模块内部的顶层变量,外部不可见。
    • +
    • 模块脚本自动采用严格模式,不管有没有声明use strict
    • +
    • 模块之中,可以使用import命令加载其他模块(.js后缀不可省略,需要提供绝对 URL 或相对 URL),也可以使用export命令输出对外接口。
    • +
    • 模块之中,顶层的this关键字返回undefined,而不是指向window。也就是说,在模块顶层使用this关键字,是无意义的。
    • +
    • 同一个模块如果加载多次,将只执行一次。
    • +
    +

    下面是一个示例模块。

    +
    1
    2
    3
    4
    5
    6
    import utils from "https://example.com/js/utils.js";

    const x = 1;

    console.log(x === window.x); //false
    console.log(this === undefined); // true
    + +

    利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。

    +
    1
    const isNotModuleScript = this !== undefined;
    + +

    ES6 模块与 CommonJS 模块的差异

    讨论 Node 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。

    +

    它们有两个重大差异。

    +
      +
    • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
    • +
    • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
    • +
    +

    第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

    +

    下面重点解释第一个差异。

    +

    CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件lib.js的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // lib.js
    var counter = 3;
    function incCounter() {
    counter++;
    }
    module.exports = {
    counter: counter,
    incCounter: incCounter,
    };
    + +

    上面代码输出内部变量counter和改写这个变量的内部方法incCounter。然后,在main.js里面加载这个模块。

    +
    1
    2
    3
    4
    5
    6
    // main.js
    var mod = require("./lib");

    console.log(mod.counter); // 3
    mod.incCounter();
    console.log(mod.counter); // 3
    + +

    上面代码说明,lib.js模块加载以后,它的内部变化就影响不到输出的mod.counter了。这是因为mod.counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // lib.js
    var counter = 3;
    function incCounter() {
    counter++;
    }
    module.exports = {
    get counter() {
    return counter;
    },
    incCounter: incCounter,
    };
    + +

    上面代码中,输出的counter属性实际上是一个取值器函数。现在再执行main.js,就可以正确读取内部变量counter的变动了。

    +
    1
    2
    3
    $ node main.js
    3
    4
    + +

    ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import有点像 Unix 系统的“符号连接”,原始值变了,import加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

    +

    还是举上面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // lib.js
    export let counter = 3;
    export function incCounter() {
    counter++;
    }

    // main.js
    import { counter, incCounter } from "./lib";
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4
    + +

    上面代码说明,ES6 模块输入的变量counter是活的,完全反应其所在模块lib.js内部的变化。

    +

    再举一个出现在export一节中的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // m1.js
    export var foo = "bar";
    setTimeout(() => (foo = "baz"), 500);

    // m2.js
    import { foo } from "./m1.js";
    console.log(foo);
    setTimeout(() => console.log(foo), 500);
    + +

    上面代码中,m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz

    +

    让我们看看,m2.js能否正确读取这个变化。

    +
    1
    2
    3
    4
    $ babel-node m2.js

    bar
    baz
    + +

    上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

    +

    由于 ES6 输入的模块变量,只是一个“符号连接”,所以这个变量是只读的,对它进行重新赋值会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // lib.js
    export let obj = {};

    // main.js
    import { obj } from "./lib";

    obj.prop = 123; // OK
    obj = {}; // TypeError
    + +

    上面代码中,main.jslib.js输入变量obj,可以对obj添加属性,但是重新赋值就会报错。因为变量obj指向的地址是只读的,不能重新赋值,这就好比main.js创造了一个名为objconst变量。

    +

    最后,export通过接口,输出的是同一个值。不同的脚本加载这个接口,得到的都是同样的实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // mod.js
    function C() {
    this.sum = 0;
    this.add = function () {
    this.sum += 1;
    };
    this.show = function () {
    console.log(this.sum);
    };
    }

    export let c = new C();
    + +

    上面的脚本mod.js,输出的是一个C的实例。不同的脚本加载这个模块,得到的都是同一个实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // x.js
    import { c } from "./mod";
    c.add();

    // y.js
    import { c } from "./mod";
    c.show();

    // main.js
    import "./x";
    import "./y";
    + +

    现在执行main.js,输出的是1

    +
    1
    2
    $ babel-node main.js
    1
    + +

    这就证明了x.jsy.js加载的都是C的同一个实例。

    +

    Node 加载

    概述

    Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。

    +

    Node 要求 ES6 模块采用.mjs后缀文件名。也就是说,只要脚本文件里面使用import或者export命令,那么就必须采用.mjs后缀名。require命令不能加载.mjs文件,会报错,只有import命令才可以加载.mjs文件。反过来,.mjs文件里面也不能使用require命令,必须使用import

    +

    目前,这项功能还在试验阶段。安装 Node v8.5.0 或以上版本,要用--experimental-modules参数才能打开该功能。

    +
    1
    $ node --experimental-modules my-app.mjs
    + +

    为了与浏览器的import加载规则相同,Node 的.mjs文件支持 URL 路径。

    +
    1
    import "./foo?query=1"; // 加载 ./foo 传入参数 ?query=1
    + +

    上面代码中,脚本路径带有参数?query=1,Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。由于这个原因,只要文件名中含有:%#?等特殊字符,最好对这些字符进行转义。

    +

    目前,Node 的import命令只支持加载本地模块(file:协议),不支持加载远程模块。

    +

    如果模块名不含路径,那么import命令会去node_modules目录寻找这个模块。

    +
    1
    2
    import "baz";
    import "abc/123";
    + +

    如果模块名包含路径,那么import命令会按照路径去寻找这个名字的脚本文件。

    +
    1
    2
    3
    4
    5
    import "file:///etc/config/app.json";
    import "./foo";
    import "./foo?search";
    import "../bar";
    import "/baz";
    + +

    如果脚本文件省略了后缀名,比如import './foo',Node 会依次尝试四个后缀名:./foo.mjs./foo.js./foo.json./foo.node。如果这些脚本文件都不存在,Node 就会去加载./foo/package.jsonmain字段指定的脚本。如果./foo/package.json不存在或者没有main字段,那么就会依次加载./foo/index.mjs./foo/index.js./foo/index.json./foo/index.node。如果以上四个文件还是都不存在,就会抛出错误。

    +

    最后,Node 的import命令是异步加载,这一点与浏览器的处理方法相同。

    +

    内部变量

    ES6 模块应该是通用的,同一个模块不用修改,就可以用在浏览器环境和服务器环境。为了达到这个目标,Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。

    +

    首先,就是this关键字。ES6 模块之中,顶层的this指向undefined;CommonJS 模块的顶层this指向当前模块,这是两者的一个重大差异。

    +

    其次,以下这些顶层变量在 ES6 模块之中都是不存在的。

    +
      +
    • arguments
    • +
    • require
    • +
    • module
    • +
    • exports
    • +
    • __filename
    • +
    • __dirname
    • +
    +

    如果你一定要使用这些变量,有一个变通方法,就是写一个 CommonJS 模块输出这些变量,然后再用 ES6 模块加载这个 CommonJS 模块。但是这样一来,该 ES6 模块就不能直接用于浏览器环境了,所以不推荐这样做。

    +
    1
    2
    3
    4
    5
    6
    // expose.js
    module.exports = { __dirname };

    // use.mjs
    import expose from "./expose.js";
    const { __dirname } = expose;
    + +

    上面代码中,expose.js是一个 CommonJS 模块,输出变量__dirname,该变量在 ES6 模块之中不存在。ES6 模块加载expose.js,就可以得到__dirname

    +

    ES6 模块加载 CommonJS 模块

    CommonJS 模块的输出都定义在module.exports这个属性上面。Node 的import命令加载 CommonJS 模块,Node 会自动将module.exports属性,当作模块的默认输出,即等同于export default xxx

    +

    下面是一个 CommonJS 模块。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // a.js
    module.exports = {
    foo: "hello",
    bar: "world",
    };

    // 等同于
    export default {
    foo: "hello",
    bar: "world",
    };
    + +

    import命令加载上面的模块,module.exports会被视为默认输出,即import命令实际上输入的是这样一个对象{ default: module.exports }

    +

    所以,一共有三种写法,可以拿到 CommonJS 模块的module.exports

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 写法一
    import baz from "./a";
    // baz = {foo: 'hello', bar: 'world'};

    // 写法二
    import { default as baz } from "./a";
    // baz = {foo: 'hello', bar: 'world'};

    // 写法三
    import * as baz from "./a";
    // baz = {
    // get default() {return module.exports;},
    // get foo() {return this.default.foo}.bind(baz),
    // get bar() {return this.default.bar}.bind(baz)
    // }
    + +

    上面代码的第三种写法,可以通过baz.default拿到module.exportsfoo属性和bar属性就是可以通过这种方法拿到了module.exports

    +

    下面是一些例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // b.js
    module.exports = null;

    // es.js
    import foo from "./b";
    // foo = null;

    import * as bar from "./b";
    // bar = { default:null };
    + +

    上面代码中,es.js采用第二种写法时,要通过bar.default这样的写法,才能拿到module.exports

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // c.js
    module.exports = function two() {
    return 2;
    };

    // es.js
    import foo from "./c";
    foo(); // 2

    import * as bar from "./c";
    bar.default(); // 2
    bar(); // throws, bar is not a function
    + +

    上面代码中,bar本身是一个对象,不能当作函数调用,只能通过bar.default调用。

    +

    CommonJS 模块的输出缓存机制,在 ES6 加载方式下依然有效。

    +
    1
    2
    3
    // foo.js
    module.exports = 123;
    setTimeout((_) => (module.exports = null));
    + +

    上面代码中,对于加载foo.js的脚本,module.exports将一直是123,而不会变成null

    +

    由于 ES6 模块是编译时确定输出接口,CommonJS 模块是运行时确定输出接口,所以采用import命令加载 CommonJS 模块时,不允许采用下面的写法。

    +
    1
    2
    // 不正确
    import { readFile } from "fs";
    + +

    上面的写法不正确,因为fs是 CommonJS 格式,只有在运行时才能确定readFile接口,而import命令要求编译时就确定这个接口。解决方法就是改为整体输入。

    +
    1
    2
    3
    4
    5
    6
    7
    // 正确的写法一
    import * as express from "express";
    const app = express.default();

    // 正确的写法二
    import express from "express";
    const app = express();
    + +

    CommonJS 模块加载 ES6 模块

    CommonJS 模块加载 ES6 模块,不能使用require命令,而要使用import()函数。ES6 模块的所有输出接口,会成为输入对象的属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // es.mjs
    let foo = { bar: "my-default" };
    export default foo;

    // cjs.js
    const es_namespace = await import("./es.mjs");
    // es_namespace = {
    // get default() {
    // ...
    // }
    // }
    console.log(es_namespace.default);
    // { bar:'my-default' }
    + +

    上面代码中,default接口变成了es_namespace.default属性。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // es.js
    export let foo = { bar: "my-default" };
    export { foo as bar };
    export function f() {}
    export class c {}

    // cjs.js
    const es_namespace = await import("./es");
    // es_namespace = {
    // get foo() {return foo;}
    // get bar() {return foo;}
    // get f() {return f;}
    // get c() {return c;}
    // }
    + +

    循环加载

    “循环加载”(circular dependency)指的是,a脚本的执行依赖b脚本,而b脚本的执行又依赖a脚本。

    +
    1
    2
    3
    4
    5
    // a.js
    var b = require("b");

    // b.js
    var a = require("a");
    + +

    通常,“循环加载”表示存在强耦合,如果处理不好,还可能导致递归加载,使得程序无法执行,因此应该避免出现。

    +

    但是实际上,这是很难避免的,尤其是依赖关系复杂的大项目,很容易出现a依赖bb依赖cc又依赖a这样的情况。这意味着,模块加载机制必须考虑“循环加载”的情况。

    +

    对于 JavaScript 语言来说,目前最常见的两种模块格式 CommonJS 和 ES6,处理“循环加载”的方法是不一样的,返回的结果也不一样。

    +

    CommonJS 模块的加载原理

    介绍 ES6 如何处理“循环加载”之前,先介绍目前最流行的 CommonJS 模块格式的加载原理。

    +

    CommonJS 的一个模块,就是一个脚本文件。require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

    +
    1
    2
    3
    4
    5
    6
    {
    id: '...',
    exports: { ... },
    loaded: true,
    ...
    }
    + +

    上面代码就是 Node 内部加载模块后生成的一个对象。该对象的id属性是模块名,exports属性是模块输出的各个接口,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。

    +

    以后需要用到这个模块的时候,就会到exports属性上面取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。

    +

    CommonJS 模块的循环加载

    CommonJS 模块的重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

    +

    让我们来看,Node 官方文档里面的例子。脚本文件a.js代码如下。

    +
    1
    2
    3
    4
    5
    exports.done = false;
    var b = require("./b.js");
    console.log("在 a.js 之中,b.done = %j", b.done);
    exports.done = true;
    console.log("a.js 执行完毕");
    + +

    上面代码之中,a.js脚本先输出一个done变量,然后加载另一个脚本文件b.js。注意,此时a.js代码就停在这里,等待b.js执行完毕,再往下执行。

    +

    再看b.js的代码。

    +
    1
    2
    3
    4
    5
    exports.done = false;
    var a = require("./a.js");
    console.log("在 b.js 之中,a.done = %j", a.done);
    exports.done = true;
    console.log("b.js 执行完毕");
    + +

    上面代码之中,b.js执行到第二行,就会去加载a.js,这时,就发生了“循环加载”。系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。

    +

    a.js已经执行的部分,只有一行。

    +
    1
    exports.done = false;
    + +

    因此,对于b.js来说,它从a.js只输入一个变量done,值为false

    +

    然后,b.js接着往下执行,等到全部执行完毕,再把执行权交还给a.js。于是,a.js接着往下执行,直到执行完毕。我们写一个脚本main.js,验证这个过程。

    +
    1
    2
    3
    var a = require("./a.js");
    var b = require("./b.js");
    console.log("在 main.js 之中, a.done=%j, b.done=%j", a.done, b.done);
    + +

    执行main.js,运行结果如下。

    +
    1
    2
    3
    4
    5
    6
    7
    $ node main.js

    在 b.js 之中,a.done = false
    b.js 执行完毕
    在 a.js 之中,b.done = true
    a.js 执行完毕
    在 main.js 之中, a.done=true, b.done=true
    + +

    上面的代码证明了两件事。一是,在b.js之中,a.js没有执行完毕,只执行了第一行。二是,main.js执行到第二行时,不会再次执行b.js,而是输出缓存的b.js的执行结果,即它的第四行。

    +
    1
    exports.done = true;
    + +

    总之,CommonJS 输入的是被输出值的拷贝,不是引用。

    +

    另外,由于 CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异。所以,输入变量的时候,必须非常小心。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var a = require("a"); // 安全的写法
    var foo = require("a").foo; // 危险的写法

    exports.good = function (arg) {
    return a.foo("good", arg); // 使用的是 a.foo 的最新值
    };

    exports.bad = function (arg) {
    return foo("bad", arg); // 使用的是一个部分加载时的值
    };
    + +

    上面代码中,如果发生循环加载,require('a').foo的值很可能后面会被改写,改用require('a')会更保险一点。

    +

    ES6 模块的循环加载

    ES6 处理“循环加载”与 CommonJS 有本质的不同。ES6 模块是动态引用,如果使用import从一个模块加载变量(即import foo from 'foo'),那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。

    +

    请看下面这个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // a.mjs
    import { bar } from "./b";
    console.log("a.mjs");
    console.log(bar);
    export let foo = "foo";

    // b.mjs
    import { foo } from "./a";
    console.log("b.mjs");
    console.log(foo);
    export let bar = "bar";
    + +

    上面代码中,a.mjs加载b.mjsb.mjs又加载a.mjs,构成循环加载。执行a.mjs,结果如下。

    +
    1
    2
    3
    $ node --experimental-modules a.mjs
    b.mjs
    ReferenceError: foo is not defined
    + +

    上面代码中,执行a.mjs以后会报错,foo变量未定义,这是为什么?

    +

    让我们一行行来看,ES6 循环加载是怎么处理的。首先,执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。

    +

    解决这个问题的方法,就是让b.mjs运行的时候,foo已经有定义了。这可以通过将foo写成函数来解决。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // a.mjs
    import { bar } from "./b";
    console.log("a.mjs");
    console.log(bar());
    function foo() {
    return "foo";
    }
    export { foo };

    // b.mjs
    import { foo } from "./a";
    console.log("b.mjs");
    console.log(foo());
    function bar() {
    return "bar";
    }
    export { bar };
    + +

    这时再执行a.mjs就可以得到预期结果。

    +
    1
    2
    3
    4
    5
    $ node --experimental-modules a.mjs
    b.mjs
    foo
    a.mjs
    bar
    + +

    这是因为函数具有提升作用,在执行import {bar} from './b'时,函数foo就已经有定义了,所以b.mjs加载的时候不会报错。这也意味着,如果把函数foo改写成函数表达式,也会报错。

    +
    1
    2
    3
    4
    5
    6
    // a.mjs
    import { bar } from "./b";
    console.log("a.mjs");
    console.log(bar());
    const foo = () => "foo";
    export { foo };
    + +

    上面代码的第四行,改成了函数表达式,就不具有提升作用,执行就会报错。

    +

    我们再来看 ES6 模块加载器SystemJS给出的一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // even.js
    import { odd } from "./odd";
    export var counter = 0;
    export function even(n) {
    counter++;
    return n === 0 || odd(n - 1);
    }

    // odd.js
    import { even } from "./even";
    export function odd(n) {
    return n !== 0 && even(n - 1);
    }
    + +

    上面代码中,even.js里面的函数even有一个参数n,只要不等于 0,就会减去 1,传入加载的odd()odd.js也会做类似操作。

    +

    运行上面这段代码,结果如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    $ babel-node
    > import * as m from './even.js';
    > m.even(10);
    true
    > m.counter
    6
    > m.even(20)
    true
    > m.counter
    17
    + +

    上面代码中,参数n从 10 变为 0 的过程中,even()一共会执行 6 次,所以变量counter等于 6。第二次调用even()时,参数n从 20 变为 0,even()一共会执行 11 次,加上前面的 6 次,所以变量counter等于 17。

    +

    这个例子要是改写成 CommonJS,就根本无法执行,会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // even.js
    var odd = require("./odd");
    var counter = 0;
    exports.counter = counter;
    exports.even = function (n) {
    counter++;
    return n == 0 || odd(n - 1);
    };

    // odd.js
    var even = require("./even").even;
    module.exports = function (n) {
    return n != 0 && even(n - 1);
    };
    + +

    上面代码中,even.js加载odd.js,而odd.js又去加载even.js,形成“循环加载”。这时,执行引擎就会输出even.js已经执行的部分(不存在任何结果),所以在odd.js之中,变量even等于undefined,等到后面调用even(n - 1)就会报错。

    +
    1
    2
    3
    4
    $ node
    > var m = require('./even');
    > m.even(10)
    TypeError: even is not a function
    + +

    ES6 模块的转码

    浏览器目前还不支持 ES6 模块,为了现在就能使用,可以将转为 ES5 的写法。除了 Babel 可以用来转码之外,还有以下两个方法,也可以用来转码。

    +

    ES6 module transpiler

    ES6 module transpiler是 square 公司开源的一个转码器,可以将 ES6 模块转为 CommonJS 模块或 AMD 模块的写法,从而在浏览器中使用。

    +

    首先,安装这个转码器。

    +
    1
    $ npm install -g es6-module-transpiler
    + +

    然后,使用compile-modules convert命令,将 ES6 模块文件转码。

    +
    1
    $ compile-modules convert file1.js file2.js
    + +

    -o参数可以指定转码后的文件名。

    +
    1
    $ compile-modules convert -o out.js file1.js
    + +

    SystemJS

    另一种解决方法是使用 SystemJS。它是一个垫片库(polyfill),可以在浏览器内加载 ES6 模块、AMD 模块和 CommonJS 模块,将其转为 ES5 格式。它在后台调用的是 Google 的 Traceur 转码器。

    +

    使用时,先在网页内载入system.js文件。

    +
    1
    <script src="system.js"></script>
    + +

    然后,使用System.import方法加载模块文件。

    +
    1
    2
    3
    <script>
    System.import("./app.js");
    </script>
    + +

    上面代码中的./app,指的是当前目录下的 app.js 文件。它可以是 ES6 模块文件,System.import会自动将其转码。

    +

    需要注意的是,System.import使用异步加载,返回一个 Promise 对象,可以针对这个对象编程。下面是一个模块文件。

    +
    1
    2
    3
    4
    5
    6
    7
    // app/es6-file.js:

    export class q {
    constructor() {
    this.es6 = "hello";
    }
    }
    + +

    然后,在网页内加载这个模块文件。

    +
    1
    2
    3
    4
    5
    <script>
    System.import("app/es6-file").then(function (m) {
    console.log(new m.q().es6); // hello
    });
    </script>
    + +

    上面代码中,System.import方法返回的是一个 Promise 对象,所以可以用then方法指定回调函数。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Module%20%E7%9A%84%E5%8A%A0%E8%BD%BD%E5%AE%9E%E7%8E%B0/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Module \347\232\204\350\257\255\346\263\225/index.html" "b/posts/ES6-Module \347\232\204\350\257\255\346\263\225/index.html" new file mode 100644 index 00000000..a8bea555 --- /dev/null +++ "b/posts/ES6-Module \347\232\204\350\257\255\346\263\225/index.html" @@ -0,0 +1,474 @@ +Module 的语法 | JCAlways + + + + + + + + + + + + + +

    Module 的语法

    Module 的语法

    概述

    历史上,JavaScript 一直没有模块(module)体系,无法将一个大程序拆分成互相依赖的小文件,再用简单的方法拼装起来。其他语言都有这项功能,比如 Ruby 的require、Python 的import,甚至就连 CSS 都有@import,但是 JavaScript 任何这方面的支持都没有,这对开发大型的、复杂的项目形成了巨大障碍。

    +

    在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案。

    +

    ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。比如,CommonJS 模块就是对象,输入时必须查找对象属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // CommonJS模块
    let { stat, exists, readFile } = require("fs");

    // 等同于
    let _fs = require("fs");
    let stat = _fs.stat;
    let exists = _fs.exists;
    let readfile = _fs.readfile;
    + +

    上面代码的实质是整体加载fs模块(即加载fs的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。

    +

    ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。

    +
    1
    2
    // ES6模块
    import { stat, exists, readFile } from "fs";
    + +

    上面代码的实质是从fs模块加载 3 个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比 CommonJS 模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。

    +

    由于 ES6 模块是编译时加载,使得静态分析成为可能。有了它,就能进一步拓宽 JavaScript 的语法,比如引入宏(macro)和类型检验(type system)这些只能靠静态分析实现的功能。

    +

    除了静态加载带来的各种好处,ES6 模块还有以下好处。

    +
      +
    • 不再需要UMD模块格式了,将来服务器和浏览器都会支持 ES6 模块格式。目前,通过各种工具库,其实已经做到了这一点。
    • +
    • 将来浏览器的新 API 就能用模块格式提供,不再必须做成全局变量或者navigator对象的属性。
    • +
    • 不再需要对象作为命名空间(比如Math对象),未来这些功能可以通过模块提供。
    • +
    +

    本章介绍 ES6 模块的语法,下一章介绍如何在浏览器和 Node 之中,加载 ES6 模块。

    +

    严格模式

    ES6 的模块自动采用严格模式,不管你有没有在模块头部加上"use strict";

    +

    严格模式主要有以下限制。

    +
      +
    • 变量必须声明后再使用
    • +
    • 函数的参数不能有同名属性,否则报错
    • +
    • 不能使用with语句
    • +
    • 不能对只读属性赋值,否则报错
    • +
    • 不能使用前缀 0 表示八进制数,否则报错
    • +
    • 不能删除不可删除的属性,否则报错
    • +
    • 不能删除变量delete prop,会报错,只能删除属性delete global[prop]
    • +
    • eval不会在它的外层作用域引入变量
    • +
    • evalarguments不能被重新赋值
    • +
    • arguments不会自动反映函数参数的变化
    • +
    • 不能使用arguments.callee
    • +
    • 不能使用arguments.caller
    • +
    • 禁止this指向全局对象
    • +
    • 不能使用fn.callerfn.arguments获取函数调用的堆栈
    • +
    • 增加了保留字(比如protectedstaticinterface
    • +
    +

    上面这些限制,模块都必须遵守。由于严格模式是 ES5 引入的,不属于 ES6,所以请参阅相关 ES5 书籍,本书不再详细介绍了。

    +

    其中,尤其需要注意this的限制。ES6 模块之中,顶层的this指向undefined,即不应该在顶层代码使用this

    +

    export 命令

    模块功能主要由两个命令构成:exportimportexport命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能。

    +

    一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个 JS 文件,里面使用export命令输出变量。

    +
    1
    2
    3
    4
    // profile.js
    export var firstName = "Michael";
    export var lastName = "Jackson";
    export var year = 1958;
    + +

    上面代码是profile.js文件,保存了用户信息。ES6 将其视为一个模块,里面用export命令对外部输出了三个变量。

    +

    export的写法,除了像上面这样,还有另外一种。

    +
    1
    2
    3
    4
    5
    6
    // profile.js
    var firstName = "Michael";
    var lastName = "Jackson";
    var year = 1958;

    export { firstName, lastName, year };
    + +

    上面代码在export命令后面,使用大括号指定所要输出的一组变量。它与前一种写法(直接放置在var语句前)是等价的,但是应该优先考虑使用这种写法。因为这样就可以在脚本尾部,一眼看清楚输出了哪些变量。

    +

    export命令除了输出变量,还可以输出函数或类(class)。

    +
    1
    2
    3
    export function multiply(x, y) {
    return x * y;
    }
    + +

    上面代码对外输出一个函数multiply

    +

    通常情况下,export输出的变量就是本来的名字,但是可以使用as关键字重命名。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function v1() { ... }
    function v2() { ... }

    export {
    v1 as streamV1,
    v2 as streamV2,
    v2 as streamLatestVersion
    };
    + +

    上面代码使用as关键字,重命名了函数v1v2的对外接口。重命名后,v2可以用不同的名字输出两次。

    +

    需要特别注意的是,export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系。

    +
    1
    2
    3
    4
    5
    6
    // 报错
    export 1;

    // 报错
    var m = 1;
    export m;
    + +

    上面两种写法都会报错,因为没有提供对外的接口。第一种写法直接输出 1,第二种写法通过变量m,还是直接输出 1。1只是一个值,不是接口。正确的写法是下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 写法一
    export var m = 1;

    // 写法二
    var m = 1;
    export { m };

    // 写法三
    var n = 1;
    export { n as m };
    + +

    上面三种写法都是正确的,规定了对外的接口m。其他脚本可以通过这个接口,取到值1。它们的实质是,在接口名与模块内部变量之间,建立了一一对应的关系。

    +

    同样的,functionclass的输出,也必须遵守这样的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 报错
    function f() {}
    export f;

    // 正确
    export function f() {};

    // 正确
    function f() {}
    export {f};
    + +

    另外,export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值。

    +
    1
    2
    export var foo = "bar";
    setTimeout(() => (foo = "baz"), 500);
    + +

    上面代码输出变量foo,值为bar,500 毫秒之后变成baz

    +

    这一点与 CommonJS 规范完全不同。CommonJS 模块输出的是值的缓存,不存在动态更新,详见下文《Module 的加载实现》一节。

    +

    最后,export命令可以出现在模块的任何位置,只要处于模块顶层就可以。如果处于块级作用域内,就会报错,下一节的import命令也是如此。这是因为处于条件代码块之中,就没法做静态优化了,违背了 ES6 模块的设计初衷。

    +
    1
    2
    3
    4
    function foo() {
    export default "bar"; // SyntaxError
    }
    foo();
    + +

    上面代码中,export语句放在函数之中,结果报错。

    +

    import 命令

    使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块。

    +
    1
    2
    3
    4
    5
    6
    // main.js
    import { firstName, lastName, year } from "./profile.js";

    function setName(element) {
    element.textContent = firstName + " " + lastName;
    }
    + +

    上面代码的import命令,用于加载profile.js文件,并从中输入变量。import命令接受一对大括号,里面指定要从其他模块导入的变量名。大括号里面的变量名,必须与被导入模块(profile.js)对外接口的名称相同。

    +

    如果想为输入的变量重新取一个名字,import命令要使用as关键字,将输入的变量重命名。

    +
    1
    import { lastName as surname } from "./profile.js";
    + +

    import命令输入的变量都是只读的,因为它的本质是输入接口。也就是说,不允许在加载模块的脚本里面,改写接口。

    +
    1
    2
    3
    import { a } from "./xxx.js";

    a = {}; // Syntax Error : 'a' is read-only;
    + +

    上面代码中,脚本加载了变量a,对其重新赋值就会报错,因为a是一个只读的接口。但是,如果a是一个对象,改写a的属性是允许的。

    +
    1
    2
    3
    import { a } from "./xxx.js";

    a.foo = "hello"; // 合法操作
    + +

    上面代码中,a的属性可以成功改写,并且其他模块也可以读到改写后的值。不过,这种写法很难查错,建议凡是输入的变量,都当作完全只读,轻易不要改变它的属性。

    +

    import后面的from指定模块文件的位置,可以是相对路径,也可以是绝对路径,.js后缀可以省略。如果只是模块名,不带有路径,那么必须有配置文件,告诉 JavaScript 引擎该模块的位置。

    +
    1
    import { myMethod } from "util";
    + +

    上面代码中,util是模块文件名,由于不带有路径,必须通过配置,告诉引擎怎么取到这个模块。

    +

    注意,import命令具有提升效果,会提升到整个模块的头部,首先执行。

    +
    1
    2
    3
    foo();

    import { foo } from "my_module";
    + +

    上面的代码不会报错,因为import的执行早于foo的调用。这种行为的本质是,import命令是编译阶段执行的,在代码运行之前。

    +

    由于import是静态执行,所以不能使用表达式和变量,这些只有在运行时才能得到结果的语法结构。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // 报错
    import { 'f' + 'oo' } from 'my_module';

    // 报错
    let module = 'my_module';
    import { foo } from module;

    // 报错
    if (x === 1) {
    import { foo } from 'module1';
    } else {
    import { foo } from 'module2';
    }
    + +

    上面三种写法都会报错,因为它们用到了表达式、变量和if结构。在静态分析阶段,这些语法都是没法得到值的。

    +

    最后,import语句会执行所加载的模块,因此可以有下面的写法。

    +
    1
    import "lodash";
    + +

    上面代码仅仅执行lodash模块,但是不输入任何值。

    +

    如果多次重复执行同一句import语句,那么只会执行一次,而不会执行多次。

    +
    1
    2
    import "lodash";
    import "lodash";
    + +

    上面代码加载了两次lodash,但是只会执行一次。

    +
    1
    2
    3
    4
    5
    import { foo } from "my_module";
    import { bar } from "my_module";

    // 等同于
    import { foo, bar } from "my_module";
    + +

    上面代码中,虽然foobar在两个语句中加载,但是它们对应的是同一个my_module实例。也就是说,import语句是 Singleton 模式。

    +

    目前阶段,通过 Babel 转码,CommonJS 模块的require命令和 ES6 模块的import命令,可以写在同一个模块里面,但是最好不要这样做。因为import在静态解析阶段执行,所以它是一个模块之中最早执行的。下面的代码可能不会得到预期结果。

    +
    1
    2
    3
    require("core-js/modules/es6.symbol");
    require("core-js/modules/es6.promise");
    import React from "React";
    + +

    模块的整体加载

    除了指定加载某个输出值,还可以使用整体加载,即用星号(*)指定一个对象,所有输出值都加载在这个对象上面。

    +

    下面是一个circle.js文件,它输出两个方法areacircumference

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // circle.js

    export function area(radius) {
    return Math.PI * radius * radius;
    }

    export function circumference(radius) {
    return 2 * Math.PI * radius;
    }
    + +

    现在,加载这个模块。

    +
    1
    2
    3
    4
    5
    6
    // main.js

    import { area, circumference } from "./circle";

    console.log("圆面积:" + area(4));
    console.log("圆周长:" + circumference(14));
    + +

    上面写法是逐一指定要加载的方法,整体加载的写法如下。

    +
    1
    2
    3
    4
    import * as circle from "./circle";

    console.log("圆面积:" + circle.area(4));
    console.log("圆周长:" + circle.circumference(14));
    + +

    注意,模块整体加载所在的那个对象(上例是circle),应该是可以静态分析的,所以不允许运行时改变。下面的写法都是不允许的。

    +
    1
    2
    3
    4
    5
    import * as circle from "./circle";

    // 下面两行都是不允许的
    circle.foo = "hello";
    circle.area = function () {};
    + +

    export default 命令

    从前面的例子可以看出,使用import命令的时候,用户需要知道所要加载的变量名或函数名,否则无法加载。但是,用户肯定希望快速上手,未必愿意阅读文档,去了解模块有哪些属性和方法。

    +

    为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。

    +
    1
    2
    3
    4
    // export-default.js
    export default function () {
    console.log("foo");
    }
    + +

    上面代码是一个模块文件export-default.js,它的默认输出是一个函数。

    +

    其他模块加载该模块时,import命令可以为该匿名函数指定任意名字。

    +
    1
    2
    3
    // import-default.js
    import customName from "./export-default";
    customName(); // 'foo'
    + +

    上面代码的import命令,可以用任意名称指向export-default.js输出的方法,这时就不需要知道原模块输出的函数名。需要注意的是,这时import命令后面,不使用大括号。

    +

    export default命令用在非匿名函数前,也是可以的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // export-default.js
    export default function foo() {
    console.log("foo");
    }

    // 或者写成

    function foo() {
    console.log("foo");
    }

    export default foo;
    + +

    上面代码中,foo函数的函数名foo,在模块外部是无效的。加载的时候,视同匿名函数加载。

    +

    下面比较一下默认输出和正常输出。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 第一组
    export default function crc32() {
    // 输出
    // ...
    }

    import crc32 from "crc32"; // 输入

    // 第二组
    export function crc32() {
    // 输出
    // ...
    }

    import { crc32 } from "crc32"; // 输入
    + +

    上面代码的两组写法,第一组是使用export default时,对应的import语句不需要使用大括号;第二组是不使用export default时,对应的import语句需要使用大括号。

    +

    export default命令用于指定模块的默认输出。显然,一个模块只能有一个默认输出,因此export default命令只能使用一次。所以,import 命令后面才不用加大括号,因为只可能唯一对应export default命令。

    +

    本质上,export default就是输出一个叫做default的变量或方法,然后系统允许你为它取任意名字。所以,下面的写法是有效的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // modules.js
    function add(x, y) {
    return x * y;
    }
    export { add as default };
    // 等同于
    // export default add;

    // app.js
    import { default as foo } from "modules";
    // 等同于
    // import foo from 'modules';
    + +

    正是因为export default命令其实只是输出一个叫做default的变量,所以它后面不能跟变量声明语句。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 正确
    export var a = 1;

    // 正确
    var a = 1;
    export default a;

    // 错误
    export default var a = 1;
    + +

    上面代码中,export default a的含义是将变量a的值赋给变量default。所以,最后一种写法会报错。

    +

    同样地,因为export default命令的本质是将后面的值,赋给default变量,所以可以直接将一个值写在export default之后。

    +
    1
    2
    3
    4
    5
    // 正确
    export default 42;

    // 报错
    export 42;
    + +

    上面代码中,后一句报错是因为没有指定对外的接口,而前一句指定对外接口为default

    +

    有了export default命令,输入模块时就非常直观了,以输入 lodash 模块为例。

    +
    1
    import _ from "lodash";
    + +

    如果想在一条import语句中,同时输入默认方法和其他接口,可以写成下面这样。

    +
    1
    import _, { each, each as forEach } from "lodash";
    + +

    对应上面代码的export语句如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    export default function (obj) {
    // ···
    }

    export function each(obj, iterator, context) {
    // ···
    }

    export { each as forEach };
    + +

    上面代码的最后一行的意思是,暴露出forEach接口,默认指向each接口,即forEacheach指向同一个方法。

    +

    export default也可以用来输出类。

    +
    1
    2
    3
    4
    5
    6
    // MyClass.js
    export default class { ... }

    // main.js
    import MyClass from 'MyClass';
    let o = new MyClass();
    + +

    export 与 import 的复合写法

    如果在一个模块之中,先输入后输出同一个模块,import语句可以与export语句写在一起。

    +
    1
    2
    3
    4
    5
    export { foo, bar } from "my_module";

    // 可以简单理解为
    import { foo, bar } from "my_module";
    export { foo, bar };
    + +

    上面代码中,exportimport语句可以结合在一起,写成一行。但需要注意的是,写成一行以后,foobar实际上并没有被导入当前模块,只是相当于对外转发了这两个接口,导致当前模块不能直接使用foobar

    +

    模块的接口改名和整体输出,也可以采用这种写法。

    +
    1
    2
    3
    4
    5
    // 接口改名
    export { foo as myFoo } from "my_module";

    // 整体输出
    export * from "my_module";
    + +

    默认接口的写法如下。

    +
    1
    export { default } from "foo";
    + +

    具名接口改为默认接口的写法如下。

    +
    1
    2
    3
    4
    5
    export { es6 as default } from "./someModule";

    // 等同于
    import { es6 } from "./someModule";
    export default es6;
    + +

    同样地,默认接口也可以改名为具名接口。

    +
    1
    export { default as es6 } from "./someModule";
    + +

    下面三种import语句,没有对应的复合写法。

    +
    1
    2
    3
    import * as someIdentifier from "someModule";
    import someIdentifier from "someModule";
    import someIdentifier, { namedIdentifier } from "someModule";
    + +

    为了做到形式的对称,现在有提案,提出补上这三种复合写法。

    +
    1
    2
    3
    export * as someIdentifier from "someModule";
    export someIdentifier from "someModule";
    export someIdentifier, { namedIdentifier } from "someModule";
    + +

    模块的继承

    模块之间也可以继承。

    +

    假设有一个circleplus模块,继承了circle模块。

    +
    1
    2
    3
    4
    5
    6
    7
    // circleplus.js

    export * from "circle";
    export var e = 2.71828182846;
    export default function (x) {
    return Math.exp(x);
    }
    + +

    上面代码中的export *,表示再输出circle模块的所有属性和方法。注意,export *命令会忽略circle模块的default方法。然后,上面代码又输出了自定义的e变量和默认方法。

    +

    这时,也可以将circle的属性或方法,改名后再输出。

    +
    1
    2
    3
    // circleplus.js

    export { area as circleArea } from "circle";
    + +

    上面代码表示,只输出circle模块的area方法,且将其改名为circleArea

    +

    加载上面模块的写法如下。

    +
    1
    2
    3
    4
    5
    // main.js

    import * as math from "circleplus";
    import exp from "circleplus";
    console.log(exp(math.e));
    + +

    上面代码中的import exp表示,将circleplus模块的默认方法加载为exp方法。

    +

    跨模块常量

    本书介绍const命令的时候说过,const声明的常量只在当前代码块有效。如果想设置跨模块的常量(即跨多个文件),或者说一个值要被多个模块共享,可以采用下面的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // constants.js 模块
    export const A = 1;
    export const B = 3;
    export const C = 4;

    // test1.js 模块
    import * as constants from "./constants";
    console.log(constants.A); // 1
    console.log(constants.B); // 3

    // test2.js 模块
    import { A, B } from "./constants";
    console.log(A); // 1
    console.log(B); // 3
    + +

    如果要使用的常量非常多,可以建一个专门的constants目录,将各种常量写在不同的文件里面,保存在该目录下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // constants/db.js
    export const db = {
    url: "http://my.couchdbserver.local:5984",
    admin_username: "admin",
    admin_password: "admin password",
    };

    // constants/user.js
    export const users = ["root", "admin", "staff", "ceo", "chief", "moderator"];
    + +

    然后,将这些文件输出的常量,合并在index.js里面。

    +
    1
    2
    3
    // constants/index.js
    export { db } from "./db";
    export { users } from "./users";
    + +

    使用的时候,直接加载index.js就可以了。

    +
    1
    2
    // script.js
    import { db, users } from "./constants/index";
    + +

    import()

    简介

    前面介绍过,import命令会被 JavaScript 引擎静态分析,先于模块内的其他语句执行(import命令叫做“连接” binding 其实更合适)。所以,下面的代码会报错。

    +
    1
    2
    3
    4
    // 报错
    if (x === 2) {
    import MyModual from "./myModual";
    }
    + +

    上面代码中,引擎处理import语句是在编译时,这时不会去分析或执行if语句,所以import语句放在if代码块之中毫无意义,因此会报句法错误,而不是执行时错误。也就是说,importexport命令只能在模块的顶层,不能在代码块之中(比如,在if代码块之中,或在函数之中)。

    +

    这样的设计,固然有利于编译器提高效率,但也导致无法在运行时加载模块。在语法上,条件加载就不可能实现。如果import命令要取代 Node 的require方法,这就形成了一个障碍。因为require是运行时加载模块,import命令无法取代require的动态加载功能。

    +
    1
    2
    const path = "./" + fileName;
    const myModual = require(path);
    + +

    上面的语句就是动态加载,require到底加载哪一个模块,只有运行时才知道。import命令做不到这一点。

    +

    因此,有一个提案,建议引入import()函数,完成动态加载。

    +
    1
    import(specifier);
    + +

    上面代码中,import函数的参数specifier,指定所要加载的模块的位置。import命令能够接受什么参数,import()函数就能接受什么参数,两者区别主要是后者为动态加载。

    +

    import()返回一个 Promise 对象。下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const main = document.querySelector("main");

    import(`./section-modules/${someVariable}.js`)
    .then((module) => {
    module.loadPageInto(main);
    })
    .catch((err) => {
    main.textContent = err.message;
    });
    + +

    import()函数可以用在任何地方,不仅仅是模块,非模块的脚本也可以使用。它是运行时执行,也就是说,什么时候运行到这一句,就会加载指定的模块。另外,import()函数与所加载的模块没有静态连接关系,这点也是与import语句不相同。import()类似于 Node 的require方法,区别主要是前者是异步加载,后者是同步加载。

    +

    适用场合

    下面是import()的一些适用场合。

    +

    (1)按需加载。

    +

    import()可以在需要的时候,再加载某个模块。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    button.addEventListener("click", (event) => {
    import("./dialogBox.js")
    .then((dialogBox) => {
    dialogBox.open();
    })
    .catch((error) => {
    /* Error handling */
    });
    });
    + +

    上面代码中,import()方法放在click事件的监听函数之中,只有用户点击了按钮,才会加载这个模块。

    +

    (2)条件加载

    +

    import()可以放在if代码块,根据不同的情况,加载不同的模块。

    +
    1
    2
    3
    4
    5
    if (condition) {
    import('moduleA').then(...);
    } else {
    import('moduleB').then(...);
    }
    + +

    上面代码中,如果满足条件,就加载模块 A,否则加载模块 B。

    +

    (3)动态的模块路径

    +

    import()允许模块路径动态生成。

    +
    1
    2
    import(f())
    .then(...);
    + +

    上面代码中,根据函数f的返回结果,加载不同的模块。

    +

    注意点

    import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。

    +
    1
    2
    3
    import("./myModule.js").then(({ export1, export2 }) => {
    // ...·
    });
    + +

    上面代码中,export1export2都是myModule.js的输出接口,可以解构获得。

    +

    如果模块有default输出接口,可以用参数直接获得。

    +
    1
    2
    3
    import("./myModule.js").then((myModule) => {
    console.log(myModule.default);
    });
    + +

    上面的代码也可以使用具名输入的形式。

    +
    1
    2
    3
    import("./myModule.js").then(({ default: theDefault }) => {
    console.log(theDefault);
    });
    + +

    如果想同时加载多个模块,可以采用下面的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Promise.all([
    import('./module1.js'),
    import('./module2.js'),
    import('./module3.js'),
    ])
    .then(([module1, module2, module3]) => {
    ···
    });
    + +

    import()也可以用在 async 函数之中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function main() {
    const myModule = await import("./myModule.js");
    const { export1, export2 } = await import("./myModule.js");
    const [module1, module2, module3] = await Promise.all([
    import("./module1.js"),
    import("./module2.js"),
    import("./module3.js"),
    ]);
    }
    main();
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Module%20%E7%9A%84%E8%AF%AD%E6%B3%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-Promise \345\257\271\350\261\241/index.html" "b/posts/ES6-Promise \345\257\271\350\261\241/index.html" new file mode 100644 index 00000000..53aed84b --- /dev/null +++ "b/posts/ES6-Promise \345\257\271\350\261\241/index.html" @@ -0,0 +1,459 @@ +Promise 对象 | JCAlways + + + + + + + + + + + + + +

    Promise 对象

    Promise 对象

    Promise 的含义

    Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

    +

    所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

    +

    Promise对象有以下两个特点。

    +

    (1)对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是Promise这个名字的由来,它的英语意思就是“承诺”,表示其他手段无法改变。

    +

    (2)一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果,这时就称为 resolved(已定型)。如果改变已经发生了,你再对Promise对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

    +

    注意,为了行文方便,本章后面的resolved统一只指fulfilled状态,不包含rejected状态。

    +

    有了Promise对象,就可以将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

    +

    Promise也有一些缺点。首先,无法取消Promise,一旦新建它就会立即执行,无法中途取消。其次,如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。第三,当处于pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

    +

    如果某些事件不断地反复发生,一般来说,使用 Stream 模式是比部署Promise更好的选择。

    +

    基本用法

    ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。

    +

    下面代码创造了一个Promise实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const promise = new Promise(function(resolve, reject) {
    // ... some code

    if (/* 异步操作成功 */){
    resolve(value);
    } else {
    reject(error);
    }
    });
    + +

    Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolvereject。它们是两个函数,由 JavaScript 引擎提供,不用自己部署。

    +

    resolve函数的作用是,将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去;reject函数的作用是,将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。

    +

    Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    promise.then(
    function (value) {
    // success
    },
    function (error) {
    // failure
    }
    );
    + +

    then方法可以接受两个回调函数作为参数。第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象的状态变为rejected时调用。其中,第二个函数是可选的,不一定要提供。这两个函数都接受Promise对象传出的值作为参数。

    +

    下面是一个Promise对象的简单例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function timeout(ms) {
    return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, "done");
    });
    }

    timeout(100).then((value) => {
    console.log(value);
    });
    + +

    上面代码中,timeout方法返回一个Promise实例,表示一段时间以后才会发生的结果。过了指定的时间(ms参数)以后,Promise实例的状态变为resolved,就会触发then方法绑定的回调函数。

    +

    Promise 新建后就会立即执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let promise = new Promise(function (resolve, reject) {
    console.log("Promise");
    resolve();
    });

    promise.then(function () {
    console.log("resolved.");
    });

    console.log("Hi!");

    // Promise
    // Hi!
    // resolved
    + +

    上面代码中,Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行,所以resolved最后输出。

    +

    下面是异步加载图片的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function loadImageAsync(url) {
    return new Promise(function (resolve, reject) {
    const image = new Image();

    image.onload = function () {
    resolve(image);
    };

    image.onerror = function () {
    reject(new Error("Could not load image at " + url));
    };

    image.src = url;
    });
    }
    + +

    上面代码中,使用Promise包装了一个图片加载的异步操作。如果加载成功,就调用resolve方法,否则就调用reject方法。

    +

    下面是一个用Promise对象实现的 Ajax 操作的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    const getJSON = function (url) {
    const promise = new Promise(function (resolve, reject) {
    const handler = function () {
    if (this.readyState !== 4) {
    return;
    }
    if (this.status === 200) {
    resolve(this.response);
    } else {
    reject(new Error(this.statusText));
    }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();
    });

    return promise;
    };

    getJSON("/posts.json").then(
    function (json) {
    console.log("Contents: " + json);
    },
    function (error) {
    console.error("出错了", error);
    }
    );
    + +

    上面代码中,getJSON是对 XMLHttpRequest 对象的封装,用于发出一个针对 JSON 数据的 HTTP 请求,并且返回一个Promise对象。需要注意的是,在getJSON内部,resolve函数和reject函数调用时,都带有参数。

    +

    如果调用resolve函数和reject函数时带有参数,那么它们的参数会被传递给回调函数。reject函数的参数通常是Error对象的实例,表示抛出的错误;resolve函数的参数除了正常的值以外,还可能是另一个 Promise 实例,比如像下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const p1 = new Promise(function (resolve, reject) {
    // ...
    });

    const p2 = new Promise(function (resolve, reject) {
    // ...
    resolve(p1);
    });
    + +

    上面代码中,p1p2都是 Promise 的实例,但是p2resolve方法将p1作为参数,即一个异步操作的结果是返回另一个异步操作。

    +

    注意,这时p1的状态就会传递给p2,也就是说,p1的状态决定了p2的状态。如果p1的状态是pending,那么p2的回调函数就会等待p1的状态改变;如果p1的状态已经是resolved或者rejected,那么p2的回调函数将会立刻执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const p1 = new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error("fail")), 3000);
    });

    const p2 = new Promise(function (resolve, reject) {
    setTimeout(() => resolve(p1), 1000);
    });

    p2.then((result) => console.log(result)).catch((error) => console.log(error));
    // Error: fail
    + +

    上面代码中,p1是一个 Promise,3 秒之后变为rejectedp2的状态在 1 秒之后改变,resolve方法返回的是p1。由于p2返回的是另一个 Promise,导致p2自己的状态无效了,由p1的状态决定p2的状态。所以,后面的then语句都变成针对后者(p1)。又过了 2 秒,p1变为rejected,导致触发catch方法指定的回调函数。

    +

    注意,调用resolvereject并不会终结 Promise 的参数函数的执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    new Promise((resolve, reject) => {
    resolve(1);
    console.log(2);
    }).then((r) => {
    console.log(r);
    });
    // 2
    // 1
    + +

    上面代码中,调用resolve(1)以后,后面的console.log(2)还是会执行,并且会首先打印出来。这是因为立即 resolved 的 Promise 是在本轮事件循环的末尾执行,总是晚于本轮循环的同步任务。

    +

    一般来说,调用resolvereject以后,Promise 的使命就完成了,后继操作应该放到then方法里面,而不应该直接写在resolvereject的后面。所以,最好在它们前面加上return语句,这样就不会有意外。

    +
    1
    2
    3
    4
    5
    new Promise((resolve, reject) => {
    return resolve(1);
    // 后面的语句不会执行
    console.log(2);
    });
    + +

    Promise.prototype.then()

    Promise 实例具有then方法,也就是说,then方法是定义在原型对象Promise.prototype上的。它的作用是为 Promise 实例添加状态改变时的回调函数。前面说过,then方法的第一个参数是resolved状态的回调函数,第二个参数(可选)是rejected状态的回调函数。

    +

    then方法返回的是一个新的Promise实例(注意,不是原来那个Promise实例)。因此可以采用链式写法,即then方法后面再调用另一个then方法。

    +
    1
    2
    3
    4
    5
    6
    7
    getJSON("/posts.json")
    .then(function (json) {
    return json.post;
    })
    .then(function (post) {
    // ...
    });
    + +

    上面的代码使用then方法,依次指定了两个回调函数。第一个回调函数完成以后,会将返回结果作为参数,传入第二个回调函数。

    +

    采用链式的then,可以指定一组按照次序调用的回调函数。这时,前一个回调函数,有可能返回的还是一个Promise对象(即有异步操作),这时后一个回调函数,就会等待该Promise对象的状态发生变化,才会被调用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    getJSON("/post/1.json")
    .then(function (post) {
    return getJSON(post.commentURL);
    })
    .then(
    function funcA(comments) {
    console.log("resolved: ", comments);
    },
    function funcB(err) {
    console.log("rejected: ", err);
    }
    );
    + +

    上面代码中,第一个then方法指定的回调函数,返回的是另一个Promise对象。这时,第二个then方法指定的回调函数,就会等待这个新的Promise对象状态发生变化。如果变为resolved,就调用funcA,如果状态变为rejected,就调用funcB

    +

    如果采用箭头函数,上面的代码可以写得更简洁。

    +
    1
    2
    3
    4
    5
    6
    getJSON("/post/1.json")
    .then((post) => getJSON(post.commentURL))
    .then(
    (comments) => console.log("resolved: ", comments),
    (err) => console.log("rejected: ", err)
    );
    + +

    Promise.prototype.catch()

    Promise.prototype.catch方法是.then(null, rejection)的别名,用于指定发生错误时的回调函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    getJSON("/posts.json")
    .then(function (posts) {
    // ...
    })
    .catch(function (error) {
    // 处理 getJSON 和 前一个回调函数运行时发生的错误
    console.log("发生错误!", error);
    });
    + +

    上面代码中,getJSON方法返回一个 Promise 对象,如果该对象状态变为resolved,则会调用then方法指定的回调函数;如果异步操作抛出错误,状态就会变为rejected,就会调用catch方法指定的回调函数,处理这个错误。另外,then方法指定的回调函数,如果运行中抛出错误,也会被catch方法捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    p.then((val) => console.log("fulfilled:", val)).catch((err) =>
    console.log("rejected", err)
    );

    // 等同于
    p.then((val) => console.log("fulfilled:", val)).then(null, (err) =>
    console.log("rejected:", err)
    );
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    const promise = new Promise(function (resolve, reject) {
    throw new Error("test");
    });
    promise.catch(function (error) {
    console.log(error);
    });
    // Error: test
    + +

    上面代码中,promise抛出一个错误,就被catch方法指定的回调函数捕获。注意,上面的写法与下面两种写法是等价的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 写法一
    const promise = new Promise(function (resolve, reject) {
    try {
    throw new Error("test");
    } catch (e) {
    reject(e);
    }
    });
    promise.catch(function (error) {
    console.log(error);
    });

    // 写法二
    const promise = new Promise(function (resolve, reject) {
    reject(new Error("test"));
    });
    promise.catch(function (error) {
    console.log(error);
    });
    + +

    比较上面两种写法,可以发现reject方法的作用,等同于抛出错误。

    +

    如果 Promise 状态已经变成resolved,再抛出错误是无效的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const promise = new Promise(function (resolve, reject) {
    resolve("ok");
    throw new Error("test");
    });
    promise
    .then(function (value) {
    console.log(value);
    })
    .catch(function (error) {
    console.log(error);
    });
    // ok
    + +

    上面代码中,Promise 在resolve语句后面,再抛出错误,不会被捕获,等于没有抛出。因为 Promise 的状态一旦改变,就永久保持该状态,不会再变了。

    +

    Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    getJSON("/post/1.json")
    .then(function (post) {
    return getJSON(post.commentURL);
    })
    .then(function (comments) {
    // some code
    })
    .catch(function (error) {
    // 处理前面三个Promise产生的错误
    });
    + +

    上面代码中,一共有三个 Promise 对象:一个由getJSON产生,两个由then产生。它们之中任何一个抛出的错误,都会被最后一个catch捕获。

    +

    一般来说,不要在then方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // bad
    promise.then(
    function (data) {
    // success
    },
    function (err) {
    // error
    }
    );

    // good
    promise
    .then(function (data) {
    //cb
    // success
    })
    .catch(function (err) {
    // error
    });
    + +

    上面代码中,第二种写法要好于第一种写法,理由是第二种写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。因此,建议总是使用catch方法,而不使用then方法的第二个参数。

    +

    跟传统的try/catch代码块不同的是,如果没有使用catch方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const someAsyncThing = function () {
    return new Promise(function (resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
    });
    };

    someAsyncThing().then(function () {
    console.log("everything is great");
    });

    setTimeout(() => {
    console.log(123);
    }, 2000);
    // Uncaught (in promise) ReferenceError: x is not defined
    // 123
    + +

    上面代码中,someAsyncThing函数产生的 Promise 对象,内部有语法错误。浏览器运行到这一行,会打印出错误提示ReferenceError: x is not defined,但是不会退出进程、终止脚本执行,2 秒之后还是会输出123。这就是说,Promise 内部的错误不会影响到 Promise 外部的代码,通俗的说法就是“Promise 会吃掉错误”。

    +

    这个脚本放在服务器执行,退出码就是0(即表示执行成功)。不过,Node 有一个unhandledRejection事件,专门监听未捕获的reject错误,上面的脚本会触发这个事件的监听函数,可以在监听函数里面抛出错误。

    +
    1
    2
    3
    process.on("unhandledRejection", function (err, p) {
    throw err;
    });
    + +

    上面代码中,unhandledRejection事件的监听函数有两个参数,第一个是错误对象,第二个是报错的 Promise 实例,它可以用来了解发生错误的环境信息。

    +

    注意,Node 有计划在未来废除unhandledRejection事件。如果 Promise 内部有未捕获的错误,会直接终止进程,并且进程的退出码不为 0。

    +

    再看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const promise = new Promise(function (resolve, reject) {
    resolve("ok");
    setTimeout(function () {
    throw new Error("test");
    }, 0);
    });
    promise.then(function (value) {
    console.log(value);
    });
    // ok
    // Uncaught Error: test
    + +

    上面代码中,Promise 指定在下一轮“事件循环”再抛出错误。到了那个时候,Promise 的运行已经结束了,所以这个错误是在 Promise 函数体外抛出的,会冒泡到最外层,成了未捕获的错误。

    +

    一般总是建议,Promise 对象后面要跟catch方法,这样可以处理 Promise 内部发生的错误。catch方法返回的还是一个 Promise 对象,因此后面还可以接着调用then方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const someAsyncThing = function () {
    return new Promise(function (resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
    });
    };

    someAsyncThing()
    .catch(function (error) {
    console.log("oh no", error);
    })
    .then(function () {
    console.log("carry on");
    });
    // oh no [ReferenceError: x is not defined]
    // carry on
    + +

    上面代码运行完catch方法指定的回调函数,会接着运行后面那个then方法指定的回调函数。如果没有报错,则会跳过catch方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Promise.resolve()
    .catch(function (error) {
    console.log("oh no", error);
    })
    .then(function () {
    console.log("carry on");
    });
    // carry on
    + +

    上面的代码因为没有报错,跳过了catch方法,直接执行后面的then方法。此时,要是then方法里面报错,就与前面的catch无关了。

    +

    catch方法之中,还能再抛出错误。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const someAsyncThing = function () {
    return new Promise(function (resolve, reject) {
    // 下面一行会报错,因为x没有声明
    resolve(x + 2);
    });
    };

    someAsyncThing()
    .then(function () {
    return someOtherAsyncThing();
    })
    .catch(function (error) {
    console.log("oh no", error);
    // 下面一行会报错,因为 y 没有声明
    y + 2;
    })
    .then(function () {
    console.log("carry on");
    });
    // oh no [ReferenceError: x is not defined]
    + +

    上面代码中,catch方法抛出一个错误,因为后面没有别的catch方法了,导致这个错误不会被捕获,也不会传递到外层。如果改写一下,结果就不一样了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    someAsyncThing()
    .then(function () {
    return someOtherAsyncThing();
    })
    .catch(function (error) {
    console.log("oh no", error);
    // 下面一行会报错,因为y没有声明
    y + 2;
    })
    .catch(function (error) {
    console.log("carry on", error);
    });
    // oh no [ReferenceError: x is not defined]
    // carry on [ReferenceError: y is not defined]
    + +

    上面代码中,第二个catch方法用来捕获前一个catch方法抛出的错误。

    +

    Promise.prototype.finally()

    finally方法用于指定不管 Promise 对象最后状态如何,都会执行的操作。该方法是 ES2018 引入标准的。

    +
    1
    2
    3
    4
    promise
    .then(result => {···})
    .catch(error => {···})
    .finally(() => {···});
    + +

    上面代码中,不管promise最后的状态,在执行完thencatch指定的回调函数以后,都会执行finally方法指定的回调函数。

    +

    下面是一个例子,服务器使用 Promise 处理请求,然后使用finally方法关掉服务器。

    +
    1
    2
    3
    4
    5
    6
    server
    .listen(port)
    .then(function () {
    // ...
    })
    .finally(server.stop);
    + +

    finally方法的回调函数不接受任何参数,这意味着没有办法知道,前面的 Promise 状态到底是fulfilled还是rejected。这表明,finally方法里面的操作,应该是与状态无关的,不依赖于 Promise 的执行结果。

    +

    finally本质上是then方法的特例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    promise.finally(() => {
    // 语句
    });

    // 等同于
    promise.then(
    (result) => {
    // 语句
    return result;
    },
    (error) => {
    // 语句
    throw error;
    }
    );
    + +

    上面代码中,如果不使用finally方法,同样的语句需要为成功和失败两种情况各写一次。有了finally方法,则只需要写一次。

    +

    它的实现也很简单。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    return this.then(
    (value) => P.resolve(callback()).then(() => value),
    (reason) =>
    P.resolve(callback()).then(() => {
    throw reason;
    })
    );
    };
    + +

    上面代码中,不管前面的 Promise 是fulfilled还是rejected,都会执行回调函数callback

    +

    从上面的实现还可以看到,finally方法总是会返回原来的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // resolve 的值是 undefined
    Promise.resolve(2).then(
    () => {},
    () => {}
    );

    // resolve 的值是 2
    Promise.resolve(2).finally(() => {});

    // reject 的值是 undefined
    Promise.reject(3).then(
    () => {},
    () => {}
    );

    // reject 的值是 3
    Promise.reject(3).finally(() => {});
    + +

    Promise.all()

    Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例。

    +
    1
    const p = Promise.all([p1, p2, p3]);
    + +

    上面代码中,Promise.all方法接受一个数组作为参数,p1p2p3都是 Promise 实例,如果不是,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。(Promise.all方法的参数可以不是数组,但必须具有 Iterator 接口,且返回的每个成员都是 Promise 实例。)

    +

    p的状态由p1p2p3决定,分成两种情况。

    +

    (1)只有p1p2p3的状态都变成fulfilledp的状态才会变成fulfilled,此时p1p2p3的返回值组成一个数组,传递给p的回调函数。

    +

    (2)只要p1p2p3之中有一个被rejectedp的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数。

    +

    下面是一个具体的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 生成一个Promise对象的数组
    const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
    return getJSON("/post/" + id + ".json");
    });

    Promise.all(promises)
    .then(function (posts) {
    // ...
    })
    .catch(function (reason) {
    // ...
    });
    + +

    上面代码中,promises是包含 6 个 Promise 实例的数组,只有这 6 个实例的状态都变成fulfilled,或者其中有一个变为rejected,才会调用Promise.all方法后面的回调函数。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const databasePromise = connectDatabase();

    const booksPromise = databasePromise.then(findAllBooks);

    const userPromise = databasePromise.then(getCurrentUser);

    Promise.all([booksPromise, userPromise]).then(([books, user]) =>
    pickTopRecommentations(books, user)
    );
    + +

    上面代码中,booksPromiseuserPromise是两个异步操作,只有等到它们的结果都返回了,才会触发pickTopRecommentations这个回调函数。

    +

    注意,如果作为参数的 Promise 实例,自己定义了catch方法,那么它一旦被rejected,并不会触发Promise.all()catch方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const p1 = new Promise((resolve, reject) => {
    resolve("hello");
    })
    .then((result) => result)
    .catch((e) => e);

    const p2 = new Promise((resolve, reject) => {
    throw new Error("报错了");
    })
    .then((result) => result)
    .catch((e) => e);

    Promise.all([p1, p2])
    .then((result) => console.log(result))
    .catch((e) => console.log(e));
    // ["hello", Error: 报错了]
    + +

    上面代码中,p1resolvedp2首先会rejected,但是p2有自己的catch方法,该方法返回的是一个新的 Promise 实例,p2指向的实际上是这个实例。该实例执行完catch方法后,也会变成resolved,导致Promise.all()方法参数里面的两个实例都会resolved,因此会调用then方法指定的回调函数,而不会调用catch方法指定的回调函数。

    +

    如果p2没有自己的catch方法,就会调用Promise.all()catch方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const p1 = new Promise((resolve, reject) => {
    resolve("hello");
    }).then((result) => result);

    const p2 = new Promise((resolve, reject) => {
    throw new Error("报错了");
    }).then((result) => result);

    Promise.all([p1, p2])
    .then((result) => console.log(result))
    .catch((e) => console.log(e));
    // Error: 报错了
    + +

    Promise.race()

    Promise.race方法同样是将多个 Promise 实例,包装成一个新的 Promise 实例。

    +
    1
    const p = Promise.race([p1, p2, p3]);
    + +

    上面代码中,只要p1p2p3之中有一个实例率先改变状态,p的状态就跟着改变。那个率先改变的 Promise 实例的返回值,就传递给p的回调函数。

    +

    Promise.race方法的参数与Promise.all方法一样,如果不是 Promise 实例,就会先调用下面讲到的Promise.resolve方法,将参数转为 Promise 实例,再进一步处理。

    +

    下面是一个例子,如果指定时间内没有获得结果,就将 Promise 的状态变为reject,否则变为resolve

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const p = Promise.race([
    fetch("/resource-that-may-take-a-while"),
    new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error("request timeout")), 5000);
    }),
    ]);

    p.then(console.log).catch(console.error);
    + +

    上面代码中,如果 5 秒之内fetch方法无法返回结果,变量p的状态就会变为rejected,从而触发catch方法指定的回调函数。

    +

    Promise.resolve()

    有时需要将现有对象转为 Promise 对象,Promise.resolve方法就起到这个作用。

    +
    1
    const jsPromise = Promise.resolve($.ajax("/whatever.json"));
    + +

    上面代码将 jQuery 生成的deferred对象,转为一个新的 Promise 对象。

    +

    Promise.resolve等价于下面的写法。

    +
    1
    2
    3
    Promise.resolve("foo");
    // 等价于
    new Promise((resolve) => resolve("foo"));
    + +

    Promise.resolve方法的参数分成四种情况。

    +

    (1)参数是一个 Promise 实例

    +

    如果参数是 Promise 实例,那么Promise.resolve将不做任何修改、原封不动地返回这个实例。

    +

    (2)参数是一个thenable对象

    +

    thenable对象指的是具有then方法的对象,比如下面这个对象。

    +
    1
    2
    3
    4
    5
    let thenable = {
    then: function (resolve, reject) {
    resolve(42);
    },
    };
    + +

    Promise.resolve方法会将这个对象转为 Promise 对象,然后就立即执行thenable对象的then方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let thenable = {
    then: function (resolve, reject) {
    resolve(42);
    },
    };

    let p1 = Promise.resolve(thenable);
    p1.then(function (value) {
    console.log(value); // 42
    });
    + +

    上面代码中,thenable对象的then方法执行后,对象p1的状态就变为resolved,从而立即执行最后那个then方法指定的回调函数,输出 42。

    +

    (3)参数不是具有then方法的对象,或根本就不是对象

    +

    如果参数是一个原始值,或者是一个不具有then方法的对象,则Promise.resolve方法返回一个新的 Promise 对象,状态为resolved

    +
    1
    2
    3
    4
    5
    6
    const p = Promise.resolve("Hello");

    p.then(function (s) {
    console.log(s);
    });
    // Hello
    + +

    上面代码生成一个新的 Promise 对象的实例p。由于字符串Hello不属于异步操作(判断方法是字符串对象不具有 then 方法),返回 Promise 实例的状态从一生成就是resolved,所以回调函数会立即执行。Promise.resolve方法的参数,会同时传给回调函数。

    +

    (4)不带有任何参数

    +

    Promise.resolve方法允许调用时不带参数,直接返回一个resolved状态的 Promise 对象。

    +

    所以,如果希望得到一个 Promise 对象,比较方便的方法就是直接调用Promise.resolve方法。

    +
    1
    2
    3
    4
    5
    const p = Promise.resolve();

    p.then(function () {
    // ...
    });
    + +

    上面代码的变量p就是一个 Promise 对象。

    +

    需要注意的是,立即resolve的 Promise 对象,是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    setTimeout(function () {
    console.log("three");
    }, 0);

    Promise.resolve().then(function () {
    console.log("two");
    });

    console.log("one");

    // one
    // two
    // three
    + +

    上面代码中,setTimeout(fn, 0)在下一轮“事件循环”开始时执行,Promise.resolve()在本轮“事件循环”结束时执行,console.log('one')则是立即执行,因此最先输出。

    +

    Promise.reject()

    Promise.reject(reason)方法也会返回一个新的 Promise 实例,该实例的状态为rejected

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const p = Promise.reject("出错了");
    // 等同于
    const p = new Promise((resolve, reject) => reject("出错了"));

    p.then(null, function (s) {
    console.log(s);
    });
    // 出错了
    + +

    上面代码生成一个 Promise 对象的实例p,状态为rejected,回调函数会立即执行。

    +

    注意,Promise.reject()方法的参数,会原封不动地作为reject的理由,变成后续方法的参数。这一点与Promise.resolve方法不一致。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const thenable = {
    then(resolve, reject) {
    reject("出错了");
    },
    };

    Promise.reject(thenable).catch((e) => {
    console.log(e === thenable);
    });
    // true
    + +

    上面代码中,Promise.reject方法的参数是一个thenable对象,执行以后,后面catch方法的参数不是reject抛出的“出错了”这个字符串,而是thenable对象。

    +

    应用

    加载图片

    我们可以将图片的加载写成一个Promise,一旦加载完成,Promise的状态就发生变化。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const preloadImage = function (path) {
    return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload = resolve;
    image.onerror = reject;
    image.src = path;
    });
    };
    + +

    Generator 函数与 Promise 的结合

    使用 Generator 函数管理流程,遇到异步操作的时候,通常返回一个Promise对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    function getFoo() {
    return new Promise(function (resolve, reject) {
    resolve("foo");
    });
    }

    const g = function* () {
    try {
    const foo = yield getFoo();
    console.log(foo);
    } catch (e) {
    console.log(e);
    }
    };

    function run(generator) {
    const it = generator();

    function go(result) {
    if (result.done) return result.value;

    return result.value.then(
    function (value) {
    return go(it.next(value));
    },
    function (error) {
    return go(it.throw(error));
    }
    );
    }

    go(it.next());
    }

    run(g);
    + +

    上面代码的 Generator 函数g之中,有一个异步操作getFoo,它返回的就是一个Promise对象。函数run用来处理这个Promise对象,并调用下一个next方法。

    +

    Promise.try()

    实际开发中,经常遇到一种情况:不知道或者不想区分,函数f是同步函数还是异步操作,但是想用 Promise 来处理它。因为这样就可以不管f是否包含异步操作,都用then方法指定下一步流程,用catch方法处理f抛出的错误。一般就会采用下面的写法。

    +
    1
    Promise.resolve().then(f);
    + +

    上面的写法有一个缺点,就是如果f是同步函数,那么它会在本轮事件循环的末尾执行。

    +
    1
    2
    3
    4
    5
    const f = () => console.log("now");
    Promise.resolve().then(f);
    console.log("next");
    // next
    // now
    + +

    上面代码中,函数f是同步的,但是用 Promise 包装了以后,就变成异步执行了。

    +

    那么有没有一种方法,让同步函数同步执行,异步函数异步执行,并且让它们具有统一的 API 呢?回答是可以的,并且还有两种写法。第一种写法是用async函数来写。

    +
    1
    2
    3
    4
    5
    const f = () => console.log("now");
    (async () => f())();
    console.log("next");
    // now
    // next
    + +

    上面代码中,第二行是一个立即执行的匿名函数,会立即执行里面的async函数,因此如果f是同步的,就会得到同步的结果;如果f是异步的,就可以用then指定下一步,就像下面的写法。

    +
    1
    2
    (async () => f())()
    .then(...)
    + +

    需要注意的是,async () => f()会吃掉f()抛出的错误。所以,如果想捕获错误,要使用promise.catch方法。

    +
    1
    2
    3
    (async () => f())()
    .then(...)
    .catch(...)
    + +

    第二种写法是使用new Promise()

    +
    1
    2
    3
    4
    5
    const f = () => console.log("now");
    (() => new Promise((resolve) => resolve(f())))();
    console.log("next");
    // now
    // next
    + +

    上面代码也是使用立即执行的匿名函数,执行new Promise()。这种情况下,同步函数也是同步执行的。

    +

    鉴于这是一个很常见的需求,所以现在有一个提案,提供Promise.try方法替代上面的写法。

    +
    1
    2
    3
    4
    5
    const f = () => console.log("now");
    Promise.try(f);
    console.log("next");
    // now
    // next
    + +

    事实上,Promise.try存在已久,Promise 库BluebirdQwhen,早就提供了这个方法。

    +

    由于Promise.try为所有操作提供了统一的处理机制,所以如果想用then方法管理流程,最好都用Promise.try包装一下。这样有许多好处,其中一点就是可以更好地管理异常。

    +
    1
    2
    3
    4
    5
    function getUsername(userId) {
    return database.users.get({ id: userId }).then(function (user) {
    return user.name;
    });
    }
    + +

    上面代码中,database.users.get()返回一个 Promise 对象,如果抛出异步错误,可以用catch方法捕获,就像下面这样写。

    +
    1
    2
    3
    database.users.get({id: userId})
    .then(...)
    .catch(...)
    + +

    但是database.users.get()可能还会抛出同步错误(比如数据库连接错误,具体要看实现方法),这时你就不得不用try...catch去捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    try {
    database.users.get({id: userId})
    .then(...)
    .catch(...)
    } catch (e) {
    // ...
    }
    + +

    上面这样的写法就很笨拙了,这时就可以统一用promise.catch()捕获所有同步和异步的错误。

    +
    1
    2
    3
    Promise.try(database.users.get({id: userId}))
    .then(...)
    .catch(...)
    + +

    事实上,Promise.try就是模拟try代码块,就像promise.catch模拟的是catch代码块。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Promise%20%E5%AF%B9%E8%B1%A1/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/ES6-Proxy/index.html b/posts/ES6-Proxy/index.html new file mode 100644 index 00000000..3cea13c3 --- /dev/null +++ b/posts/ES6-Proxy/index.html @@ -0,0 +1,477 @@ +Proxy | JCAlways + + + + + + + + + + + + + +

    Proxy

    Proxy

    概述

    Proxy 用于修改某些操作的默认行为,等同于在语言层面做出修改,所以属于一种“元编程”(meta programming),即对编程语言进行编程。

    +

    Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。Proxy 这个词的原意是代理,用在这里表示由它来“代理”某些操作,可以译为“代理器”。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var obj = new Proxy(
    {},
    {
    get: function (target, key, receiver) {
    console.log(`getting ${key}!`);
    return Reflect.get(target, key, receiver);
    },
    set: function (target, key, value, receiver) {
    console.log(`setting ${key}!`);
    return Reflect.set(target, key, value, receiver);
    },
    }
    );
    + +

    上面代码对一个空对象架设了一层拦截,重定义了属性的读取(get)和设置(set)行为。这里暂时先不解释具体的语法,只看运行结果。对设置了拦截行为的对象obj,去读写它的属性,就会得到下面的结果。

    +
    1
    2
    3
    4
    5
    6
    obj.count = 1;
    // setting count!
    ++obj.count;
    // getting count!
    // setting count!
    // 2
    + +

    上面代码说明,Proxy 实际上重载(overload)了点运算符,即用自己的定义覆盖了语言的原始定义。

    +

    ES6 原生提供 Proxy 构造函数,用来生成 Proxy 实例。

    +
    1
    var proxy = new Proxy(target, handler);
    + +

    Proxy 对象的所有用法,都是上面这种形式,不同的只是handler参数的写法。其中,new Proxy()表示生成一个Proxy实例,target参数表示所要拦截的目标对象,handler参数也是一个对象,用来定制拦截行为。

    +

    下面是另一个拦截读取属性行为的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var proxy = new Proxy(
    {},
    {
    get: function (target, property) {
    return 35;
    },
    }
    );

    proxy.time; // 35
    proxy.name; // 35
    proxy.title; // 35
    + +

    上面代码中,作为构造函数,Proxy接受两个参数。第一个参数是所要代理的目标对象(上例是一个空对象),即如果没有Proxy的介入,操作原来要访问的就是这个对象;第二个参数是一个配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。比如,上面代码中,配置对象有一个get方法,用来拦截对目标对象属性的访问请求。get方法的两个参数分别是目标对象和所要访问的属性。可以看到,由于拦截函数总是返回35,所以访问任何属性都得到35

    +

    注意,要使得Proxy起作用,必须针对Proxy实例(上例是proxy对象)进行操作,而不是针对目标对象(上例是空对象)进行操作。

    +

    如果handler没有设置任何拦截,那就等同于直接通向原对象。

    +
    1
    2
    3
    4
    5
    var target = {};
    var handler = {};
    var proxy = new Proxy(target, handler);
    proxy.a = "b";
    target.a; // "b"
    + +

    上面代码中,handler是一个空对象,没有任何拦截效果,访问proxy就等同于访问target

    +

    一个技巧是将 Proxy 对象,设置到object.proxy属性,从而可以在object对象上调用。

    +
    1
    var object = { proxy: new Proxy(target, handler) };
    + +

    Proxy 实例也可以作为其他对象的原型对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var proxy = new Proxy(
    {},
    {
    get: function (target, property) {
    return 35;
    },
    }
    );

    let obj = Object.create(proxy);
    obj.time; // 35
    + +

    上面代码中,proxy对象是obj对象的原型,obj对象本身并没有time属性,所以根据原型链,会在proxy对象上读取该属性,导致被拦截。

    +

    同一个拦截器函数,可以设置拦截多个操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    var handler = {
    get: function (target, name) {
    if (name === "prototype") {
    return Object.prototype;
    }
    return "Hello, " + name;
    },

    apply: function (target, thisBinding, args) {
    return args[0];
    },

    construct: function (target, args) {
    return { value: args[1] };
    },
    };

    var fproxy = new Proxy(function (x, y) {
    return x + y;
    }, handler);

    fproxy(1, 2); // 1
    new fproxy(1, 2); // {value: 2}
    fproxy.prototype === Object.prototype; // true
    fproxy.foo === "Hello, foo"; // true
    + +

    对于可以设置、但没有设置拦截的操作,则直接落在目标对象上,按照原先的方式产生结果。

    +

    下面是 Proxy 支持的拦截操作一览,一共 13 种。

    +
      +
    • **get(target, propKey, receiver)**:拦截对象属性的读取,比如proxy.fooproxy['foo']
    • +
    • **set(target, propKey, value, receiver)**:拦截对象属性的设置,比如proxy.foo = vproxy['foo'] = v,返回一个布尔值。
    • +
    • **has(target, propKey)**:拦截propKey in proxy的操作,返回一个布尔值。
    • +
    • **deleteProperty(target, propKey)**:拦截delete proxy[propKey]的操作,返回一个布尔值。
    • +
    • **ownKeys(target)**:拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy)Object.keys(proxy)for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
    • +
    • **getOwnPropertyDescriptor(target, propKey)**:拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
    • +
    • **defineProperty(target, propKey, propDesc)**:拦截Object.defineProperty(proxy, propKey, propDesc)Object.defineProperties(proxy, propDescs),返回一个布尔值。
    • +
    • **preventExtensions(target)**:拦截Object.preventExtensions(proxy),返回一个布尔值。
    • +
    • **getPrototypeOf(target)**:拦截Object.getPrototypeOf(proxy),返回一个对象。
    • +
    • **isExtensible(target)**:拦截Object.isExtensible(proxy),返回一个布尔值。
    • +
    • **setPrototypeOf(target, proto)**:拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
    • +
    • **apply(target, object, args)**:拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
    • +
    • **construct(target, args)**:拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)
    • +
    +

    Proxy 实例的方法

    下面是上面这些拦截方法的详细介绍。

    +

    get()

    get方法用于拦截某个属性的读取操作,可以接受三个参数,依次为目标对象、属性名和 proxy 实例本身(严格地说,是操作行为所针对的对象),其中最后一个参数可选。

    +

    get方法的用法,上文已经有一个例子,下面是另一个拦截读取操作的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var person = {
    name: "张三",
    };

    var proxy = new Proxy(person, {
    get: function (target, property) {
    if (property in target) {
    return target[property];
    } else {
    throw new ReferenceError('Property "' + property + '" does not exist.');
    }
    },
    });

    proxy.name; // "张三"
    proxy.age; // 抛出一个错误
    + +

    上面代码表示,如果访问目标对象不存在的属性,会抛出一个错误。如果没有这个拦截函数,访问不存在的属性,只会返回undefined

    +

    get方法可以继承。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let proto = new Proxy(
    {},
    {
    get(target, propertyKey, receiver) {
    console.log("GET " + propertyKey);
    return target[propertyKey];
    },
    }
    );

    let obj = Object.create(proto);
    obj.foo; // "GET foo"
    + +

    上面代码中,拦截操作定义在Prototype对象上面,所以如果读取obj对象继承的属性时,拦截会生效。

    +

    下面的例子使用get拦截,实现数组读取负数的索引。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function createArray(...elements) {
    let handler = {
    get(target, propKey, receiver) {
    let index = Number(propKey);
    if (index < 0) {
    propKey = String(target.length + index);
    }
    return Reflect.get(target, propKey, receiver);
    },
    };

    let target = [];
    target.push(...elements);
    return new Proxy(target, handler);
    }

    let arr = createArray("a", "b", "c");
    arr[-1]; // c
    + +

    上面代码中,数组的位置参数是-1,就会输出数组的倒数第一个成员。

    +

    利用 Proxy,可以将读取属性的操作(get),转变为执行某个函数,从而实现属性的链式操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var pipe = (function () {
    return function (value) {
    var funcStack = [];
    var oproxy = new Proxy(
    {},
    {
    get: function (pipeObject, fnName) {
    if (fnName === "get") {
    return funcStack.reduce(function (val, fn) {
    return fn(val);
    }, value);
    }
    funcStack.push(window[fnName]);
    return oproxy;
    },
    }
    );

    return oproxy;
    };
    })();

    var double = (n) => n * 2;
    var pow = (n) => n * n;
    var reverseInt = (n) => n.toString().split("").reverse().join("") | 0;

    pipe(3).double.pow.reverseInt.get; // 63
    + +

    上面代码设置 Proxy 以后,达到了将函数名链式使用的效果。

    +

    下面的例子则是利用get拦截,实现一个生成各种 DOM 节点的通用函数dom

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    const dom = new Proxy(
    {},
    {
    get(target, property) {
    return function (attrs = {}, ...children) {
    const el = document.createElement(property);
    for (let prop of Object.keys(attrs)) {
    el.setAttribute(prop, attrs[prop]);
    }
    for (let child of children) {
    if (typeof child === "string") {
    child = document.createTextNode(child);
    }
    el.appendChild(child);
    }
    return el;
    };
    },
    }
    );

    const el = dom.div(
    {},
    "Hello, my name is ",
    dom.a({ href: "//example.com" }, "Mark"),
    ". I like:",
    dom.ul(
    {},
    dom.li({}, "The web"),
    dom.li({}, "Food"),
    dom.li({}, "…actually that's it")
    )
    );

    document.body.appendChild(el);
    + +

    下面是一个get方法的第三个参数的例子,它总是指向原始的读操作所在的那个对象,一般情况下就是 Proxy 实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const proxy = new Proxy(
    {},
    {
    get: function (target, property, receiver) {
    return receiver;
    },
    }
    );
    proxy.getReceiver === proxy; // true
    + +

    上面代码中,proxy对象的getReceiver属性是由proxy对象提供的,所以receiver指向proxy对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const proxy = new Proxy(
    {},
    {
    get: function (target, property, receiver) {
    return receiver;
    },
    }
    );

    const d = Object.create(proxy);
    d.a === d; // true
    + +

    上面代码中,d对象本身没有a属性,所以读取d.a的时候,会去d的原型proxy对象找。这时,receiver就指向d,代表原始的读操作所在的那个对象。

    +

    如果一个属性不可配置(configurable)且不可写(writable),则 Proxy 不能修改该属性,否则通过 Proxy 对象访问该属性会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    const target = Object.defineProperties(
    {},
    {
    foo: {
    value: 123,
    writable: false,
    configurable: false,
    },
    }
    );

    const handler = {
    get(target, propKey) {
    return "abc";
    },
    };

    const proxy = new Proxy(target, handler);

    proxy.foo;
    // TypeError: Invariant check failed
    + +

    set()

    set方法用来拦截某个属性的赋值操作,可以接受四个参数,依次为目标对象、属性名、属性值和 Proxy 实例本身,其中最后一个参数可选。

    +

    假定Person对象有一个age属性,该属性应该是一个不大于 200 的整数,那么可以使用Proxy保证age的属性值符合要求。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    let validator = {
    set: function (obj, prop, value) {
    if (prop === "age") {
    if (!Number.isInteger(value)) {
    throw new TypeError("The age is not an integer");
    }
    if (value > 200) {
    throw new RangeError("The age seems invalid");
    }
    }

    // 对于满足条件的 age 属性以及其他属性,直接保存
    obj[prop] = value;
    },
    };

    let person = new Proxy({}, validator);

    person.age = 100;

    person.age; // 100
    person.age = "young"; // 报错
    person.age = 300; // 报错
    + +

    上面代码中,由于设置了存值函数set,任何不符合要求的age属性赋值,都会抛出一个错误,这是数据验证的一种实现方法。利用set方法,还可以数据绑定,即每当对象发生变化时,会自动更新 DOM。

    +

    有时,我们会在对象上面设置内部属性,属性名的第一个字符使用下划线开头,表示这些属性不应该被外部使用。结合getset方法,就可以做到防止这些内部属性被外部读写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    const handler = {
    get(target, key) {
    invariant(key, "get");
    return target[key];
    },
    set(target, key, value) {
    invariant(key, "set");
    target[key] = value;
    return true;
    },
    };
    function invariant(key, action) {
    if (key[0] === "_") {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
    }
    const target = {};
    const proxy = new Proxy(target, handler);
    proxy._prop;
    // Error: Invalid attempt to get private "_prop" property
    proxy._prop = "c";
    // Error: Invalid attempt to set private "_prop" property
    + +

    上面代码中,只要读写的属性名的第一个字符是下划线,一律抛错,从而达到禁止读写内部属性的目的。

    +

    下面是set方法第四个参数的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const handler = {
    set: function (obj, prop, value, receiver) {
    obj[prop] = receiver;
    },
    };
    const proxy = new Proxy({}, handler);
    proxy.foo = "bar";
    proxy.foo === proxy; // true
    + +

    上面代码中,set方法的第四个参数receiver,指的是原始的操作行为所在的那个对象,一般情况下是proxy实例本身,请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const handler = {
    set: function (obj, prop, value, receiver) {
    obj[prop] = receiver;
    },
    };
    const proxy = new Proxy({}, handler);
    const myObj = {};
    Object.setPrototypeOf(myObj, proxy);

    myObj.foo = "bar";
    myObj.foo === myObj; // true
    + +

    上面代码中,设置myObj.foo属性的值时,myObj并没有foo属性,因此引擎会到myObj的原型链去找foo属性。myObj的原型对象proxy是一个 Proxy 实例,设置它的foo属性会触发set方法。这时,第四个参数receiver就指向原始赋值行为所在的对象myObj

    +

    注意,如果目标对象自身的某个属性,不可写且不可配置,那么set方法将不起作用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const obj = {};
    Object.defineProperty(obj, "foo", {
    value: "bar",
    writable: false,
    });

    const handler = {
    set: function (obj, prop, value, receiver) {
    obj[prop] = "baz";
    },
    };

    const proxy = new Proxy(obj, handler);
    proxy.foo = "baz";
    proxy.foo; // "bar"
    + +

    上面代码中,obj.foo属性不可写,Proxy 对这个属性的set代理将不会生效。

    +

    注意,严格模式下,set代理如果没有返回true,就会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    "use strict";
    const handler = {
    set: function (obj, prop, value, receiver) {
    obj[prop] = receiver;
    // 无论有没有下面这一行,都会报错
    return false;
    },
    };
    const proxy = new Proxy({}, handler);
    proxy.foo = "bar";
    // TypeError: 'set' on proxy: trap returned falsish for property 'foo'
    + +

    上面代码中,严格模式下,set代理返回false或者undefined,都会报错。

    +

    apply()

    apply方法拦截函数的调用、callapply操作。

    +

    apply方法可以接受三个参数,分别是目标对象、目标对象的上下文对象(this)和目标对象的参数数组。

    +
    1
    2
    3
    4
    5
    var handler = {
    apply(target, ctx, args) {
    return Reflect.apply(...arguments);
    },
    };
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var target = function () {
    return "I am the target";
    };
    var handler = {
    apply: function () {
    return "I am the proxy";
    },
    };

    var p = new Proxy(target, handler);

    p();
    // "I am the proxy"
    + +

    上面代码中,变量p是 Proxy 的实例,当它作为函数调用时(p()),就会被apply方法拦截,返回一个字符串。

    +

    下面是另外一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    var twice = {
    apply(target, ctx, args) {
    return Reflect.apply(...arguments) * 2;
    },
    };
    function sum(left, right) {
    return left + right;
    }
    var proxy = new Proxy(sum, twice);
    proxy(1, 2); // 6
    proxy.call(null, 5, 6); // 22
    proxy.apply(null, [7, 8]); // 30
    + +

    上面代码中,每当执行proxy函数(直接调用或callapply调用),就会被apply方法拦截。

    +

    另外,直接调用Reflect.apply方法,也会被拦截。

    +
    1
    Reflect.apply(proxy, null, [9, 10]); // 38
    + +

    has()

    has方法用来拦截HasProperty操作,即判断对象是否具有某个属性时,这个方法会生效。典型的操作就是in运算符。

    +

    has方法可以接受两个参数,分别是目标对象、需查询的属性名。

    +

    下面的例子使用has方法隐藏某些属性,不被in运算符发现。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var handler = {
    has(target, key) {
    if (key[0] === "_") {
    return false;
    }
    return key in target;
    },
    };
    var target = { _prop: "foo", prop: "foo" };
    var proxy = new Proxy(target, handler);
    "_prop" in proxy; // false
    + +

    上面代码中,如果原对象的属性名的第一个字符是下划线,proxy.has就会返回false,从而不会被in运算符发现。

    +

    如果原对象不可配置或者禁止扩展,这时has拦截会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = { a: 10 };
    Object.preventExtensions(obj);

    var p = new Proxy(obj, {
    has: function (target, prop) {
    return false;
    },
    });

    "a" in p; // TypeError is thrown
    + +

    上面代码中,obj对象禁止扩展,结果使用has拦截就会报错。也就是说,如果某个属性不可配置(或者目标对象不可扩展),则has方法就不得“隐藏”(即返回false)目标对象的该属性。

    +

    值得注意的是,has方法拦截的是HasProperty操作,而不是HasOwnProperty操作,即has方法不判断一个属性是对象自身的属性,还是继承的属性。

    +

    另外,虽然for...in循环也用到了in运算符,但是has拦截对for...in循环不生效。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    let stu1 = { name: "张三", score: 59 };
    let stu2 = { name: "李四", score: 99 };

    let handler = {
    has(target, prop) {
    if (prop === "score" && target[prop] < 60) {
    console.log(`${target.name} 不及格`);
    return false;
    }
    return prop in target;
    },
    };

    let oproxy1 = new Proxy(stu1, handler);
    let oproxy2 = new Proxy(stu2, handler);

    "score" in oproxy1;
    // 张三 不及格
    // false

    "score" in oproxy2;
    // true

    for (let a in oproxy1) {
    console.log(oproxy1[a]);
    }
    // 张三
    // 59

    for (let b in oproxy2) {
    console.log(oproxy2[b]);
    }
    // 李四
    // 99
    + +

    上面代码中,has拦截只对in运算符生效,对for...in循环不生效,导致不符合要求的属性没有被for...in循环所排除。

    +

    construct()

    construct方法用于拦截new命令,下面是拦截对象的写法。

    +
    1
    2
    3
    4
    5
    var handler = {
    construct(target, args, newTarget) {
    return new target(...args);
    },
    };
    + +

    construct方法可以接受两个参数。

    +
      +
    • target:目标对象
    • +
    • args:构造函数的参数对象
    • +
    • newTarget:创造实例对象时,new命令作用的构造函数(下面例子的p
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var p = new Proxy(function () {}, {
    construct: function (target, args) {
    console.log("called: " + args.join(", "));
    return { value: args[0] * 10 };
    },
    });

    new p(1).value;
    // "called: 1"
    // 10
    + +

    construct方法返回的必须是一个对象,否则会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    var p = new Proxy(function () {}, {
    construct: function (target, argumentsList) {
    return 1;
    },
    });

    new p(); // 报错
    + +

    deleteProperty()

    deleteProperty方法用于拦截delete操作,如果这个方法抛出错误或者返回false,当前属性就无法被delete命令删除。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var handler = {
    deleteProperty(target, key) {
    invariant(key, "delete");
    delete target[key];
    return true;
    },
    };
    function invariant(key, action) {
    if (key[0] === "_") {
    throw new Error(`Invalid attempt to ${action} private "${key}" property`);
    }
    }

    var target = { _prop: "foo" };
    var proxy = new Proxy(target, handler);
    delete proxy._prop;
    // Error: Invalid attempt to delete private "_prop" property
    + +

    上面代码中,deleteProperty方法拦截了delete操作符,删除第一个字符为下划线的属性会报错。

    +

    注意,目标对象自身的不可配置(configurable)的属性,不能被deleteProperty方法删除,否则报错。

    +

    defineProperty()

    defineProperty方法拦截了Object.defineProperty操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var handler = {
    defineProperty(target, key, descriptor) {
    return false;
    },
    };
    var target = {};
    var proxy = new Proxy(target, handler);
    proxy.foo = "bar"; // 不会生效
    + +

    上面代码中,defineProperty方法返回false,导致添加新属性总是无效。

    +

    注意,如果目标对象不可扩展(extensible),则defineProperty不能增加目标对象上不存在的属性,否则会报错。另外,如果目标对象的某个属性不可写(writable)或不可配置(configurable),则defineProperty方法不得改变这两个设置。

    +

    getOwnPropertyDescriptor()

    getOwnPropertyDescriptor方法拦截Object.getOwnPropertyDescriptor(),返回一个属性描述对象或者undefined

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var handler = {
    getOwnPropertyDescriptor(target, key) {
    if (key[0] === "_") {
    return;
    }
    return Object.getOwnPropertyDescriptor(target, key);
    },
    };
    var target = { _foo: "bar", baz: "tar" };
    var proxy = new Proxy(target, handler);
    Object.getOwnPropertyDescriptor(proxy, "wat");
    // undefined
    Object.getOwnPropertyDescriptor(proxy, "_foo");
    // undefined
    Object.getOwnPropertyDescriptor(proxy, "baz");
    // { value: 'tar', writable: true, enumerable: true, configurable: true }
    + +

    上面代码中,handler.getOwnPropertyDescriptor方法对于第一个字符为下划线的属性名会返回undefined

    +

    getPrototypeOf()

    getPrototypeOf方法主要用来拦截获取对象原型。具体来说,拦截下面这些操作。

    +
      +
    • Object.prototype.__proto__
    • +
    • Object.prototype.isPrototypeOf()
    • +
    • Object.getPrototypeOf()
    • +
    • Reflect.getPrototypeOf()
    • +
    • instanceof
    • +
    +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var proto = {};
    var p = new Proxy(
    {},
    {
    getPrototypeOf(target) {
    return proto;
    },
    }
    );
    Object.getPrototypeOf(p) === proto; // true
    + +

    上面代码中,getPrototypeOf方法拦截Object.getPrototypeOf(),返回proto对象。

    +

    注意,getPrototypeOf方法的返回值必须是对象或者null,否则报错。另外,如果目标对象不可扩展(extensible), getPrototypeOf方法必须返回目标对象的原型对象。

    +

    isExtensible()

    isExtensible方法拦截Object.isExtensible操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var p = new Proxy(
    {},
    {
    isExtensible: function (target) {
    console.log("called");
    return true;
    },
    }
    );

    Object.isExtensible(p);
    // "called"
    // true
    + +

    上面代码设置了isExtensible方法,在调用Object.isExtensible时会输出called

    +

    注意,该方法只能返回布尔值,否则返回值会被自动转为布尔值。

    +

    这个方法有一个强限制,它的返回值必须与目标对象的isExtensible属性保持一致,否则就会抛出错误。

    +
    1
    Object.isExtensible(proxy) === Object.isExtensible(target);
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var p = new Proxy(
    {},
    {
    isExtensible: function (target) {
    return false;
    },
    }
    );

    Object.isExtensible(p); // 报错
    + +

    ownKeys()

    ownKeys方法用来拦截对象自身属性的读取操作。具体来说,拦截以下操作。

    +
      +
    • Object.getOwnPropertyNames()
    • +
    • Object.getOwnPropertySymbols()
    • +
    • Object.keys()
    • +
    • for...in循环
    • +
    +

    下面是拦截Object.keys()的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    let target = {
    a: 1,
    b: 2,
    c: 3,
    };

    let handler = {
    ownKeys(target) {
    return ["a"];
    },
    };

    let proxy = new Proxy(target, handler);

    Object.keys(proxy);
    // [ 'a' ]
    + +

    上面代码拦截了对于target对象的Object.keys()操作,只返回abc三个属性之中的a属性。

    +

    下面的例子是拦截第一个字符为下划线的属性名。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let target = {
    _bar: "foo",
    _prop: "bar",
    prop: "baz",
    };

    let handler = {
    ownKeys(target) {
    return Reflect.ownKeys(target).filter((key) => key[0] !== "_");
    },
    };

    let proxy = new Proxy(target, handler);
    for (let key of Object.keys(proxy)) {
    console.log(target[key]);
    }
    // "baz"
    + +

    注意,使用Object.keys方法时,有三类属性会被ownKeys方法自动过滤,不会返回。

    +
      +
    • 目标对象上不存在的属性
    • +
    • 属性名为 Symbol 值
    • +
    • 不可遍历(enumerable)的属性
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    let target = {
    a: 1,
    b: 2,
    c: 3,
    [Symbol.for("secret")]: "4",
    };

    Object.defineProperty(target, "key", {
    enumerable: false,
    configurable: true,
    writable: true,
    value: "static",
    });

    let handler = {
    ownKeys(target) {
    return ["a", "d", Symbol.for("secret"), "key"];
    },
    };

    let proxy = new Proxy(target, handler);

    Object.keys(proxy);
    // ['a']
    + +

    上面代码中,ownKeys方法之中,显式返回不存在的属性(d)、Symbol 值(Symbol.for('secret'))、不可遍历的属性(key),结果都被自动过滤掉。

    +

    ownKeys方法还可以拦截Object.getOwnPropertyNames()

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var p = new Proxy(
    {},
    {
    ownKeys: function (target) {
    return ["a", "b", "c"];
    },
    }
    );

    Object.getOwnPropertyNames(p);
    // [ 'a', 'b', 'c' ]
    + +

    for...in循环也受到ownKeys方法的拦截。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const obj = { hello: "world" };
    const proxy = new Proxy(obj, {
    ownKeys: function () {
    return ["a", "b"];
    },
    });

    for (let key in proxy) {
    console.log(key); // 没有任何输出
    }
    + +

    上面代码中,ownkeys指定只返回ab属性,由于obj没有这两个属性,因此for...in循环不会有任何输出。

    +

    ownKeys方法返回的数组成员,只能是字符串或 Symbol 值。如果有其他类型的值,或者返回的根本不是数组,就会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var obj = {};

    var p = new Proxy(obj, {
    ownKeys: function (target) {
    return [123, true, undefined, null, {}, []];
    },
    });

    Object.getOwnPropertyNames(p);
    // Uncaught TypeError: 123 is not a valid property name
    + +

    上面代码中,ownKeys方法虽然返回一个数组,但是每一个数组成员都不是字符串或 Symbol 值,因此就报错了。

    +

    如果目标对象自身包含不可配置的属性,则该属性必须被ownKeys方法返回,否则报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var obj = {};
    Object.defineProperty(obj, "a", {
    configurable: false,
    enumerable: true,
    value: 10,
    });

    var p = new Proxy(obj, {
    ownKeys: function (target) {
    return ["b"];
    },
    });

    Object.getOwnPropertyNames(p);
    // Uncaught TypeError: 'ownKeys' on proxy: trap result did not include 'a'
    + +

    上面代码中,obj对象的a属性是不可配置的,这时ownKeys方法返回的数组之中,必须包含a,否则会报错。

    +

    另外,如果目标对象是不可扩展的(non-extensition),这时ownKeys方法返回的数组之中,必须包含原对象的所有属性,且不能包含多余的属性,否则报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var obj = {
    a: 1,
    };

    Object.preventExtensions(obj);

    var p = new Proxy(obj, {
    ownKeys: function (target) {
    return ["a", "b"];
    },
    });

    Object.getOwnPropertyNames(p);
    // Uncaught TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible
    + +

    上面代码中,obj对象是不可扩展的,这时ownKeys方法返回的数组之中,包含了obj对象的多余属性b,所以导致了报错。

    +

    preventExtensions()

    preventExtensions方法拦截Object.preventExtensions()。该方法必须返回一个布尔值,否则会被自动转为布尔值。

    +

    这个方法有一个限制,只有目标对象不可扩展时(即Object.isExtensible(proxy)false),proxy.preventExtensions才能返回true,否则会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var p = new Proxy(
    {},
    {
    preventExtensions: function (target) {
    return true;
    },
    }
    );

    Object.preventExtensions(p); // 报错
    + +

    上面代码中,proxy.preventExtensions方法返回true,但这时Object.isExtensible(proxy)会返回true,因此报错。

    +

    为了防止出现这个问题,通常要在proxy.preventExtensions方法里面,调用一次Object.preventExtensions

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var p = new Proxy(
    {},
    {
    preventExtensions: function (target) {
    console.log("called");
    Object.preventExtensions(target);
    return true;
    },
    }
    );

    Object.preventExtensions(p);
    // "called"
    // true
    + +

    setPrototypeOf()

    setPrototypeOf方法主要用来拦截Object.setPrototypeOf方法。

    +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var handler = {
    setPrototypeOf(target, proto) {
    throw new Error("Changing the prototype is forbidden");
    },
    };
    var proto = {};
    var target = function () {};
    var proxy = new Proxy(target, handler);
    Object.setPrototypeOf(proxy, proto);
    // Error: Changing the prototype is forbidden
    + +

    上面代码中,只要修改target的原型对象,就会报错。

    +

    注意,该方法只能返回布尔值,否则会被自动转为布尔值。另外,如果目标对象不可扩展(extensible),setPrototypeOf方法不得改变目标对象的原型。

    +

    Proxy.revocable()

    Proxy.revocable方法返回一个可取消的 Proxy 实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let target = {};
    let handler = {};

    let { proxy, revoke } = Proxy.revocable(target, handler);

    proxy.foo = 123;
    proxy.foo; // 123

    revoke();
    proxy.foo; // TypeError: Revoked
    + +

    Proxy.revocable方法返回一个对象,该对象的proxy属性是Proxy实例,revoke属性是一个函数,可以取消Proxy实例。上面代码中,当执行revoke函数之后,再访问Proxy实例,就会抛出一个错误。

    +

    Proxy.revocable的一个使用场景是,目标对象不允许直接访问,必须通过代理访问,一旦访问结束,就收回代理权,不允许再次访问。

    +

    this 问题

    虽然 Proxy 可以代理针对目标对象的访问,但它不是目标对象的透明代理,即不做任何拦截的情况下,也无法保证与目标对象的行为一致。主要原因就是在 Proxy 代理的情况下,目标对象内部的this关键字会指向 Proxy 代理。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const target = {
    m: function () {
    console.log(this === proxy);
    },
    };
    const handler = {};

    const proxy = new Proxy(target, handler);

    target.m(); // false
    proxy.m(); // true
    + +

    上面代码中,一旦proxy代理target.m,后者内部的this就是指向proxy,而不是target

    +

    下面是一个例子,由于this指向的变化,导致 Proxy 无法代理目标对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const _name = new WeakMap();

    class Person {
    constructor(name) {
    _name.set(this, name);
    }
    get name() {
    return _name.get(this);
    }
    }

    const jane = new Person("Jane");
    jane.name; // 'Jane'

    const proxy = new Proxy(jane, {});
    proxy.name; // undefined
    + +

    上面代码中,目标对象janename属性,实际保存在外部WeakMap对象_name上面,通过this键区分。由于通过proxy.name访问时,this指向proxy,导致无法取到值,所以返回undefined

    +

    此外,有些原生对象的内部属性,只有通过正确的this才能拿到,所以 Proxy 也无法代理这些原生对象的属性。

    +
    1
    2
    3
    4
    5
    6
    const target = new Date();
    const handler = {};
    const proxy = new Proxy(target, handler);

    proxy.getDate();
    // TypeError: this is not a Date object.
    + +

    上面代码中,getDate方法只能在Date对象实例上面拿到,如果this不是Date对象实例就会报错。这时,this绑定原始对象,就可以解决这个问题。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const target = new Date("2015-01-01");
    const handler = {
    get(target, prop) {
    if (prop === "getDate") {
    return target.getDate.bind(target);
    }
    return Reflect.get(target, prop);
    },
    };
    const proxy = new Proxy(target, handler);

    proxy.getDate(); // 1
    + +

    实例:Web 服务的客户端

    Proxy 对象可以拦截目标对象的任意属性,这使得它很合适用来写 Web 服务的客户端。

    +
    1
    2
    3
    4
    5
    6
    const service = createWebService("http://example.com/data");

    service.employees().then((json) => {
    const employees = JSON.parse(json);
    // ···
    });
    + +

    上面代码新建了一个 Web 服务的接口,这个接口返回各种数据。Proxy 可以拦截这个对象的任意属性,所以不用为每一种数据写一个适配方法,只要写一个 Proxy 拦截就可以了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function createWebService(baseUrl) {
    return new Proxy(
    {},
    {
    get(target, propKey, receiver) {
    return () => httpGet(baseUrl + "/" + propKey);
    },
    }
    );
    }
    + +

    同理,Proxy 也可以用来实现数据库的 ORM 层。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Proxy/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/ES6-Reflect/index.html b/posts/ES6-Reflect/index.html new file mode 100644 index 00000000..ce4e475c --- /dev/null +++ b/posts/ES6-Reflect/index.html @@ -0,0 +1,338 @@ +Reflect | JCAlways + + + + + + + + + + + + + +

    Reflect

    Reflect

    概述

    Reflect对象与Proxy对象一样,也是 ES6 为了操作对象而提供的新 API。Reflect对象的设计目的有这样几个。

    +

    (1) 将Object对象的一些明显属于语言内部的方法(比如Object.defineProperty),放到Reflect对象上。现阶段,某些方法同时在ObjectReflect对象上部署,未来的新方法将只部署在Reflect对象上。也就是说,从Reflect对象上可以拿到语言内部的方法。

    +

    (2) 修改某些Object方法的返回结果,让其变得更合理。比如,Object.defineProperty(obj, name, desc)在无法定义属性时,会抛出一个错误,而Reflect.defineProperty(obj, name, desc)则会返回false

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 老写法
    try {
    Object.defineProperty(target, property, attributes);
    // success
    } catch (e) {
    // failure
    }

    // 新写法
    if (Reflect.defineProperty(target, property, attributes)) {
    // success
    } else {
    // failure
    }
    + +

    (3) 让Object操作都变成函数行为。某些Object操作是命令式,比如name in objdelete obj[name],而Reflect.has(obj, name)Reflect.deleteProperty(obj, name)让它们变成了函数行为。

    +
    1
    2
    3
    4
    5
    // 老写法
    "assign" in Object; // true

    // 新写法
    Reflect.has(Object, "assign"); // true
    + +

    (4)Reflect对象的方法与Proxy对象的方法一一对应,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。这就让Proxy对象可以方便地调用对应的Reflect方法,完成默认行为,作为修改行为的基础。也就是说,不管Proxy怎么修改默认行为,你总可以在Reflect上获取默认行为。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Proxy(target, {
    set: function (target, name, value, receiver) {
    var success = Reflect.set(target, name, value, receiver);
    if (success) {
    log("property " + name + " on " + target + " set to " + value);
    }
    return success;
    },
    });
    + +

    上面代码中,Proxy方法拦截target对象的属性赋值行为。它采用Reflect.set方法将值赋值给对象的属性,确保完成原有的行为,然后再部署额外的功能。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var loggedObj = new Proxy(obj, {
    get(target, name) {
    console.log("get", target, name);
    return Reflect.get(target, name);
    },
    deleteProperty(target, name) {
    console.log("delete" + name);
    return Reflect.deleteProperty(target, name);
    },
    has(target, name) {
    console.log("has" + name);
    return Reflect.has(target, name);
    },
    });
    + +

    上面代码中,每一个Proxy对象的拦截操作(getdeletehas),内部都调用对应的Reflect方法,保证原生行为能够正常执行。添加的工作,就是将每一个操作输出一行日志。

    +

    有了Reflect对象以后,很多操作会更易读。

    +
    1
    2
    3
    4
    5
    // 老写法
    Function.prototype.apply.call(Math.floor, undefined, [1.75]); // 1

    // 新写法
    Reflect.apply(Math.floor, undefined, [1.75]); // 1
    + +

    静态方法

    Reflect对象一共有 13 个静态方法。

    +
      +
    • Reflect.apply(target, thisArg, args)
    • +
    • Reflect.construct(target, args)
    • +
    • Reflect.get(target, name, receiver)
    • +
    • Reflect.set(target, name, value, receiver)
    • +
    • Reflect.defineProperty(target, name, desc)
    • +
    • Reflect.deleteProperty(target, name)
    • +
    • Reflect.has(target, name)
    • +
    • Reflect.ownKeys(target)
    • +
    • Reflect.isExtensible(target)
    • +
    • Reflect.preventExtensions(target)
    • +
    • Reflect.getOwnPropertyDescriptor(target, name)
    • +
    • Reflect.getPrototypeOf(target)
    • +
    • Reflect.setPrototypeOf(target, prototype)
    • +
    +

    上面这些方法的作用,大部分与Object对象的同名方法的作用都是相同的,而且它与Proxy对象的方法是一一对应的。下面是对它们的解释。

    +

    Reflect.get(target, name, receiver)

    Reflect.get方法查找并返回target对象的name属性,如果没有该属性,则返回undefined

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var myObject = {
    foo: 1,
    bar: 2,
    get baz() {
    return this.foo + this.bar;
    },
    };

    Reflect.get(myObject, "foo"); // 1
    Reflect.get(myObject, "bar"); // 2
    Reflect.get(myObject, "baz"); // 3
    + +

    如果name属性部署了读取函数(getter),则读取函数的this绑定receiver

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var myObject = {
    foo: 1,
    bar: 2,
    get baz() {
    return this.foo + this.bar;
    },
    };

    var myReceiverObject = {
    foo: 4,
    bar: 4,
    };

    Reflect.get(myObject, "baz", myReceiverObject); // 8
    + +

    如果第一个参数不是对象,Reflect.get方法会报错。

    +
    1
    2
    Reflect.get(1, "foo"); // 报错
    Reflect.get(false, "foo"); // 报错
    + +

    Reflect.set(target, name, value, receiver)

    Reflect.set方法设置target对象的name属性等于value

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var myObject = {
    foo: 1,
    set bar(value) {
    return (this.foo = value);
    },
    };

    myObject.foo; // 1

    Reflect.set(myObject, "foo", 2);
    myObject.foo; // 2

    Reflect.set(myObject, "bar", 3);
    myObject.foo; // 3
    + +

    如果name属性设置了赋值函数,则赋值函数的this绑定receiver

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var myObject = {
    foo: 4,
    set bar(value) {
    return (this.foo = value);
    },
    };

    var myReceiverObject = {
    foo: 0,
    };

    Reflect.set(myObject, "bar", 1, myReceiverObject);
    myObject.foo; // 4
    myReceiverObject.foo; // 1
    + +

    注意,如果 Proxy 对象和 Reflect 对象联合使用,前者拦截赋值操作,后者完成赋值的默认行为,而且传入了receiver,那么Reflect.set会触发Proxy.defineProperty拦截。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let p = {
    a: "a",
    };

    let handler = {
    set(target, key, value, receiver) {
    console.log("set");
    Reflect.set(target, key, value, receiver);
    },
    defineProperty(target, key, attribute) {
    console.log("defineProperty");
    Reflect.defineProperty(target, key, attribute);
    },
    };

    let obj = new Proxy(p, handler);
    obj.a = "A";
    // set
    // defineProperty
    + +

    上面代码中,Proxy.set拦截里面使用了Reflect.set,而且传入了receiver,导致触发Proxy.defineProperty拦截。这是因为Proxy.setreceiver参数总是指向当前的 Proxy 实例(即上例的obj),而Reflect.set一旦传入receiver,就会将属性赋值到receiver上面(即obj),导致触发defineProperty拦截。如果Reflect.set没有传入receiver,那么就不会触发defineProperty拦截。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let p = {
    a: "a",
    };

    let handler = {
    set(target, key, value, receiver) {
    console.log("set");
    Reflect.set(target, key, value);
    },
    defineProperty(target, key, attribute) {
    console.log("defineProperty");
    Reflect.defineProperty(target, key, attribute);
    },
    };

    let obj = new Proxy(p, handler);
    obj.a = "A";
    // set
    + +

    如果第一个参数不是对象,Reflect.set会报错。

    +
    1
    2
    Reflect.set(1, "foo", {}); // 报错
    Reflect.set(false, "foo", {}); // 报错
    + +

    Reflect.has(obj, name)

    Reflect.has方法对应name in obj里面的in运算符。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var myObject = {
    foo: 1,
    };

    // 旧写法
    "foo" in myObject; // true

    // 新写法
    Reflect.has(myObject, "foo"); // true
    + +

    如果第一个参数不是对象,Reflect.hasin运算符都会报错。

    +

    Reflect.deleteProperty(obj, name)

    Reflect.deleteProperty方法等同于delete obj[name],用于删除对象的属性。

    +
    1
    2
    3
    4
    5
    6
    7
    const myObj = { foo: "bar" };

    // 旧写法
    delete myObj.foo;

    // 新写法
    Reflect.deleteProperty(myObj, "foo");
    + +

    该方法返回一个布尔值。如果删除成功,或者被删除的属性不存在,返回true;删除失败,被删除的属性依然存在,返回false

    +

    Reflect.construct(target, args)

    Reflect.construct方法等同于new target(...args),这提供了一种不使用new,来调用构造函数的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function Greeting(name) {
    this.name = name;
    }

    // new 的写法
    const instance = new Greeting("张三");

    // Reflect.construct 的写法
    const instance = Reflect.construct(Greeting, ["张三"]);
    + +

    Reflect.getPrototypeOf(obj)

    Reflect.getPrototypeOf方法用于读取对象的__proto__属性,对应Object.getPrototypeOf(obj)

    +
    1
    2
    3
    4
    5
    6
    7
    const myObj = new FancyThing();

    // 旧写法
    Object.getPrototypeOf(myObj) === FancyThing.prototype;

    // 新写法
    Reflect.getPrototypeOf(myObj) === FancyThing.prototype;
    + +

    Reflect.getPrototypeOfObject.getPrototypeOf的一个区别是,如果参数不是对象,Object.getPrototypeOf会将这个参数转为对象,然后再运行,而Reflect.getPrototypeOf会报错。

    +
    1
    2
    Object.getPrototypeOf(1); // Number {[[PrimitiveValue]]: 0}
    Reflect.getPrototypeOf(1); // 报错
    + +

    Reflect.setPrototypeOf(obj, newProto)

    Reflect.setPrototypeOf方法用于设置目标对象的原型(prototype),对应Object.setPrototypeOf(obj, newProto)方法。它返回一个布尔值,表示是否设置成功。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const myObj = {};

    // 旧写法
    Object.setPrototypeOf(myObj, Array.prototype);

    // 新写法
    Reflect.setPrototypeOf(myObj, Array.prototype);

    myObj.length; // 0
    + +

    如果无法设置目标对象的原型(比如,目标对象禁止扩展),Reflect.setPrototypeOf方法返回false

    +
    1
    2
    3
    4
    Reflect.setPrototypeOf({}, null);
    // true
    Reflect.setPrototypeOf(Object.freeze({}), null);
    // false
    + +

    如果第一个参数不是对象,Object.setPrototypeOf会返回第一个参数本身,而Reflect.setPrototypeOf会报错。

    +
    1
    2
    3
    4
    5
    Object.setPrototypeOf(1, {});
    // 1

    Reflect.setPrototypeOf(1, {});
    // TypeError: Reflect.setPrototypeOf called on non-object
    + +

    如果第一个参数是undefinednullObject.setPrototypeOfReflect.setPrototypeOf都会报错。

    +
    1
    2
    3
    4
    5
    Object.setPrototypeOf(null, {});
    // TypeError: Object.setPrototypeOf called on null or undefined

    Reflect.setPrototypeOf(null, {});
    // TypeError: Reflect.setPrototypeOf called on non-object
    + +

    Reflect.apply(func, thisArg, args)

    Reflect.apply方法等同于Function.prototype.apply.call(func, thisArg, args),用于绑定this对象后执行给定函数。

    +

    一般来说,如果要绑定一个函数的this对象,可以这样写fn.apply(obj, args),但是如果函数定义了自己的apply方法,就只能写成Function.prototype.apply.call(fn, obj, args),采用Reflect对象可以简化这种操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const ages = [11, 33, 12, 54, 18, 96];

    // 旧写法
    const youngest = Math.min.apply(Math, ages);
    const oldest = Math.max.apply(Math, ages);
    const type = Object.prototype.toString.call(youngest);

    // 新写法
    const youngest = Reflect.apply(Math.min, Math, ages);
    const oldest = Reflect.apply(Math.max, Math, ages);
    const type = Reflect.apply(Object.prototype.toString, youngest, []);
    + +

    Reflect.defineProperty(target, propertyKey, attributes)

    Reflect.defineProperty方法基本等同于Object.defineProperty,用来为对象定义属性。未来,后者会被逐渐废除,请从现在开始就使用Reflect.defineProperty代替它。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function MyDate() {
    /*…*/
    }

    // 旧写法
    Object.defineProperty(MyDate, "now", {
    value: () => Date.now(),
    });

    // 新写法
    Reflect.defineProperty(MyDate, "now", {
    value: () => Date.now(),
    });
    + +

    如果Reflect.defineProperty的第一个参数不是对象,就会抛出错误,比如Reflect.defineProperty(1, 'foo')

    +

    这个方法可以与Proxy.defineProperty配合使用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const p = new Proxy(
    {},
    {
    defineProperty(target, prop, descriptor) {
    console.log(descriptor);
    return Reflect.defineProperty(target, prop, descriptor);
    },
    }
    );

    p.foo = "bar";
    // {value: "bar", writable: true, enumerable: true, configurable: true}

    p.foo; // "bar"
    + +

    上面代码中,Proxy.defineProperty对属性赋值设置了拦截,然后使用Reflect.defineProperty完成了赋值。

    +

    Reflect.getOwnPropertyDescriptor(target, propertyKey)

    Reflect.getOwnPropertyDescriptor基本等同于Object.getOwnPropertyDescriptor,用于得到指定属性的描述对象,将来会替代掉后者。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var myObject = {};
    Object.defineProperty(myObject, "hidden", {
    value: true,
    enumerable: false,
    });

    // 旧写法
    var theDescriptor = Object.getOwnPropertyDescriptor(myObject, "hidden");

    // 新写法
    var theDescriptor = Reflect.getOwnPropertyDescriptor(myObject, "hidden");
    + +

    Reflect.getOwnPropertyDescriptorObject.getOwnPropertyDescriptor的一个区别是,如果第一个参数不是对象,Object.getOwnPropertyDescriptor(1, 'foo')不报错,返回undefined,而Reflect.getOwnPropertyDescriptor(1, 'foo')会抛出错误,表示参数非法。

    +

    Reflect.isExtensible (target)

    Reflect.isExtensible方法对应Object.isExtensible,返回一个布尔值,表示当前对象是否可扩展。

    +
    1
    2
    3
    4
    5
    6
    7
    const myObject = {};

    // 旧写法
    Object.isExtensible(myObject); // true

    // 新写法
    Reflect.isExtensible(myObject); // true
    + +

    如果参数不是对象,Object.isExtensible会返回false,因为非对象本来就是不可扩展的,而Reflect.isExtensible会报错。

    +
    1
    2
    Object.isExtensible(1); // false
    Reflect.isExtensible(1); // 报错
    + +

    Reflect.preventExtensions(target)

    Reflect.preventExtensions对应Object.preventExtensions方法,用于让一个对象变为不可扩展。它返回一个布尔值,表示是否操作成功。

    +
    1
    2
    3
    4
    5
    6
    7
    var myObject = {};

    // 旧写法
    Object.preventExtensions(myObject); // Object {}

    // 新写法
    Reflect.preventExtensions(myObject); // true
    + +

    如果参数不是对象,Object.preventExtensions在 ES5 环境报错,在 ES6 环境返回传入的参数,而Reflect.preventExtensions会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // ES5 环境
    Object.preventExtensions(1); // 报错

    // ES6 环境
    Object.preventExtensions(1); // 1

    // 新写法
    Reflect.preventExtensions(1); // 报错
    + +

    Reflect.ownKeys (target)

    Reflect.ownKeys方法用于返回对象的所有属性,基本等同于Object.getOwnPropertyNamesObject.getOwnPropertySymbols之和。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var myObject = {
    foo: 1,
    bar: 2,
    [Symbol.for("baz")]: 3,
    [Symbol.for("bing")]: 4,
    };

    // 旧写法
    Object.getOwnPropertyNames(myObject);
    // ['foo', 'bar']

    Object.getOwnPropertySymbols(myObject);
    //[Symbol(baz), Symbol(bing)]

    // 新写法
    Reflect.ownKeys(myObject);
    // ['foo', 'bar', Symbol(baz), Symbol(bing)]
    + +

    实例:使用 Proxy 实现观察者模式

    观察者模式(Observer mode)指的是函数自动观察数据对象,一旦对象有变化,函数就会自动执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const person = observable({
    name: "张三",
    age: 20,
    });

    function print() {
    console.log(`${person.name}, ${person.age}`);
    }

    observe(print);
    person.name = "李四";
    // 输出
    // 李四, 20
    + +

    上面代码中,数据对象person是观察目标,函数print是观察者。一旦数据对象发生变化,print就会自动执行。

    +

    下面,使用 Proxy 写一个观察者模式的最简单实现,即实现observableobserve这两个函数。思路是observable函数返回一个原始对象的 Proxy 代理,拦截赋值操作,触发充当观察者的各个函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const queuedObservers = new Set();

    const observe = (fn) => queuedObservers.add(fn);
    const observable = (obj) => new Proxy(obj, { set });

    function set(target, key, value, receiver) {
    const result = Reflect.set(target, key, value, receiver);
    queuedObservers.forEach((observer) => observer());
    return result;
    }
    + +

    上面代码中,先定义了一个Set集合,所有观察者函数都放进这个集合。然后,observable函数返回原始对象的代理,拦截赋值操作。拦截函数set之中,会自动执行所有观察者。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Reflect/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/ES6-SIMD/index.html b/posts/ES6-SIMD/index.html new file mode 100644 index 00000000..d1477158 --- /dev/null +++ b/posts/ES6-SIMD/index.html @@ -0,0 +1,440 @@ +SIMD | JCAlways + + + + + + + + + + + + + +

    SIMD

    SIMD

    概述

    SIMD(发音/sim-dee/)是“Single Instruction/Multiple Data”的缩写,意为“单指令,多数据”。它是 JavaScript 操作 CPU 对应指令的接口,你可以看做这是一种不同的运算执行模式。与它相对的是 SISD(“Single Instruction/Single Data”),即“单指令,单数据”。

    +

    SIMD 的含义是使用一个指令,完成多个数据的运算;SISD 的含义是使用一个指令,完成单个数据的运算,这是 JavaScript 的默认运算模式。显而易见,SIMD 的执行效率要高于 SISD,所以被广泛用于 3D 图形运算、物理模拟等运算量超大的项目之中。

    +

    为了理解 SIMD,请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = [1, 2, 3, 4];
    var b = [5, 6, 7, 8];
    var c = [];

    c[0] = a[0] + b[0];
    c[1] = a[1] + b[1];
    c[2] = a[2] + b[2];
    c[3] = a[3] + b[3];
    c; // Array[6, 8, 10, 12]
    + +

    上面代码中,数组ab的对应成员相加,结果放入数组c。它的运算模式是依次处理每个数组成员,一共有四个数组成员,所以需要运算 4 次。

    +

    如果采用 SIMD 模式,只要运算一次就够了。

    +
    1
    2
    3
    var a = SIMD.Float32x4(1, 2, 3, 4);
    var b = SIMD.Float32x4(5, 6, 7, 8);
    var c = SIMD.Float32x4.add(a, b); // Float32x4[6, 8, 10, 12]
    + +

    上面代码之中,数组ab的四个成员的各自相加,只用一条指令就完成了。因此,速度比上一种写法提高了 4 倍。

    +

    一次 SIMD 运算,可以处理多个数据,这些数据被称为“通道”(lane)。上面代码中,一次运算了四个数据,因此就是四个通道。

    +

    SIMD 通常用于矢量运算。

    +
    1
    2
    v + w	= 〈v1, …, vn〉+ 〈w1, …, wn〉
    = 〈v1+w1, …, vn+wn〉
    + +

    上面代码中,vw是两个多元矢量。它们的加运算,在 SIMD 下是一个指令、而不是 n 个指令完成的,这就大大提高了效率。这对于 3D 动画、图像处理、信号处理、数值处理、加密等运算是非常重要的。比如,Canvas 的getImageData()会将图像文件读成一个二进制数组,SIMD 就很适合对于这种数组的处理。

    +

    总的来说,SIMD 是数据并行处理(parallelism)的一种手段,可以加速一些运算密集型操作的速度。将来与 WebAssembly 结合以后,可以让 JavaScript 达到二进制代码的运行速度。

    +

    数据类型

    SIMD 提供 12 种数据类型,总长度都是 128 个二进制位。

    +
      +
    • Float32x4:四个 32 位浮点数
    • +
    • Float64x2:两个 64 位浮点数
    • +
    • Int32x4:四个 32 位整数
    • +
    • Int16x8:八个 16 位整数
    • +
    • Int8x16:十六个 8 位整数
    • +
    • Uint32x4:四个无符号的 32 位整数
    • +
    • Uint16x8:八个无符号的 16 位整数
    • +
    • Uint8x16:十六个无符号的 8 位整数
    • +
    • Bool32x4:四个 32 位布尔值
    • +
    • Bool16x8:八个 16 位布尔值
    • +
    • Bool8x16:十六个 8 位布尔值
    • +
    • Bool64x2:两个 64 位布尔值
    • +
    +

    每种数据类型被x符号分隔成两部分,后面的部分表示通道数,前面的部分表示每个通道的宽度和类型。比如,Float32x4就表示这个值有 4 个通道,每个通道是一个 32 位浮点数。

    +

    每个通道之中,可以放置四种数据。

    +
      +
    • 浮点数(float,比如 1.0)
    • +
    • 带符号的整数(Int,比如-1)
    • +
    • 无符号的整数(Uint,比如 1)
    • +
    • 布尔值(Bool,包含truefalse两种值)
    • +
    +

    每种 SIMD 的数据类型都是一个函数方法,可以传入参数,生成对应的值。

    +
    1
    var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
    + +

    上面代码中,变量a就是一个 128 位、包含四个 32 位浮点数(即四个通道)的值。

    +

    注意,这些数据类型方法都不是构造函数,前面不能加new,否则会报错。

    +
    1
    2
    var v = new SIMD.Float32x4(0, 1, 2, 3);
    // TypeError: SIMD.Float32x4 is not a constructor
    + +

    静态方法:数学运算

    每种数据类型都有一系列运算符,支持基本的数学运算。

    +

    SIMD.%type%.abs(),SIMD.%type%.neg()

    abs方法接受一个 SIMD 值作为参数,将它的每个通道都转成绝对值,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    var a = SIMD.Float32x4(-1, -2, 0, NaN);
    SIMD.Float32x4.abs(a);
    // Float32x4[1, 2, 0, NaN]
    + +

    neg方法接受一个 SIMD 值作为参数,将它的每个通道都转成负值,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    4
    5
    6
    7
    var a = SIMD.Float32x4(-1, -2, 3, 0);
    SIMD.Float32x4.neg(a);
    // Float32x4[1, 2, -3, -0]

    var b = SIMD.Float64x2(NaN, Infinity);
    SIMD.Float64x2.neg(b);
    // Float64x2[NaN, -Infinity]
    + +

    SIMD.%type%.add(),SIMD.%type%.addSaturate()

    add方法接受两个 SIMD 值作为参数,将它们的每个通道相加,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
    var b = SIMD.Float32x4(5.0, 10.0, 15.0, 20.0);
    var c = SIMD.Float32x4.add(a, b);
    + +

    上面代码中,经过加法运算,新的 SIMD 值为(6.0, 12.0, 18.0. 24.0)

    +

    addSaturate方法跟add方法的作用相同,都是两个通道相加,但是溢出的处理不一致。对于add方法,如果两个值相加发生溢出,溢出的二进制位会被丢弃; addSaturate方法则是返回该数据类型的最大值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = SIMD.Uint16x8(65533, 65534, 65535, 65535, 1, 1, 1, 1);
    var b = SIMD.Uint16x8(1, 1, 1, 5000, 1, 1, 1, 1);
    SIMD.Uint16x8.addSaturate(a, b);
    // Uint16x8[65534, 65535, 65535, 65535, 2, 2, 2, 2]

    var c = SIMD.Int16x8(32765, 32766, 32767, 32767, 1, 1, 1, 1);
    var d = SIMD.Int16x8(1, 1, 1, 5000, 1, 1, 1, 1);
    SIMD.Int16x8.addSaturate(c, d);
    // Int16x8[32766, 32767, 32767, 32767, 2, 2, 2, 2]
    + +

    上面代码中,Uint16的最大值是 65535,Int16的最大值是 32767。一旦发生溢出,就返回这两个值。

    +

    注意,Uint32x4Int32x4这两种数据类型没有addSaturate方法。

    +

    SIMD.%type%.sub(),SIMD.%type%.subSaturate()

    sub方法接受两个 SIMD 值作为参数,将它们的每个通道相减,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    4
    var a = SIMD.Float32x4(-1, -2, 3, 4);
    var b = SIMD.Float32x4(3, 3, 3, 3);
    SIMD.Float32x4.sub(a, b);
    // Float32x4[-4, -5, 0, 1]
    + +

    subSaturate方法跟sub方法的作用相同,都是两个通道相减,但是溢出的处理不一致。对于sub方法,如果两个值相减发生溢出,溢出的二进制位会被丢弃; subSaturate方法则是返回该数据类型的最小值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a = SIMD.Uint16x8(5, 1, 1, 1, 1, 1, 1, 1);
    var b = SIMD.Uint16x8(10, 1, 1, 1, 1, 1, 1, 1);
    SIMD.Uint16x8.subSaturate(a, b);
    // Uint16x8[0, 0, 0, 0, 0, 0, 0, 0]

    var c = SIMD.Int16x8(-100, 0, 0, 0, 0, 0, 0, 0);
    var d = SIMD.Int16x8(32767, 0, 0, 0, 0, 0, 0, 0);
    SIMD.Int16x8.subSaturate(c, d);
    // Int16x8[-32768, 0, 0, 0, 0, 0, 0, 0, 0]
    + +

    上面代码中,Uint16的最小值是0Int16的最小值是-32678。一旦运算发生溢出,就返回最小值。

    +

    SIMD.%type%.mul(),SIMD.%type%.div(),SIMD.%type%.sqrt()

    mul方法接受两个 SIMD 值作为参数,将它们的每个通道相乘,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    4
    var a = SIMD.Float32x4(-1, -2, 3, 4);
    var b = SIMD.Float32x4(3, 3, 3, 3);
    SIMD.Float32x4.mul(a, b);
    // Float32x4[-3, -6, 9, 12]
    + +

    div方法接受两个 SIMD 值作为参数,将它们的每个通道相除,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    4
    var a = SIMD.Float32x4(2, 2, 2, 2);
    var b = SIMD.Float32x4(4, 4, 4, 4);
    SIMD.Float32x4.div(a, b);
    // Float32x4[0.5, 0.5, 0.5, 0.5]
    + +

    sqrt方法接受一个 SIMD 值作为参数,求出每个通道的平方根,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    var b = SIMD.Float64x2(4, 8);
    SIMD.Float64x2.sqrt(b);
    // Float64x2[2, 2.8284271247461903]
    + +

    SIMD.%FloatType%.reciprocalApproximation(),SIMD.%type%.reciprocalSqrtApproximation()

    reciprocalApproximation方法接受一个 SIMD 值作为参数,求出每个通道的倒数(1 / x),作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    var a = SIMD.Float32x4(1, 2, 3, 4);
    SIMD.Float32x4.reciprocalApproximation(a);
    // Float32x4[1, 0.5, 0.3333333432674408, 0.25]
    + +

    reciprocalSqrtApproximation方法接受一个 SIMD 值作为参数,求出每个通道的平方根的倒数(1 / (x^0.5)),作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    var a = SIMD.Float32x4(1, 2, 3, 4);
    SIMD.Float32x4.reciprocalSqrtApproximation(a);
    // Float32x4[1, 0.7071067690849304, 0.5773502588272095, 0.5]
    + +

    注意,只有浮点数的数据类型才有这两个方法。

    +

    SIMD.%IntegerType%.shiftLeftByScalar()

    shiftLeftByScalar方法接受一个 SIMD 值作为参数,然后将每个通道的值左移指定的位数,作为一个新的 SIMD 值返回。

    +
    1
    2
    3
    var a = SIMD.Int32x4(1, 2, 4, 8);
    SIMD.Int32x4.shiftLeftByScalar(a, 1);
    // Int32x4[2, 4, 8, 16]
    + +

    如果左移后,新的值超出了当前数据类型的位数,溢出的部分会被丢弃。

    +
    1
    2
    3
    var ix4 = SIMD.Int32x4(1, 2, 3, 4);
    var jx4 = SIMD.Int32x4.shiftLeftByScalar(ix4, 32);
    // Int32x4[0, 0, 0, 0]
    + +

    注意,只有整数的数据类型才有这个方法。

    +

    SIMD.%IntegerType%.shiftRightByScalar()

    shiftRightByScalar方法接受一个 SIMD 值作为参数,然后将每个通道的值右移指定的位数,返回一个新的 SIMD 值。

    +
    1
    2
    3
    var a = SIMD.Int32x4(1, 2, 4, -8);
    SIMD.Int32x4.shiftRightByScalar(a, 1);
    // Int32x4[0, 1, 2, -4]
    + +

    如果原来通道的值是带符号的值,则符号位保持不变,不受右移影响。如果是不带符号位的值,则右移后头部会补0

    +
    1
    2
    3
    var a = SIMD.Uint32x4(1, 2, 4, -8);
    SIMD.Uint32x4.shiftRightByScalar(a, 1);
    // Uint32x4[0, 1, 2, 2147483644]
    + +

    上面代码中,-8右移一位变成了2147483644,是因为对于 32 位无符号整数来说,-8的二进制形式是11111111111111111111111111111000,右移一位就变成了01111111111111111111111111111100,相当于2147483644

    +

    注意,只有整数的数据类型才有这个方法。

    +

    静态方法:通道处理

    SIMD.%type%.check()

    check方法用于检查一个值是否为当前类型的 SIMD 值。如果是的,就返回这个值,否则就报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var a = SIMD.Float32x4(1, 2, 3, 9);

    SIMD.Float32x4.check(a);
    // Float32x4[1, 2, 3, 9]

    SIMD.Float32x4.check([1, 2, 3, 4]); // 报错
    SIMD.Int32x4.check(a); // 报错
    SIMD.Int32x4.check("hello world"); // 报错
    + +

    SIMD.%type%.extractLane(),SIMD.%type%.replaceLane()

    extractLane方法用于返回给定通道的值。它接受两个参数,分别是 SIMD 值和通道编号。

    +
    1
    2
    var t = SIMD.Float32x4(1, 2, 3, 4);
    SIMD.Float32x4.extractLane(t, 2); // 3
    + +

    replaceLane方法用于替换指定通道的值,并返回一个新的 SIMD 值。它接受三个参数,分别是原来的 SIMD 值、通道编号和新的通道值。

    +
    1
    2
    3
    var t = SIMD.Float32x4(1, 2, 3, 4);
    SIMD.Float32x4.replaceLane(t, 2, 42);
    // Float32x4[1, 2, 42, 4]
    + +

    SIMD.%type%.load()

    load方法用于从二进制数组读入数据,生成一个新的 SIMD 值。

    +
    1
    2
    3
    4
    5
    6
    7
    var a = new Int32Array([1, 2, 3, 4, 5, 6, 7, 8]);
    SIMD.Int32x4.load(a, 0);
    // Int32x4[1, 2, 3, 4]

    var b = new Int32Array([1, 2, 3, 4, 5, 6, 7, 8]);
    SIMD.Int32x4.load(a, 2);
    // Int32x4[3, 4, 5, 6]
    + +

    load方法接受两个参数:一个二进制数组和开始读取的位置(从 0 开始)。如果位置不合法(比如-1或者超出二进制数组的大小),就会抛出一个错误。

    +

    这个方法还有三个变种load1()load2()load3(),表示从指定位置开始,只加载一个通道、二个通道、三个通道的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 格式
    SIMD.Int32x4.load(tarray, index);
    SIMD.Int32x4.load1(tarray, index);
    SIMD.Int32x4.load2(tarray, index);
    SIMD.Int32x4.load3(tarray, index);

    // 实例
    var a = new Int32Array([1, 2, 3, 4, 5, 6, 7, 8]);
    SIMD.Int32x4.load1(a, 0);
    // Int32x4[1, 0, 0, 0]
    SIMD.Int32x4.load2(a, 0);
    // Int32x4[1, 2, 0, 0]
    SIMD.Int32x4.load3(a, 0);
    // Int32x4[1, 2, 3,0]
    + +

    SIMD.%type%.store()

    store方法用于将一个 SIMD 值,写入一个二进制数组。它接受三个参数,分别是二进制数组、开始写入的数组位置、SIMD 值。它返回写入值以后的二进制数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var t1 = new Int32Array(8);
    var v1 = SIMD.Int32x4(1, 2, 3, 4);
    SIMD.Int32x4.store(t1, 0, v1);
    // Int32Array[1, 2, 3, 4, 0, 0, 0, 0]

    var t2 = new Int32Array(8);
    var v2 = SIMD.Int32x4(1, 2, 3, 4);
    SIMD.Int32x4.store(t2, 2, v2);
    // Int32Array[0, 0, 1, 2, 3, 4, 0, 0]
    + +

    上面代码中,t1是一个二进制数组,v1是一个 SIMD 值,只有四个通道。所以写入t1以后,只有前四个位置有值,后四个位置都是 0。而t2是从 2 号位置开始写入,所以前两个位置和后两个位置都是 0。

    +

    这个方法还有三个变种store1()store2()store3(),表示只写入一个通道、二个通道和三个通道的值。

    +
    1
    2
    3
    4
    var tarray = new Int32Array(8);
    var value = SIMD.Int32x4(1, 2, 3, 4);
    SIMD.Int32x4.store1(tarray, 0, value);
    // Int32Array[1, 0, 0, 0, 0, 0, 0, 0]
    + +

    SIMD.%type%.splat()

    splat方法返回一个新的 SIMD 值,该值的所有通道都会设成同一个预先给定的值。

    +
    1
    2
    3
    4
    SIMD.Float32x4.splat(3);
    // Float32x4[3, 3, 3, 3]
    SIMD.Float64x2.splat(3);
    // Float64x2[3, 3]
    + +

    如果省略参数,所有整数型的 SIMD 值都会设定0,浮点型的 SIMD 值都会设成NaN

    +

    SIMD.%type%.swizzle()

    swizzle方法返回一个新的 SIMD 值,重新排列原有的 SIMD 值的通道顺序。

    +
    1
    2
    3
    var t = SIMD.Float32x4(1, 2, 3, 4);
    SIMD.Float32x4.swizzle(t, 1, 2, 0, 3);
    // Float32x4[2,3,1,4]
    + +

    上面代码中,swizzle方法的第一个参数是原有的 SIMD 值,后面的参数对应将要返回的 SIMD 值的四个通道。它的意思是新的 SIMD 的四个通道,依次是原来 SIMD 值的 1 号通道、2 号通道、0 号通道、3 号通道。由于 SIMD 值最多可以有 16 个通道,所以swizzle方法除了第一个参数以外,最多还可以接受 16 个参数。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var a = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
    // Float32x4[1.0, 2.0, 3.0, 4.0]

    var b = SIMD.Float32x4.swizzle(a, 0, 0, 1, 1);
    // Float32x4[1.0, 1.0, 2.0, 2.0]

    var c = SIMD.Float32x4.swizzle(a, 3, 3, 3, 3);
    // Float32x4[4.0, 4.0, 4.0, 4.0]

    var d = SIMD.Float32x4.swizzle(a, 3, 2, 1, 0);
    // Float32x4[4.0, 3.0, 2.0, 1.0]
    + +

    SIMD.%type%.shuffle()

    shuffle方法从两个 SIMD 值之中取出指定通道,返回一个新的 SIMD 值。

    +
    1
    2
    3
    4
    5
    var a = SIMD.Float32x4(1, 2, 3, 4);
    var b = SIMD.Float32x4(5, 6, 7, 8);

    SIMD.Float32x4.shuffle(a, b, 1, 5, 7, 2);
    // Float32x4[2, 6, 8, 3]
    + +

    上面代码中,ab一共有 8 个通道,依次编号为 0 到 7。shuffle根据编号,取出相应的通道,返回一个新的 SIMD 值。

    +

    静态方法:比较运算

    SIMD.%type%.equal(),SIMD.%type%.notEqual()

    equal方法用来比较两个 SIMD 值ab的每一个通道,根据两者是否精确相等(a === b),得到一个布尔值。最后,所有通道的比较结果,组成一个新的 SIMD 值,作为掩码返回。notEqual方法则是比较两个通道是否不相等(a !== b)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var a = SIMD.Float32x4(1, 2, 3, 9);
    var b = SIMD.Float32x4(1, 4, 7, 9);

    SIMD.Float32x4.equal(a, b);
    // Bool32x4[true, false, false, true]

    SIMD.Float32x4.notEqual(a, b);
    // Bool32x4[false, true, true, false]
    + +

    SIMD.%type%.greaterThan(),SIMD.%type%.greaterThanOrEqual()

    greatThan方法用来比较两个 SIMD 值ab的每一个通道,如果在该通道中,a较大就得到true,否则得到false。最后,所有通道的比较结果,组成一个新的 SIMD 值,作为掩码返回。greaterThanOrEqual则是比较a是否大于等于b

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var a = SIMD.Float32x4(1, 6, 3, 11);
    var b = SIMD.Float32x4(1, 4, 7, 9);

    SIMD.Float32x4.greaterThan(a, b);
    // Bool32x4[false, true, false, true]

    SIMD.Float32x4.greaterThanOrEqual(a, b);
    // Bool32x4[true, true, false, true]
    + +

    SIMD.%type%.lessThan(),SIMD.%type%.lessThanOrEqual()

    lessThan方法用来比较两个 SIMD 值ab的每一个通道,如果在该通道中,a较小就得到true,否则得到false。最后,所有通道的比较结果,会组成一个新的 SIMD 值,作为掩码返回。lessThanOrEqual方法则是比较a是否等于b

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var a = SIMD.Float32x4(1, 2, 3, 11);
    var b = SIMD.Float32x4(1, 4, 7, 9);

    SIMD.Float32x4.lessThan(a, b);
    // Bool32x4[false, true, true, false]

    SIMD.Float32x4.lessThanOrEqual(a, b);
    // Bool32x4[true, true, true, false]
    + +

    SIMD.%type%.select()

    select方法通过掩码生成一个新的 SIMD 值。它接受三个参数,分别是掩码和两个 SIMD 值。

    +
    1
    2
    3
    4
    5
    6
    7
    var a = SIMD.Float32x4(1, 2, 3, 4);
    var b = SIMD.Float32x4(5, 6, 7, 8);

    var mask = SIMD.Bool32x4(true, false, false, true);

    SIMD.Float32x4.select(mask, a, b);
    // Float32x4[1, 6, 7, 4]
    + +

    上面代码中,select方法接受掩码和两个 SIMD 值作为参数。当某个通道对应的掩码为true时,会选择第一个 SIMD 值的对应通道,否则选择第二个 SIMD 值的对应通道。

    +

    这个方法通常与比较运算符结合使用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var a = SIMD.Float32x4(0, 12, 3, 4);
    var b = SIMD.Float32x4(0, 6, 7, 50);

    var mask = SIMD.Float32x4.lessThan(a, b);
    // Bool32x4[false, false, true, true]

    var result = SIMD.Float32x4.select(mask, a, b);
    // Float32x4[0, 6, 3, 4]
    + +

    上面代码中,先通过lessThan方法生成一个掩码,然后通过select方法生成一个由每个通道的较小值组成的新的 SIMD 值。

    +

    SIMD.%BooleanType%.allTrue(),SIMD.%BooleanType%.anyTrue()

    allTrue方法接受一个 SIMD 值作为参数,然后返回一个布尔值,表示该 SIMD 值的所有通道是否都为true

    +
    1
    2
    3
    4
    5
    var a = SIMD.Bool32x4(true, true, true, true);
    var b = SIMD.Bool32x4(true, false, true, true);

    SIMD.Bool32x4.allTrue(a); // true
    SIMD.Bool32x4.allTrue(b); // false
    + +

    anyTrue方法则是只要有一个通道为true,就返回true,否则返回false

    +
    1
    2
    3
    4
    5
    var a = SIMD.Bool32x4(false, false, false, false);
    var b = SIMD.Bool32x4(false, false, true, false);

    SIMD.Bool32x4.anyTrue(a); // false
    SIMD.Bool32x4.anyTrue(b); // true
    + +

    注意,只有四种布尔值数据类型(Bool32x4Bool16x8Bool8x16Bool64x2)才有这两个方法。

    +

    这两个方法通常与比较运算符结合使用。

    +
    1
    2
    3
    4
    5
    var ax4 = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
    var bx4 = SIMD.Float32x4(0.0, 6.0, 7.0, 8.0);
    var ix4 = SIMD.Float32x4.lessThan(ax4, bx4);
    var b1 = SIMD.Int32x4.allTrue(ix4); // false
    var b2 = SIMD.Int32x4.anyTrue(ix4); // true
    + +

    SIMD.%type%.min(),SIMD.%type%.minNum()

    min方法接受两个 SIMD 值作为参数,将两者的对应通道的较小值,组成一个新的 SIMD 值返回。

    +
    1
    2
    3
    4
    var a = SIMD.Float32x4(-1, -2, 3, 5.2);
    var b = SIMD.Float32x4(0, -4, 6, 5.5);
    SIMD.Float32x4.min(a, b);
    // Float32x4[-1, -4, 3, 5.2]
    + +

    如果有一个通道的值是NaN,则会优先返回NaN

    +
    1
    2
    3
    4
    var c = SIMD.Float64x2(NaN, Infinity);
    var d = SIMD.Float64x2(1337, 42);
    SIMD.Float64x2.min(c, d);
    // Float64x2[NaN, 42]
    + +

    minNum方法与min的作用一模一样,唯一的区别是如果有一个通道的值是NaN,则会优先返回另一个通道的值。

    +
    1
    2
    3
    4
    5
    6
    var ax4 = SIMD.Float32x4(1.0, 2.0, NaN, NaN);
    var bx4 = SIMD.Float32x4(2.0, 1.0, 3.0, NaN);
    var cx4 = SIMD.Float32x4.min(ax4, bx4);
    // Float32x4[1.0, 1.0, NaN, NaN]
    var dx4 = SIMD.Float32x4.minNum(ax4, bx4);
    // Float32x4[1.0, 1.0, 3.0, NaN]
    + +

    SIMD.%type%.max(),SIMD.%type%.maxNum()

    max方法接受两个 SIMD 值作为参数,将两者的对应通道的较大值,组成一个新的 SIMD 值返回。

    +
    1
    2
    3
    4
    var a = SIMD.Float32x4(-1, -2, 3, 5.2);
    var b = SIMD.Float32x4(0, -4, 6, 5.5);
    SIMD.Float32x4.max(a, b);
    // Float32x4[0, -2, 6, 5.5]
    + +

    如果有一个通道的值是NaN,则会优先返回NaN

    +
    1
    2
    3
    4
    var c = SIMD.Float64x2(NaN, Infinity);
    var d = SIMD.Float64x2(1337, 42);
    SIMD.Float64x2.max(c, d);
    // Float64x2[NaN, Infinity]
    + +

    maxNum方法与max的作用一模一样,唯一的区别是如果有一个通道的值是NaN,则会优先返回另一个通道的值。

    +
    1
    2
    3
    4
    var c = SIMD.Float64x2(NaN, Infinity);
    var d = SIMD.Float64x2(1337, 42);
    SIMD.Float64x2.maxNum(c, d);
    // Float64x2[1337, Infinity]
    + +

    静态方法:位运算

    SIMD.%type%.and(),SIMD.%type%.or(),SIMD.%type%.xor(),SIMD.%type%.not()

    and方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制AND运算(&)后得到的新的 SIMD 值。

    +
    1
    2
    3
    4
    var a = SIMD.Int32x4(1, 2, 4, 8);
    var b = SIMD.Int32x4(5, 5, 5, 5);
    SIMD.Int32x4.and(a, b);
    // Int32x4[1, 0, 4, 0]
    + +

    上面代码中,以通道0为例,1的二进制形式是00015的二进制形式是01001,所以进行AND运算以后,得到0001

    +

    or方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制OR运算(|)后得到的新的 SIMD 值。

    +
    1
    2
    3
    4
    var a = SIMD.Int32x4(1, 2, 4, 8);
    var b = SIMD.Int32x4(5, 5, 5, 5);
    SIMD.Int32x4.or(a, b);
    // Int32x4[5, 7, 5, 13]
    + +

    xor方法接受两个 SIMD 值作为参数,返回两者对应的通道进行二进制”异或“运算(^)后得到的新的 SIMD 值。

    +
    1
    2
    3
    4
    var a = SIMD.Int32x4(1, 2, 4, 8);
    var b = SIMD.Int32x4(5, 5, 5, 5);
    SIMD.Int32x4.xor(a, b);
    // Int32x4[4, 7, 1, 13]
    + +

    not方法接受一个 SIMD 值作为参数,返回每个通道进行二进制”否“运算(~)后得到的新的 SIMD 值。

    +
    1
    2
    3
    var a = SIMD.Int32x4(1, 2, 4, 8);
    SIMD.Int32x4.not(a);
    // Int32x4[-2, -3, -5, -9]
    + +

    上面代码中,1的否运算之所以得到-2,是因为在计算机内部,负数采用”2 的补码“这种形式进行表示。也就是说,整数n的负数形式-n,是对每一个二进制位取反以后,再加上 1。因此,直接取反就相当于负数形式再减去 1,比如1的负数形式是-1,再减去 1,就得到了-2

    +

    静态方法:数据类型转换

    SIMD 提供以下方法,用来将一种数据类型转为另一种数据类型。

    +
      +
    • SIMD.%type%.fromFloat32x4()
    • +
    • SIMD.%type%.fromFloat32x4Bits()
    • +
    • SIMD.%type%.fromFloat64x2Bits()
    • +
    • SIMD.%type%.fromInt32x4()
    • +
    • SIMD.%type%.fromInt32x4Bits()
    • +
    • SIMD.%type%.fromInt16x8Bits()
    • +
    • SIMD.%type%.fromInt8x16Bits()
    • +
    • SIMD.%type%.fromUint32x4()
    • +
    • SIMD.%type%.fromUint32x4Bits()
    • +
    • SIMD.%type%.fromUint16x8Bits()
    • +
    • SIMD.%type%.fromUint8x16Bits()
    • +
    +

    带有Bits后缀的方法,会原封不动地将二进制位拷贝到新的数据类型;不带后缀的方法,则会进行数据类型转换。

    +
    1
    2
    3
    4
    5
    6
    var t = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
    SIMD.Int32x4.fromFloat32x4(t);
    // Int32x4[1, 2, 3, 4]

    SIMD.Int32x4.fromFloat32x4Bits(t);
    // Int32x4[1065353216, 1073741824, 1077936128, 1082130432]
    + +

    上面代码中,fromFloat32x4是将浮点数转为整数,然后存入新的数据类型;fromFloat32x4Bits则是将二进制位原封不动地拷贝进入新的数据类型,然后进行解读。

    +

    Bits后缀的方法,还可以用于通道数目不对等的拷贝。

    +
    1
    2
    3
    var t = SIMD.Float32x4(1.0, 2.0, 3.0, 4.0);
    SIMD.Int16x8.fromFloat32x4Bits(t);
    // Int16x8[0, 16256, 0, 16384, 0, 16448, 0, 16512]
    + +

    上面代码中,原始 SIMD 值t是 4 通道的,而目标值是 8 通道的。

    +

    如果数据转换时,原通道的数据大小,超过了目标通道的最大宽度,就会报错。

    +

    实例方法

    SIMD.%type%.prototype.toString()

    toString方法返回一个 SIMD 值的字符串形式。

    +
    1
    2
    var a = SIMD.Float32x4(11, 22, 33, 44);
    a.toString(); // "SIMD.Float32x4(11, 22, 33, 44)"
    + +

    实例:求平均值

    正常模式下,计算n个值的平均值,需要运算n次。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function average(list) {
    var n = list.length;
    var sum = 0.0;
    for (var i = 0; i < n; i++) {
    sum += list[i];
    }
    return sum / n;
    }
    + +

    使用 SIMD,可以将计算次数减少到n次的四分之一。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function average(list) {
    var n = list.length;
    var sum = SIMD.Float32x4.splat(0.0);
    for (var i = 0; i < n; i += 4) {
    sum = SIMD.Float32x4.add(sum, SIMD.Float32x4.load(list, i));
    }
    var total =
    SIMD.Float32x4.extractLane(sum, 0) +
    SIMD.Float32x4.extractLane(sum, 1) +
    SIMD.Float32x4.extractLane(sum, 2) +
    SIMD.Float32x4.extractLane(sum, 3);
    return total / n;
    }
    + +

    上面代码先是每隔四位,将所有的值读入一个 SIMD,然后立刻累加。然后,得到累加值四个通道的总和,再除以n就可以了。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-SIMD/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/ES6-Set \345\222\214 Map \346\225\260\346\215\256\347\273\223\346\236\204/index.html" "b/posts/ES6-Set \345\222\214 Map \346\225\260\346\215\256\347\273\223\346\236\204/index.html" new file mode 100644 index 00000000..dfddcda8 --- /dev/null +++ "b/posts/ES6-Set \345\222\214 Map \346\225\260\346\215\256\347\273\223\346\236\204/index.html" @@ -0,0 +1,505 @@ +Set 和 Map 数据结构 | JCAlways + + + + + + + + + + + + + +

    Set 和 Map 数据结构

    Set 和 Map 数据结构

    Set

    基本用法

    ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。

    +

    Set 本身是一个构造函数,用来生成 Set 数据结构。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const s = new Set();

    [2, 3, 5, 4, 5, 2, 2].forEach((x) => s.add(x));

    for (let i of s) {
    console.log(i);
    }
    // 2 3 5 4
    + +

    上面代码通过add方法向 Set 结构加入成员,结果表明 Set 结构不会添加重复的值。

    +

    Set 函数可以接受一个数组(或者具有 iterable 接口的其他数据结构)作为参数,用来初始化。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 例一
    const set = new Set([1, 2, 3, 4, 4]);
    [...set];
    // [1, 2, 3, 4]

    // 例二
    const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]);
    items.size; // 5

    // 例三
    const set = new Set(document.querySelectorAll("div"));
    set.size; // 56

    // 类似于
    const set = new Set();
    document.querySelectorAll("div").forEach((div) => set.add(div));
    set.size; // 56
    + +

    上面代码中,例一和例二都是Set函数接受数组作为参数,例三是接受类似数组的对象作为参数。

    +

    上面代码也展示了一种去除数组重复成员的方法。

    +
    1
    2
    // 去除数组的重复成员
    [...new Set(array)];
    + +

    向 Set 加入值的时候,不会发生类型转换,所以5"5"是两个不同的值。Set 内部判断两个值是否不同,使用的算法叫做“Same-value-zero equality”,它类似于精确相等运算符(===),主要的区别是NaN等于自身,而精确相等运算符认为NaN不等于自身。

    +
    1
    2
    3
    4
    5
    6
    let set = new Set();
    let a = NaN;
    let b = NaN;
    set.add(a);
    set.add(b);
    set; // Set {NaN}
    + +

    上面代码向 Set 实例添加了两个NaN,但是只能加入一个。这表明,在 Set 内部,两个NaN是相等。

    +

    另外,两个对象总是不相等的。

    +
    1
    2
    3
    4
    5
    6
    7
    let set = new Set();

    set.add({});
    set.size; // 1

    set.add({});
    set.size; // 2
    + +

    上面代码表示,由于两个空对象不相等,所以它们被视为两个值。

    +

    Set 实例的属性和方法

    Set 结构的实例有以下属性。

    +
      +
    • Set.prototype.constructor:构造函数,默认就是Set函数。
    • +
    • Set.prototype.size:返回Set实例的成员总数。
    • +
    +

    Set 实例的方法分为两大类:操作方法(用于操作数据)和遍历方法(用于遍历成员)。下面先介绍四个操作方法。

    +
      +
    • add(value):添加某个值,返回 Set 结构本身。
    • +
    • delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
    • +
    • has(value):返回一个布尔值,表示该值是否为Set的成员。
    • +
    • clear():清除所有成员,没有返回值。
    • +
    +

    上面这些属性和方法的实例如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    s.add(1).add(2).add(2);
    // 注意2被加入了两次

    s.size; // 2

    s.has(1); // true
    s.has(2); // true
    s.has(3); // false

    s.delete(2);
    s.has(2); // false
    + +

    下面是一个对比,看看在判断是否包括一个键上面,Object结构和Set结构的写法不同。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 对象的写法
    const properties = {
    width: 1,
    height: 1,
    };

    if (properties[someName]) {
    // do something
    }

    // Set的写法
    const properties = new Set();

    properties.add("width");
    properties.add("height");

    if (properties.has(someName)) {
    // do something
    }
    + +

    Array.from方法可以将 Set 结构转为数组。

    +
    1
    2
    const items = new Set([1, 2, 3, 4, 5]);
    const array = Array.from(items);
    + +

    这就提供了去除数组重复成员的另一种方法。

    +
    1
    2
    3
    4
    5
    function dedupe(array) {
    return Array.from(new Set(array));
    }

    dedupe([1, 1, 2, 3]); // [1, 2, 3]
    + +

    遍历操作

    Set 结构的实例有四个遍历方法,可以用于遍历成员。

    +
      +
    • keys():返回键名的遍历器
    • +
    • values():返回键值的遍历器
    • +
    • entries():返回键值对的遍历器
    • +
    • forEach():使用回调函数遍历每个成员
    • +
    +

    需要特别指出的是,Set的遍历顺序就是插入顺序。这个特性有时非常有用,比如使用 Set 保存一个回调函数列表,调用时就能保证按照添加顺序调用。

    +

    (1)keys()values()entries()

    +

    keys方法、values方法、entries方法返回的都是遍历器对象(详见《Iterator 对象》一章)。由于 Set 结构没有键名,只有键值(或者说键名和键值是同一个值),所以keys方法和values方法的行为完全一致。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    let set = new Set(["red", "green", "blue"]);

    for (let item of set.keys()) {
    console.log(item);
    }
    // red
    // green
    // blue

    for (let item of set.values()) {
    console.log(item);
    }
    // red
    // green
    // blue

    for (let item of set.entries()) {
    console.log(item);
    }
    // ["red", "red"]
    // ["green", "green"]
    // ["blue", "blue"]
    + +

    上面代码中,entries方法返回的遍历器,同时包括键名和键值,所以每次输出一个数组,它的两个成员完全相等。

    +

    Set 结构的实例默认可遍历,它的默认遍历器生成函数就是它的values方法。

    +
    1
    2
    Set.prototype[Symbol.iterator] === Set.prototype.values;
    // true
    + +

    这意味着,可以省略values方法,直接用for...of循环遍历 Set。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let set = new Set(["red", "green", "blue"]);

    for (let x of set) {
    console.log(x);
    }
    // red
    // green
    // blue
    + +

    (2)forEach()

    +

    Set 结构的实例与数组一样,也拥有forEach方法,用于对每个成员执行某种操作,没有返回值。

    +
    1
    2
    3
    4
    5
    set = new Set([1, 4, 9]);
    set.forEach((value, key) => console.log(key + " : " + value));
    // 1 : 1
    // 4 : 4
    // 9 : 9
    + +

    上面代码说明,forEach方法的参数就是一个处理函数。该函数的参数与数组的forEach一致,依次为键值、键名、集合本身(上例省略了该参数)。这里需要注意,Set 结构的键名就是键值(两者是同一个值),因此第一个参数与第二个参数的值永远都是一样的。

    +

    另外,forEach方法还可以有第二个参数,表示绑定处理函数内部的this对象。

    +

    (3)遍历的应用

    +

    扩展运算符(...)内部使用for...of循环,所以也可以用于 Set 结构。

    +
    1
    2
    3
    let set = new Set(["red", "green", "blue"]);
    let arr = [...set];
    // ['red', 'green', 'blue']
    + +

    扩展运算符和 Set 结构相结合,就可以去除数组的重复成员。

    +
    1
    2
    3
    let arr = [3, 5, 2, 2, 5, 5];
    let unique = [...new Set(arr)];
    // [3, 5, 2]
    + +

    而且,数组的mapfilter方法也可以间接用于 Set 了。

    +
    1
    2
    3
    4
    5
    6
    7
    let set = new Set([1, 2, 3]);
    set = new Set([...set].map((x) => x * 2));
    // 返回Set结构:{2, 4, 6}

    let set = new Set([1, 2, 3, 4, 5]);
    set = new Set([...set].filter((x) => x % 2 == 0));
    // 返回Set结构:{2, 4}
    + +

    因此使用 Set 可以很容易地实现并集(Union)、交集(Intersect)和差集(Difference)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let a = new Set([1, 2, 3]);
    let b = new Set([4, 3, 2]);

    // 并集
    let union = new Set([...a, ...b]);
    // Set {1, 2, 3, 4}

    // 交集
    let intersect = new Set([...a].filter((x) => b.has(x)));
    // set {2, 3}

    // 差集
    let difference = new Set([...a].filter((x) => !b.has(x)));
    // Set {1}
    + +

    如果想在遍历操作中,同步改变原来的 Set 结构,目前没有直接的方法,但有两种变通方法。一种是利用原 Set 结构映射出一个新的结构,然后赋值给原来的 Set 结构;另一种是利用Array.from方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 方法一
    let set = new Set([1, 2, 3]);
    set = new Set([...set].map((val) => val * 2));
    // set的值是2, 4, 6

    // 方法二
    let set = new Set([1, 2, 3]);
    set = new Set(Array.from(set, (val) => val * 2));
    // set的值是2, 4, 6
    + +

    上面代码提供了两种方法,直接在遍历操作中改变原来的 Set 结构。

    +

    WeakSet

    含义

    WeakSet 结构与 Set 类似,也是不重复的值的集合。但是,它与 Set 有两个区别。

    +

    首先,WeakSet 的成员只能是对象,而不能是其他类型的值。

    +
    1
    2
    3
    4
    5
    const ws = new WeakSet();
    ws.add(1);
    // TypeError: Invalid value used in weak set
    ws.add(Symbol());
    // TypeError: invalid value used in weak set
    + +

    上面代码试图向 WeakSet 添加一个数值和Symbol值,结果报错,因为 WeakSet 只能放置对象。

    +

    其次,WeakSet 中的对象都是弱引用,即垃圾回收机制不考虑 WeakSet 对该对象的引用,也就是说,如果其他对象都不再引用该对象,那么垃圾回收机制会自动回收该对象所占用的内存,不考虑该对象还存在于 WeakSet 之中。

    +

    这是因为垃圾回收机制依赖引用计数,如果一个值的引用次数不为0,垃圾回收机制就不会释放这块内存。结束使用该值之后,有时会忘记取消引用,导致内存无法释放,进而可能会引发内存泄漏。WeakSet 里面的引用,都不计入垃圾回收机制,所以就不存在这个问题。因此,WeakSet 适合临时存放一组对象,以及存放跟对象绑定的信息。只要这些对象在外部消失,它在 WeakSet 里面的引用就会自动消失。

    +

    由于上面这个特点,WeakSet 的成员是不适合引用的,因为它会随时消失。另外,由于 WeakSet 内部有多少个成员,取决于垃圾回收机制有没有运行,运行前后很可能成员个数是不一样的,而垃圾回收机制何时运行是不可预测的,因此 ES6 规定 WeakSet 不可遍历。

    +

    这些特点同样适用于本章后面要介绍的 WeakMap 结构。

    +

    语法

    WeakSet 是一个构造函数,可以使用new命令,创建 WeakSet 数据结构。

    +
    1
    const ws = new WeakSet();
    + +

    作为构造函数,WeakSet 可以接受一个数组或类似数组的对象作为参数。(实际上,任何具有 Iterable 接口的对象,都可以作为 WeakSet 的参数。)该数组的所有成员,都会自动成为 WeakSet 实例对象的成员。

    +
    1
    2
    3
    4
    5
    6
    const a = [
    [1, 2],
    [3, 4],
    ];
    const ws = new WeakSet(a);
    // WeakSet {[1, 2], [3, 4]}
    + +

    上面代码中,a是一个数组,它有两个成员,也都是数组。将a作为 WeakSet 构造函数的参数,a的成员会自动成为 WeakSet 的成员。

    +

    注意,是a数组的成员成为 WeakSet 的成员,而不是a数组本身。这意味着,数组的成员只能是对象。

    +
    1
    2
    3
    const b = [3, 4];
    const ws = new WeakSet(b);
    // Uncaught TypeError: Invalid value used in weak set(…)
    + +

    上面代码中,数组b的成员不是对象,加入 WeaKSet 就会报错。

    +

    WeakSet 结构有以下三个方法。

    +
      +
    • **WeakSet.prototype.add(value)**:向 WeakSet 实例添加一个新成员。
    • +
    • **WeakSet.prototype.delete(value)**:清除 WeakSet 实例的指定成员。
    • +
    • **WeakSet.prototype.has(value)**:返回一个布尔值,表示某个值是否在 WeakSet 实例之中。
    • +
    +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const ws = new WeakSet();
    const obj = {};
    const foo = {};

    ws.add(window);
    ws.add(obj);

    ws.has(window); // true
    ws.has(foo); // false

    ws.delete(window);
    ws.has(window); // false
    + +

    WeakSet 没有size属性,没有办法遍历它的成员。

    +
    1
    2
    3
    4
    5
    6
    7
    ws.size; // undefined
    ws.forEach; // undefined

    ws.forEach(function (item) {
    console.log("WeakSet has " + item);
    });
    // TypeError: undefined is not a function
    + +

    上面代码试图获取sizeforEach属性,结果都不能成功。

    +

    WeakSet 不能遍历,是因为成员都是弱引用,随时可能消失,遍历机制无法保证成员的存在,很可能刚刚遍历结束,成员就取不到了。WeakSet 的一个用处,是储存 DOM 节点,而不用担心这些节点从文档移除时,会引发内存泄漏。

    +

    下面是 WeakSet 的另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const foos = new WeakSet();
    class Foo {
    constructor() {
    foos.add(this);
    }
    method() {
    if (!foos.has(this)) {
    throw new TypeError("Foo.prototype.method 只能在Foo的实例上调用!");
    }
    }
    }
    + +

    上面代码保证了Foo的实例方法,只能在Foo的实例上调用。这里使用 WeakSet 的好处是,foos对实例的引用,不会被计入内存回收机制,所以删除实例的时候,不用考虑foos,也不会出现内存泄漏。

    +

    Map

    含义和基本用法

    JavaScript 的对象(Object),本质上是键值对的集合(Hash 结构),但是传统上只能用字符串当作键。这给它的使用带来了很大的限制。

    +
    1
    2
    3
    4
    5
    const data = {};
    const element = document.getElementById("myDiv");

    data[element] = "metadata";
    data["[object HTMLDivElement]"]; // "metadata"
    + +

    上面代码原意是将一个 DOM 节点作为对象data的键,但是由于对象只接受字符串作为键名,所以element被自动转为字符串[object HTMLDivElement]

    +

    为了解决这个问题,ES6 提供了 Map 数据结构。它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串,各种类型的值(包括对象)都可以当作键。也就是说,Object 结构提供了“字符串—值”的对应,Map 结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。如果你需要“键值对”的数据结构,Map 比 Object 更合适。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const m = new Map();
    const o = { p: "Hello World" };

    m.set(o, "content");
    m.get(o); // "content"

    m.has(o); // true
    m.delete(o); // true
    m.has(o); // false
    + +

    上面代码使用 Map 结构的set方法,将对象o当作m的一个键,然后又使用get方法读取这个键,接着使用delete方法删除了这个键。

    +

    上面的例子展示了如何向 Map 添加成员。作为构造函数,Map 也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const map = new Map([
    ["name", "张三"],
    ["title", "Author"],
    ]);

    map.size; // 2
    map.has("name"); // true
    map.get("name"); // "张三"
    map.has("title"); // true
    map.get("title"); // "Author"
    + +

    上面代码在新建 Map 实例时,就指定了两个键nametitle

    +

    Map构造函数接受数组作为参数,实际上执行的是下面的算法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const items = [
    ["name", "张三"],
    ["title", "Author"],
    ];

    const map = new Map();

    items.forEach(([key, value]) => map.set(key, value));
    + +

    事实上,不仅仅是数组,任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构(详见《Iterator》一章)都可以当作Map构造函数的参数。这就是说,SetMap都可以用来生成新的 Map。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const set = new Set([
    ["foo", 1],
    ["bar", 2],
    ]);
    const m1 = new Map(set);
    m1.get("foo"); // 1

    const m2 = new Map([["baz", 3]]);
    const m3 = new Map(m2);
    m3.get("baz"); // 3
    + +

    上面代码中,我们分别使用 Set 对象和 Map 对象,当作Map构造函数的参数,结果都生成了新的 Map 对象。

    +

    如果对同一个键多次赋值,后面的值将覆盖前面的值。

    +
    1
    2
    3
    4
    5
    const map = new Map();

    map.set(1, "aaa").set(1, "bbb");

    map.get(1); // "bbb"
    + +

    上面代码对键1连续赋值两次,后一次的值覆盖前一次的值。

    +

    如果读取一个未知的键,则返回undefined

    +
    1
    2
    new Map().get("asfddfsasadf");
    // undefined
    + +

    注意,只有对同一个对象的引用,Map 结构才将其视为同一个键。这一点要非常小心。

    +
    1
    2
    3
    4
    const map = new Map();

    map.set(["a"], 555);
    map.get(["a"]); // undefined
    + +

    上面代码的setget方法,表面是针对同一个键,但实际上这是两个值,内存地址是不一样的,因此get方法无法读取该键,返回undefined

    +

    同理,同样的值的两个实例,在 Map 结构中被视为两个键。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const map = new Map();

    const k1 = ["a"];
    const k2 = ["a"];

    map.set(k1, 111).set(k2, 222);

    map.get(k1); // 111
    map.get(k2); // 222
    + +

    上面代码中,变量k1k2的值是一样的,但是它们在 Map 结构中被视为两个键。

    +

    由上可知,Map 的键实际上是跟内存地址绑定的,只要内存地址不一样,就视为两个键。这就解决了同名属性碰撞(clash)的问题,我们扩展别人的库的时候,如果使用对象作为键名,就不用担心自己的属性与原作者的属性同名。

    +

    如果 Map 的键是一个简单类型的值(数字、字符串、布尔值),则只要两个值严格相等,Map 将其视为一个键,比如0-0就是一个键,布尔值true和字符串true则是两个不同的键。另外,undefinednull也是两个不同的键。虽然NaN不严格相等于自身,但 Map 将其视为同一个键。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let map = new Map();

    map.set(-0, 123);
    map.get(+0); // 123

    map.set(true, 1);
    map.set("true", 2);
    map.get(true); // 1

    map.set(undefined, 3);
    map.set(null, 4);
    map.get(undefined); // 3

    map.set(NaN, 123);
    map.get(NaN); // 123
    + +

    实例的属性和操作方法

    Map 结构的实例有以下属性和操作方法。

    +

    (1)size 属性

    +

    size属性返回 Map 结构的成员总数。

    +
    1
    2
    3
    4
    5
    const map = new Map();
    map.set("foo", true);
    map.set("bar", false);

    map.size; // 2
    + +

    (2)set(key, value)

    +

    set方法设置键名key对应的键值为value,然后返回整个 Map 结构。如果key已经有值,则键值会被更新,否则就新生成该键。

    +
    1
    2
    3
    4
    5
    const m = new Map();

    m.set("edition", 6); // 键是字符串
    m.set(262, "standard"); // 键是数值
    m.set(undefined, "nah"); // 键是 undefined
    + +

    set方法返回的是当前的Map对象,因此可以采用链式写法。

    +
    1
    let map = new Map().set(1, "a").set(2, "b").set(3, "c");
    + +

    (3)get(key)

    +

    get方法读取key对应的键值,如果找不到key,返回undefined

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const m = new Map();

    const hello = function () {
    console.log("hello");
    };
    m.set(hello, "Hello ES6!"); // 键是函数

    m.get(hello); // Hello ES6!
    + +

    (4)has(key)

    +

    has方法返回一个布尔值,表示某个键是否在当前 Map 对象之中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const m = new Map();

    m.set("edition", 6);
    m.set(262, "standard");
    m.set(undefined, "nah");

    m.has("edition"); // true
    m.has("years"); // false
    m.has(262); // true
    m.has(undefined); // true
    + +

    (5)delete(key)

    +

    delete方法删除某个键,返回true。如果删除失败,返回false

    +
    1
    2
    3
    4
    5
    6
    const m = new Map();
    m.set(undefined, "nah");
    m.has(undefined); // true

    m.delete(undefined);
    m.has(undefined); // false
    + +

    (6)clear()

    +

    clear方法清除所有成员,没有返回值。

    +
    1
    2
    3
    4
    5
    6
    7
    let map = new Map();
    map.set("foo", true);
    map.set("bar", false);

    map.size; // 2
    map.clear();
    map.size; // 0
    + +

    遍历方法

    Map 结构原生提供三个遍历器生成函数和一个遍历方法。

    +
      +
    • keys():返回键名的遍历器。
    • +
    • values():返回键值的遍历器。
    • +
    • entries():返回所有成员的遍历器。
    • +
    • forEach():遍历 Map 的所有成员。
    • +
    +

    需要特别注意的是,Map 的遍历顺序就是插入顺序。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    const map = new Map([
    ["F", "no"],
    ["T", "yes"],
    ]);

    for (let key of map.keys()) {
    console.log(key);
    }
    // "F"
    // "T"

    for (let value of map.values()) {
    console.log(value);
    }
    // "no"
    // "yes"

    for (let item of map.entries()) {
    console.log(item[0], item[1]);
    }
    // "F" "no"
    // "T" "yes"

    // 或者
    for (let [key, value] of map.entries()) {
    console.log(key, value);
    }
    // "F" "no"
    // "T" "yes"

    // 等同于使用map.entries()
    for (let [key, value] of map) {
    console.log(key, value);
    }
    // "F" "no"
    // "T" "yes"
    + +

    上面代码最后的那个例子,表示 Map 结构的默认遍历器接口(Symbol.iterator属性),就是entries方法。

    +
    1
    2
    map[Symbol.iterator] === map.entries;
    // true
    + +

    Map 结构转为数组结构,比较快速的方法是使用扩展运算符(...)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const map = new Map([
    [1, 'one'],
    [2, 'two'],
    [3, 'three'],
    ]);

    [...map.keys()]
    // [1, 2, 3]

    [...map.values()]
    // ['one', 'two', 'three']

    [...map.entries()]
    // [[1,'one'], [2, 'two'], [3, 'three']]

    [...map]
    // [[1,'one'], [2, 'two'], [3, 'three']]
    + +

    结合数组的map方法、filter方法,可以实现 Map 的遍历和过滤(Map 本身没有mapfilter方法)。

    +
    1
    2
    3
    4
    5
    6
    7
    const map0 = new Map().set(1, "a").set(2, "b").set(3, "c");

    const map1 = new Map([...map0].filter(([k, v]) => k < 3));
    // 产生 Map 结构 {1 => 'a', 2 => 'b'}

    const map2 = new Map([...map0].map(([k, v]) => [k * 2, "_" + v]));
    // 产生 Map 结构 {2 => '_a', 4 => '_b', 6 => '_c'}
    + +

    此外,Map 还有一个forEach方法,与数组的forEach方法类似,也可以实现遍历。

    +
    1
    2
    3
    map.forEach(function (value, key, map) {
    console.log("Key: %s, Value: %s", key, value);
    });
    + +

    forEach方法还可以接受第二个参数,用来绑定this

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const reporter = {
    report: function (key, value) {
    console.log("Key: %s, Value: %s", key, value);
    },
    };

    map.forEach(function (value, key, map) {
    this.report(key, value);
    }, reporter);
    + +

    上面代码中,forEach方法的回调函数的this,就指向reporter

    +

    与其他数据结构的互相转换

    (1)Map 转为数组

    +

    前面已经提过,Map 转为数组最方便的方法,就是使用扩展运算符(...)。

    +
    1
    2
    3
    const myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]);
    [...myMap];
    // [ [ true, 7 ], [ { foo: 3 }, [ 'abc' ] ] ]
    + +

    (2)数组 转为 Map

    +

    将数组传入 Map 构造函数,就可以转为 Map。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    new Map([
    [true, 7],
    [{ foo: 3 }, ["abc"]],
    ]);
    // Map {
    // true => 7,
    // Object {foo: 3} => ['abc']
    // }
    + +

    (3)Map 转为对象

    +

    如果所有 Map 的键都是字符串,它可以无损地转为对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function strMapToObj(strMap) {
    let obj = Object.create(null);
    for (let [k, v] of strMap) {
    obj[k] = v;
    }
    return obj;
    }

    const myMap = new Map().set("yes", true).set("no", false);
    strMapToObj(myMap);
    // { yes: true, no: false }
    + +

    如果有非字符串的键名,那么这个键名会被转成字符串,再作为对象的键名。

    +

    (4)对象转为 Map

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function objToStrMap(obj) {
    let strMap = new Map();
    for (let k of Object.keys(obj)) {
    strMap.set(k, obj[k]);
    }
    return strMap;
    }

    objToStrMap({ yes: true, no: false });
    // Map {"yes" => true, "no" => false}
    + +

    (5)Map 转为 JSON

    +

    Map 转为 JSON 要区分两种情况。一种情况是,Map 的键名都是字符串,这时可以选择转为对象 JSON。

    +
    1
    2
    3
    4
    5
    6
    7
    function strMapToJson(strMap) {
    return JSON.stringify(strMapToObj(strMap));
    }

    let myMap = new Map().set("yes", true).set("no", false);
    strMapToJson(myMap);
    // '{"yes":true,"no":false}'
    + +

    另一种情况是,Map 的键名有非字符串,这时可以选择转为数组 JSON。

    +
    1
    2
    3
    4
    5
    6
    7
    function mapToArrayJson(map) {
    return JSON.stringify([...map]);
    }

    let myMap = new Map().set(true, 7).set({ foo: 3 }, ["abc"]);
    mapToArrayJson(myMap);
    // '[[true,7],[{"foo":3},["abc"]]]'
    + +

    (6)JSON 转为 Map

    +

    JSON 转为 Map,正常情况下,所有键名都是字符串。

    +
    1
    2
    3
    4
    5
    6
    function jsonToStrMap(jsonStr) {
    return objToStrMap(JSON.parse(jsonStr));
    }

    jsonToStrMap('{"yes": true, "no": false}');
    // Map {'yes' => true, 'no' => false}
    + +

    但是,有一种特殊情况,整个 JSON 就是一个数组,且每个数组成员本身,又是一个有两个成员的数组。这时,它可以一一对应地转为 Map。这往往是 Map 转为数组 JSON 的逆操作。

    +
    1
    2
    3
    4
    5
    6
    function jsonToMap(jsonStr) {
    return new Map(JSON.parse(jsonStr));
    }

    jsonToMap('[[true,7],[{"foo":3},["abc"]]]');
    // Map {true => 7, Object {foo: 3} => ['abc']}
    + +

    WeakMap

    含义

    WeakMap结构与Map结构类似,也是用于生成键值对的集合。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // WeakMap 可以使用 set 方法添加成员
    const wm1 = new WeakMap();
    const key = { foo: 1 };
    wm1.set(key, 2);
    wm1.get(key); // 2

    // WeakMap 也可以接受一个数组,
    // 作为构造函数的参数
    const k1 = [1, 2, 3];
    const k2 = [4, 5, 6];
    const wm2 = new WeakMap([
    [k1, "foo"],
    [k2, "bar"],
    ]);
    wm2.get(k2); // "bar"
    + +

    WeakMapMap的区别有两点。

    +

    首先,WeakMap只接受对象作为键名(null除外),不接受其他类型的值作为键名。

    +
    1
    2
    3
    4
    5
    6
    7
    const map = new WeakMap();
    map.set(1, 2);
    // TypeError: 1 is not an object!
    map.set(Symbol(), 2);
    // TypeError: Invalid value used as weak map key
    map.set(null, 2);
    // TypeError: Invalid value used as weak map key
    + +

    上面代码中,如果将数值1Symbol值作为 WeakMap 的键名,都会报错。

    +

    其次,WeakMap的键名所指向的对象,不计入垃圾回收机制。

    +

    WeakMap的设计目的在于,有时我们想在某个对象上面存放一些数据,但是这会形成对于这个对象的引用。请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    const e1 = document.getElementById("foo");
    const e2 = document.getElementById("bar");
    const arr = [
    [e1, "foo 元素"],
    [e2, "bar 元素"],
    ];
    + +

    上面代码中,e1e2是两个对象,我们通过arr数组对这两个对象添加一些文字说明。这就形成了arre1e2的引用。

    +

    一旦不再需要这两个对象,我们就必须手动删除这个引用,否则垃圾回收机制就不会释放e1e2占用的内存。

    +
    1
    2
    3
    4
    // 不需要 e1 和 e2 的时候
    // 必须手动删除引用
    arr[0] = null;
    arr[1] = null;
    + +

    上面这样的写法显然很不方便。一旦忘了写,就会造成内存泄露。

    +

    WeakMap 就是为了解决这个问题而诞生的,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用。

    +

    基本上,如果你要往对象上添加数据,又不想干扰垃圾回收机制,就可以使用 WeakMap。一个典型应用场景是,在网页的 DOM 元素上添加数据,就可以使用WeakMap结构。当该 DOM 元素被清除,其所对应的WeakMap记录就会自动被移除。

    +
    1
    2
    3
    4
    5
    6
    const wm = new WeakMap();

    const element = document.getElementById("example");

    wm.set(element, "some information");
    wm.get(element); // "some information"
    + +

    上面代码中,先新建一个 Weakmap 实例。然后,将一个 DOM 节点作为键名存入该实例,并将一些附加信息作为键值,一起存放在 WeakMap 里面。这时,WeakMap 里面对element的引用就是弱引用,不会被计入垃圾回收机制。

    +

    也就是说,上面的 DOM 节点对象的引用计数是1,而不是2。这时,一旦消除对该节点的引用,它占用的内存就会被垃圾回收机制释放。Weakmap 保存的这个键值对,也会自动消失。

    +

    总之,WeakMap的专用场合就是,它的键所对应的对象,可能会在将来消失。WeakMap结构有助于防止内存泄漏。

    +

    注意,WeakMap 弱引用的只是键名,而不是键值。键值依然是正常引用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const wm = new WeakMap();
    let key = {};
    let obj = { foo: 1 };

    wm.set(key, obj);
    obj = null;
    wm.get(key);
    // Object {foo: 1}
    + +

    上面代码中,键值obj是正常引用。所以,即使在 WeakMap 外部消除了obj的引用,WeakMap 内部的引用依然存在。

    +

    WeakMap 的语法

    WeakMap 与 Map 在 API 上的区别主要是两个,一是没有遍历操作(即没有keys()values()entries()方法),也没有size属性。因为没有办法列出所有键名,某个键名是否存在完全不可预测,跟垃圾回收机制是否运行相关。这一刻可以取到键名,下一刻垃圾回收机制突然运行了,这个键名就没了,为了防止出现不确定性,就统一规定不能取到键名。二是无法清空,即不支持clear方法。因此,WeakMap只有四个方法可用:get()set()has()delete()

    +
    1
    2
    3
    4
    5
    6
    const wm = new WeakMap();

    // size、forEach、clear 方法都不存在
    wm.size; // undefined
    wm.forEach; // undefined
    wm.clear; // undefined
    + +

    WeakMap 的示例

    WeakMap 的例子很难演示,因为无法观察它里面的引用会自动消失。此时,其他引用都解除了,已经没有引用指向 WeakMap 的键名了,导致无法证实那个键名是不是存在。

    +

    贺师俊老师提示,如果引用所指向的值占用特别多的内存,就可以通过 Node 的process.memoryUsage方法看出来。根据这个思路,网友vtxf补充了下面的例子。

    +

    首先,打开 Node 命令行。

    +
    1
    $ node --expose-gc
    + +

    上面代码中,--expose-gc参数表示允许手动执行垃圾回收机制。

    +

    然后,执行下面的代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    // 手动执行一次垃圾回收,保证获取的内存使用状态准确
    > global.gc();
    undefined

    // 查看内存占用的初始状态,heapUsed 为 4M 左右
    > process.memoryUsage();
    { rss: 21106688,
    heapTotal: 7376896,
    heapUsed: 4153936,
    external: 9059 }

    > let wm = new WeakMap();
    undefined

    // 新建一个变量 key,指向一个 5*1024*1024 的数组
    > let key = new Array(5 * 1024 * 1024);
    undefined

    // 设置 WeakMap 实例的键名,也指向 key 数组
    // 这时,key 数组实际被引用了两次,
    // 变量 key 引用一次,WeakMap 的键名引用了第二次
    // 但是,WeakMap 是弱引用,对于引擎来说,引用计数还是1
    > wm.set(key, 1);
    WeakMap {}

    > global.gc();
    undefined

    // 这时内存占用 heapUsed 增加到 45M 了
    > process.memoryUsage();
    { rss: 67538944,
    heapTotal: 7376896,
    heapUsed: 45782816,
    external: 8945 }

    // 清除变量 key 对数组的引用,
    // 但没有手动清除 WeakMap 实例的键名对数组的引用
    > key = null;
    null

    // 再次执行垃圾回收
    > global.gc();
    undefined

    // 内存占用 heapUsed 变回 4M 左右,
    // 可以看到 WeakMap 的键名引用没有阻止 gc 对内存的回收
    > process.memoryUsage();
    { rss: 20639744,
    heapTotal: 8425472,
    heapUsed: 3979792,
    external: 8956 }
    + +

    上面代码中,只要外部的引用消失,WeakMap 内部的引用,就会自动被垃圾回收清除。由此可见,有了 WeakMap 的帮助,解决内存泄漏就会简单很多。

    +

    WeakMap 的用途

    前文说过,WeakMap 应用的典型场合就是 DOM 节点作为键名。下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let myElement = document.getElementById("logo");
    let myWeakmap = new WeakMap();

    myWeakmap.set(myElement, { timesClicked: 0 });

    myElement.addEventListener(
    "click",
    function () {
    let logoData = myWeakmap.get(myElement);
    logoData.timesClicked++;
    },
    false
    );
    + +

    上面代码中,myElement是一个 DOM 节点,每当发生click事件,就更新一下状态。我们将这个状态作为键值放在 WeakMap 里,对应的键名就是myElement。一旦这个 DOM 节点删除,该状态就会自动消失,不存在内存泄漏风险。

    +

    WeakMap 的另一个用处是部署私有属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const _counter = new WeakMap();
    const _action = new WeakMap();

    class Countdown {
    constructor(counter, action) {
    _counter.set(this, counter);
    _action.set(this, action);
    }
    dec() {
    let counter = _counter.get(this);
    if (counter < 1) return;
    counter--;
    _counter.set(this, counter);
    if (counter === 0) {
    _action.get(this)();
    }
    }
    }

    const c = new Countdown(2, () => console.log("DONE"));

    c.dec();
    c.dec();
    // DONE
    + +

    上面代码中,Countdown类的两个内部属性_counter_action,是实例的弱引用,所以如果删除实例,它们也就随之消失,不会造成内存泄漏。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Set%20%E5%92%8C%20Map%20%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/ES6-Symbol/index.html b/posts/ES6-Symbol/index.html new file mode 100644 index 00000000..94c1a8b8 --- /dev/null +++ b/posts/ES6-Symbol/index.html @@ -0,0 +1,430 @@ +Symbol | JCAlways + + + + + + + + + + + + + +

    Symbol

    Symbol

    概述

    ES5 的对象属性名都是字符串,这容易造成属性名的冲突。比如,你使用了一个他人提供的对象,但又想为这个对象添加新的方法(mixin 模式),新方法的名字就有可能与现有方法产生冲突。如果有一种机制,保证每个属性的名字都是独一无二的就好了,这样就从根本上防止属性名的冲突。这就是 ES6 引入Symbol的原因。

    +

    ES6 引入了一种新的原始数据类型Symbol,表示独一无二的值。它是 JavaScript 语言的第七种数据类型,前六种是:undefinednull、布尔值(Boolean)、字符串(String)、数值(Number)、对象(Object)。

    +

    Symbol 值通过Symbol函数生成。这就是说,对象的属性名现在可以有两种类型,一种是原来就有的字符串,另一种就是新增的 Symbol 类型。凡是属性名属于 Symbol 类型,就都是独一无二的,可以保证不会与其他属性名产生冲突。

    +
    1
    2
    3
    4
    let s = Symbol();

    typeof s;
    // "symbol"
    + +

    上面代码中,变量s就是一个独一无二的值。typeof运算符的结果,表明变量s是 Symbol 数据类型,而不是字符串之类的其他类型。

    +

    注意,Symbol函数前不能使用new命令,否则会报错。这是因为生成的 Symbol 是一个原始类型的值,不是对象。也就是说,由于 Symbol 值不是对象,所以不能添加属性。基本上,它是一种类似于字符串的数据类型。

    +

    Symbol函数可以接受一个字符串作为参数,表示对 Symbol 实例的描述,主要是为了在控制台显示,或者转为字符串时,比较容易区分。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let s1 = Symbol("foo");
    let s2 = Symbol("bar");

    s1; // Symbol(foo)
    s2; // Symbol(bar)

    s1.toString(); // "Symbol(foo)"
    s2.toString(); // "Symbol(bar)"
    + +

    上面代码中,s1s2是两个 Symbol 值。如果不加参数,它们在控制台的输出都是Symbol(),不利于区分。有了参数以后,就等于为它们加上了描述,输出的时候就能够分清,到底是哪一个值。

    +

    如果 Symbol 的参数是一个对象,就会调用该对象的toString方法,将其转为字符串,然后才生成一个 Symbol 值。

    +
    1
    2
    3
    4
    5
    6
    7
    const obj = {
    toString() {
    return "abc";
    },
    };
    const sym = Symbol(obj);
    sym; // Symbol(abc)
    + +

    注意,Symbol函数的参数只是表示对当前 Symbol 值的描述,因此相同参数的Symbol函数的返回值是不相等的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 没有参数的情况
    let s1 = Symbol();
    let s2 = Symbol();

    s1 === s2; // false

    // 有参数的情况
    let s1 = Symbol("foo");
    let s2 = Symbol("foo");

    s1 === s2; // false
    + +

    上面代码中,s1s2都是Symbol函数的返回值,而且参数相同,但是它们是不相等的。

    +

    Symbol 值不能与其他类型的值进行运算,会报错。

    +
    1
    2
    3
    4
    5
    6
    let sym = Symbol("My symbol");

    "your symbol is " +
    sym // TypeError: can't convert symbol to string
    `your symbol is ${sym}`;
    // TypeError: can't convert symbol to string
    + +

    但是,Symbol 值可以显式转为字符串。

    +
    1
    2
    3
    4
    let sym = Symbol("My symbol");

    String(sym); // 'Symbol(My symbol)'
    sym.toString(); // 'Symbol(My symbol)'
    + +

    另外,Symbol 值也可以转为布尔值,但是不能转为数值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let sym = Symbol();
    Boolean(sym); // true
    !sym; // false

    if (sym) {
    // ...
    }

    Number(sym); // TypeError
    sym + 2; // TypeError
    + +

    作为属性名的 Symbol

    由于每一个 Symbol 值都是不相等的,这意味着 Symbol 值可以作为标识符,用于对象的属性名,就能保证不会出现同名的属性。这对于一个对象由多个模块构成的情况非常有用,能防止某一个键被不小心改写或覆盖。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let mySymbol = Symbol();

    // 第一种写法
    let a = {};
    a[mySymbol] = "Hello!";

    // 第二种写法
    let a = {
    [mySymbol]: "Hello!",
    };

    // 第三种写法
    let a = {};
    Object.defineProperty(a, mySymbol, { value: "Hello!" });

    // 以上写法都得到同样结果
    a[mySymbol]; // "Hello!"
    + +

    上面代码通过方括号结构和Object.defineProperty,将对象的属性名指定为一个 Symbol 值。

    +

    注意,Symbol 值作为对象属性名时,不能用点运算符。

    +
    1
    2
    3
    4
    5
    6
    const mySymbol = Symbol();
    const a = {};

    a.mySymbol = "Hello!";
    a[mySymbol]; // undefined
    a["mySymbol"]; // "Hello!"
    + +

    上面代码中,因为点运算符后面总是字符串,所以不会读取mySymbol作为标识名所指代的那个值,导致a的属性名实际上是一个字符串,而不是一个 Symbol 值。

    +

    同理,在对象的内部,使用 Symbol 值定义属性时,Symbol 值必须放在方括号之中。

    +
    1
    2
    3
    4
    5
    6
    7
    let s = Symbol();

    let obj = {
    [s]: function (arg) { ... }
    };

    obj[s](123);
    + +

    上面代码中,如果s不放在方括号中,该属性的键名就是字符串s,而不是s所代表的那个 Symbol 值。

    +

    采用增强的对象写法,上面代码的obj对象可以写得更简洁一些。

    +
    1
    2
    3
    let obj = {
    [s](arg) { ... }
    };
    + +

    Symbol 类型还可以用于定义一组常量,保证这组常量的值都是不相等的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const log = {};

    log.levels = {
    DEBUG: Symbol("debug"),
    INFO: Symbol("info"),
    WARN: Symbol("warn"),
    };
    console.log(log.levels.DEBUG, "debug message");
    console.log(log.levels.INFO, "info message");
    + +

    下面是另外一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const COLOR_RED = Symbol();
    const COLOR_GREEN = Symbol();

    function getComplement(color) {
    switch (color) {
    case COLOR_RED:
    return COLOR_GREEN;
    case COLOR_GREEN:
    return COLOR_RED;
    default:
    throw new Error("Undefined color");
    }
    }
    + +

    常量使用 Symbol 值最大的好处,就是其他任何值都不可能有相同的值了,因此可以保证上面的switch语句会按设计的方式工作。

    +

    还有一点需要注意,Symbol 值作为属性名时,该属性还是公开属性,不是私有属性。

    +

    实例:消除魔术字符串

    魔术字符串指的是,在代码之中多次出现、与代码形成强耦合的某一个具体的字符串或者数值。风格良好的代码,应该尽量消除魔术字符串,改由含义清晰的变量代替。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function getArea(shape, options) {
    let area = 0;

    switch (shape) {
    case "Triangle": // 魔术字符串
    area = 0.5 * options.width * options.height;
    break;
    /* ... more code ... */
    }

    return area;
    }

    getArea("Triangle", { width: 100, height: 100 }); // 魔术字符串
    + +

    上面代码中,字符串Triangle就是一个魔术字符串。它多次出现,与代码形成“强耦合”,不利于将来的修改和维护。

    +

    常用的消除魔术字符串的方法,就是把它写成一个变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const shapeType = {
    triangle: "Triangle",
    };

    function getArea(shape, options) {
    let area = 0;
    switch (shape) {
    case shapeType.triangle:
    area = 0.5 * options.width * options.height;
    break;
    }
    return area;
    }

    getArea(shapeType.triangle, { width: 100, height: 100 });
    + +

    上面代码中,我们把Triangle写成shapeType对象的triangle属性,这样就消除了强耦合。

    +

    如果仔细分析,可以发现shapeType.triangle等于哪个值并不重要,只要确保不会跟其他shapeType属性的值冲突即可。因此,这里就很适合改用 Symbol 值。

    +
    1
    2
    3
    const shapeType = {
    triangle: Symbol(),
    };
    + +

    上面代码中,除了将shapeType.triangle的值设为一个 Symbol,其他地方都不用修改。

    +

    属性名的遍历

    Symbol 作为属性名,该属性不会出现在for...infor...of循环中,也不会被Object.keys()Object.getOwnPropertyNames()JSON.stringify()返回。但是,它也不是私有属性,有一个Object.getOwnPropertySymbols方法,可以获取指定对象的所有 Symbol 属性名。

    +

    Object.getOwnPropertySymbols方法返回一个数组,成员是当前对象的所有用作属性名的 Symbol 值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const obj = {};
    let a = Symbol("a");
    let b = Symbol("b");

    obj[a] = "Hello";
    obj[b] = "World";

    const objectSymbols = Object.getOwnPropertySymbols(obj);

    objectSymbols;
    // [Symbol(a), Symbol(b)]
    + +

    下面是另一个例子,Object.getOwnPropertySymbols方法与for...in循环、Object.getOwnPropertyNames方法进行对比的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const obj = {};

    let foo = Symbol("foo");

    Object.defineProperty(obj, foo, {
    value: "foobar",
    });

    for (let i in obj) {
    console.log(i); // 无输出
    }

    Object.getOwnPropertyNames(obj);
    // []

    Object.getOwnPropertySymbols(obj);
    // [Symbol(foo)]
    + +

    上面代码中,使用Object.getOwnPropertyNames方法得不到Symbol属性名,需要使用Object.getOwnPropertySymbols方法。

    +

    另一个新的 API,Reflect.ownKeys方法可以返回所有类型的键名,包括常规键名和 Symbol 键名。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let obj = {
    [Symbol("my_key")]: 1,
    enum: 2,
    nonEnum: 3,
    };

    Reflect.ownKeys(obj);
    // ["enum", "nonEnum", Symbol(my_key)]
    + +

    由于以 Symbol 值作为名称的属性,不会被常规方法遍历得到。我们可以利用这个特性,为对象定义一些非私有的、但又希望只用于内部的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    let size = Symbol("size");

    class Collection {
    constructor() {
    this[size] = 0;
    }

    add(item) {
    this[this[size]] = item;
    this[size]++;
    }

    static sizeOf(instance) {
    return instance[size];
    }
    }

    let x = new Collection();
    Collection.sizeOf(x); // 0

    x.add("foo");
    Collection.sizeOf(x); // 1

    Object.keys(x); // ['0']
    Object.getOwnPropertyNames(x); // ['0']
    Object.getOwnPropertySymbols(x); // [Symbol(size)]
    + +

    上面代码中,对象xsize属性是一个 Symbol 值,所以Object.keys(x)Object.getOwnPropertyNames(x)都无法获取它。这就造成了一种非私有的内部方法的效果。

    +

    Symbol.for(),Symbol.keyFor()

    有时,我们希望重新使用同一个 Symbol 值,Symbol.for方法可以做到这一点。它接受一个字符串作为参数,然后搜索有没有以该参数作为名称的 Symbol 值。如果有,就返回这个 Symbol 值,否则就新建并返回一个以该字符串为名称的 Symbol 值。

    +
    1
    2
    3
    4
    let s1 = Symbol.for("foo");
    let s2 = Symbol.for("foo");

    s1 === s2; // true
    + +

    上面代码中,s1s2都是 Symbol 值,但是它们都是同样参数的Symbol.for方法生成的,所以实际上是同一个值。

    +

    Symbol.for()Symbol()这两种写法,都会生成新的 Symbol。它们的区别是,前者会被登记在全局环境中供搜索,后者不会。Symbol.for()不会每次调用就返回一个新的 Symbol 类型的值,而是会先检查给定的key是否已经存在,如果不存在才会新建一个值。比如,如果你调用Symbol.for("cat")30 次,每次都会返回同一个 Symbol 值,但是调用Symbol("cat")30 次,会返回 30 个不同的 Symbol 值。

    +
    1
    2
    3
    4
    5
    Symbol.for("bar") === Symbol.for("bar");
    // true

    Symbol("bar") === Symbol("bar");
    // false
    + +

    上面代码中,由于Symbol()写法没有登记机制,所以每次调用都会返回一个不同的值。

    +

    Symbol.keyFor方法返回一个已登记的 Symbol 类型值的key

    +
    1
    2
    3
    4
    5
    let s1 = Symbol.for("foo");
    Symbol.keyFor(s1); // "foo"

    let s2 = Symbol("foo");
    Symbol.keyFor(s2); // undefined
    + +

    上面代码中,变量s2属于未登记的 Symbol 值,所以返回undefined

    +

    需要注意的是,Symbol.for为 Symbol 值登记的名字,是全局环境的,可以在不同的 iframe 或 service worker 中取到同一个值。

    +
    1
    2
    3
    4
    5
    6
    iframe = document.createElement("iframe");
    iframe.src = String(window.location);
    document.body.appendChild(iframe);

    iframe.contentWindow.Symbol.for("foo") === Symbol.for("foo");
    // true
    + +

    上面代码中,iframe 窗口生成的 Symbol 值,可以在主页面得到。

    +

    实例:模块的 Singleton 模式

    Singleton 模式指的是调用一个类,任何时候返回的都是同一个实例。

    +

    对于 Node 来说,模块文件可以看成是一个类。怎么保证每次执行这个模块文件,返回的都是同一个实例呢?

    +

    很容易想到,可以把实例放到顶层对象global

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // mod.js
    function A() {
    this.foo = "hello";
    }

    if (!global._foo) {
    global._foo = new A();
    }

    module.exports = global._foo;
    + +

    然后,加载上面的mod.js

    +
    1
    2
    const a = require("./mod.js");
    console.log(a.foo);
    + +

    上面代码中,变量a任何时候加载的都是A的同一个实例。

    +

    但是,这里有一个问题,全局变量global._foo是可写的,任何文件都可以修改。

    +
    1
    2
    3
    4
    global._foo = { foo: "world" };

    const a = require("./mod.js");
    console.log(a.foo);
    + +

    上面的代码,会使得加载mod.js的脚本都失真。

    +

    为了防止这种情况出现,我们就可以使用 Symbol。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // mod.js
    const FOO_KEY = Symbol.for("foo");

    function A() {
    this.foo = "hello";
    }

    if (!global[FOO_KEY]) {
    global[FOO_KEY] = new A();
    }

    module.exports = global[FOO_KEY];
    + +

    上面代码中,可以保证global[FOO_KEY]不会被无意间覆盖,但还是可以被改写。

    +
    1
    2
    3
    global[Symbol.for("foo")] = { foo: "world" };

    const a = require("./mod.js");
    + +

    如果键名使用Symbol方法生成,那么外部将无法引用这个值,当然也就无法改写。

    +
    1
    2
    3
    4
    // mod.js
    const FOO_KEY = Symbol("foo");

    // 后面代码相同 ……
    + +

    上面代码将导致其他脚本都无法引用FOO_KEY。但这样也有一个问题,就是如果多次执行这个脚本,每次得到的FOO_KEY都是不一样的。虽然 Node 会将脚本的执行结果缓存,一般情况下,不会多次执行同一个脚本,但是用户可以手动清除缓存,所以也不是绝对可靠。

    +

    内置的 Symbol 值

    除了定义自己使用的 Symbol 值以外,ES6 还提供了 11 个内置的 Symbol 值,指向语言内部使用的方法。

    +

    Symbol.hasInstance

    对象的Symbol.hasInstance属性,指向一个内部方法。当其他对象使用instanceof运算符,判断是否为该对象的实例时,会调用这个方法。比如,foo instanceof Foo在语言内部,实际调用的是Foo[Symbol.hasInstance](foo)

    +
    1
    2
    3
    4
    5
    6
    7
    class MyClass {
    [Symbol.hasInstance](foo) {
    return foo instanceof Array;
    }
    }

    [1, 2, 3] instanceof new MyClass(); // true
    + +

    上面代码中,MyClass是一个类,new MyClass()会返回一个实例。该实例的Symbol.hasInstance方法,会在进行instanceof运算时自动调用,判断左侧的运算子是否为Array的实例。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    class Even {
    static [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
    }
    }

    // 等同于
    const Even = {
    [Symbol.hasInstance](obj) {
    return Number(obj) % 2 === 0;
    },
    };

    1 instanceof Even; // false
    2 instanceof Even; // true
    12345 instanceof Even; // false
    + +

    Symbol.isConcatSpreadable

    对象的Symbol.isConcatSpreadable属性等于一个布尔值,表示该对象用于Array.prototype.concat()时,是否可以展开。

    +
    1
    2
    3
    4
    5
    6
    7
    let arr1 = ["c", "d"];
    ["a", "b"].concat(arr1, "e"); // ['a', 'b', 'c', 'd', 'e']
    arr1[Symbol.isConcatSpreadable]; // undefined

    let arr2 = ["c", "d"];
    arr2[Symbol.isConcatSpreadable] = false;
    ["a", "b"].concat(arr2, "e"); // ['a', 'b', ['c','d'], 'e']
    + +

    上面代码说明,数组的默认行为是可以展开,Symbol.isConcatSpreadable默认等于undefined。该属性等于true时,也有展开的效果。

    +

    类似数组的对象正好相反,默认不展开。它的Symbol.isConcatSpreadable属性设为true,才可以展开。

    +
    1
    2
    3
    4
    5
    let obj = { length: 2, 0: "c", 1: "d" };
    ["a", "b"].concat(obj, "e"); // ['a', 'b', obj, 'e']

    obj[Symbol.isConcatSpreadable] = true;
    ["a", "b"].concat(obj, "e"); // ['a', 'b', 'c', 'd', 'e']
    + +

    Symbol.isConcatSpreadable属性也可以定义在类里面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class A1 extends Array {
    constructor(args) {
    super(args);
    this[Symbol.isConcatSpreadable] = true;
    }
    }
    class A2 extends Array {
    constructor(args) {
    super(args);
    }
    get [Symbol.isConcatSpreadable]() {
    return false;
    }
    }
    let a1 = new A1();
    a1[0] = 3;
    a1[1] = 4;
    let a2 = new A2();
    a2[0] = 5;
    a2[1] = 6;
    [1, 2].concat(a1).concat(a2);
    // [1, 2, 3, 4, [5, 6]]
    + +

    上面代码中,类A1是可展开的,类A2是不可展开的,所以使用concat时有不一样的结果。

    +

    注意,Symbol.isConcatSpreadable的位置差异,A1是定义在实例上,A2是定义在类本身,效果相同。

    +

    Symbol.species

    对象的Symbol.species属性,指向一个构造函数。创建衍生对象时,会使用该属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    class MyArray extends Array {}

    const a = new MyArray(1, 2, 3);
    const b = a.map((x) => x);
    const c = a.filter((x) => x > 1);

    b instanceof MyArray; // true
    c instanceof MyArray; // true
    + +

    上面代码中,子类MyArray继承了父类ArrayaMyArray的实例,bca的衍生对象。你可能会认为,bc都是调用数组方法生成的,所以应该是数组(Array的实例),但实际上它们也是MyArray的实例。

    +

    Symbol.species属性就是为了解决这个问题而提供的。现在,我们可以为MyArray设置Symbol.species属性。

    +
    1
    2
    3
    4
    5
    class MyArray extends Array {
    static get [Symbol.species]() {
    return Array;
    }
    }
    + +

    上面代码中,由于定义了Symbol.species属性,创建衍生对象时就会使用这个属性返回的函数,作为构造函数。这个例子也说明,定义Symbol.species属性要采用get取值器。默认的Symbol.species属性等同于下面的写法。

    +
    1
    2
    3
    static get [Symbol.species]() {
    return this;
    }
    + +

    现在,再来看前面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class MyArray extends Array {
    static get [Symbol.species]() {
    return Array;
    }
    }

    const a = new MyArray();
    const b = a.map((x) => x);

    b instanceof MyArray; // false
    b instanceof Array; // true
    + +

    上面代码中,a.map(x => x)生成的衍生对象,就不是MyArray的实例,而直接就是Array的实例。

    +

    再看一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    class T1 extends Promise {}

    class T2 extends Promise {
    static get [Symbol.species]() {
    return Promise;
    }
    }

    new T1((r) => r()).then((v) => v) instanceof T1; // true
    new T2((r) => r()).then((v) => v) instanceof T2; // false
    + +

    上面代码中,T2定义了Symbol.species属性,T1没有。结果就导致了创建衍生对象时(then方法),T1调用的是自身的构造方法,而T2调用的是Promise的构造方法。

    +

    总之,Symbol.species的作用在于,实例对象在运行过程中,需要再次调用自身的构造函数时,会调用该属性指定的构造函数。它主要的用途是,有些类库是在基类的基础上修改的,那么子类使用继承的方法时,作者可能希望返回基类的实例,而不是子类的实例。

    +

    Symbol.match

    对象的Symbol.match属性,指向一个函数。当执行str.match(myObject)时,如果该属性存在,会调用它,返回该方法的返回值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    String.prototype.match(regexp);
    // 等同于
    regexp[Symbol.match](this);

    class MyMatcher {
    [Symbol.match](string) {
    return "hello world".indexOf(string);
    }
    }

    "e".match(new MyMatcher()); // 1
    + +

    Symbol.replace

    对象的Symbol.replace属性,指向一个方法,当该对象被String.prototype.replace方法调用时,会返回该方法的返回值。

    +
    1
    2
    3
    String.prototype.replace(searchValue, replaceValue);
    // 等同于
    searchValue[Symbol.replace](this, replaceValue);
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    const x = {};
    x[Symbol.replace] = (...s) => console.log(s);

    "Hello".replace(x, "World"); // ["Hello", "World"]
    + +

    Symbol.replace方法会收到两个参数,第一个参数是replace方法正在作用的对象,上面例子是Hello,第二个参数是替换后的值,上面例子是World

    +

    对象的Symbol.search属性,指向一个方法,当该对象被String.prototype.search方法调用时,会返回该方法的返回值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    String.prototype.search(regexp);
    // 等同于
    regexp[Symbol.search](this);

    class MySearch {
    constructor(value) {
    this.value = value;
    }
    [Symbol.search](string) {
    return string.indexOf(this.value);
    }
    }
    "foobar".search(new MySearch("foo")); // 0
    + +

    Symbol.split

    对象的Symbol.split属性,指向一个方法,当该对象被String.prototype.split方法调用时,会返回该方法的返回值。

    +
    1
    2
    3
    String.prototype.split(separator, limit);
    // 等同于
    separator[Symbol.split](this, limit);
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    class MySplitter {
    constructor(value) {
    this.value = value;
    }
    [Symbol.split](string) {
    let index = string.indexOf(this.value);
    if (index === -1) {
    return string;
    }
    return [string.substr(0, index), string.substr(index + this.value.length)];
    }
    }

    "foobar".split(new MySplitter("foo"));
    // ['', 'bar']

    "foobar".split(new MySplitter("bar"));
    // ['foo', '']

    "foobar".split(new MySplitter("baz"));
    // 'foobar'
    + +

    上面方法使用Symbol.split方法,重新定义了字符串对象的split方法的行为,

    +

    Symbol.iterator

    对象的Symbol.iterator属性,指向该对象的默认遍历器方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const myIterable = {};
    myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
    yield 3;
    };

    [...myIterable]; // [1, 2, 3]
    + +

    对象进行for...of循环时,会调用Symbol.iterator方法,返回该对象的默认遍历器,详细介绍参见《Iterator 和 for…of 循环》一章。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    class Collection {
    *[Symbol.iterator]() {
    let i = 0;
    while (this[i] !== undefined) {
    yield this[i];
    ++i;
    }
    }
    }

    let myCollection = new Collection();
    myCollection[0] = 1;
    myCollection[1] = 2;

    for (let value of myCollection) {
    console.log(value);
    }
    // 1
    // 2
    + +

    Symbol.toPrimitive

    对象的Symbol.toPrimitive属性,指向一个方法。该对象被转为原始类型的值时,会调用这个方法,返回该对象对应的原始类型值。

    +

    Symbol.toPrimitive被调用时,会接受一个字符串参数,表示当前运算的模式,一共有三种模式。

    +
      +
    • Number:该场合需要转成数值
    • +
    • String:该场合需要转成字符串
    • +
    • Default:该场合可以转成数值,也可以转成字符串
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    let obj = {
    [Symbol.toPrimitive](hint) {
    switch (hint) {
    case "number":
    return 123;
    case "string":
    return "str";
    case "default":
    return "default";
    default:
    throw new Error();
    }
    },
    };

    2 * obj; // 246
    3 + obj; // '3default'
    obj == "default"; // true
    String(obj); // 'str'
    + +

    Symbol.toStringTag

    对象的Symbol.toStringTag属性,指向一个方法。在该对象上面调用Object.prototype.toString方法时,如果这个属性存在,它的返回值会出现在toString方法返回的字符串之中,表示对象的类型。也就是说,这个属性可以用来定制[object Object][object Array]object后面的那个字符串。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 例一
    ({ [Symbol.toStringTag]: "Foo" }).toString();
    // "[object Foo]"

    // 例二
    class Collection {
    get [Symbol.toStringTag]() {
    return "xxx";
    }
    }
    let x = new Collection();
    Object.prototype.toString.call(x); // "[object xxx]"
    + +

    ES6 新增内置对象的Symbol.toStringTag属性值如下。

    +
      +
    • JSON[Symbol.toStringTag]:’JSON’
    • +
    • Math[Symbol.toStringTag]:’Math’
    • +
    • Module 对象M[Symbol.toStringTag]:’Module’
    • +
    • ArrayBuffer.prototype[Symbol.toStringTag]:’ArrayBuffer’
    • +
    • DataView.prototype[Symbol.toStringTag]:’DataView’
    • +
    • Map.prototype[Symbol.toStringTag]:’Map’
    • +
    • Promise.prototype[Symbol.toStringTag]:’Promise’
    • +
    • Set.prototype[Symbol.toStringTag]:’Set’
    • +
    • %TypedArray%.prototype[Symbol.toStringTag]:’Uint8Array’等
    • +
    • WeakMap.prototype[Symbol.toStringTag]:’WeakMap’
    • +
    • WeakSet.prototype[Symbol.toStringTag]:’WeakSet’
    • +
    • %MapIteratorPrototype%[Symbol.toStringTag]:’Map Iterator’
    • +
    • %SetIteratorPrototype%[Symbol.toStringTag]:’Set Iterator’
    • +
    • %StringIteratorPrototype%[Symbol.toStringTag]:’String Iterator’
    • +
    • Symbol.prototype[Symbol.toStringTag]:’Symbol’
    • +
    • Generator.prototype[Symbol.toStringTag]:’Generator’
    • +
    • GeneratorFunction.prototype[Symbol.toStringTag]:’GeneratorFunction’
    • +
    +

    Symbol.unscopables

    对象的Symbol.unscopables属性,指向一个对象。该对象指定了使用with关键字时,哪些属性会被with环境排除。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Array.prototype[Symbol.unscopables];
    // {
    // copyWithin: true,
    // entries: true,
    // fill: true,
    // find: true,
    //   findIndex: true,
    // includes: true,
    // keys: true
    // }

    Object.keys(Array.prototype[Symbol.unscopables]);
    // ['copyWithin', 'entries', 'fill', 'find', 'findIndex', 'includes', 'keys']
    + +

    上面代码说明,数组有 7 个属性,会被with命令排除。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    // 没有 unscopables 时
    class MyClass {
    foo() {
    return 1;
    }
    }

    var foo = function () {
    return 2;
    };

    with (MyClass.prototype) {
    foo(); // 1
    }

    // 有 unscopables 时
    class MyClass {
    foo() {
    return 1;
    }
    get [Symbol.unscopables]() {
    return { foo: true };
    }
    }

    var foo = function () {
    return 2;
    };

    with (MyClass.prototype) {
    foo(); // 2
    }
    + +

    上面代码通过指定Symbol.unscopables属性,使得with语法块不会在当前作用域寻找foo属性,即foo将指向外层作用域的变量。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-Symbol/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-async \345\207\275\346\225\260/index.html" "b/posts/ES6-async \345\207\275\346\225\260/index.html" new file mode 100644 index 00000000..6dc25da7 --- /dev/null +++ "b/posts/ES6-async \345\207\275\346\225\260/index.html" @@ -0,0 +1,451 @@ +async 函数 | JCAlways + + + + + + + + + + + + + +

    async 函数

    含义

    ES2017 标准引入了 async 函数,使得异步操作变得更加方便。

    +

    async 函数是什么?一句话,它就是 Generator 函数的语法糖。

    +

    前文有一个 Generator 函数,依次读取两个文件。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const fs = require("fs");

    const readFile = function (fileName) {
    return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
    if (error) return reject(error);
    resolve(data);
    });
    });
    };

    const gen = function* () {
    const f1 = yield readFile("/etc/fstab");
    const f2 = yield readFile("/etc/shells");
    console.log(f1.toString());
    console.log(f2.toString());
    };
    + +

    写成async函数,就是下面这样。

    +
    1
    2
    3
    4
    5
    6
    const asyncReadFile = async function () {
    const f1 = await readFile("/etc/fstab");
    const f2 = await readFile("/etc/shells");
    console.log(f1.toString());
    console.log(f2.toString());
    };
    + +

    一比较就会发现,async函数就是将 Generator 函数的星号(*)替换成async,将yield替换成await,仅此而已。

    +

    async函数对 Generator 函数的改进,体现在以下四点。

    +

    (1)内置执行器。

    +

    Generator 函数的执行必须靠执行器,所以才有了co模块,而async函数自带执行器。也就是说,async函数的执行,与普通函数一模一样,只要一行。

    +
    1
    asyncReadFile();
    + +

    上面的代码调用了asyncReadFile函数,然后它就会自动执行,输出最后结果。这完全不像 Generator 函数,需要调用next方法,或者用co模块,才能真正执行,得到最后结果。

    +

    (2)更好的语义。

    +

    asyncawait,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。

    +

    (3)更广的适用性。

    +

    co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

    +

    (4)返回值是 Promise。

    +

    async函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了。你可以用then方法指定下一步的操作。

    +

    进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。

    +

    基本用法

    async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

    +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function getStockPriceByName(name) {
    const symbol = await getStockSymbol(name);
    const stockPrice = await getStockPrice(symbol);
    return stockPrice;
    }

    getStockPriceByName("goog").then(function (result) {
    console.log(result);
    });
    + +

    上面代码是一个获取股票报价的函数,函数前面的async关键字,表明该函数内部有异步操作。调用该函数时,会立即返回一个Promise对象。

    +

    下面是另一个例子,指定多少毫秒后输出一个值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function timeout(ms) {
    return new Promise((resolve) => {
    setTimeout(resolve, ms);
    });
    }

    async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value);
    }

    asyncPrint("hello world", 50);
    + +

    上面代码指定 50 毫秒以后,输出hello world

    +

    由于async函数返回的是 Promise 对象,可以作为await命令的参数。所以,上面的例子也可以写成下面的形式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    async function timeout(ms) {
    await new Promise((resolve) => {
    setTimeout(resolve, ms);
    });
    }

    async function asyncPrint(value, ms) {
    await timeout(ms);
    console.log(value);
    }

    asyncPrint("hello world", 50);
    + +

    async 函数有多种使用形式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    // 函数声明
    async function foo() {}

    // 函数表达式
    const foo = async function () {};

    // 对象的方法
    let obj = { async foo() {} };
    obj.foo().then(...)

    // Class 的方法
    class Storage {
    constructor() {
    this.cachePromise = caches.open('avatars');
    }

    async getAvatar(name) {
    const cache = await this.cachePromise;
    return cache.match(`/avatars/${name}.jpg`);
    }
    }

    const storage = new Storage();
    storage.getAvatar('jake').then(…);

    // 箭头函数
    const foo = async () => {};
    + +

    语法

    async函数的语法规则总体上比较简单,难点是错误处理机制。

    +

    返回 Promise 对象

    async函数返回一个 Promise 对象。

    +

    async函数内部return语句返回的值,会成为then方法回调函数的参数。

    +
    1
    2
    3
    4
    5
    6
    async function f() {
    return "hello world";
    }

    f().then((v) => console.log(v));
    // "hello world"
    + +

    上面代码中,函数f内部return命令返回的值,会被then方法回调函数接收到。

    +

    async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function f() {
    throw new Error("出错了");
    }

    f().then(
    (v) => console.log(v),
    (e) => console.log(e)
    );
    // Error: 出错了
    + +

    Promise 对象的状态变化

    async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

    +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    async function getTitle(url) {
    let response = await fetch(url);
    let html = await response.text();
    return html.match(/<title>([\s\S]+)<\/title>/i)[1];
    }
    getTitle("https://tc39.github.io/ecma262/").then(console.log);
    // "ECMAScript 2017 Language Specification"
    + +

    上面代码中,函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log

    +

    await 命令

    正常情况下,await命令后面是一个 Promise 对象。如果不是,就返回对应的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    async function f() {
    // 等同于
    // return 123;
    return await 123;
    }

    f().then((v) => console.log(v));
    // 123
    + +

    上面代码中,await命令的参数是数值123,这时等同于return 123

    +

    await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    async function f() {
    await Promise.reject("出错了");
    }

    f()
    .then((v) => console.log(v))
    .catch((e) => console.log(e));
    // 出错了
    + +

    注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。

    +

    只要一个await语句后面的 Promise 变为reject,那么整个async函数都会中断执行。

    +
    1
    2
    3
    4
    async function f() {
    await Promise.reject("出错了");
    await Promise.resolve("hello world"); // 不会执行
    }
    + +

    上面代码中,第二个await语句是不会执行的,因为第一个await语句状态变成了reject

    +

    有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try...catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function f() {
    try {
    await Promise.reject("出错了");
    } catch (e) {}
    return await Promise.resolve("hello world");
    }

    f().then((v) => console.log(v));
    // hello world
    + +

    另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    async function f() {
    await Promise.reject("出错了").catch((e) => console.log(e));
    return await Promise.resolve("hello world");
    }

    f().then((v) => console.log(v));
    // 出错了
    // hello world
    + +

    错误处理

    如果await后面的异步操作出错,那么等同于async函数返回的 Promise 对象被reject

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function f() {
    await new Promise(function (resolve, reject) {
    throw new Error("出错了");
    });
    }

    f()
    .then((v) => console.log(v))
    .catch((e) => console.log(e));
    // Error:出错了
    + +

    上面代码中,async函数f执行后,await后面的 Promise 对象会抛出一个错误对象,导致catch方法的回调函数被调用,它的参数就是抛出的错误对象。具体的执行机制,可以参考后文的“async 函数的实现原理”。

    +

    防止出错的方法,也是将其放在try...catch代码块之中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    async function f() {
    try {
    await new Promise(function (resolve, reject) {
    throw new Error("出错了");
    });
    } catch (e) {}
    return await "hello world";
    }
    + +

    如果有多个await命令,可以统一放在try...catch结构中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function main() {
    try {
    const val1 = await firstStep();
    const val2 = await secondStep(val1);
    const val3 = await thirdStep(val1, val2);

    console.log("Final: ", val3);
    } catch (err) {
    console.error(err);
    }
    }
    + +

    下面的例子使用try...catch结构,实现多次重复尝试。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const superagent = require("superagent");
    const NUM_RETRIES = 3;

    async function test() {
    let i;
    for (i = 0; i < NUM_RETRIES; ++i) {
    try {
    await superagent.get("http://google.com/this-throws-an-error");
    break;
    } catch (err) {}
    }
    console.log(i); // 3
    }

    test();
    + +

    上面代码中,如果await操作成功,就会使用break语句退出循环;如果失败,会被catch语句捕捉,然后进入下一轮循环。

    +

    使用注意点

    第一点,前面已经说过,await命令后面的Promise对象,运行结果可能是rejected,所以最好把await命令放在try...catch代码块中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    async function myFunction() {
    try {
    await somethingThatReturnsAPromise();
    } catch (err) {
    console.log(err);
    }
    }

    // 另一种写法

    async function myFunction() {
    await somethingThatReturnsAPromise().catch(function (err) {
    console.log(err);
    });
    }
    + +

    第二点,多个await命令后面的异步操作,如果不存在继发关系,最好让它们同时触发。

    +
    1
    2
    let foo = await getFoo();
    let bar = await getBar();
    + +

    上面代码中,getFoogetBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 写法一
    let [foo, bar] = await Promise.all([getFoo(), getBar()]);

    // 写法二
    let fooPromise = getFoo();
    let barPromise = getBar();
    let foo = await fooPromise;
    let bar = await barPromise;
    + +

    上面两种写法,getFoogetBar都是同时触发,这样就会缩短程序的执行时间。

    +

    第三点,await命令只能用在async函数之中,如果用在普通函数,就会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    async function dbFuc(db) {
    let docs = [{}, {}, {}];

    // 报错
    docs.forEach(function (doc) {
    await db.post(doc);
    });
    }
    + +

    上面代码会报错,因为await用在普通函数之中了。但是,如果将forEach方法的参数改成async函数,也有问题。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function dbFuc(db) {
    //这里不需要 async
    let docs = [{}, {}, {}];

    // 可能得到错误结果
    docs.forEach(async function (doc) {
    await db.post(doc);
    });
    }
    + +

    上面代码可能不会正常工作,原因是这时三个db.post操作将是并发执行,也就是同时执行,而不是继发执行。正确的写法是采用for循环。

    +
    1
    2
    3
    4
    5
    6
    7
    async function dbFuc(db) {
    let docs = [{}, {}, {}];

    for (let doc of docs) {
    await db.post(doc);
    }
    }
    + +

    如果确实希望多个请求并发执行,可以使用Promise.all方法。当三个请求都会resolved时,下面两种写法效果相同。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    async function dbFuc(db) {
    let docs = [{}, {}, {}];
    let promises = docs.map((doc) => db.post(doc));

    let results = await Promise.all(promises);
    console.log(results);
    }

    // 或者使用下面的写法

    async function dbFuc(db) {
    let docs = [{}, {}, {}];
    let promises = docs.map((doc) => db.post(doc));

    let results = [];
    for (let promise of promises) {
    results.push(await promise);
    }
    console.log(results);
    }
    + +

    目前,esm模块加载器支持顶层await,即await命令可以不放在 async 函数里面,直接使用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // async 函数的写法
    const start = async () => {
    const res = await fetch("google.com");
    return res.text();
    };

    start().then(console.log);

    // 顶层 await 的写法
    const res = await fetch("google.com");
    console.log(await res.text());
    + +

    上面代码中,第二种写法的脚本必须使用esm加载器,才会生效。

    +

    async 函数的实现原理

    async 函数的实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function fn(args) {
    // ...
    }

    // 等同于

    function fn(args) {
    return spawn(function* () {
    // ...
    });
    }
    + +

    所有的async函数都可以写成上面的第二种形式,其中的spawn函数就是自动执行器。

    +

    下面给出spawn函数的实现,基本就是前文自动执行器的翻版。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    function spawn(genF) {
    return new Promise(function (resolve, reject) {
    const gen = genF();
    function step(nextF) {
    let next;
    try {
    next = nextF();
    } catch (e) {
    return reject(e);
    }
    if (next.done) {
    return resolve(next.value);
    }
    Promise.resolve(next.value).then(
    function (v) {
    step(function () {
    return gen.next(v);
    });
    },
    function (e) {
    step(function () {
    return gen.throw(e);
    });
    }
    );
    }
    step(function () {
    return gen.next(undefined);
    });
    });
    }
    + +

    与其他异步处理方法的比较

    我们通过一个例子,来看 async 函数与 Promise、Generator 函数的比较。

    +

    假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

    +

    首先是 Promise 的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function chainAnimationsPromise(elem, animations) {
    // 变量ret用来保存上一个动画的返回值
    let ret = null;

    // 新建一个空的Promise
    let p = Promise.resolve();

    // 使用then方法,添加所有动画
    for (let anim of animations) {
    p = p.then(function (val) {
    ret = val;
    return anim(elem);
    });
    }

    // 返回一个部署了错误捕捉机制的Promise
    return p
    .catch(function (e) {
    /* 忽略错误,继续执行 */
    })
    .then(function () {
    return ret;
    });
    }
    + +

    虽然 Promise 的写法比回调函数的写法大大改进,但是一眼看上去,代码完全都是 Promise 的 API(thencatch等等),操作本身的语义反而不容易看出来。

    +

    接着是 Generator 函数的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function chainAnimationsGenerator(elem, animations) {
    return spawn(function* () {
    let ret = null;
    try {
    for (let anim of animations) {
    ret = yield anim(elem);
    }
    } catch (e) {
    /* 忽略错误,继续执行 */
    }
    return ret;
    });
    }
    + +

    上面代码使用 Generator 函数遍历了每个动画,语义比 Promise 写法更清晰,用户定义的操作全部都出现在spawn函数的内部。这个写法的问题在于,必须有一个任务运行器,自动执行 Generator 函数,上面代码的spawn函数就是自动执行器,它返回一个 Promise 对象,而且必须保证yield语句后面的表达式,必须返回一个 Promise。

    +

    最后是 async 函数的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function chainAnimationsAsync(elem, animations) {
    let ret = null;
    try {
    for (let anim of animations) {
    ret = await anim(elem);
    }
    } catch (e) {
    /* 忽略错误,继续执行 */
    }
    return ret;
    }
    + +

    可以看到 Async 函数的实现最简洁,最符合语义,几乎没有语义不相关的代码。它将 Generator 写法中的自动执行器,改在语言层面提供,不暴露给用户,因此代码量最少。如果使用 Generator 写法,自动执行器需要用户自己提供。

    +

    实例:按顺序完成异步操作

    实际开发中,经常遇到一组异步操作,需要按照顺序完成。比如,依次远程读取一组 URL,然后按照读取的顺序输出结果。

    +

    Promise 的写法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function logInOrder(urls) {
    // 远程读取所有URL
    const textPromises = urls.map((url) => {
    return fetch(url).then((response) => response.text());
    });

    // 按次序输出
    textPromises.reduce((chain, textPromise) => {
    return chain.then(() => textPromise).then((text) => console.log(text));
    }, Promise.resolve());
    }
    + +

    上面代码使用fetch方法,同时远程读取一组 URL。每个fetch操作都返回一个 Promise 对象,放入textPromises数组。然后,reduce方法依次处理每个 Promise 对象,然后使用then,将所有 Promise 对象连起来,因此就可以依次输出结果。

    +

    这种写法不太直观,可读性比较差。下面是 async 函数实现。

    +
    1
    2
    3
    4
    5
    6
    async function logInOrder(urls) {
    for (const url of urls) {
    const response = await fetch(url);
    console.log(await response.text());
    }
    }
    + +

    上面代码确实大大简化,问题是所有远程操作都是继发。只有前一个 URL 返回结果,才会去读取下一个 URL,这样做效率很差,非常浪费时间。我们需要的是并发发出远程请求。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    async function logInOrder(urls) {
    // 并发读取远程URL
    const textPromises = urls.map(async (url) => {
    const response = await fetch(url);
    return response.text();
    });

    // 按次序输出
    for (const textPromise of textPromises) {
    console.log(await textPromise);
    }
    }
    + +

    上面代码中,虽然map方法的参数是async函数,但它是并发执行的,因为只有async函数内部是继发执行,外部不受影响。后面的for..of循环内部使用了await,因此实现了按顺序输出。

    +

    异步遍历器

    《遍历器》一章说过,Iterator 接口是一种数据遍历的协议,只要调用遍历器对象的next方法,就会得到一个对象,表示当前遍历指针所在的那个位置的信息。next方法返回的对象的结构是{value, done},其中value表示当前的数据的值,done是一个布尔值,表示遍历是否结束。

    +

    这里隐含着一个规定,next方法必须是同步的,只要调用就必须立刻返回值。也就是说,一旦执行next方法,就必须同步地得到valuedone这两个属性。如果遍历指针正好指向同步操作,当然没有问题,但对于异步操作,就不太合适了。目前的解决方法是,Generator 函数里面的异步操作,返回一个 Thunk 函数或者 Promise 对象,即value属性是一个 Thunk 函数或者 Promise 对象,等待以后返回真正的值,而done属性则还是同步产生的。

    +

    ES2018 引入了”异步遍历器“(Async Iterator),为异步操作提供原生的遍历器接口,即valuedone这两个属性都是异步产生。

    +

    异步遍历的接口

    异步遍历器的最大的语法特点,就是调用遍历器的next方法,返回的是一个 Promise 对象。

    +
    1
    2
    3
    4
    5
    asyncIterator
    .next()
    .then(
    ({ value, done }) => /* ... */
    );
    + +

    上面代码中,asyncIterator是一个异步遍历器,调用next方法以后,返回一个 Promise 对象。因此,可以使用then方法指定,这个 Promise 对象的状态变为resolve以后的回调函数。回调函数的参数,则是一个具有valuedone两个属性的对象,这个跟同步遍历器是一样的。

    +

    我们知道,一个对象的同步遍历器的接口,部署在Symbol.iterator属性上面。同样地,对象的异步遍历器接口,部署在Symbol.asyncIterator属性上面。不管是什么样的对象,只要它的Symbol.asyncIterator属性有值,就表示应该对它进行异步遍历。

    +

    下面是一个异步遍历器的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const asyncIterable = createAsyncIterable(["a", "b"]);
    const asyncIterator = asyncIterable[Symbol.asyncIterator]();

    asyncIterator
    .next()
    .then((iterResult1) => {
    console.log(iterResult1); // { value: 'a', done: false }
    return asyncIterator.next();
    })
    .then((iterResult2) => {
    console.log(iterResult2); // { value: 'b', done: false }
    return asyncIterator.next();
    })
    .then((iterResult3) => {
    console.log(iterResult3); // { value: undefined, done: true }
    });
    + +

    上面代码中,异步遍历器其实返回了两次值。第一次调用的时候,返回一个 Promise 对象;等到 Promise 对象resolve了,再返回一个表示当前数据成员信息的对象。这就是说,异步遍历器与同步遍历器最终行为是一致的,只是会先返回 Promise 对象,作为中介。

    +

    由于异步遍历器的next方法,返回的是一个 Promise 对象。因此,可以把它放在await命令后面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function f() {
    const asyncIterable = createAsyncIterable(["a", "b"]);
    const asyncIterator = asyncIterable[Symbol.asyncIterator]();
    console.log(await asyncIterator.next());
    // { value: 'a', done: false }
    console.log(await asyncIterator.next());
    // { value: 'b', done: false }
    console.log(await asyncIterator.next());
    // { value: undefined, done: true }
    }
    + +

    上面代码中,next方法用await处理以后,就不必使用then方法了。整个流程已经很接近同步处理了。

    +

    注意,异步遍历器的next方法是可以连续调用的,不必等到上一步产生的 Promise 对象resolve以后再调用。这种情况下,next方法会累积起来,自动按照每一步的顺序运行下去。下面是一个例子,把所有的next方法放在Promise.all方法里面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const asyncIterable = createAsyncIterable(["a", "b"]);
    const asyncIterator = asyncIterable[Symbol.asyncIterator]();
    const [{ value: v1 }, { value: v2 }] = await Promise.all([
    asyncIterator.next(),
    asyncIterator.next(),
    ]);

    console.log(v1, v2); // a b
    + +

    另一种用法是一次性调用所有的next方法,然后await最后一步操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    async function runner() {
    const writer = openFile("someFile.txt");
    writer.next("hello");
    writer.next("world");
    await writer.return();
    }

    runner();
    + +

    for await…of

    前面介绍过,for...of循环用于遍历同步的 Iterator 接口。新引入的for await...of循环,则是用于遍历异步的 Iterator 接口。

    +
    1
    2
    3
    4
    5
    6
    7
    async function f() {
    for await (const x of createAsyncIterable(["a", "b"])) {
    console.log(x);
    }
    }
    // a
    // b
    + +

    上面代码中,createAsyncIterable()返回一个拥有异步遍历器接口的对象,for...of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for...of的循环体。

    +

    for await...of循环的一个用途,是部署了 asyncIterable 操作的异步接口,可以直接放入这个循环。

    +
    1
    2
    3
    4
    5
    6
    7
    let body = "";

    async function f() {
    for await (const data of req) body += data;
    const parsed = JSON.parse(body);
    console.log("got", parsed);
    }
    + +

    上面代码中,req是一个 asyncIterable 对象,用来异步读取数据。可以看到,使用for await...of循环以后,代码会非常简洁。

    +

    如果next方法返回的 Promise 对象被rejectfor await...of就会报错,要用try...catch捕捉。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    async function () {
    try {
    for await (const x of createRejectingIterable()) {
    console.log(x);
    }
    } catch (e) {
    console.error(e);
    }
    }
    + +

    注意,for await...of循环也可以用于同步遍历器。

    +
    1
    2
    3
    4
    5
    6
    7
    (async function () {
    for await (const x of ["a", "b"]) {
    console.log(x);
    }
    })();
    // a
    // b
    + +

    Node v10 支持异步遍历器,Stream 就部署了这个接口。下面是读取文件的传统写法与异步遍历器写法的差异。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 传统写法
    function main(inputFilePath) {
    const readStream = fs.createReadStream(inputFilePath, {
    encoding: "utf8",
    highWaterMark: 1024,
    });
    readStream.on("data", (chunk) => {
    console.log(">>> " + chunk);
    });
    readStream.on("end", () => {
    console.log("### DONE ###");
    });
    }

    // 异步遍历器写法
    async function main(inputFilePath) {
    const readStream = fs.createReadStream(inputFilePath, {
    encoding: "utf8",
    highWaterMark: 1024,
    });

    for await (const chunk of readStream) {
    console.log(">>> " + chunk);
    }
    console.log("### DONE ###");
    }
    + +

    异步 Generator 函数

    就像 Generator 函数返回一个同步遍历器对象一样,异步 Generator 函数的作用,是返回一个异步遍历器对象。

    +

    在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

    +
    1
    2
    3
    4
    5
    6
    async function* gen() {
    yield "hello";
    }
    const genObj = gen();
    genObj.next().then((x) => console.log(x));
    // { value: 'hello', done: false }
    + +

    上面代码中,gen是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用next方法,返回一个 Promise 对象。

    +

    异步遍历器的设计目的之一,就是 Generator 函数处理同步操作和异步操作时,能够使用同一套接口。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 同步 Generator 函数
    function* map(iterable, func) {
    const iter = iterable[Symbol.iterator]();
    while (true) {
    const { value, done } = iter.next();
    if (done) break;
    yield func(value);
    }
    }

    // 异步 Generator 函数
    async function* map(iterable, func) {
    const iter = iterable[Symbol.asyncIterator]();
    while (true) {
    const { value, done } = await iter.next();
    if (done) break;
    yield func(value);
    }
    }
    + +

    上面代码中,map是一个 Generator 函数,第一个参数是可遍历对象iterable,第二个参数是一个回调函数funcmap的作用是将iterable每一步返回的值,使用func进行处理。上面有两个版本的map,前一个处理同步遍历器,后一个处理异步遍历器,可以看到两个版本的写法基本上是一致的。

    +

    下面是另一个异步 Generator 函数的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    async function* readLines(path) {
    let file = await fileOpen(path);

    try {
    while (!file.EOF) {
    yield await file.readLine();
    }
    } finally {
    await file.close();
    }
    }
    + +

    上面代码中,异步操作前面使用await关键字标明,即await后面的操作,应该返回 Promise 对象。凡是使用yield关键字的地方,就是next方法停下来的地方,它后面的表达式的值(即await file.readLine()的值),会作为next()返回对象的value属性,这一点是与同步 Generator 函数一致的。

    +

    异步 Generator 函数内部,能够同时使用awaityield命令。可以这样理解,await命令用于将外部操作产生的值输入函数内部,yield命令用于将函数内部的值输出。

    +

    上面代码定义的异步 Generator 函数的用法如下。

    +
    1
    2
    3
    4
    5
    (async function () {
    for await (const line of readLines(filePath)) {
    console.log(line);
    }
    })();
    + +

    异步 Generator 函数可以与for await...of循环结合起来使用。

    +
    1
    2
    3
    4
    5
    async function* prefixLines(asyncIterable) {
    for await (const line of asyncIterable) {
    yield "> " + line;
    }
    }
    + +

    异步 Generator 函数的返回值是一个异步 Iterator,即每次调用它的next方法,会返回一个 Promise 对象,也就是说,跟在yield命令后面的,应该是一个 Promise 对象。如果像上面那个例子那样,yield命令后面是一个字符串,会被自动包装成一个 Promise 对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    function fetchRandom() {
    const url =
    "https://www.random.org/decimal-fractions/" +
    "?num=1&dec=10&col=1&format=plain&rnd=new";
    return fetch(url);
    }

    async function* asyncGenerator() {
    console.log("Start");
    const result = await fetchRandom(); // (A)
    yield "Result: " + (await result.text()); // (B)
    console.log("Done");
    }

    const ag = asyncGenerator();
    ag.next().then(({ value, done }) => {
    console.log(value);
    });
    + +

    上面代码中,agasyncGenerator函数返回的异步遍历器对象。调用ag.next()以后,上面代码的执行顺序如下。

    +
      +
    1. ag.next()立刻返回一个 Promise 对象。
    2. +
    3. asyncGenerator函数开始执行,打印出Start
    4. +
    5. await命令返回一个 Promise 对象,asyncGenerator函数停在这里。
    6. +
    7. A 处变成 fulfilled 状态,产生的值放入result变量,asyncGenerator函数继续往下执行。
    8. +
    9. 函数在 B 处的yield暂停执行,一旦yield命令取到值,ag.next()返回的那个 Promise 对象变成 fulfilled 状态。
    10. +
    11. ag.next()后面的then方法指定的回调函数开始执行。该回调函数的参数是一个对象{value, done},其中value的值是yield命令后面的那个表达式的值,done的值是false
    12. +
    +

    A 和 B 两行的作用类似于下面的代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    return new Promise((resolve, reject) => {
    fetchRandom()
    .then((result) => result.text())
    .then((result) => {
    resolve({
    value: "Result: " + result,
    done: false,
    });
    });
    });
    + +

    如果异步 Generator 函数抛出错误,会导致 Promise 对象的状态变为reject,然后抛出的错误被catch方法捕获。

    +
    1
    2
    3
    4
    5
    6
    7
    async function* asyncGenerator() {
    throw new Error("Problem!");
    }

    asyncGenerator()
    .next()
    .catch((err) => console.log(err)); // Error: Problem!
    + +

    注意,普通的 async 函数返回的是一个 Promise 对象,而异步 Generator 函数返回的是一个异步 Iterator 对象。可以这样理解,async 函数和异步 Generator 函数,是封装异步操作的两种方法,都用来达到同一种目的。区别在于,前者自带执行器,后者通过for await...of执行,或者自己编写执行器。下面就是一个异步 Generator 函数的执行器。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function takeAsync(asyncIterable, count = Infinity) {
    const result = [];
    const iterator = asyncIterable[Symbol.asyncIterator]();
    while (result.length < count) {
    const { value, done } = await iterator.next();
    if (done) break;
    result.push(value);
    }
    return result;
    }
    + +

    上面代码中,异步 Generator 函数产生的异步遍历器,会通过while循环自动执行,每当await iterator.next()完成,就会进入下一轮循环。一旦done属性变为true,就会跳出循环,异步遍历器执行结束。

    +

    下面是这个自动执行器的一个使用实例。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    async function f() {
    async function* gen() {
    yield "a";
    yield "b";
    yield "c";
    }

    return await takeAsync(gen());
    }

    f().then(function (result) {
    console.log(result); // ['a', 'b', 'c']
    });
    + +

    异步 Generator 函数出现以后,JavaScript 就有了四种函数形式:普通函数、async 函数、Generator 函数和异步 Generator 函数。请注意区分每种函数的不同之处。基本上,如果是一系列按照顺序执行的异步操作(比如读取文件,然后写入新内容,再存入硬盘),可以使用 async 函数;如果是一系列产生相同数据结构的异步操作(比如一行一行读取文件),可以使用异步 Generator 函数。

    +

    异步 Generator 函数也可以通过next方法的参数,接收外部传入的数据。

    +
    1
    2
    3
    4
    const writer = openFile("someFile.txt");
    writer.next("hello"); // 立即执行
    writer.next("world"); // 立即执行
    await writer.return(); // 等待写入结束
    + +

    上面代码中,openFile是一个异步 Generator 函数。next方法的参数,向该函数内部的操作传入数据。每次next方法都是同步执行的,最后的await命令用于等待整个写入操作结束。

    +

    最后,同步的数据结构,也可以使用异步 Generator 函数。

    +
    1
    2
    3
    4
    5
    async function* createAsyncIterable(syncIterable) {
    for (const elem of syncIterable) {
    yield elem;
    }
    }
    + +

    上面代码中,由于没有异步操作,所以也就没有使用await关键字。

    +

    yield* 语句

    yield*语句也可以跟一个异步遍历器。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    async function* gen1() {
    yield "a";
    yield "b";
    return 2;
    }

    async function* gen2() {
    // result 最终会等于 2
    const result = yield* gen1();
    }
    + +

    上面代码中,gen2函数里面的result变量,最后的值是2

    +

    与同步 Generator 函数一样,for await...of循环会展开yield*

    +
    1
    2
    3
    4
    5
    6
    7
    (async function () {
    for await (const x of gen2()) {
    console.log(x);
    }
    })();
    // a
    // b
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-async%20%E5%87%BD%E6%95%B0/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-let \345\222\214 const \345\221\275\344\273\244/index.html" "b/posts/ES6-let \345\222\214 const \345\221\275\344\273\244/index.html" new file mode 100644 index 00000000..3b9895a4 --- /dev/null +++ "b/posts/ES6-let \345\222\214 const \345\221\275\344\273\244/index.html" @@ -0,0 +1,393 @@ +let 和 const 命令 | JCAlways + + + + + + + + + + + + + +

    let 和 const 命令

    let 和 const 命令

    let 命令

    基本用法

    ES6 新增了let命令,用来声明变量。它的用法类似于var,但是所声明的变量,只在let命令所在的代码块内有效。

    +
    1
    2
    3
    4
    5
    6
    7
    {
    let a = 10;
    var b = 1;
    }

    a; // ReferenceError: a is not defined.
    b; // 1
    + +

    上面代码在代码块之中,分别用letvar声明了两个变量。然后在代码块之外调用这两个变量,结果let声明的变量报错,var声明的变量返回了正确的值。这表明,let声明的变量只在它所在的代码块有效。

    +

    for循环的计数器,就很合适使用let命令。

    +
    1
    2
    3
    4
    5
    6
    for (let i = 0; i < 10; i++) {
    // ...
    }

    console.log(i);
    // ReferenceError: i is not defined
    + +

    上面代码中,计数器i只在for循环体内有效,在循环体外引用就会报错。

    +

    下面的代码如果使用var,最后输出的是10

    +
    1
    2
    3
    4
    5
    6
    7
    var a = [];
    for (var i = 0; i < 10; i++) {
    a[i] = function () {
    console.log(i);
    };
    }
    a[6](); // 10
    + +

    上面代码中,变量ivar命令声明的,在全局范围内都有效,所以全局只有一个变量i。每一次循环,变量i的值都会发生改变,而循环内被赋给数组a的函数内部的console.log(i),里面的i指向的就是全局的i。也就是说,所有数组a的成员里面的i,指向的都是同一个i,导致运行时输出的是最后一轮的i的值,也就是 10。

    +

    如果使用let,声明的变量仅在块级作用域内有效,最后输出的是 6。

    +
    1
    2
    3
    4
    5
    6
    7
    var a = [];
    for (let i = 0; i < 10; i++) {
    a[i] = function () {
    console.log(i);
    };
    }
    a[6](); // 6
    + +

    上面代码中,变量ilet声明的,当前的i只在本轮循环有效,所以每一次循环的i其实都是一个新的变量,所以最后输出的是6。你可能会问,如果每一轮循环的变量i都是重新声明的,那它怎么知道上一轮循环的值,从而计算出本轮循环的值?这是因为 JavaScript 引擎内部会记住上一轮循环的值,初始化本轮的变量i时,就在上一轮循环的基础上进行计算。

    +

    另外,for循环还有一个特别之处,就是设置循环变量的那部分是一个父作用域,而循环体内部是一个单独的子作用域。

    +
    1
    2
    3
    4
    5
    6
    7
    for (let i = 0; i < 3; i++) {
    let i = "abc";
    console.log(i);
    }
    // abc
    // abc
    // abc
    + +

    上面代码正确运行,输出了 3 次abc。这表明函数内部的变量i与循环变量i不在同一个作用域,有各自单独的作用域。

    +

    不存在变量提升

    var命令会发生”变量提升“现象,即变量可以在声明之前使用,值为undefined。这种现象多多少少是有些奇怪的,按照一般的逻辑,变量应该在声明语句之后才可以使用。

    +

    为了纠正这种现象,let命令改变了语法行为,它所声明的变量一定要在声明后使用,否则报错。

    +
    1
    2
    3
    4
    5
    6
    7
    // var 的情况
    console.log(foo); // 输出undefined
    var foo = 2;

    // let 的情况
    console.log(bar); // 报错ReferenceError
    let bar = 2;
    + +

    上面代码中,变量foovar命令声明,会发生变量提升,即脚本开始运行时,变量foo已经存在了,但是没有值,所以会输出undefined。变量barlet命令声明,不会发生变量提升。这表示在声明它之前,变量bar是不存在的,这时如果用到它,就会抛出一个错误。

    +

    暂时性死区

    只要块级作用域内存在let命令,它所声明的变量就“绑定”(binding)这个区域,不再受外部的影响。

    +
    1
    2
    3
    4
    5
    6
    var tmp = 123;

    if (true) {
    tmp = "abc"; // ReferenceError
    let tmp;
    }
    + +

    上面代码中,存在全局变量tmp,但是块级作用域内let又声明了一个局部变量tmp,导致后者绑定这个块级作用域,所以在let声明变量前,对tmp赋值会报错。

    +

    ES6 明确规定,如果区块中存在letconst命令,这个区块对这些命令声明的变量,从一开始就形成了封闭作用域。凡是在声明之前就使用这些变量,就会报错。

    +

    总之,在代码块内,使用let命令声明变量之前,该变量都是不可用的。这在语法上,称为“暂时性死区”(temporal dead zone,简称 TDZ)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    if (true) {
    // TDZ开始
    tmp = "abc"; // ReferenceError
    console.log(tmp); // ReferenceError

    let tmp; // TDZ结束
    console.log(tmp); // undefined

    tmp = 123;
    console.log(tmp); // 123
    }
    + +

    上面代码中,在let命令声明变量tmp之前,都属于变量tmp的“死区”。

    +

    “暂时性死区”也意味着typeof不再是一个百分之百安全的操作。

    +
    1
    2
    typeof x; // ReferenceError
    let x;
    + +

    上面代码中,变量x使用let命令声明,所以在声明之前,都属于x的“死区”,只要用到该变量就会报错。因此,typeof运行时就会抛出一个ReferenceError

    +

    作为比较,如果一个变量根本没有被声明,使用typeof反而不会报错。

    +
    1
    typeof undeclared_variable; // "undefined"
    + +

    上面代码中,undeclared_variable是一个不存在的变量名,结果返回“undefined”。所以,在没有let之前,typeof运算符是百分之百安全的,永远不会报错。现在这一点不成立了。这样的设计是为了让大家养成良好的编程习惯,变量一定要在声明之后使用,否则就报错。

    +

    有些“死区”比较隐蔽,不太容易发现。

    +
    1
    2
    3
    4
    5
    function bar(x = y, y = 2) {
    return [x, y];
    }

    bar(); // 报错
    + +

    上面代码中,调用bar函数之所以报错(某些实现可能不报错),是因为参数x默认值等于另一个参数y,而此时y还没有声明,属于”死区“。如果y的默认值是x,就不会报错,因为此时x已经声明了。

    +
    1
    2
    3
    4
    function bar(x = 2, y = x) {
    return [x, y];
    }
    bar(); // [2, 2]
    + +

    另外,下面的代码也会报错,与var的行为不同。

    +
    1
    2
    3
    4
    5
    6
    // 不报错
    var x = x;

    // 报错
    let x = x;
    // ReferenceError: x is not defined
    + +

    上面代码报错,也是因为暂时性死区。使用let声明变量时,只要变量在还没有声明完成前使用,就会报错。上面这行就属于这个情况,在变量x的声明语句还没有执行完成前,就去取x的值,导致报错”x 未定义“。

    +

    ES6 规定暂时性死区和letconst语句不出现变量提升,主要是为了减少运行时错误,防止在变量声明前就使用这个变量,从而导致意料之外的行为。这样的错误在 ES5 是很常见的,现在有了这种规定,避免此类错误就很容易了。

    +

    总之,暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。

    +

    不允许重复声明

    let不允许在相同作用域内,重复声明同一个变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 报错
    function func() {
    let a = 10;
    var a = 1;
    }

    // 报错
    function func() {
    let a = 10;
    let a = 1;
    }
    + +

    因此,不能在函数内部重新声明参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function func(arg) {
    let arg; // 报错
    }

    function func(arg) {
    {
    let arg; // 不报错
    }
    }
    + +

    块级作用域

    为什么需要块级作用域?

    ES5 只有全局作用域和函数作用域,没有块级作用域,这带来很多不合理的场景。

    +

    第一种场景,内层变量可能会覆盖外层变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var tmp = new Date();

    function f() {
    console.log(tmp);
    if (false) {
    var tmp = "hello world";
    }
    }

    f(); // undefined
    + +

    上面代码的原意是,if代码块的外部使用外层的tmp变量,内部使用内层的tmp变量。但是,函数f执行后,输出结果为undefined,原因在于变量提升,导致内层的tmp变量覆盖了外层的tmp变量。

    +

    第二种场景,用来计数的循环变量泄露为全局变量。

    +
    1
    2
    3
    4
    5
    6
    7
    var s = "hello";

    for (var i = 0; i < s.length; i++) {
    console.log(s[i]);
    }

    console.log(i); // 5
    + +

    上面代码中,变量i只用来控制循环,但是循环结束后,它并没有消失,泄露成了全局变量。

    +

    ES6 的块级作用域

    let实际上为 JavaScript 新增了块级作用域。

    +
    1
    2
    3
    4
    5
    6
    7
    function f1() {
    let n = 5;
    if (true) {
    let n = 10;
    }
    console.log(n); // 5
    }
    + +

    上面的函数有两个代码块,都声明了变量n,运行后输出 5。这表示外层代码块不受内层代码块的影响。如果两次都使用var定义变量n,最后输出的值才是 10。

    +

    ES6 允许块级作用域的任意嵌套。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    {
    {
    {
    {
    {
    let insane = "Hello World";
    }
    }
    }
    }
    }
    + +

    上面代码使用了一个五层的块级作用域。外层作用域无法读取内层作用域的变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    {
    {
    {
    {
    let insane = "Hello World";
    }
    console.log(insane); // 报错
    }
    }
    }
    }
    + +

    内层作用域可以定义外层作用域的同名变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    {
    {
    {
    {
    let insane = "Hello World";
    {
    let insane = "Hello World";
    }
    }
    }
    }
    }
    + +

    块级作用域的出现,实际上使得获得广泛应用的立即执行函数表达式(IIFE)不再必要了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // IIFE 写法
    (function () {
    var tmp = ...;
    ...
    }());

    // 块级作用域写法
    {
    let tmp = ...;
    ...
    }
    + +

    块级作用域与函数声明

    函数能不能在块级作用域之中声明?这是一个相当令人混淆的问题。

    +

    ES5 规定,函数只能在顶层作用域和函数作用域之中声明,不能在块级作用域声明。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 情况一
    if (true) {
    function f() {}
    }

    // 情况二
    try {
    function f() {}
    } catch (e) {
    // ...
    }
    + +

    上面两种函数声明,根据 ES5 的规定都是非法的。

    +

    但是,浏览器没有遵守这个规定,为了兼容以前的旧代码,还是支持在块级作用域之中声明函数,因此上面两种情况实际都能运行,不会报错。

    +

    ES6 引入了块级作用域,明确允许在块级作用域之中声明函数。ES6 规定,块级作用域之中,函数声明语句的行为类似于let,在块级作用域之外不可引用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function f() {
    console.log("I am outside!");
    }

    (function () {
    if (false) {
    // 重复声明一次函数f
    function f() {
    console.log("I am inside!");
    }
    }

    f();
    })();
    + +

    上面代码在 ES5 中运行,会得到“I am inside!”,因为在if内声明的函数f会被提升到函数头部,实际运行的代码如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // ES5 环境
    function f() {
    console.log("I am outside!");
    }

    (function () {
    function f() {
    console.log("I am inside!");
    }
    if (false) {
    }
    f();
    })();
    + +

    ES6 就完全不一样了,理论上会得到“I am outside!”。因为块级作用域内声明的函数类似于let,对作用域之外没有影响。但是,如果你真的在 ES6 浏览器中运行一下上面的代码,是会报错的,这是为什么呢?

    +

    原来,如果改变了块级作用域内声明的函数的处理规则,显然会对老代码产生很大影响。为了减轻因此产生的不兼容问题,ES6 在附录 B里面规定,浏览器的实现可以不遵守上面的规定,有自己的行为方式

    +
      +
    • 允许在块级作用域内声明函数。
    • +
    • 函数声明类似于var,即会提升到全局作用域或函数作用域的头部。
    • +
    • 同时,函数声明还会提升到所在的块级作用域的头部。
    • +
    +

    注意,上面三条规则只对 ES6 的浏览器实现有效,其他环境的实现不用遵守,还是将块级作用域的函数声明当作let处理。

    +

    根据这三条规则,在浏览器的 ES6 环境中,块级作用域内声明的函数,行为类似于var声明的变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 浏览器的 ES6 环境
    function f() {
    console.log("I am outside!");
    }

    (function () {
    if (false) {
    // 重复声明一次函数f
    function f() {
    console.log("I am inside!");
    }
    }

    f();
    })();
    // Uncaught TypeError: f is not a function
    + +

    上面的代码在符合 ES6 的浏览器中,都会报错,因为实际运行的是下面的代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 浏览器的 ES6 环境
    function f() {
    console.log("I am outside!");
    }
    (function () {
    var f = undefined;
    if (false) {
    function f() {
    console.log("I am inside!");
    }
    }

    f();
    })();
    // Uncaught TypeError: f is not a function
    + +

    考虑到环境导致的行为差异太大,应该避免在块级作用域内声明函数。如果确实需要,也应该写成函数表达式,而不是函数声明语句。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 函数声明语句
    {
    let a = "secret";
    function f() {
    return a;
    }
    }

    // 函数表达式
    {
    let a = "secret";
    let f = function () {
    return a;
    };
    }
    + +

    另外,还有一个需要注意的地方。ES6 的块级作用域允许声明函数的规则,只在使用大括号的情况下成立,如果没有使用大括号,就会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 不报错
    "use strict";
    if (true) {
    function f() {}
    }

    // 报错
    ("use strict");
    if (true) function f() {}
    + +

    const 命令

    基本用法

    const声明一个只读的常量。一旦声明,常量的值就不能改变。

    +
    1
    2
    3
    4
    5
    const PI = 3.1415;
    PI; // 3.1415

    PI = 3;
    // TypeError: Assignment to constant variable.
    + +

    上面代码表明改变常量的值会报错。

    +

    const声明的变量不得改变值,这意味着,const一旦声明变量,就必须立即初始化,不能留到以后赋值。

    +
    1
    2
    const foo;
    // SyntaxError: Missing initializer in const declaration
    + +

    上面代码表示,对于const来说,只声明不赋值,就会报错。

    +

    const的作用域与let命令相同:只在声明所在的块级作用域内有效。

    +
    1
    2
    3
    4
    5
    if (true) {
    const MAX = 5;
    }

    MAX; // Uncaught ReferenceError: MAX is not defined
    + +

    const命令声明的常量也是不提升,同样存在暂时性死区,只能在声明的位置后面使用。

    +
    1
    2
    3
    4
    if (true) {
    console.log(MAX); // ReferenceError
    const MAX = 5;
    }
    + +

    上面代码在常量MAX声明之前就调用,结果报错。

    +

    const声明的常量,也与let一样不可重复声明。

    +
    1
    2
    3
    4
    5
    6
    var message = "Hello!";
    let age = 25;

    // 以下两行都会报错
    const message = "Goodbye!";
    const age = 30;
    + +

    本质

    const实际上保证的,并不是变量的值不得改动,而是变量指向的那个内存地址所保存的数据不得改动。对于简单类型的数据(数值、字符串、布尔值),值就保存在变量指向的那个内存地址,因此等同于常量。但对于复合类型的数据(主要是对象和数组),变量指向的内存地址,保存的只是一个指向实际数据的指针,const只能保证这个指针是固定的(即总是指向另一个固定的地址),至于它指向的数据结构是不是可变的,就完全不能控制了。因此,将一个对象声明为常量必须非常小心。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const foo = {};

    // 为 foo 添加一个属性,可以成功
    foo.prop = 123;
    foo.prop; // 123

    // 将 foo 指向另一个对象,就会报错
    foo = {}; // TypeError: "foo" is read-only
    + +

    上面代码中,常量foo储存的是一个地址,这个地址指向一个对象。不可变的只是这个地址,即不能把foo指向另一个地址,但对象本身是可变的,所以依然可以为其添加新属性。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    const a = [];
    a.push("Hello"); // 可执行
    a.length = 0; // 可执行
    a = ["Dave"]; // 报错
    + +

    上面代码中,常量a是一个数组,这个数组本身是可写的,但是如果将另一个数组赋值给a,就会报错。

    +

    如果真的想将对象冻结,应该使用Object.freeze方法。

    +
    1
    2
    3
    4
    5
    const foo = Object.freeze({});

    // 常规模式时,下面一行不起作用;
    // 严格模式时,该行会报错
    foo.prop = 123;
    + +

    上面代码中,常量foo指向一个冻结的对象,所以添加新属性不起作用,严格模式时还会报错。

    +

    除了将对象本身冻结,对象的属性也应该冻结。下面是一个将对象彻底冻结的函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var constantize = (obj) => {
    Object.freeze(obj);
    Object.keys(obj).forEach((key, i) => {
    if (typeof obj[key] === "object") {
    constantize(obj[key]);
    }
    });
    };
    + +

    ES6 声明变量的六种方法

    ES5 只有两种声明变量的方法:var命令和function命令。ES6 除了添加letconst命令,后面章节还会提到,另外两种声明变量的方法:import命令和class命令。所以,ES6 一共有 6 种声明变量的方法。

    +

    顶层对象的属性

    顶层对象,在浏览器环境指的是window对象,在 Node 指的是global对象。ES5 之中,顶层对象的属性与全局变量是等价的。

    +
    1
    2
    3
    4
    5
    window.a = 1;
    a; // 1

    a = 2;
    window.a; // 2
    + +

    上面代码中,顶层对象的属性赋值与全局变量的赋值,是同一件事。

    +

    顶层对象的属性与全局变量挂钩,被认为是 JavaScript 语言最大的设计败笔之一。这样的设计带来了几个很大的问题,首先是没法在编译时就报出变量未声明的错误,只有运行时才能知道(因为全局变量可能是顶层对象的属性创造的,而属性的创造是动态的);其次,程序员很容易不知不觉地就创建了全局变量(比如打字出错);最后,顶层对象的属性是到处可以读写的,这非常不利于模块化编程。另一方面,window对象有实体含义,指的是浏览器的窗口对象,顶层对象是一个有实体含义的对象,也是不合适的。

    +

    ES6 为了改变这一点,一方面规定,为了保持兼容性,var命令和function命令声明的全局变量,依旧是顶层对象的属性;另一方面规定,let命令、const命令、class命令声明的全局变量,不属于顶层对象的属性。也就是说,从 ES6 开始,全局变量将逐步与顶层对象的属性脱钩。

    +
    1
    2
    3
    4
    5
    6
    7
    var a = 1;
    // 如果在 Node 的 REPL 环境,可以写成 global.a
    // 或者采用通用方法,写成 this.a
    window.a; // 1

    let b = 1;
    window.b; // undefined
    + +

    上面代码中,全局变量avar命令声明,所以它是顶层对象的属性;全局变量blet命令声明,所以它不是顶层对象的属性,返回undefined

    +

    global 对象

    ES5 的顶层对象,本身也是一个问题,因为它在各种实现里面是不统一的。

    +
      +
    • 浏览器里面,顶层对象是window,但 Node 和 Web Worker 没有window
    • +
    • 浏览器和 Web Worker 里面,self也指向顶层对象,但是 Node 没有self
    • +
    • Node 里面,顶层对象是global,但其他环境都不支持。
    • +
    +

    同一段代码为了能够在各种环境,都能取到顶层对象,现在一般是使用this变量,但是有局限性。

    +
      +
    • 全局环境中,this会返回顶层对象。但是,Node 模块和 ES6 模块中,this返回的是当前模块。
    • +
    • 函数里面的this,如果函数不是作为对象的方法运行,而是单纯作为函数运行,this会指向顶层对象。但是,严格模式下,这时this会返回undefined
    • +
    • 不管是严格模式,还是普通模式,new Function('return this')(),总是会返回全局对象。但是,如果浏览器用了 CSP(Content Security Policy,内容安全策略),那么evalnew Function这些方法都可能无法使用。
    • +
    +

    综上所述,很难找到一种方法,可以在所有情况下,都取到顶层对象。下面是两种勉强可以使用的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    // 方法一
    typeof window !== "undefined"
    ? window
    : typeof process === "object" &&
    typeof require === "function" &&
    typeof global === "object"
    ? global
    : this;

    // 方法二
    var getGlobal = function () {
    if (typeof self !== "undefined") {
    return self;
    }
    if (typeof window !== "undefined") {
    return window;
    }
    if (typeof global !== "undefined") {
    return global;
    }
    throw new Error("unable to locate global object");
    };
    + +

    现在有一个提案,在语言标准的层面,引入global作为顶层对象。也就是说,在所有环境下,global都是存在的,都可以从它拿到顶层对象。

    +

    垫片库system.global模拟了这个提案,可以在所有环境拿到global

    +
    1
    2
    3
    4
    5
    6
    // CommonJS 的写法
    require("system.global/shim")();

    // ES6 模块的写法
    import shim from "system.global/shim";
    shim();
    + +

    上面代码可以保证各种环境里面,global对象都是存在的。

    +
    1
    2
    3
    4
    5
    6
    // CommonJS 的写法
    var global = require("system.global")();

    // ES6 模块的写法
    import getGlobal from "system.global";
    const global = getGlobal();
    + +

    上面代码将顶层对象放入变量global

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-let%20%E5%92%8C%20const%20%E5%91%BD%E4%BB%A4/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\344\277\256\351\245\260\345\231\250/index.html" "b/posts/ES6-\344\277\256\351\245\260\345\231\250/index.html" new file mode 100644 index 00000000..152aa2ed --- /dev/null +++ "b/posts/ES6-\344\277\256\351\245\260\345\231\250/index.html" @@ -0,0 +1,385 @@ +修饰器 | JCAlways + + + + + + + + + + + + + +

    修饰器

    修饰器

    类的修饰

    许多面向对象的语言都有修饰器(Decorator)函数,用来修改类的行为。目前,有一个提案将这项功能,引入了 ECMAScript。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @testable
    class MyTestableClass {
    // ...
    }

    function testable(target) {
    target.isTestable = true;
    }

    MyTestableClass.isTestable; // true
    + +

    上面代码中,@testable就是一个修饰器。它修改了MyTestableClass这个类的行为,为它加上了静态属性isTestabletestable函数的参数targetMyTestableClass类本身。

    +

    基本上,修饰器的行为就是下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    @decorator
    class A {}

    // 等同于

    class A {}
    A = decorator(A) || A;
    + +

    也就是说,修饰器是一个对类进行处理的函数。修饰器函数的第一个参数,就是所要修饰的目标类。

    +
    1
    2
    3
    function testable(target) {
    // ...
    }
    + +

    上面代码中,testable函数的参数target,就是会被修饰的类。

    +

    如果觉得一个参数不够用,可以在修饰器外面再封装一层函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function testable(isTestable) {
    return function (target) {
    target.isTestable = isTestable;
    };
    }

    @testable(true)
    class MyTestableClass {}
    MyTestableClass.isTestable; // true

    @testable(false)
    class MyClass {}
    MyClass.isTestable; // false
    + +

    上面代码中,修饰器testable可以接受参数,这就等于可以修改修饰器的行为。

    +

    注意,修饰器对类的行为的改变,是代码编译时发生的,而不是在运行时。这意味着,修饰器能在编译阶段运行代码。也就是说,修饰器本质就是编译时执行的函数。

    +

    前面的例子是为类添加一个静态属性,如果想添加实例属性,可以通过目标类的prototype对象操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function testable(target) {
    target.prototype.isTestable = true;
    }

    @testable
    class MyTestableClass {}

    let obj = new MyTestableClass();
    obj.isTestable; // true
    + +

    上面代码中,修饰器函数testable是在目标类的prototype对象上添加属性,因此就可以在实例上调用。

    +

    下面是另外一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // mixins.js
    export function mixins(...list) {
    return function (target) {
    Object.assign(target.prototype, ...list);
    };
    }

    // main.js
    import { mixins } from "./mixins";

    const Foo = {
    foo() {
    console.log("foo");
    },
    };

    @mixins(Foo)
    class MyClass {}

    let obj = new MyClass();
    obj.foo(); // 'foo'
    + +

    上面代码通过修饰器mixins,把Foo对象的方法添加到了MyClass的实例上面。可以用Object.assign()模拟这个功能。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Foo = {
    foo() {
    console.log("foo");
    },
    };

    class MyClass {}

    Object.assign(MyClass.prototype, Foo);

    let obj = new MyClass();
    obj.foo(); // 'foo'
    + +

    实际开发中,React 与 Redux 库结合使用时,常常需要写成下面这样。

    +
    1
    2
    3
    class MyReactComponent extends React.Component {}

    export default connect(mapStateToProps, mapDispatchToProps)(MyReactComponent);
    + +

    有了装饰器,就可以改写上面的代码。

    +
    1
    2
    @connect(mapStateToProps, mapDispatchToProps)
    export default class MyReactComponent extends React.Component {}
    + +

    相对来说,后一种写法看上去更容易理解。

    +

    方法的修饰

    修饰器不仅可以修饰类,还可以修饰类的属性。

    +
    1
    2
    3
    4
    5
    6
    class Person {
    @readonly
    name() {
    return `${this.first} ${this.last}`;
    }
    }
    + +

    上面代码中,修饰器readonly用来修饰“类”的name方法。

    +

    修饰器函数readonly一共可以接受三个参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function readonly(target, name, descriptor) {
    // descriptor对象原来的值如下
    // {
    // value: specifiedFunction,
    // enumerable: false,
    // configurable: true,
    // writable: true
    // };
    descriptor.writable = false;
    return descriptor;
    }

    readonly(Person.prototype, "name", descriptor);
    // 类似于
    Object.defineProperty(Person.prototype, "name", descriptor);
    + +

    修饰器第一个参数是类的原型对象,上例是Person.prototype,修饰器的本意是要“修饰”类的实例,但是这个时候实例还没生成,所以只能去修饰原型(这不同于类的修饰,那种情况时target参数指的是类本身);第二个参数是所要修饰的属性名,第三个参数是该属性的描述对象。

    +

    另外,上面代码说明,修饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。

    +

    下面是另一个例子,修改属性描述对象的enumerable属性,使得该属性不可遍历。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class Person {
    @nonenumerable
    get kidCount() {
    return this.children.length;
    }
    }

    function nonenumerable(target, name, descriptor) {
    descriptor.enumerable = false;
    return descriptor;
    }
    + +

    下面的@log修饰器,可以起到输出日志的作用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    class Math {
    @log
    add(a, b) {
    return a + b;
    }
    }

    function log(target, name, descriptor) {
    var oldValue = descriptor.value;

    descriptor.value = function () {
    console.log(`Calling ${name} with`, arguments);
    return oldValue.apply(this, arguments);
    };

    return descriptor;
    }

    const math = new Math();

    // passed parameters should get logged now
    math.add(2, 4);
    + +

    上面代码中,@log修饰器的作用就是在执行原始的操作之前,执行一次console.log,从而达到输出日志的目的。

    +

    修饰器有注释的作用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    @testable
    class Person {
    @readonly
    @nonenumerable
    name() {
    return `${this.first} ${this.last}`;
    }
    }
    + +

    从上面代码中,我们一眼就能看出,Person类是可测试的,而name方法是只读和不可枚举的。

    +

    下面是使用 Decorator 写法的组件,看上去一目了然。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component({
    tag: "my-component",
    styleUrl: "my-component.scss",
    })
    export class MyComponent {
    @Prop() first: string;
    @Prop() last: string;
    @State() isVisible: boolean = true;

    render() {
    return (
    <p>
    Hello, my name is {this.first} {this.last}
    </p>
    );
    }
    }
    + +

    如果同一个方法有多个修饰器,会像剥洋葱一样,先从外到内进入,然后由内向外执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function dec(id) {
    console.log("evaluated", id);
    return (target, property, descriptor) => console.log("executed", id);
    }

    class Example {
    @dec(1)
    @dec(2)
    method() {}
    }
    // evaluated 1
    // evaluated 2
    // executed 2
    // executed 1
    + +

    上面代码中,外层修饰器@dec(1)先进入,但是内层修饰器@dec(2)先执行。

    +

    除了注释,修饰器还能用来类型检查。所以,对于类来说,这项功能相当有用。从长期来看,它将是 JavaScript 代码静态分析的重要工具。

    +

    为什么修饰器不能用于函数?

    修饰器只能用于类和类的方法,不能用于函数,因为存在函数提升。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var counter = 0;

    var add = function () {
    counter++;
    };

    @add
    function foo() {
    }
    + +

    上面的代码,意图是执行后counter等于 1,但是实际上结果是counter等于 0。因为函数提升,使得实际执行的代码是下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @add
    function foo() {
    }

    var counter;
    var add;

    counter = 0;

    add = function () {
    counter++;
    };
    + +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    var readOnly = require("some-decorator");

    @readOnly
    function foo() {
    }
    + +

    上面代码也有问题,因为实际执行是下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    var readOnly;

    @readOnly
    function foo() {
    }

    readOnly = require("some-decorator");
    + +

    总之,由于存在函数提升,使得修饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。

    +

    另一方面,如果一定要修饰函数,可以采用高阶函数的形式直接执行。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function doSomething(name) {
    console.log("Hello, " + name);
    }

    function loggingDecorator(wrapped) {
    return function () {
    console.log("Starting");
    const result = wrapped.apply(this, arguments);
    console.log("Finished");
    return result;
    };
    }

    const wrapped = loggingDecorator(doSomething);
    + +

    core-decorators.js

    core-decorators.js是一个第三方模块,提供了几个常见的修饰器,通过它可以更好地理解修饰器。

    +

    (1)@autobind

    +

    autobind修饰器使得方法中的this对象,绑定原始对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import { autobind } from "core-decorators";

    class Person {
    @autobind
    getPerson() {
    return this;
    }
    }

    let person = new Person();
    let getPerson = person.getPerson;

    getPerson() === person;
    // true
    + +

    (2)@readonly

    +

    readonly修饰器使得属性或方法不可写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import { readonly } from "core-decorators";

    class Meal {
    @readonly
    entree = "steak";
    }

    var dinner = new Meal();
    dinner.entree = "salmon";
    // Cannot assign to read only property 'entree' of [object Object]
    + +

    (3)@override

    +

    override修饰器检查子类的方法,是否正确覆盖了父类的同名方法,如果不正确会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import { override } from "core-decorators";

    class Parent {
    speak(first, second) {}
    }

    class Child extends Parent {
    @override
    speak() {}
    // SyntaxError: Child#speak() does not properly override Parent#speak(first, second)
    }

    // or

    class Child extends Parent {
    @override
    speaks() {}
    // SyntaxError: No descriptor matching Child#speaks() was found on the prototype chain.
    //
    // Did you mean "speak"?
    }
    + +

    (4)@deprecate (别名@deprecated)

    +

    deprecatedeprecated修饰器在控制台显示一条警告,表示该方法将废除。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import { deprecate } from "core-decorators";

    class Person {
    @deprecate
    facepalm() {}

    @deprecate("We stopped facepalming")
    facepalmHard() {}

    @deprecate("We stopped facepalming", {
    url: "http://knowyourmeme.com/memes/facepalm",
    })
    facepalmHarder() {}
    }

    let person = new Person();

    person.facepalm();
    // DEPRECATION Person#facepalm: This function will be removed in future versions.

    person.facepalmHard();
    // DEPRECATION Person#facepalmHard: We stopped facepalming

    person.facepalmHarder();
    // DEPRECATION Person#facepalmHarder: We stopped facepalming
    //
    // See http://knowyourmeme.com/memes/facepalm for more details.
    //
    + +

    (5)@suppressWarnings

    +

    suppressWarnings修饰器抑制deprecated修饰器导致的console.warn()调用。但是,异步代码发出的调用除外。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    import { suppressWarnings } from "core-decorators";

    class Person {
    @deprecated
    facepalm() {}

    @suppressWarnings
    facepalmWithoutWarning() {
    this.facepalm();
    }
    }

    let person = new Person();

    person.facepalmWithoutWarning();
    // no warning is logged
    + +

    使用修饰器实现自动发布事件

    我们可以使用修饰器,使得对象的方法被调用时,自动发出一个事件。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    const postal = require("postal/lib/postal.lodash");

    export default function publish(topic, channel) {
    const channelName = channel || "/";
    const msgChannel = postal.channel(channelName);
    msgChannel.subscribe(topic, (v) => {
    console.log("频道: ", channelName);
    console.log("事件: ", topic);
    console.log("数据: ", v);
    });

    return function (target, name, descriptor) {
    const fn = descriptor.value;

    descriptor.value = function () {
    let value = fn.apply(this, arguments);
    msgChannel.publish(topic, value);
    };
    };
    }
    + +

    上面代码定义了一个名为publish的修饰器,它通过改写descriptor.value,使得原方法被调用时,会自动发出一个事件。它使用的事件“发布/订阅”库是Postal.js

    +

    它的用法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // index.js
    import publish from "./publish";

    class FooComponent {
    @publish("foo.some.message", "component")
    someMethod() {
    return { my: "data" };
    }
    @publish("foo.some.other")
    anotherMethod() {
    // ...
    }
    }

    let foo = new FooComponent();

    foo.someMethod();
    foo.anotherMethod();
    + +

    以后,只要调用someMethod或者anotherMethod,就会自动发出一个事件。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    $ bash-node index.js
    频道: component
    事件: foo.some.message
    数据: { my: 'data' }

    频道: /
    事件: foo.some.other
    数据: undefined
    + +

    Mixin

    在修饰器的基础上,可以实现Mixin模式。所谓Mixin模式,就是对象继承的一种替代方案,中文译为“混入”(mix in),意为在一个对象之中混入另外一个对象的方法。

    +

    请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const Foo = {
    foo() {
    console.log("foo");
    },
    };

    class MyClass {}

    Object.assign(MyClass.prototype, Foo);

    let obj = new MyClass();
    obj.foo(); // 'foo'
    + +

    上面代码之中,对象Foo有一个foo方法,通过Object.assign方法,可以将foo方法“混入”MyClass类,导致MyClass的实例obj对象都具有foo方法。这就是“混入”模式的一个简单实现。

    +

    下面,我们部署一个通用脚本mixins.js,将 Mixin 写成一个修饰器。

    +
    1
    2
    3
    4
    5
    export function mixins(...list) {
    return function (target) {
    Object.assign(target.prototype, ...list);
    };
    }
    + +

    然后,就可以使用上面这个修饰器,为类“混入”各种方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    import { mixins } from "./mixins";

    const Foo = {
    foo() {
    console.log("foo");
    },
    };

    @mixins(Foo)
    class MyClass {}

    let obj = new MyClass();
    obj.foo(); // "foo"
    + +

    通过mixins这个修饰器,实现了在MyClass类上面“混入”Foo对象的foo方法。

    +

    不过,上面的方法会改写MyClass类的prototype对象,如果不喜欢这一点,也可以通过类的继承实现 Mixin。

    +
    1
    2
    3
    class MyClass extends MyBaseClass {
    /* ... */
    }
    + +

    上面代码中,MyClass继承了MyBaseClass。如果我们想在MyClass里面“混入”一个foo方法,一个办法是在MyClassMyBaseClass之间插入一个混入类,这个类具有foo方法,并且继承了MyBaseClass的所有方法,然后MyClass再继承这个类。

    +
    1
    2
    3
    4
    5
    6
    let MyMixin = (superclass) =>
    class extends superclass {
    foo() {
    console.log("foo from MyMixin");
    }
    };
    + +

    上面代码中,MyMixin是一个混入类生成器,接受superclass作为参数,然后返回一个继承superclass的子类,该子类包含一个foo方法。

    +

    接着,目标类再去继承这个混入类,就达到了“混入”foo方法的目的。

    +
    1
    2
    3
    4
    5
    6
    class MyClass extends MyMixin(MyBaseClass) {
    /* ... */
    }

    let c = new MyClass();
    c.foo(); // "foo from MyMixin"
    + +

    如果需要“混入”多个方法,就生成多个混入类。

    +
    1
    2
    3
    class MyClass extends Mixin1(Mixin2(MyBaseClass)) {
    /* ... */
    }
    + +

    这种写法的一个好处,是可以调用super,因此可以避免在“混入”过程中覆盖父类的同名方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    let Mixin1 = (superclass) =>
    class extends superclass {
    foo() {
    console.log("foo from Mixin1");
    if (super.foo) super.foo();
    }
    };

    let Mixin2 = (superclass) =>
    class extends superclass {
    foo() {
    console.log("foo from Mixin2");
    if (super.foo) super.foo();
    }
    };

    class S {
    foo() {
    console.log("foo from S");
    }
    }

    class C extends Mixin1(Mixin2(S)) {
    foo() {
    console.log("foo from C");
    super.foo();
    }
    }
    + +

    上面代码中,每一次混入发生时,都调用了父类的super.foo方法,导致父类的同名方法没有被覆盖,行为被保留了下来。

    +
    1
    2
    3
    4
    5
    new C().foo();
    // foo from C
    // foo from Mixin1
    // foo from Mixin2
    // foo from S
    + +

    Trait

    Trait 也是一种修饰器,效果与 Mixin 类似,但是提供更多功能,比如防止同名方法的冲突、排除混入某些方法、为混入的方法起别名等等。

    +

    下面采用traits-decorator这个第三方模块作为例子。这个模块提供的traits修饰器,不仅可以接受对象,还可以接受 ES6 类作为参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    import { traits } from "traits-decorator";

    class TFoo {
    foo() {
    console.log("foo");
    }
    }

    const TBar = {
    bar() {
    console.log("bar");
    },
    };

    @traits(TFoo, TBar)
    class MyClass {}

    let obj = new MyClass();
    obj.foo(); // foo
    obj.bar(); // bar
    + +

    上面代码中,通过traits修饰器,在MyClass类上面“混入”了TFoo类的foo方法和TBar对象的bar方法。

    +

    Trait 不允许“混入”同名方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import { traits } from "traits-decorator";

    class TFoo {
    foo() {
    console.log("foo");
    }
    }

    const TBar = {
    bar() {
    console.log("bar");
    },
    foo() {
    console.log("foo");
    },
    };

    @traits(TFoo, TBar)
    class MyClass {}
    // 报错
    // throw new Error('Method named: ' + methodName + ' is defined twice.');
    // ^
    // Error: Method named: foo is defined twice.
    + +

    上面代码中,TFooTBar都有foo方法,结果traits修饰器报错。

    +

    一种解决方法是排除TBarfoo方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    import { traits, excludes } from "traits-decorator";

    class TFoo {
    foo() {
    console.log("foo");
    }
    }

    const TBar = {
    bar() {
    console.log("bar");
    },
    foo() {
    console.log("foo");
    },
    };

    @traits(TFoo, TBar::excludes("foo"))
    class MyClass {}

    let obj = new MyClass();
    obj.foo(); // foo
    obj.bar(); // bar
    + +

    上面代码使用绑定运算符(::)在TBar上排除foo方法,混入时就不会报错了。

    +

    另一种方法是为TBarfoo方法起一个别名。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    import { traits, alias } from "traits-decorator";

    class TFoo {
    foo() {
    console.log("foo");
    }
    }

    const TBar = {
    bar() {
    console.log("bar");
    },
    foo() {
    console.log("foo");
    },
    };

    @traits(TFoo, TBar::alias({ foo: "aliasFoo" }))
    class MyClass {}

    let obj = new MyClass();
    obj.foo(); // foo
    obj.aliasFoo(); // foo
    obj.bar(); // bar
    + +

    上面代码为TBarfoo方法起了别名aliasFoo,于是MyClass也可以混入TBarfoo方法了。

    +

    aliasexcludes方法,可以结合起来使用。

    +
    1
    2
    @traits(TExample::excludes("foo", "bar")::alias({ baz: "exampleBaz" }))
    class MyClass {}
    + +

    上面代码排除了TExamplefoo方法和bar方法,为baz方法起了别名exampleBaz

    +

    as方法则为上面的代码提供了另一种写法。

    +
    1
    2
    3
    4
    @traits(
    TExample::as({ excludes: ["foo", "bar"], alias: { baz: "exampleBaz" } })
    )
    class MyClass {}
    + +

    Babel 转码器的支持

    目前,Babel 转码器已经支持 Decorator。

    +

    首先,安装babel-corebabel-plugin-transform-decorators。由于后者包括在babel-preset-stage-0之中,所以改为安装babel-preset-stage-0亦可。

    +
    1
    $ npm install babel-core babel-plugin-transform-decorators
    + +

    然后,设置配置文件.babelrc

    +
    1
    2
    3
    {
    "plugins": ["transform-decorators"]
    }
    + +

    这时,Babel 就可以对 Decorator 转码了。

    +

    脚本中打开的命令如下。

    +
    1
    babel.transform("code", { plugins: ["transform-decorators"] });
    + +

    Babel 的官方网站提供一个在线转码器,只要勾选 Experimental,就能支持 Decorator 的在线转码。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E4%BF%AE%E9%A5%B0%E5%99%A8/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213/index.html" "b/posts/ES6-\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213/index.html" new file mode 100644 index 00000000..df0c548e --- /dev/null +++ "b/posts/ES6-\345\207\275\346\225\260\345\274\217\347\274\226\347\250\213/index.html" @@ -0,0 +1,270 @@ +函数式编程 | JCAlways + + + + + + + + + + + + + +

    函数式编程

    函数式编程

    JavaScript 语言从一诞生,就具有函数式编程的烙印。它将函数作为一种独立的数据类型,与其他数据类型处于完全平等的地位。在 JavaScript 语言中,你可以采用面向对象编程,也可以采用函数式编程。有人甚至说,JavaScript 是有史以来第一种被大规模采用的函数式编程语言。

    +

    ES6 的种种新增功能,使得函数式编程变得更方便、更强大。本章介绍 ES6 如何进行函数式编程。

    +

    柯里化

    柯里化(currying)指的是将一个多参数的函数拆分成一系列函数,每个拆分后的函数都只接受一个参数(unary)。

    +
    1
    2
    3
    4
    5
    function add(a, b) {
    return a + b;
    }

    add(1, 1); // 2
    + +

    上面代码中,函数add接受两个参数ab

    +

    柯里化就是将上面的函数拆分成两个函数,每个函数都只接受一个参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function add(a) {
    return function (b) {
    return a + b;
    };
    }
    // 或者采用箭头函数写法
    const add = (x) => (y) => x + y;

    const f = add(1);
    f(1); // 2
    + +

    上面代码中,函数add只接受一个参数a,返回一个函数f。函数f也只接受一个参数b

    +

    函数合成

    函数合成(function composition)指的是,将多个函数合成一个函数。

    +
    1
    2
    3
    4
    const compose = (f) => (g) => (x) => f(g(x));

    const f = compose((x) => x * 4)((x) => x + 3);
    f(2); // 20
    + +

    上面代码中,compose就是一个函数合成器,用于将两个函数合成一个函数。

    +

    可以发现,柯里化与函数合成有着密切的联系。前者用于将一个函数拆成多个函数,后者用于将多个函数合并成一个函数。

    +

    参数倒置

    参数倒置(flip)指的是改变函数前两个参数的顺序。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var divide = (a, b) => a / b;
    var flip = f.flip(divide);

    flip(10, 5); // 0.5
    flip(1, 10); // 10

    var three = (a, b, c) => [a, b, c];
    var flip = f.flip(three);
    flip(1, 2, 3); // => [2, 1, 3]
    + +

    上面代码中,如果按照正常的参数顺序,10 除以 5 等于 2。但是,参数倒置以后得到的新函数,结果就是 5 除以 10,结果得到 0.5。如果原函数有 3 个参数,则只颠倒前两个参数的位置。

    +

    参数倒置的代码非常简单。

    +
    1
    2
    3
    4
    5
    let f = {};
    f.flip =
    (fn) =>
    (a, b, ...args) =>
    fn(b, a, ...args.reverse());
    + +

    执行边界

    执行边界(until)指的是函数执行到满足条件为止。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let condition = (x) => x > 100;
    let inc = (x) => x + 1;
    let until = f.until(condition, inc);

    until(0); // 101

    condition = (x) => x === 5;
    until = f.until(condition, inc);

    until(3); // 5
    + +

    上面代码中,第一段的条件是执行到x大于 100 为止,所以x初值为 0 时,会一直执行到 101。第二段的条件是执行到等于 5 为止,所以x最后的值是 5。

    +

    执行边界的实现如下。

    +
    1
    2
    3
    4
    5
    6
    7
    let f = {};
    f.until =
    (condition, f) =>
    (...args) => {
    var r = f.apply(null, args);
    return condition(r) ? r : f.until(condition, f)(r);
    };
    + +

    上面代码的关键就是,如果满足条件就返回结果,否则不断递归执行。

    +

    队列操作

    队列(list)操作包括以下几种。

    +
      +
    • head: 取出队列的第一个非空成员。
    • +
    • last: 取出有限队列的最后一个非空成员。
    • +
    • tail: 取出除了“队列头”以外的其他非空成员。
    • +
    • init: 取出除了“队列尾”以外的其他非空成员。
    • +
    +

    下面是例子。

    +
    1
    2
    3
    4
    f.head(5, 27, 3, 1); // 5
    f.last(5, 27, 3, 1); // 1
    f.tail(5, 27, 3, 1); // [27, 3, 1]
    f.init(5, 27, 3, 1); // [5, 27, 3]
    + +

    这些方法的实现如下。

    +
    1
    2
    3
    4
    5
    let f = {};
    f.head = (...xs) => xs[0];
    f.last = (...xs) => xs.slice(-1);
    f.tail = (...xs) => Array.prototype.slice.call(xs, 1);
    f.init = (...xs) => xs.slice(0, -1);
    + +

    合并操作

    合并操作分为concatconcatMap两种。前者就是将多个数组合成一个,后者则是先处理一下参数,然后再将处理结果合成一个数组。

    +
    1
    2
    f.concat([5], [27], [3]); // [5, 27, 3]
    f.concatMap((x) => "hi " + x, 1, [[2]], 3); // ['hi 1', 'hi 2', 'hi 3']
    + +

    这两种方法的实现代码如下。

    +
    1
    2
    3
    let f = {};
    f.concat = (...xs) => xs.reduce((a, b) => a.concat(b));
    f.concatMap = (f, ...xs) => f.concat(xs.map(f));
    + +

    配对操作

    配对操作分为zipzipWith两种方法。zip操作将两个队列的成员,一一配对,合成一个新的队列。如果两个队列不等长,较长的那个队列多出来的成员,会被忽略。zipWith操作的第一个参数是一个函数,然后会将后面的队列成员一一配对,输入该函数,返回值就组成一个新的队列。

    +

    下面是例子。

    +
    1
    2
    3
    4
    5
    6
    let a = [0, 1, 2];
    let b = [3, 4, 5];
    let c = [6, 7, 8];

    f.zip(a, b); // [[0, 3], [1, 4], [2, 5]]
    f.zipWith((a, b) => a + b, a, b, c); // [9, 12, 15]
    + +

    上面代码中,zipWith方法的第一个参数是一个求和函数,它将后面三个队列的成员,一一配对进行相加。

    +

    这两个方法的实现如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let f = {};

    f.zip = (...xs) => {
    let r = [];
    let nple = [];
    let length = Math.min.apply(
    null,
    xs.map((x) => x.length)
    );

    for (var i = 0; i < length; i++) {
    xs.forEach((x) => nple.push(x[i]));

    r.push(nple);
    nple = [];
    }

    return r;
    };

    f.zipWith = (op, ...xs) => f.zip.apply(null, xs).map((x) => x.reduce(op));
    + +

    参考链接

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/ES6-\345\207\275\346\225\260\347\232\204\346\211\251\345\261\225/index.html" "b/posts/ES6-\345\207\275\346\225\260\347\232\204\346\211\251\345\261\225/index.html" new file mode 100644 index 00000000..c8954d1e --- /dev/null +++ "b/posts/ES6-\345\207\275\346\225\260\347\232\204\346\211\251\345\261\225/index.html" @@ -0,0 +1,562 @@ +函数的扩展 | JCAlways + + + + + + + + + + + + + +

    函数的扩展

    函数的扩展

    函数参数的默认值

    基本用法

    ES6 之前,不能直接为函数的参数指定默认值,只能采用变通的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function log(x, y) {
    y = y || "World";
    console.log(x, y);
    }

    log("Hello"); // Hello World
    log("Hello", "China"); // Hello China
    log("Hello", ""); // Hello World
    + +

    上面代码检查函数log的参数y有没有赋值,如果没有,则指定默认值为World。这种写法的缺点在于,如果参数y赋值了,但是对应的布尔值为false,则该赋值不起作用。就像上面代码的最后一行,参数y等于空字符,结果被改为默认值。

    +

    为了避免这个问题,通常需要先判断一下参数y是否被赋值,如果没有,再等于默认值。

    +
    1
    2
    3
    if (typeof y === "undefined") {
    y = "World";
    }
    + +

    ES6 允许为函数的参数设置默认值,即直接写在参数定义的后面。

    +
    1
    2
    3
    4
    5
    6
    7
    function log(x, y = "World") {
    console.log(x, y);
    }

    log("Hello"); // Hello World
    log("Hello", "China"); // Hello China
    log("Hello", ""); // Hello
    + +

    可以看到,ES6 的写法比 ES5 简洁许多,而且非常自然。下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    function Point(x = 0, y = 0) {
    this.x = x;
    this.y = y;
    }

    const p = new Point();
    p; // { x: 0, y: 0 }
    + +

    除了简洁,ES6 的写法还有两个好处:首先,阅读代码的人,可以立刻意识到哪些参数是可以省略的,不用查看函数体或文档;其次,有利于将来的代码优化,即使未来的版本在对外接口中,彻底拿掉这个参数,也不会导致以前的代码无法运行。

    +

    参数变量是默认声明的,所以不能用letconst再次声明。

    +
    1
    2
    3
    4
    function foo(x = 5) {
    let x = 1; // error
    const x = 2; // error
    }
    + +

    上面代码中,参数变量x是默认声明的,在函数体中,不能用letconst再次声明,否则会报错。

    +

    使用参数默认值时,函数不能有同名参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 不报错
    function foo(x, x, y) {
    // ...
    }

    // 报错
    function foo(x, x, y = 1) {
    // ...
    }
    // SyntaxError: Duplicate parameter name not allowed in this context
    + +

    另外,一个容易忽略的地方是,参数默认值不是传值的,而是每次都重新计算默认值表达式的值。也就是说,参数默认值是惰性求值的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let x = 99;
    function foo(p = x + 1) {
    console.log(p);
    }

    foo(); // 100

    x = 100;
    foo(); // 101
    + +

    上面代码中,参数p的默认值是x + 1。这时,每次调用函数foo,都会重新计算x + 1,而不是默认p等于 100。

    +

    与解构赋值默认值结合使用

    参数默认值可以与解构赋值的默认值,结合起来使用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function foo({ x, y = 5 }) {
    console.log(x, y);
    }

    foo({}); // undefined 5
    foo({ x: 1 }); // 1 5
    foo({ x: 1, y: 2 }); // 1 2
    foo(); // TypeError: Cannot read property 'x' of undefined
    + +

    上面代码只使用了对象的解构赋值默认值,没有使用函数参数的默认值。只有当函数foo的参数是一个对象时,变量xy才会通过解构赋值生成。如果函数foo调用时没提供参数,变量xy就不会生成,从而报错。通过提供函数参数的默认值,就可以避免这种情况。

    +
    1
    2
    3
    4
    5
    function foo({ x, y = 5 } = {}) {
    console.log(x, y);
    }

    foo(); // undefined 5
    + +

    上面代码指定,如果没有提供参数,函数foo的参数默认为一个空对象。

    +

    下面是另一个解构赋值默认值的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function fetch(url, { body = "", method = "GET", headers = {} }) {
    console.log(method);
    }

    fetch("http://example.com", {});
    // "GET"

    fetch("http://example.com");
    // 报错
    + +

    上面代码中,如果函数fetch的第二个参数是一个对象,就可以为它的三个属性设置默认值。这种写法不能省略第二个参数,如果结合函数参数的默认值,就可以省略第二个参数。这时,就出现了双重默认值。

    +
    1
    2
    3
    4
    5
    6
    function fetch(url, { body = "", method = "GET", headers = {} } = {}) {
    console.log(method);
    }

    fetch("http://example.com");
    // "GET"
    + +

    上面代码中,函数fetch没有第二个参数时,函数参数的默认值就会生效,然后才是解构赋值的默认值生效,变量method才会取到默认值GET

    +

    作为练习,请问下面两种写法有什么差别?

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 写法一
    function m1({ x = 0, y = 0 } = {}) {
    return [x, y];
    }

    // 写法二
    function m2({ x, y } = { x: 0, y: 0 }) {
    return [x, y];
    }
    + +

    上面两种写法都对函数的参数设定了默认值,区别是写法一函数参数的默认值是空对象,但是设置了对象解构赋值的默认值;写法二函数参数的默认值是一个有具体属性的对象,但是没有设置对象解构赋值的默认值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    // 函数没有参数的情况
    m1(); // [0, 0]
    m2(); // [0, 0]

    // x 和 y 都有值的情况
    m1({ x: 3, y: 8 }); // [3, 8]
    m2({ x: 3, y: 8 }); // [3, 8]

    // x 有值,y 无值的情况
    m1({ x: 3 }); // [3, 0]
    m2({ x: 3 }); // [3, undefined]

    // x 和 y 都无值的情况
    m1({}); // [0, 0];
    m2({}); // [undefined, undefined]

    m1({ z: 3 }); // [0, 0]
    m2({ z: 3 }); // [undefined, undefined]
    + +

    参数默认值的位置

    通常情况下,定义了默认值的参数,应该是函数的尾参数。因为这样比较容易看出来,到底省略了哪些参数。如果非尾部的参数设置默认值,实际上这个参数是没法省略的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    // 例一
    function f(x = 1, y) {
    return [x, y];
    }

    f() // [1, undefined]
    f(2) // [2, undefined])
    f(, 1) // 报错
    f(undefined, 1) // [1, 1]

    // 例二
    function f(x, y = 5, z) {
    return [x, y, z];
    }

    f() // [undefined, 5, undefined]
    f(1) // [1, 5, undefined]
    f(1, ,2) // 报错
    f(1, undefined, 2) // [1, 5, 2]
    + +

    上面代码中,有默认值的参数都不是尾参数。这时,无法只省略该参数,而不省略它后面的参数,除非显式输入undefined

    +

    如果传入undefined,将触发该参数等于默认值,null则没有这个效果。

    +
    1
    2
    3
    4
    5
    6
    function foo(x = 5, y = 6) {
    console.log(x, y);
    }

    foo(undefined, null);
    // 5 null
    + +

    上面代码中,x参数对应undefined,结果触发了默认值,y参数等于null,就没有触发默认值。

    +

    函数的 length 属性

    指定了默认值以后,函数的length属性,将返回没有指定默认值的参数个数。也就是说,指定了默认值后,length属性将失真。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    (function (a) {})
    .length(
    // 1
    function (a = 5) {}
    )
    .length(
    // 0
    function (a, b, c = 5) {}
    ).length; // 2
    + +

    上面代码中,length属性的返回值,等于函数的参数个数减去指定了默认值的参数个数。比如,上面最后一个函数,定义了 3 个参数,其中有一个参数c指定了默认值,因此length属性等于3减去1,最后得到2

    +

    这是因为length属性的含义是,该函数预期传入的参数个数。某个参数指定默认值以后,预期传入的参数个数就不包括这个参数了。同理,后文的 rest 参数也不会计入length属性。

    +
    1
    (function (...args) {}).length; // 0
    + +

    如果设置了默认值的参数不是尾参数,那么length属性也不再计入后面的参数了。

    +
    1
    2
    3
    4
    (function (a = 0, b, c) {}).length(
    // 0
    function (a, b = 1, c) {}
    ).length; // 1
    + +

    作用域

    一旦设置了参数的默认值,函数进行声明初始化时,参数会形成一个单独的作用域(context)。等到初始化结束,这个作用域就会消失。这种语法行为,在不设置参数默认值时,是不会出现的。

    +
    1
    2
    3
    4
    5
    6
    7
    var x = 1;

    function f(x, y = x) {
    console.log(y);
    }

    f(2); // 2
    + +

    上面代码中,参数y的默认值等于变量x。调用函数f时,参数形成一个单独的作用域。在这个作用域里面,默认值变量x指向第一个参数x,而不是全局变量x,所以输出是2

    +

    再看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let x = 1;

    function f(y = x) {
    let x = 2;
    console.log(y);
    }

    f(); // 1
    + +

    上面代码中,函数f调用时,参数y = x形成一个单独的作用域。这个作用域里面,变量x本身没有定义,所以指向外层的全局变量x。函数调用时,函数体内部的局部变量x影响不到默认值变量x

    +

    如果此时,全局变量x不存在,就会报错。

    +
    1
    2
    3
    4
    5
    6
    function f(y = x) {
    let x = 2;
    console.log(y);
    }

    f(); // ReferenceError: x is not defined
    + +

    下面这样写,也会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    var x = 1;

    function foo(x = x) {
    // ...
    }

    foo(); // ReferenceError: x is not defined
    + +

    上面代码中,参数x = x形成一个单独作用域。实际执行的是let x = x,由于暂时性死区的原因,这行代码会报错”x 未定义“。

    +

    如果参数的默认值是一个函数,该函数的作用域也遵守这个规则。请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let foo = "outer";

    function bar(func = () => foo) {
    let foo = "inner";
    console.log(func());
    }

    bar(); // outer
    + +

    上面代码中,函数bar的参数func的默认值是一个匿名函数,返回值为变量foo。函数参数形成的单独作用域里面,并没有定义变量foo,所以foo指向外层的全局变量foo,因此输出outer

    +

    如果写成下面这样,就会报错。

    +
    1
    2
    3
    4
    5
    6
    function bar(func = () => foo) {
    let foo = "inner";
    console.log(func());
    }

    bar(); // ReferenceError: foo is not defined
    + +

    上面代码中,匿名函数里面的foo指向函数外层,但是函数外层并没有声明变量foo,所以就报错了。

    +

    下面是一个更复杂的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var x = 1;
    function foo(
    x,
    y = function () {
    x = 2;
    }
    ) {
    var x = 3;
    y();
    console.log(x);
    }

    foo(); // 3
    x; // 1
    + +

    上面代码中,函数foo的参数形成一个单独作用域。这个作用域里面,首先声明了变量x,然后声明了变量yy的默认值是一个匿名函数。这个匿名函数内部的变量x,指向同一个作用域的第一个参数x。函数foo内部又声明了一个内部变量x,该变量与第一个参数x由于不是同一个作用域,所以不是同一个变量,因此执行y后,内部变量x和外部全局变量x的值都没变。

    +

    如果将var x = 3var去除,函数foo的内部变量x就指向第一个参数x,与匿名函数内部的x是一致的,所以最后输出的就是2,而外层的全局变量x依然不受影响。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var x = 1;
    function foo(
    x,
    y = function () {
    x = 2;
    }
    ) {
    x = 3;
    y();
    console.log(x);
    }

    foo(); // 2
    x; // 1
    + +

    应用

    利用参数默认值,可以指定某一个参数不得省略,如果省略就抛出一个错误。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function throwIfMissing() {
    throw new Error("Missing parameter");
    }

    function foo(mustBeProvided = throwIfMissing()) {
    return mustBeProvided;
    }

    foo();
    // Error: Missing parameter
    + +

    上面代码的foo函数,如果调用的时候没有参数,就会调用默认值throwIfMissing函数,从而抛出一个错误。

    +

    从上面代码还可以看到,参数mustBeProvided的默认值等于throwIfMissing函数的运行结果(注意函数名throwIfMissing之后有一对圆括号),这表明参数的默认值不是在定义时执行,而是在运行时执行。如果参数已经赋值,默认值中的函数就不会运行。

    +

    另外,可以将参数默认值设为undefined,表明这个参数是可以省略的。

    +
    1
    function foo(optional = undefined) { ··· }
    + +

    rest 参数

    ES6 引入 rest 参数(形式为...变量名),用于获取函数的多余参数,这样就不需要使用arguments对象了。rest 参数搭配的变量是一个数组,该变量将多余的参数放入数组中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function add(...values) {
    let sum = 0;

    for (var val of values) {
    sum += val;
    }

    return sum;
    }

    add(2, 5, 3); // 10
    + +

    上面代码的add函数是一个求和函数,利用 rest 参数,可以向该函数传入任意数目的参数。

    +

    下面是一个 rest 参数代替arguments变量的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    // arguments变量的写法
    function sortNumbers() {
    return Array.prototype.slice.call(arguments).sort();
    }

    // rest参数的写法
    const sortNumbers = (...numbers) => numbers.sort();
    + +

    上面代码的两种写法,比较后可以发现,rest 参数的写法更自然也更简洁。

    +

    arguments对象不是数组,而是一个类似数组的对象。所以为了使用数组的方法,必须使用Array.prototype.slice.call先将其转为数组。rest 参数就不存在这个问题,它就是一个真正的数组,数组特有的方法都可以使用。下面是一个利用 rest 参数改写数组push方法的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function push(array, ...items) {
    items.forEach(function (item) {
    array.push(item);
    console.log(item);
    });
    }

    var a = [];
    push(a, 1, 2, 3);
    + +

    注意,rest 参数之后不能再有其他参数(即只能是最后一个参数),否则会报错。

    +
    1
    2
    3
    4
    // 报错
    function f(a, ...b, c) {
    // ...
    }
    + +

    函数的length属性,不包括 rest 参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    (function (a) {})
    .length(
    // 1
    function (...a) {}
    )
    .length(
    // 0
    function (a, ...b) {}
    ).length; // 1
    + +

    严格模式

    从 ES5 开始,函数内部可以设定为严格模式。

    +
    1
    2
    3
    4
    function doSomething(a, b) {
    "use strict";
    // code
    }
    + +

    ES2016 做了一点修改,规定只要函数参数使用了默认值、解构赋值、或者扩展运算符,那么函数内部就不能显式设定为严格模式,否则会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    // 报错
    function doSomething(a, b = a) {
    "use strict";
    // code
    }

    // 报错
    const doSomething = function({ a, b }) {
    "use strict";
    // code
    };

    // 报错
    const doSomething = (...a) => {
    "use strict";
    // code
    };

    const obj = {
    // 报错
    doSomething({ a, b }) {
    "use strict";
    // code
    },
    };
    + +

    这样规定的原因是,函数内部的严格模式,同时适用于函数体和函数参数。但是,函数执行的时候,先执行函数参数,然后再执行函数体。这样就有一个不合理的地方,只有从函数体之中,才能知道参数是否应该以严格模式执行,但是参数却应该先于函数体执行。

    +
    1
    2
    3
    4
    5
    // 报错
    function doSomething(value = 070) {
    "use strict";
    return value;
    }
    + +

    上面代码中,参数value的默认值是八进制数070,但是严格模式下不能用前缀0表示八进制,所以应该报错。但是实际上,JavaScript 引擎会先成功执行value = 070,然后进入函数体内部,发现需要用严格模式执行,这时才会报错。

    +

    虽然可以先解析函数体代码,再执行参数代码,但是这样无疑就增加了复杂性。因此,标准索性禁止了这种用法,只要参数使用了默认值、解构赋值、或者扩展运算符,就不能显式指定严格模式。

    +

    两种方法可以规避这种限制。第一种是设定全局性的严格模式,这是合法的。

    +
    1
    2
    3
    4
    5
    "use strict";

    function doSomething(a, b = a) {
    // code
    }
    + +

    第二种是把函数包在一个无参数的立即执行函数里面。

    +
    1
    2
    3
    4
    5
    6
    const doSomething = (function () {
    "use strict";
    return function (value = 42) {
    return value;
    };
    })();
    + +

    name 属性

    函数的name属性,返回该函数的函数名。

    +
    1
    2
    function foo() {}
    foo.name; // "foo"
    + +

    这个属性早就被浏览器广泛支持,但是直到 ES6,才将其写入了标准。

    +

    需要注意的是,ES6 对这个属性的行为做出了一些修改。如果将一个匿名函数赋值给一个变量,ES5 的name属性,会返回空字符串,而 ES6 的name属性会返回实际的函数名。

    +
    1
    2
    3
    4
    5
    6
    7
    var f = function () {};

    // ES5
    f.name; // ""

    // ES6
    f.name; // "f"
    + +

    上面代码中,变量f等于一个匿名函数,ES5 和 ES6 的name属性返回的值不一样。

    +

    如果将一个具名函数赋值给一个变量,则 ES5 和 ES6 的name属性都返回这个具名函数原本的名字。

    +
    1
    2
    3
    4
    5
    6
    7
    const bar = function baz() {};

    // ES5
    bar.name; // "baz"

    // ES6
    bar.name; // "baz"
    + +

    Function构造函数返回的函数实例,name属性的值为anonymous

    +
    1
    new Function().name; // "anonymous"
    + +

    bind返回的函数,name属性值会加上bound前缀。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function foo() {}
    foo
    .bind({})
    .name(
    // "bound foo"

    function () {}
    )
    .bind({}).name; // "bound "
    + +

    箭头函数

    基本用法

    ES6 允许使用“箭头”(=>)定义函数。

    +
    1
    2
    3
    4
    5
    6
    var f = (v) => v;

    // 等同于
    var f = function (v) {
    return v;
    };
    + +

    如果箭头函数不需要参数或需要多个参数,就使用一个圆括号代表参数部分。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var f = () => 5;
    // 等同于
    var f = function () {
    return 5;
    };

    var sum = (num1, num2) => num1 + num2;
    // 等同于
    var sum = function (num1, num2) {
    return num1 + num2;
    };
    + +

    如果箭头函数的代码块部分多于一条语句,就要使用大括号将它们括起来,并且使用return语句返回。

    +
    1
    2
    3
    var sum = (num1, num2) => {
    return num1 + num2;
    };
    + +

    由于大括号被解释为代码块,所以如果箭头函数直接返回一个对象,必须在对象外面加上括号,否则会报错。

    +
    1
    2
    3
    4
    5
    // 报错
    let getTempItem = id => { id: id, name: "Temp" };

    // 不报错
    let getTempItem = id => ({ id: id, name: "Temp" });
    + +

    下面是一种特殊情况,虽然可以运行,但会得到错误的结果。

    +
    1
    2
    3
    4
    let foo = () => {
    a: 1;
    };
    foo(); // undefined
    + +

    上面代码中,原始意图是返回一个对象{ a: 1 },但是由于引擎认为大括号是代码块,所以执行了一行语句a: 1。这时,a可以被解释为语句的标签,因此实际执行的语句是1;,然后函数就结束了,没有返回值。

    +

    如果箭头函数只有一行语句,且不需要返回值,可以采用下面的写法,就不用写大括号了。

    +
    1
    let fn = () => void doesNotReturn();
    + +

    箭头函数可以与变量解构结合使用。

    +
    1
    2
    3
    4
    5
    6
    const full = ({ first, last }) => first + " " + last;

    // 等同于
    function full(person) {
    return person.first + " " + person.last;
    }
    + +

    箭头函数使得表达更加简洁。

    +
    1
    2
    const isEven = (n) => n % 2 === 0;
    const square = (n) => n * n;
    + +

    上面代码只用了两行,就定义了两个简单的工具函数。如果不用箭头函数,可能就要占用多行,而且还不如现在这样写醒目。

    +

    箭头函数的一个用处是简化回调函数。

    +
    1
    2
    3
    4
    5
    6
    7
    // 正常函数写法
    [1, 2, 3].map(function (x) {
    return x * x;
    });

    // 箭头函数写法
    [1, 2, 3].map((x) => x * x);
    + +

    另一个例子是

    +
    1
    2
    3
    4
    5
    6
    7
    // 正常函数写法
    var result = values.sort(function (a, b) {
    return a - b;
    });

    // 箭头函数写法
    var result = values.sort((a, b) => a - b);
    + +

    下面是 rest 参数与箭头函数结合的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const numbers = (...nums) => nums;

    numbers(1, 2, 3, 4, 5);
    // [1,2,3,4,5]

    const headAndTail = (head, ...tail) => [head, tail];

    headAndTail(1, 2, 3, 4, 5);
    // [1,[2,3,4,5]]
    + +

    使用注意点

    箭头函数有几个使用注意点。

    +

    (1)函数体内的this对象,就是定义时所在的对象,而不是使用时所在的对象。

    +

    (2)不可以当作构造函数,也就是说,不可以使用new命令,否则会抛出一个错误。

    +

    (3)不可以使用arguments对象,该对象在函数体内不存在。如果要用,可以用 rest 参数代替。

    +

    (4)不可以使用yield命令,因此箭头函数不能用作 Generator 函数。

    +

    上面四点中,第一点尤其值得注意。this对象的指向是可变的,但是在箭头函数中,它是固定的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function foo() {
    setTimeout(() => {
    console.log("id:", this.id);
    }, 100);
    }

    var id = 21;

    foo.call({ id: 42 });
    // id: 42
    + +

    上面代码中,setTimeout的参数是一个箭头函数,这个箭头函数的定义生效是在foo函数生成时,而它的真正执行要等到 100 毫秒后。如果是普通函数,执行时this应该指向全局对象window,这时应该输出21。但是,箭头函数导致this总是指向函数定义生效时所在的对象(本例是{id: 42}),所以输出的是42

    +

    箭头函数可以让setTimeout里面的this,绑定定义时所在的作用域,而不是指向运行时所在的作用域。下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    function Timer() {
    this.s1 = 0;
    this.s2 = 0;
    // 箭头函数
    setInterval(() => this.s1++, 1000);
    // 普通函数
    setInterval(function () {
    this.s2++;
    }, 1000);
    }

    var timer = new Timer();

    setTimeout(() => console.log("s1: ", timer.s1), 3100);
    setTimeout(() => console.log("s2: ", timer.s2), 3100);
    // s1: 3
    // s2: 0
    + +

    上面代码中,Timer函数内部设置了两个定时器,分别使用了箭头函数和普通函数。前者的this绑定定义时所在的作用域(即Timer函数),后者的this指向运行时所在的作用域(即全局对象)。所以,3100 毫秒之后,timer.s1被更新了 3 次,而timer.s2一次都没更新。

    +

    箭头函数可以让this指向固定化,这种特性很有利于封装回调函数。下面是一个例子,DOM 事件的回调函数封装在一个对象里面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var handler = {
    id: "123456",

    init: function () {
    document.addEventListener(
    "click",
    (event) => this.doSomething(event.type),
    false
    );
    },

    doSomething: function (type) {
    console.log("Handling " + type + " for " + this.id);
    },
    };
    + +

    上面代码的init方法中,使用了箭头函数,这导致这个箭头函数里面的this,总是指向handler对象。否则,回调函数运行时,this.doSomething这一行会报错,因为此时this指向document对象。

    +

    this指向的固定化,并不是因为箭头函数内部有绑定this的机制,实际原因是箭头函数根本没有自己的this,导致内部的this就是外层代码块的this。正是因为它没有this,所以也就不能用作构造函数。

    +

    所以,箭头函数转成 ES5 的代码如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // ES6
    function foo() {
    setTimeout(() => {
    console.log("id:", this.id);
    }, 100);
    }

    // ES5
    function foo() {
    var _this = this;

    setTimeout(function () {
    console.log("id:", _this.id);
    }, 100);
    }
    + +

    上面代码中,转换后的 ES5 版本清楚地说明了,箭头函数里面根本没有自己的this,而是引用外层的this

    +

    请问下面的代码之中有几个this

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function foo() {
    return () => {
    return () => {
    return () => {
    console.log("id:", this.id);
    };
    };
    };
    }

    var f = foo.call({ id: 1 });

    var t1 = f.call({ id: 2 })()(); // id: 1
    var t2 = f().call({ id: 3 })(); // id: 1
    var t3 = f()().call({ id: 4 }); // id: 1
    + +

    上面代码之中,只有一个this,就是函数foothis,所以t1t2t3都输出同样的结果。因为所有的内层函数都是箭头函数,都没有自己的this,它们的this其实都是最外层foo函数的this

    +

    除了this,以下三个变量在箭头函数之中也是不存在的,指向外层函数的对应变量:argumentssupernew.target

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function foo() {
    setTimeout(() => {
    console.log("args:", arguments);
    }, 100);
    }

    foo(2, 4, 6, 8);
    // args: [2, 4, 6, 8]
    + +

    上面代码中,箭头函数内部的变量arguments,其实是函数fooarguments变量。

    +

    另外,由于箭头函数没有自己的this,所以当然也就不能用call()apply()bind()这些方法去改变this的指向。

    +
    1
    2
    3
    4
    (function () {
    return [(() => this.x).bind({ x: "inner" })()];
    }).call({ x: "outer" });
    // ['outer']
    + +

    上面代码中,箭头函数没有自己的this,所以bind方法无效,内部的this指向外部的this

    +

    长期以来,JavaScript 语言的this对象一直是一个令人头痛的问题,在对象方法中使用this,必须非常小心。箭头函数”绑定”this,很大程度上解决了这个困扰。

    +

    不适用场合

    由于箭头函数使得this从“动态”变成“静态”,下面两个场合不应该使用箭头函数。

    +

    第一个场合是定义函数的方法,且该方法内部包括this

    +
    1
    2
    3
    4
    5
    6
    const cat = {
    lives: 9,
    jumps: () => {
    this.lives--;
    },
    };
    + +

    上面代码中,cat.jumps()方法是一个箭头函数,这是错误的。调用cat.jumps()时,如果是普通函数,该方法内部的this指向cat;如果写成上面那样的箭头函数,使得this指向全局对象,因此不会得到预期结果。

    +

    第二个场合是需要动态this的时候,也不应使用箭头函数。

    +
    1
    2
    3
    4
    var button = document.getElementById("press");
    button.addEventListener("click", () => {
    this.classList.toggle("on");
    });
    + +

    上面代码运行时,点击按钮会报错,因为button的监听函数是一个箭头函数,导致里面的this就是全局对象。如果改成普通函数,this就会动态指向被点击的按钮对象。

    +

    另外,如果函数体很复杂,有许多行,或者函数内部有大量的读写操作,不单纯是为了计算值,这时也不应该使用箭头函数,而是要使用普通函数,这样可以提高代码可读性。

    +

    嵌套的箭头函数

    箭头函数内部,还可以再使用箭头函数。下面是一个 ES5 语法的多重嵌套函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function insert(value) {
    return {
    into: function (array) {
    return {
    after: function (afterValue) {
    array.splice(array.indexOf(afterValue) + 1, 0, value);
    return array;
    },
    };
    },
    };
    }

    insert(2).into([1, 3]).after(1); //[1, 2, 3]
    + +

    上面这个函数,可以使用箭头函数改写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let insert = (value) => ({
    into: (array) => ({
    after: (afterValue) => {
    array.splice(array.indexOf(afterValue) + 1, 0, value);
    return array;
    },
    }),
    });

    insert(2).into([1, 3]).after(1); //[1, 2, 3]
    + +

    下面是一个部署管道机制(pipeline)的例子,即前一个函数的输出是后一个函数的输入。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const pipeline =
    (...funcs) =>
    (val) =>
    funcs.reduce((a, b) => b(a), val);

    const plus1 = (a) => a + 1;
    const mult2 = (a) => a * 2;
    const addThenMult = pipeline(plus1, mult2);

    addThenMult(5);
    // 12
    + +

    如果觉得上面的写法可读性比较差,也可以采用下面的写法。

    +
    1
    2
    3
    4
    5
    const plus1 = (a) => a + 1;
    const mult2 = (a) => a * 2;

    mult2(plus1(5));
    // 12
    + +

    箭头函数还有一个功能,就是可以很方便地改写 λ 演算。

    +
    1
    2
    3
    4
    5
    6
    // λ演算的写法
    fix = λf.(λx.f(λv.x(x)(v)))(λx.f(λv.x(x)(v)))

    // ES6的写法
    var fix = f => (x => f(v => x(x)(v)))
    (x => f(v => x(x)(v)));
    + +

    上面两种写法,几乎是一一对应的。由于 λ 演算对于计算机科学非常重要,这使得我们可以用 ES6 作为替代工具,探索计算机科学。

    +

    双冒号运算符

    箭头函数可以绑定this对象,大大减少了显式绑定this对象的写法(callapplybind)。但是,箭头函数并不适用于所有场合,所以现在有一个提案,提出了“函数绑定”(function bind)运算符,用来取代callapplybind调用。

    +

    函数绑定运算符是并排的两个冒号(::),双冒号左边是一个对象,右边是一个函数。该运算符会自动将左边的对象,作为上下文环境(即this对象),绑定到右边的函数上面。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    foo::bar;
    // 等同于
    bar.bind(foo);

    foo::bar(...arguments);
    // 等同于
    bar.apply(foo, arguments);

    const hasOwnProperty = Object.prototype.hasOwnProperty;
    function hasOwn(obj, key) {
    return obj::hasOwnProperty(key);
    }
    + +

    如果双冒号左边为空,右边是一个对象的方法,则等于将该方法绑定在该对象上面。

    +
    1
    2
    3
    4
    5
    6
    7
    var method = obj::obj.foo;
    // 等同于
    var method = ::obj.foo;

    let log = ::console.log;
    // 等同于
    var log = console.log.bind(console);
    + +

    如果双冒号运算符的运算结果,还是一个对象,就可以采用链式写法。

    +
    1
    2
    3
    4
    5
    6
    import { map, takeWhile, forEach } from "iterlib";

    getPlayers()
    ::map((x) => x.character())
    ::takeWhile((x) => x.strength > 100)
    ::forEach((x) => console.log(x));
    + +

    尾调用优化

    什么是尾调用?

    尾调用(Tail Call)是函数式编程的一个重要概念,本身非常简单,一句话就能说清楚,就是指某个函数的最后一步是调用另一个函数。

    +
    1
    2
    3
    function f(x) {
    return g(x);
    }
    + +

    上面代码中,函数f的最后一步是调用函数g,这就叫尾调用。

    +

    以下三种情况,都不属于尾调用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 情况一
    function f(x) {
    let y = g(x);
    return y;
    }

    // 情况二
    function f(x) {
    return g(x) + 1;
    }

    // 情况三
    function f(x) {
    g(x);
    }
    + +

    上面代码中,情况一是调用函数g之后,还有赋值操作,所以不属于尾调用,即使语义完全一样。情况二也属于调用后还有操作,即使写在一行内。情况三等同于下面的代码。

    +
    1
    2
    3
    4
    function f(x) {
    g(x);
    return undefined;
    }
    + +

    尾调用不一定出现在函数尾部,只要是最后一步操作即可。

    +
    1
    2
    3
    4
    5
    6
    function f(x) {
    if (x > 0) {
    return m(x);
    }
    return n(x);
    }
    + +

    上面代码中,函数mn都属于尾调用,因为它们都是函数f的最后一步操作。

    +

    尾调用优化

    尾调用之所以与其他调用不同,就在于它的特殊的调用位置。

    +

    我们知道,函数调用会在内存形成一个“调用记录”,又称“调用帧”(call frame),保存调用位置和内部变量等信息。如果在函数A的内部调用函数B,那么在A的调用帧上方,还会形成一个B的调用帧。等到B运行结束,将结果返回到AB的调用帧才会消失。如果函数B内部还调用函数C,那就还有一个C的调用帧,以此类推。所有的调用帧,就形成一个“调用栈”(call stack)。

    +

    尾调用由于是函数的最后一步操作,所以不需要保留外层函数的调用帧,因为调用位置、内部变量等信息都不会再用到了,只要直接用内层函数的调用帧,取代外层函数的调用帧就可以了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    function f() {
    let m = 1;
    let n = 2;
    return g(m + n);
    }
    f();

    // 等同于
    function f() {
    return g(3);
    }
    f();

    // 等同于
    g(3);
    + +

    上面代码中,如果函数g不是尾调用,函数f就需要保存内部变量mn的值、g的调用位置等信息。但由于调用g之后,函数f就结束了,所以执行到最后一步,完全可以删除f(x)的调用帧,只保留g(3)的调用帧。

    +

    这就叫做“尾调用优化”(Tail call optimization),即只保留内层函数的调用帧。如果所有函数都是尾调用,那么完全可以做到每次执行时,调用帧只有一项,这将大大节省内存。这就是“尾调用优化”的意义。

    +

    注意,只有不再用到外层函数的内部变量,内层函数的调用帧才会取代外层函数的调用帧,否则就无法进行“尾调用优化”。

    +
    1
    2
    3
    4
    5
    6
    7
    function addOne(a) {
    var one = 1;
    function inner(b) {
    return b + one;
    }
    return inner(a);
    }
    + +

    上面的函数不会进行尾调用优化,因为内层函数inner用到了外层函数addOne的内部变量one

    +

    尾递归

    函数调用自身,称为递归。如果尾调用自身,就称为尾递归。

    +

    递归非常耗费内存,因为需要同时保存成千上百个调用帧,很容易发生“栈溢出”错误(stack overflow)。但对于尾递归来说,由于只存在一个调用帧,所以永远不会发生“栈溢出”错误。

    +
    1
    2
    3
    4
    5
    6
    function factorial(n) {
    if (n === 1) return 1;
    return n * factorial(n - 1);
    }

    factorial(5); // 120
    + +

    上面代码是一个阶乘函数,计算n的阶乘,最多需要保存n个调用记录,复杂度 O(n) 。

    +

    如果改写成尾递归,只保留一个调用记录,复杂度 O(1) 。

    +
    1
    2
    3
    4
    5
    6
    function factorial(n, total) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
    }

    factorial(5, 1); // 120
    + +

    还有一个比较著名的例子,就是计算 Fibonacci 数列,也能充分说明尾递归优化的重要性。

    +

    非尾递归的 Fibonacci 数列实现如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Fibonacci(n) {
    if (n <= 1) {
    return 1;
    }

    return Fibonacci(n - 1) + Fibonacci(n - 2);
    }

    Fibonacci(10); // 89
    Fibonacci(100); // 堆栈溢出
    Fibonacci(500); // 堆栈溢出
    + +

    尾递归优化过的 Fibonacci 数列实现如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function Fibonacci2(n, ac1 = 1, ac2 = 1) {
    if (n <= 1) {
    return ac2;
    }

    return Fibonacci2(n - 1, ac2, ac1 + ac2);
    }

    Fibonacci2(100); // 573147844013817200000
    Fibonacci2(1000); // 7.0330367711422765e+208
    Fibonacci2(10000); // Infinity
    + +

    由此可见,“尾调用优化”对递归操作意义重大,所以一些函数式编程语言将其写入了语言规格。ES6 是如此,第一次明确规定,所有 ECMAScript 的实现,都必须部署“尾调用优化”。这就是说,ES6 中只要使用尾递归,就不会发生栈溢出,相对节省内存。

    +

    递归函数的改写

    尾递归的实现,往往需要改写递归函数,确保最后一步只调用自身。做到这一点的方法,就是把所有用到的内部变量改写成函数的参数。比如上面的例子,阶乘函数 factorial 需要用到一个中间变量total,那就把这个中间变量改写成函数的参数。这样做的缺点就是不太直观,第一眼很难看出来,为什么计算5的阶乘,需要传入两个参数51

    +

    两个方法可以解决这个问题。方法一是在尾递归函数之外,再提供一个正常形式的函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function tailFactorial(n, total) {
    if (n === 1) return total;
    return tailFactorial(n - 1, n * total);
    }

    function factorial(n) {
    return tailFactorial(n, 1);
    }

    factorial(5); // 120
    + +

    上面代码通过一个正常形式的阶乘函数factorial,调用尾递归函数tailFactorial,看起来就正常多了。

    +

    函数式编程有一个概念,叫做柯里化(currying),意思是将多参数的函数转换成单参数的形式。这里也可以使用柯里化。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function currying(fn, n) {
    return function (m) {
    return fn.call(this, m, n);
    };
    }

    function tailFactorial(n, total) {
    if (n === 1) return total;
    return tailFactorial(n - 1, n * total);
    }

    const factorial = currying(tailFactorial, 1);

    factorial(5); // 120
    + +

    上面代码通过柯里化,将尾递归函数tailFactorial变为只接受一个参数的factorial

    +

    第二种方法就简单多了,就是采用 ES6 的函数默认值。

    +
    1
    2
    3
    4
    5
    6
    function factorial(n, total = 1) {
    if (n === 1) return total;
    return factorial(n - 1, n * total);
    }

    factorial(5); // 120
    + +

    上面代码中,参数total有默认值1,所以调用时不用提供这个值。

    +

    总结一下,递归本质上是一种循环操作。纯粹的函数式编程语言没有循环操作命令,所有的循环都用递归实现,这就是为什么尾递归对这些语言极其重要。对于其他支持“尾调用优化”的语言(比如 Lua,ES6),只需要知道循环可以用递归代替,而一旦使用递归,就最好使用尾递归。

    +

    严格模式

    ES6 的尾调用优化只在严格模式下开启,正常模式是无效的。

    +

    这是因为在正常模式下,函数内部有两个变量,可以跟踪函数的调用栈。

    +
      +
    • func.arguments:返回调用时函数的参数。
    • +
    • func.caller:返回调用当前函数的那个函数。
    • +
    +

    尾调用优化发生时,函数的调用栈会改写,因此上面两个变量就会失真。严格模式禁用这两个变量,所以尾调用模式仅在严格模式下生效。

    +
    1
    2
    3
    4
    5
    6
    function restricted() {
    "use strict";
    restricted.caller; // 报错
    restricted.arguments; // 报错
    }
    restricted();
    + +

    尾递归优化的实现

    尾递归优化只在严格模式下生效,那么正常模式下,或者那些不支持该功能的环境中,有没有办法也使用尾递归优化呢?回答是可以的,就是自己实现尾递归优化。

    +

    它的原理非常简单。尾递归之所以需要优化,原因是调用栈太多,造成溢出,那么只要减少调用栈,就不会溢出。怎么做可以减少调用栈呢?就是采用“循环”换掉“递归”。

    +

    下面是一个正常的递归函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function sum(x, y) {
    if (y > 0) {
    return sum(x + 1, y - 1);
    } else {
    return x;
    }
    }

    sum(1, 100000);
    // Uncaught RangeError: Maximum call stack size exceeded(…)
    + +

    上面代码中,sum是一个递归函数,参数x是需要累加的值,参数y控制递归次数。一旦指定sum递归 100000 次,就会报错,提示超出调用栈的最大次数。

    +

    蹦床函数(trampoline)可以将递归执行转为循环执行。

    +
    1
    2
    3
    4
    5
    6
    function trampoline(f) {
    while (f && f instanceof Function) {
    f = f();
    }
    return f;
    }
    + +

    上面就是蹦床函数的一个实现,它接受一个函数f作为参数。只要f执行后返回一个函数,就继续执行。注意,这里是返回一个函数,然后执行该函数,而不是函数里面调用函数,这样就避免了递归执行,从而就消除了调用栈过大的问题。

    +

    然后,要做的就是将原来的递归函数,改写为每一步返回另一个函数。

    +
    1
    2
    3
    4
    5
    6
    7
    function sum(x, y) {
    if (y > 0) {
    return sum.bind(null, x + 1, y - 1);
    } else {
    return x;
    }
    }
    + +

    上面代码中,sum函数的每次执行,都会返回自身的另一个版本。

    +

    现在,使用蹦床函数执行sum,就不会发生调用栈溢出。

    +
    1
    2
    trampoline(sum(1, 100000));
    // 100001
    + +

    蹦床函数并不是真正的尾递归优化,下面的实现才是。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function tco(f) {
    var value;
    var active = false;
    var accumulated = [];

    return function accumulator() {
    accumulated.push(arguments);
    if (!active) {
    active = true;
    while (accumulated.length) {
    value = f.apply(this, accumulated.shift());
    }
    active = false;
    return value;
    }
    };
    }

    var sum = tco(function (x, y) {
    if (y > 0) {
    return sum(x + 1, y - 1);
    } else {
    return x;
    }
    });

    sum(1, 100000);
    // 100001
    + +

    上面代码中,tco函数是尾递归优化的实现,它的奥妙就在于状态变量active。默认情况下,这个变量是不激活的。一旦进入尾递归优化的过程,这个变量就激活了。然后,每一轮递归sum返回的都是undefined,所以就避免了递归执行;而accumulated数组存放每一轮sum执行的参数,总是有值的,这就保证了accumulator函数内部的while循环总是会执行。这样就很巧妙地将“递归”改成了“循环”,而后一轮的参数会取代前一轮的参数,保证了调用栈只有一层。

    +

    函数参数的尾逗号

    ES2017 允许函数的最后一个参数有尾逗号(trailing comma)。

    +

    此前,函数定义和调用时,都不允许最后一个参数后面出现逗号。

    +
    1
    2
    3
    4
    5
    function clownsEverywhere(param1, param2) {
    /* ... */
    }

    clownsEverywhere("foo", "bar");
    + +

    上面代码中,如果在param2bar后面加一个逗号,就会报错。

    +

    如果像上面这样,将参数写成多行(即每个参数占据一行),以后修改代码的时候,想为函数clownsEverywhere添加第三个参数,或者调整参数的次序,就势必要在原来最后一个参数后面添加一个逗号。这对于版本管理系统来说,就会显示添加逗号的那一行也发生了变动。这看上去有点冗余,因此新的语法允许定义和调用时,尾部直接有一个逗号。

    +
    1
    2
    3
    4
    5
    function clownsEverywhere(param1, param2) {
    /* ... */
    }

    clownsEverywhere("foo", "bar");
    + +

    这样的规定也使得,函数参数与数组和对象的尾逗号规则,保持一致了。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E5%87%BD%E6%95%B0%E7%9A%84%E6%89%A9%E5%B1%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\345\217\202\350\200\203\351\223\276\346\216\245/index.html" "b/posts/ES6-\345\217\202\350\200\203\351\223\276\346\216\245/index.html" new file mode 100644 index 00000000..60f45991 --- /dev/null +++ "b/posts/ES6-\345\217\202\350\200\203\351\223\276\346\216\245/index.html" @@ -0,0 +1,444 @@ +参考链接 | JCAlways + + + + + + + + + + + + + +

    参考链接

    参考链接

    官方文件

    +

    综合介绍

    +

    let 和 const

    +

    解构赋值

    +

    字符串

    +

    正则

    +

    数值

    +

    数组

    +

    函数

    +

    对象

    +

    Symbol

    +

    Set 和 Map

    +

    Proxy 和 Reflect

    +

    Promise 对象

    +

    Iterator

    +

    Generator

    +

    异步操作和 Async 函数

    +

    Class

    +

    Decorator

    +

    Module

    +

    二进制数组

    +

    SIMD

    +

    工具

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\345\217\230\351\207\217\347\232\204\350\247\243\346\236\204\350\265\213\345\200\274/index.html" "b/posts/ES6-\345\217\230\351\207\217\347\232\204\350\247\243\346\236\204\350\265\213\345\200\274/index.html" new file mode 100644 index 00000000..d6e79a24 --- /dev/null +++ "b/posts/ES6-\345\217\230\351\207\217\347\232\204\350\247\243\346\236\204\350\265\213\345\200\274/index.html" @@ -0,0 +1,408 @@ +变量的解构赋值 | JCAlways + + + + + + + + + + + + + +

    变量的解构赋值

    变量的解构赋值

    数组的解构赋值

    基本用法

    ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring)。

    +

    以前,为变量赋值,只能直接指定值。

    +
    1
    2
    3
    let a = 1;
    let b = 2;
    let c = 3;
    + +

    ES6 允许写成下面这样。

    +
    1
    let [a, b, c] = [1, 2, 3];
    + +

    上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值。

    +

    本质上,这种写法属于“模式匹配”,只要等号两边的模式相同,左边的变量就会被赋予对应的值。下面是一些使用嵌套数组进行解构的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    let [foo, [[bar], baz]] = [1, [[2], 3]];
    foo; // 1
    bar; // 2
    baz; // 3

    let [, , third] = ["foo", "bar", "baz"];
    third; // "baz"

    let [x, , y] = [1, 2, 3];
    x; // 1
    y; // 3

    let [head, ...tail] = [1, 2, 3, 4];
    head; // 1
    tail; // [2, 3, 4]

    let [x, y, ...z] = ["a"];
    x; // "a"
    y; // undefined
    z; // []
    + +

    如果解构不成功,变量的值就等于undefined

    +
    1
    2
    let [foo] = [];
    let [bar, foo] = [1];
    + +

    以上两种情况都属于解构不成功,foo的值都会等于undefined

    +

    另一种情况是不完全解构,即等号左边的模式,只匹配一部分的等号右边的数组。这种情况下,解构依然可以成功。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let [x, y] = [1, 2, 3];
    x; // 1
    y; // 2

    let [a, [b], d] = [1, [2, 3], 4];
    a; // 1
    b; // 2
    d; // 4
    + +

    上面两个例子,都属于不完全解构,但是可以成功。

    +

    如果等号的右边不是数组(或者严格地说,不是可遍历的结构,参见《Iterator》一章),那么将会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    // 报错
    let [foo] = 1;
    let [foo] = false;
    let [foo] = NaN;
    let [foo] = undefined;
    let [foo] = null;
    let [foo] = {};
    + +

    上面的语句都会报错,因为等号右边的值,要么转为对象以后不具备 Iterator 接口(前五个表达式),要么本身就不具备 Iterator 接口(最后一个表达式)。

    +

    对于 Set 结构,也可以使用数组的解构赋值。

    +
    1
    2
    let [x, y, z] = new Set(["a", "b", "c"]);
    x; // "a"
    + +

    事实上,只要某种数据结构具有 Iterator 接口,都可以采用数组形式的解构赋值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function* fibs() {
    let a = 0;
    let b = 1;
    while (true) {
    yield a;
    [a, b] = [b, a + b];
    }
    }

    let [first, second, third, fourth, fifth, sixth] = fibs();
    sixth; // 5
    + +

    上面代码中,fibs是一个 Generator 函数(参见《Generator 函数》一章),原生具有 Iterator 接口。解构赋值会依次从这个接口获取值。

    +

    默认值

    解构赋值允许指定默认值。

    +
    1
    2
    3
    4
    5
    let [foo = true] = [];
    foo; // true

    let [x, y = "b"] = ["a"]; // x='a', y='b'
    let [x, y = "b"] = ["a", undefined]; // x='a', y='b'
    + +

    注意,ES6 内部使用严格相等运算符(===),判断一个位置是否有值。所以,只有当一个数组成员严格等于undefined,默认值才会生效。

    +
    1
    2
    3
    4
    5
    let [x = 1] = [undefined];
    x; // 1

    let [x = 1] = [null];
    x; // null
    + +

    上面代码中,如果一个数组成员是null,默认值就不会生效,因为null不严格等于undefined

    +

    如果默认值是一个表达式,那么这个表达式是惰性求值的,即只有在用到的时候,才会求值。

    +
    1
    2
    3
    4
    5
    function f() {
    console.log("aaa");
    }

    let [x = f()] = [1];
    + +

    上面代码中,因为x能取到值,所以函数f根本不会执行。上面的代码其实等价于下面的代码。

    +
    1
    2
    3
    4
    5
    6
    let x;
    if ([1][0] === undefined) {
    x = f();
    } else {
    x = [1][0];
    }
    + +

    默认值可以引用解构赋值的其他变量,但该变量必须已经声明。

    +
    1
    2
    3
    4
    let [x = 1, y = x] = []; // x=1; y=1
    let [x = 1, y = x] = [2]; // x=2; y=2
    let [x = 1, y = x] = [1, 2]; // x=1; y=2
    let [x = y, y = 1] = []; // ReferenceError: y is not defined
    + +

    上面最后一个表达式之所以会报错,是因为xy做默认值时,y还没有声明。

    +

    对象的解构赋值

    解构不仅可以用于数组,还可以用于对象。

    +
    1
    2
    3
    let { foo, bar } = { foo: "aaa", bar: "bbb" };
    foo; // "aaa"
    bar; // "bbb"
    + +

    对象的解构与数组有一个重要的不同。数组的元素是按次序排列的,变量的取值由它的位置决定;而对象的属性没有次序,变量必须与属性同名,才能取到正确的值。

    +
    1
    2
    3
    4
    5
    6
    let { bar, foo } = { foo: "aaa", bar: "bbb" };
    foo; // "aaa"
    bar; // "bbb"

    let { baz } = { foo: "aaa", bar: "bbb" };
    baz; // undefined
    + +

    上面代码的第一个例子,等号左边的两个变量的次序,与等号右边两个同名属性的次序不一致,但是对取值完全没有影响。第二个例子的变量没有对应的同名属性,导致取不到值,最后等于undefined

    +

    如果变量名与属性名不一致,必须写成下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    let { foo: baz } = { foo: "aaa", bar: "bbb" };
    baz; // "aaa"

    let obj = { first: "hello", last: "world" };
    let { first: f, last: l } = obj;
    f; // 'hello'
    l; // 'world'
    + +

    这实际上说明,对象的解构赋值是下面形式的简写(参见《对象的扩展》一章)。

    +
    1
    let { foo: foo, bar: bar } = { foo: "aaa", bar: "bbb" };
    + +

    也就是说,对象的解构赋值的内部机制,是先找到同名属性,然后再赋给对应的变量。真正被赋值的是后者,而不是前者。

    +
    1
    2
    3
    let { foo: baz } = { foo: "aaa", bar: "bbb" };
    baz; // "aaa"
    foo; // error: foo is not defined
    + +

    上面代码中,foo是匹配的模式,baz才是变量。真正被赋值的是变量baz,而不是模式foo

    +

    与数组一样,解构也可以用于嵌套结构的对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let obj = {
    p: ["Hello", { y: "World" }],
    };

    let {
    p: [x, { y }],
    } = obj;
    x; // "Hello"
    y; // "World"
    + +

    注意,这时p是模式,不是变量,因此不会被赋值。如果p也要作为变量赋值,可以写成下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    let obj = {
    p: ["Hello", { y: "World" }],
    };

    let {
    p,
    p: [x, { y }],
    } = obj;
    x; // "Hello"
    y; // "World"
    p; // ["Hello", {y: "World"}]
    + +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const node = {
    loc: {
    start: {
    line: 1,
    column: 5,
    },
    },
    };

    let {
    loc,
    loc: { start },
    loc: {
    start: { line },
    },
    } = node;
    line; // 1
    loc; // Object {start: Object}
    start; // Object {line: 1, column: 5}
    + +

    上面代码有三次解构赋值,分别是对locstartline三个属性的解构赋值。注意,最后一次对line属性的解构赋值之中,只有line是变量,locstart都是模式,不是变量。

    +

    下面是嵌套赋值的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    let obj = {};
    let arr = [];

    ({ foo: obj.prop, bar: arr[0] } = { foo: 123, bar: true });

    obj; // {prop:123}
    arr; // [true]
    + +

    对象的解构也可以指定默认值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var { x = 3 } = {};
    x; // 3

    var { x, y = 5 } = { x: 1 };
    x; // 1
    y; // 5

    var { x: y = 3 } = {};
    y; // 3

    var { x: y = 3 } = { x: 5 };
    y; // 5

    var { message: msg = "Something went wrong" } = {};
    msg; // "Something went wrong"
    + +

    默认值生效的条件是,对象的属性值严格等于undefined

    +
    1
    2
    3
    4
    5
    var { x = 3 } = { x: undefined };
    x; // 3

    var { x = 3 } = { x: null };
    x; // null
    + +

    上面代码中,属性x等于null,因为nullundefined不严格相等,所以是个有效的赋值,导致默认值3不会生效。

    +

    如果解构失败,变量的值等于undefined

    +
    1
    2
    let { foo } = { bar: "baz" };
    foo; // undefined
    + +

    如果解构模式是嵌套的对象,而且子对象所在的父属性不存在,那么将会报错。

    +
    1
    2
    3
    4
    // 报错
    let {
    foo: { bar },
    } = { baz: "baz" };
    + +

    上面代码中,等号左边对象的foo属性,对应一个子对象。该子对象的bar属性,解构时会报错。原因很简单,因为foo这时等于undefined,再取子属性就会报错,请看下面的代码。

    +
    1
    2
    let _tmp = { baz: "baz" };
    _tmp.foo.bar; // 报错
    + +

    如果要将一个已经声明的变量用于解构赋值,必须非常小心。

    +
    1
    2
    3
    4
    // 错误的写法
    let x;
    {x} = {x: 1};
    // SyntaxError: syntax error
    + +

    上面代码的写法会报错,因为 JavaScript 引擎会将{x}理解成一个代码块,从而发生语法错误。只有不将大括号写在行首,避免 JavaScript 将其解释为代码块,才能解决这个问题。

    +
    1
    2
    3
    // 正确的写法
    let x;
    ({ x } = { x: 1 });
    + +

    上面代码将整个解构赋值语句,放在一个圆括号里面,就可以正确执行。关于圆括号与解构赋值的关系,参见下文。

    +

    解构赋值允许等号左边的模式之中,不放置任何变量名。因此,可以写出非常古怪的赋值表达式。

    +
    1
    2
    3
    ({} = [true, false]);
    ({} = "abc");
    ({} = []);
    + +

    上面的表达式虽然毫无意义,但是语法是合法的,可以执行。

    +

    对象的解构赋值,可以很方便地将现有对象的方法,赋值到某个变量。

    +
    1
    let { log, sin, cos } = Math;
    + +

    上面代码将Math对象的对数、正弦、余弦三个方法,赋值到对应的变量上,使用起来就会方便很多。

    +

    由于数组本质是特殊的对象,因此可以对数组进行对象属性的解构。

    +
    1
    2
    3
    4
    let arr = [1, 2, 3];
    let { 0: first, [arr.length - 1]: last } = arr;
    first; // 1
    last; // 3
    + +

    上面代码对数组进行对象解构。数组arr0键对应的值是1[arr.length - 1]就是2键,对应的值是3。方括号这种写法,属于“属性名表达式”(参见《对象的扩展》一章)。

    +

    字符串的解构赋值

    字符串也可以解构赋值。这是因为此时,字符串被转换成了一个类似数组的对象。

    +
    1
    2
    3
    4
    5
    6
    const [a, b, c, d, e] = "hello";
    a; // "h"
    b; // "e"
    c; // "l"
    d; // "l"
    e; // "o"
    + +

    类似数组的对象都有一个length属性,因此还可以对这个属性解构赋值。

    +
    1
    2
    let { length: len } = "hello";
    len; // 5
    + +

    数值和布尔值的解构赋值

    解构赋值时,如果等号右边是数值和布尔值,则会先转为对象。

    +
    1
    2
    3
    4
    5
    let { toString: s } = 123;
    s === Number.prototype.toString; // true

    let { toString: s } = true;
    s === Boolean.prototype.toString; // true
    + +

    上面代码中,数值和布尔值的包装对象都有toString属性,因此变量s都能取到值。

    +

    解构赋值的规则是,只要等号右边的值不是对象或数组,就先将其转为对象。由于undefinednull无法转为对象,所以对它们进行解构赋值,都会报错。

    +
    1
    2
    let { prop: x } = undefined; // TypeError
    let { prop: y } = null; // TypeError
    + +

    函数参数的解构赋值

    函数的参数也可以使用解构赋值。

    +
    1
    2
    3
    4
    5
    function add([x, y]) {
    return x + y;
    }

    add([1, 2]); // 3
    + +

    上面代码中,函数add的参数表面上是一个数组,但在传入参数的那一刻,数组参数就被解构成变量xy。对于函数内部的代码来说,它们能感受到的参数就是xy

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    [
    [1, 2],
    [3, 4],
    ].map(([a, b]) => a + b);
    // [ 3, 7 ]
    + +

    函数参数的解构也可以使用默认值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function move({ x = 0, y = 0 } = {}) {
    return [x, y];
    }

    move({ x: 3, y: 8 }); // [3, 8]
    move({ x: 3 }); // [3, 0]
    move({}); // [0, 0]
    move(); // [0, 0]
    + +

    上面代码中,函数move的参数是一个对象,通过对这个对象进行解构,得到变量xy的值。如果解构失败,xy等于默认值。

    +

    注意,下面的写法会得到不一样的结果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function move({ x, y } = { x: 0, y: 0 }) {
    return [x, y];
    }

    move({ x: 3, y: 8 }); // [3, 8]
    move({ x: 3 }); // [3, undefined]
    move({}); // [undefined, undefined]
    move(); // [0, 0]
    + +

    上面代码是为函数move的参数指定默认值,而不是为变量xy指定默认值,所以会得到与前一种写法不同的结果。

    +

    undefined就会触发函数参数的默认值。

    +
    1
    2
    [1, undefined, 3].map((x = "yes") => x);
    // [ 1, 'yes', 3 ]
    + +

    圆括号问题

    解构赋值虽然很方便,但是解析起来并不容易。对于编译器来说,一个式子到底是模式,还是表达式,没有办法从一开始就知道,必须解析到(或解析不到)等号才能知道。

    +

    由此带来的问题是,如果模式中出现圆括号怎么处理。ES6 的规则是,只要有可能导致解构的歧义,就不得使用圆括号。

    +

    但是,这条规则实际上不那么容易辨别,处理起来相当麻烦。因此,建议只要有可能,就不要在模式中放置圆括号。

    +

    不能使用圆括号的情况

    以下三种解构赋值不得使用圆括号。

    +

    (1)变量声明语句

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 全部报错
    let [(a)] = [1];

    let {x: (c)} = {};
    let ({x: c}) = {};
    let {(x: c)} = {};
    let {(x): c} = {};

    let { o: ({ p: p }) } = { o: { p: 2 } };
    + +

    上面 6 个语句都会报错,因为它们都是变量声明语句,模式不能使用圆括号。

    +

    (2)函数参数

    +

    函数参数也属于变量声明,因此不能带有圆括号。

    +
    1
    2
    3
    4
    // 报错
    function f([(z)]) { return z; }
    // 报错
    function f([z,(x)]) { return x; }
    + +

    (3)赋值语句的模式

    +
    1
    2
    3
    // 全部报错
    ({ p: a } = { p: 42 });
    [a] = [5];
    + +

    上面代码将整个模式放在圆括号之中,导致报错。

    +
    1
    2
    // 报错
    [{ p: a }, { x: c }] = [{}, {}];
    + +

    上面代码将一部分模式放在圆括号之中,导致报错。

    +

    可以使用圆括号的情况

    可以使用圆括号的情况只有一种:赋值语句的非模式部分,可以使用圆括号。

    +
    1
    2
    3
    [b] = [3]; // 正确
    ({ p: d } = {}); // 正确
    [parseInt.prop] = [3]; // 正确
    + +

    上面三行语句都可以正确执行,因为首先它们都是赋值语句,而不是声明语句;其次它们的圆括号都不属于模式的一部分。第一行语句中,模式是取数组的第一个成员,跟圆括号无关;第二行语句中,模式是p,而不是d;第三行语句与第一行语句的性质一致。

    +

    用途

    变量的解构赋值用途很多。

    +

    (1)交换变量的值

    +
    1
    2
    3
    4
    let x = 1;
    let y = 2;

    [x, y] = [y, x];
    + +

    上面代码交换变量xy的值,这样的写法不仅简洁,而且易读,语义非常清晰。

    +

    (2)从函数返回多个值

    +

    函数只能返回一个值,如果要返回多个值,只能将它们放在数组或对象里返回。有了解构赋值,取出这些值就非常方便。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 返回一个数组

    function example() {
    return [1, 2, 3];
    }
    let [a, b, c] = example();

    // 返回一个对象

    function example() {
    return {
    foo: 1,
    bar: 2,
    };
    }
    let { foo, bar } = example();
    + +

    (3)函数参数的定义

    +

    解构赋值可以方便地将一组参数与变量名对应起来。

    +
    1
    2
    3
    4
    5
    6
    7
    // 参数是一组有次序的值
    function f([x, y, z]) { ... }
    f([1, 2, 3]);

    // 参数是一组无次序的值
    function f({x, y, z}) { ... }
    f({z: 3, y: 2, x: 1});
    + +

    (4)提取 JSON 数据

    +

    解构赋值对提取 JSON 对象中的数据,尤其有用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let jsonData = {
    id: 42,
    status: "OK",
    data: [867, 5309],
    };

    let { id, status, data: number } = jsonData;

    console.log(id, status, number);
    // 42, "OK", [867, 5309]
    + +

    上面代码可以快速提取 JSON 数据的值。

    +

    (5)函数参数的默认值

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    jQuery.ajax = function (
    url,
    {
    async = true,
    beforeSend = function () {},
    cache = true,
    complete = function () {},
    crossDomain = false,
    global = true,
    // ... more config
    } = {}
    ) {
    // ... do stuff
    };
    + +

    指定参数的默认值,就避免了在函数体内部再写var foo = config.foo || 'default foo';这样的语句。

    +

    (6)遍历 Map 结构

    +

    任何部署了 Iterator 接口的对象,都可以用for...of循环遍历。Map 结构原生支持 Iterator 接口,配合变量的解构赋值,获取键名和键值就非常方便。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const map = new Map();
    map.set("first", "hello");
    map.set("second", "world");

    for (let [key, value] of map) {
    console.log(key + " is " + value);
    }
    // first is hello
    // second is world
    + +

    如果只想获取键名,或者只想获取键值,可以写成下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 获取键名
    for (let [key] of map) {
    // ...
    }

    // 获取键值
    for (let [, value] of map) {
    // ...
    }
    + +

    (7)输入模块的指定方法

    +

    加载模块时,往往需要指定输入哪些方法。解构赋值使得输入语句非常清晰。

    +
    1
    const { SourceMapConsumer, SourceNode } = require("source-map");
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\345\255\227\347\254\246\344\270\262\347\232\204\346\211\251\345\261\225/index.html" "b/posts/ES6-\345\255\227\347\254\246\344\270\262\347\232\204\346\211\251\345\261\225/index.html" new file mode 100644 index 00000000..e330c2cb --- /dev/null +++ "b/posts/ES6-\345\255\227\347\254\246\344\270\262\347\232\204\346\211\251\345\261\225/index.html" @@ -0,0 +1,476 @@ +字符串的扩展 | JCAlways + + + + + + + + + + + + + +

    字符串的扩展

    字符串的扩展

    ES6 加强了对 Unicode 的支持,并且扩展了字符串对象。

    +

    字符的 Unicode 表示法

    JavaScript 允许采用\uxxxx形式表示一个字符,其中xxxx表示字符的 Unicode 码点。

    +
    1
    2
    "\u0061";
    // "a"
    + +

    但是,这种表示法只限于码点在\u0000~`\uFFFF`之间的字符。超出这个范围的字符,必须用两个双字节的形式表示。

    +
    1
    2
    3
    4
    5
    "\uD842\uDFB7";
    // "𠮷"

    "\u20BB7";
    // " 7"
    + +

    上面代码表示,如果直接在\u后面跟上超过0xFFFF的数值(比如\u20BB7),JavaScript 会理解成\u20BB+7。由于\u20BB是一个不可打印字符,所以只会显示一个空格,后面跟着一个7

    +

    ES6 对这一点做出了改进,只要将码点放入大括号,就能正确解读该字符。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    "\u{20BB7}";
    // "𠮷"

    "\u{41}\u{42}\u{43}";
    // "ABC"

    let hello = 123;
    hello; // 123

    "\u{1F680}" === "\uD83D\uDE80";
    // true
    + +

    上面代码中,最后一个例子表明,大括号表示法与四字节的 UTF-16 编码是等价的。

    +

    有了这种表示法之后,JavaScript 共有 6 种方法可以表示一个字符。

    +
    1
    2
    3
    4
    5
    "z" === "z"; // true
    "\172" === "z"; // true
    "\x7A" === "z"; // true
    "\u007A" === "z"; // true
    "\u{7A}" === "z"; // true
    + +

    codePointAt()

    JavaScript 内部,字符以 UTF-16 的格式储存,每个字符固定为2个字节。对于那些需要4个字节储存的字符(Unicode 码点大于0xFFFF的字符),JavaScript 会认为它们是两个字符。

    +
    1
    2
    3
    4
    5
    6
    7
    var s = "𠮷";

    s.length; // 2
    s.charAt(0); // ''
    s.charAt(1); // ''
    s.charCodeAt(0); // 55362
    s.charCodeAt(1); // 57271
    + +

    上面代码中,汉字“𠮷”(注意,这个字不是“吉祥”的“吉”)的码点是0x20BB7,UTF-16 编码为0xD842 0xDFB7(十进制为55362 57271),需要4个字节储存。对于这种4个字节的字符,JavaScript 不能正确处理,字符串长度会误判为2,而且charAt方法无法读取整个字符,charCodeAt方法只能分别返回前两个字节和后两个字节的值。

    +

    ES6 提供了codePointAt方法,能够正确处理 4 个字节储存的字符,返回一个字符的码点。

    +
    1
    2
    3
    4
    5
    6
    let s = "𠮷a";

    s.codePointAt(0); // 134071
    s.codePointAt(1); // 57271

    s.codePointAt(2); // 97
    + +

    codePointAt方法的参数,是字符在字符串中的位置(从 0 开始)。上面代码中,JavaScript 将“𠮷 a”视为三个字符,codePointAt 方法在第一个字符上,正确地识别了“𠮷”,返回了它的十进制码点 134071(即十六进制的20BB7)。在第二个字符(即“𠮷”的后两个字节)和第三个字符“a”上,codePointAt方法的结果与charCodeAt方法相同。

    +

    总之,codePointAt方法会正确返回 32 位的 UTF-16 字符的码点。对于那些两个字节储存的常规字符,它的返回结果与charCodeAt方法相同。

    +

    codePointAt方法返回的是码点的十进制值,如果想要十六进制的值,可以使用toString方法转换一下。

    +
    1
    2
    3
    4
    let s = "𠮷a";

    s.codePointAt(0).toString(16); // "20bb7"
    s.codePointAt(2).toString(16); // "61"
    + +

    你可能注意到了,codePointAt方法的参数,仍然是不正确的。比如,上面代码中,字符a在字符串s的正确位置序号应该是 1,但是必须向codePointAt方法传入 2。解决这个问题的一个办法是使用for...of循环,因为它会正确识别 32 位的 UTF-16 字符。

    +
    1
    2
    3
    4
    5
    6
    let s = "𠮷a";
    for (let ch of s) {
    console.log(ch.codePointAt(0).toString(16));
    }
    // 20bb7
    // 61
    + +

    codePointAt方法是测试一个字符由两个字节还是由四个字节组成的最简单方法。

    +
    1
    2
    3
    4
    5
    6
    function is32Bit(c) {
    return c.codePointAt(0) > 0xffff;
    }

    is32Bit("𠮷"); // true
    is32Bit("a"); // false
    + +

    String.fromCodePoint()

    ES5 提供String.fromCharCode方法,用于从码点返回对应字符,但是这个方法不能识别 32 位的 UTF-16 字符(Unicode 编号大于0xFFFF)。

    +
    1
    2
    String.fromCharCode(0x20bb7);
    // "ஷ"
    + +

    上面代码中,String.fromCharCode不能识别大于0xFFFF的码点,所以0x20BB7就发生了溢出,最高位2被舍弃了,最后返回码点U+0BB7对应的字符,而不是码点U+20BB7对应的字符。

    +

    ES6 提供了String.fromCodePoint方法,可以识别大于0xFFFF的字符,弥补了String.fromCharCode方法的不足。在作用上,正好与codePointAt方法相反。

    +
    1
    2
    3
    4
    String.fromCodePoint(0x20bb7);
    // "𠮷"
    String.fromCodePoint(0x78, 0x1f680, 0x79) === "x\uD83D\uDE80y";
    // true
    + +

    上面代码中,如果String.fromCodePoint方法有多个参数,则它们会被合并成一个字符串返回。

    +

    注意,fromCodePoint方法定义在String对象上,而codePointAt方法定义在字符串的实例对象上。

    +

    字符串的遍历器接口

    ES6 为字符串添加了遍历器接口(详见《Iterator》一章),使得字符串可以被for...of循环遍历。

    +
    1
    2
    3
    4
    5
    6
    for (let codePoint of "foo") {
    console.log(codePoint);
    }
    // "f"
    // "o"
    // "o"
    + +

    除了遍历字符串,这个遍历器最大的优点是可以识别大于0xFFFF的码点,传统的for循环无法识别这样的码点。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let text = String.fromCodePoint(0x20bb7);

    for (let i = 0; i < text.length; i++) {
    console.log(text[i]);
    }
    // " "
    // " "

    for (let i of text) {
    console.log(i);
    }
    // "𠮷"
    + +

    上面代码中,字符串text只有一个字符,但是for循环会认为它包含两个字符(都不可打印),而for...of循环会正确识别出这一个字符。

    +

    normalize()

    许多欧洲语言有语调符号和重音符号。为了表示它们,Unicode 提供了两种方法。一种是直接提供带重音符号的字符,比如Ǒ(\u01D1)。另一种是提供合成符号(combining character),即原字符与重音符号的合成,两个字符合成一个字符,比如O(\u004F)和ˇ(\u030C)合成Ǒ(\u004F\u030C)。

    +

    这两种表示方法,在视觉和语义上都等价,但是 JavaScript 不能识别。

    +
    1
    2
    3
    4
    "\u01D1" === "\u004F\u030C"; //false

    "\u01D1".length; // 1
    "\u004F\u030C".length; // 2
    + +

    上面代码表示,JavaScript 将合成字符视为两个字符,导致两种表示方法不相等。

    +

    ES6 提供字符串实例的normalize()方法,用来将字符的不同表示方法统一为同样的形式,这称为 Unicode 正规化。

    +
    1
    2
    "\u01D1".normalize() === "\u004F\u030C".normalize();
    // true
    + +

    normalize方法可以接受一个参数来指定normalize的方式,参数的四个可选值如下。

    +
      +
    • NFC,默认参数,表示“标准等价合成”(Normalization Form Canonical Composition),返回多个简单字符的合成字符。所谓“标准等价”指的是视觉和语义上的等价。
    • +
    • NFD,表示“标准等价分解”(Normalization Form Canonical Decomposition),即在标准等价的前提下,返回合成字符分解的多个简单字符。
    • +
    • NFKC,表示“兼容等价合成”(Normalization Form Compatibility Composition),返回合成字符。所谓“兼容等价”指的是语义上存在等价,但视觉上不等价,比如“囍”和“喜喜”。(这只是用来举例,normalize方法不能识别中文。)
    • +
    • NFKD,表示“兼容等价分解”(Normalization Form Compatibility Decomposition),即在兼容等价的前提下,返回合成字符分解的多个简单字符。
    • +
    +
    1
    2
    "\u004F\u030C".normalize("NFC").length; // 1
    "\u004F\u030C".normalize("NFD").length; // 2
    + +

    上面代码表示,NFC参数返回字符的合成形式,NFD参数返回字符的分解形式。

    +

    不过,normalize方法目前不能识别三个或三个以上字符的合成。这种情况下,还是只能使用正则表达式,通过 Unicode 编号区间判断。

    +

    includes(), startsWith(), endsWith()

    传统上,JavaScript 只有indexOf方法,可以用来确定一个字符串是否包含在另一个字符串中。ES6 又提供了三种新方法。

    +
      +
    • **includes()**:返回布尔值,表示是否找到了参数字符串。
    • +
    • **startsWith()**:返回布尔值,表示参数字符串是否在原字符串的头部。
    • +
    • **endsWith()**:返回布尔值,表示参数字符串是否在原字符串的尾部。
    • +
    +
    1
    2
    3
    4
    5
    let s = "Hello world!";

    s.startsWith("Hello"); // true
    s.endsWith("!"); // true
    s.includes("o"); // true
    + +

    这三个方法都支持第二个参数,表示开始搜索的位置。

    +
    1
    2
    3
    4
    5
    let s = "Hello world!";

    s.startsWith("world", 6); // true
    s.endsWith("Hello", 5); // true
    s.includes("Hello", 6); // false
    + +

    上面代码表示,使用第二个参数n时,endsWith的行为与其他两个方法有所不同。它针对前n个字符,而其他两个方法针对从第n个位置直到字符串结束。

    +

    repeat()

    repeat方法返回一个新字符串,表示将原字符串重复n次。

    +
    1
    2
    3
    "x".repeat(3); // "xxx"
    "hello".repeat(2); // "hellohello"
    "na".repeat(0); // ""
    + +

    参数如果是小数,会被取整。

    +
    1
    "na".repeat(2.9); // "nana"
    + +

    如果repeat的参数是负数或者Infinity,会报错。

    +
    1
    2
    3
    4
    "na".repeat(Infinity);
    // RangeError
    "na".repeat(-1);
    // RangeError
    + +

    但是,如果参数是 0 到-1 之间的小数,则等同于 0,这是因为会先进行取整运算。0 到-1 之间的小数,取整以后等于-0repeat视同为 0。

    +
    1
    "na".repeat(-0.9); // ""
    + +

    参数NaN等同于 0。

    +
    1
    "na".repeat(NaN); // ""
    + +

    如果repeat的参数是字符串,则会先转换成数字。

    +
    1
    2
    "na".repeat("na"); // ""
    "na".repeat("3"); // "nanana"
    + +

    padStart(),padEnd()

    ES2017 引入了字符串补全长度的功能。如果某个字符串不够指定长度,会在头部或尾部补全。padStart()用于头部补全,padEnd()用于尾部补全。

    +
    1
    2
    3
    4
    5
    "x".padStart(5, "ab"); // 'ababx'
    "x".padStart(4, "ab"); // 'abax'

    "x".padEnd(5, "ab"); // 'xabab'
    "x".padEnd(4, "ab"); // 'xaba'
    + +

    上面代码中,padStartpadEnd一共接受两个参数,第一个参数用来指定字符串的最小长度,第二个参数是用来补全的字符串。

    +

    如果原字符串的长度,等于或大于指定的最小长度,则返回原字符串。

    +
    1
    2
    "xxx".padStart(2, "ab"); // 'xxx'
    "xxx".padEnd(2, "ab"); // 'xxx'
    + +

    如果用来补全的字符串与原字符串,两者的长度之和超过了指定的最小长度,则会截去超出位数的补全字符串。

    +
    1
    2
    "abc".padStart(10, "0123456789");
    // '0123456abc'
    + +

    如果省略第二个参数,默认使用空格补全长度。

    +
    1
    2
    "x".padStart(4); // '   x'
    "x".padEnd(4); // 'x '
    + +

    padStart的常见用途是为数值补全指定位数。下面代码生成 10 位的数值字符串。

    +
    1
    2
    3
    "1".padStart(10, "0"); // "0000000001"
    "12".padStart(10, "0"); // "0000000012"
    "123456".padStart(10, "0"); // "0000123456"
    + +

    另一个用途是提示字符串格式。

    +
    1
    2
    "12".padStart(10, "YYYY-MM-DD"); // "YYYY-MM-12"
    "09-12".padStart(10, "YYYY-MM-DD"); // "YYYY-09-12"
    + +

    matchAll()

    matchAll方法返回一个正则表达式在当前字符串的所有匹配,详见《正则的扩展》的一章。

    +

    模板字符串

    传统的 JavaScript 语言,输出模板通常是这样写的(下面使用了 jQuery 的方法)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $("#result").append(
    "There are <b>" +
    basket.count +
    "</b> " +
    "items in your basket, " +
    "<em>" +
    basket.onSale +
    "</em> are on sale!"
    );
    + +

    上面这种写法相当繁琐不方便,ES6 引入了模板字符串解决这个问题。

    +
    1
    2
    3
    4
    5
    $("#result").append(`
    There are <b>${basket.count}</b> items
    in your basket, <em>${basket.onSale}</em>
    are on sale!
    `);
    + +

    模板字符串(template string)是增强版的字符串,用反引号(`)标识。它可以当作普通字符串使用,也可以用来定义多行字符串,或者在字符串中嵌入变量。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 普通字符串
    `In JavaScript '\n' is a line-feed.` // 多行字符串
    `In JavaScript this is
    not legal.`;

    console.log(`string text line 1
    string text line 2`);

    // 字符串中嵌入变量
    let name = "Bob",
    time = "today";
    `Hello ${name}, how are you ${time}?`;
    + +

    上面代码中的模板字符串,都是用反引号表示。如果在模板字符串中需要使用反引号,则前面要用反斜杠转义。

    +
    1
    let greeting = `\`Yo\` World!`;
    + +

    如果使用模板字符串表示多行字符串,所有的空格和缩进都会被保留在输出之中。

    +
    1
    2
    3
    4
    5
    6
    $("#list").html(`
    <ul>
    <li>first</li>
    <li>second</li>
    </ul>
    `);
    + +

    上面代码中,所有模板字符串的空格和换行,都是被保留的,比如<ul>标签前面会有一个换行。如果你不想要这个换行,可以使用trim方法消除它。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    $("#list").html(
    `
    <ul>
    <li>first</li>
    <li>second</li>
    </ul>
    `.trim()
    );
    + +

    模板字符串中嵌入变量,需要将变量名写在${}之中。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    function authorize(user, action) {
    if (!user.hasPrivilege(action)) {
    throw new Error(
    // 传统写法为
    // 'User '
    // + user.name
    // + ' is not authorized to do '
    // + action
    // + '.'
    `User ${user.name} is not authorized to do ${action}.`
    );
    }
    }
    + +

    大括号内部可以放入任意的 JavaScript 表达式,可以进行运算,以及引用对象属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let x = 1;
    let y = 2;

    `${x} + ${y} = ${
    x + y
    }` // "1 + 2 = 3"
    `${x} + ${y * 2} = ${x + y * 2}`;
    // "1 + 4 = 5"

    let obj = { x: 1, y: 2 };
    `${obj.x + obj.y}`;
    // "3"
    + +

    模板字符串之中还能调用函数。

    +
    1
    2
    3
    4
    5
    6
    function fn() {
    return "Hello World";
    }

    `foo ${fn()} bar`;
    // foo Hello World bar
    + +

    如果大括号中的值不是字符串,将按照一般的规则转为字符串。比如,大括号中是一个对象,将默认调用对象的toString方法。

    +

    如果模板字符串中的变量没有声明,将报错。

    +
    1
    2
    3
    // 变量place没有声明
    let msg = `Hello, ${place}`;
    // 报错
    + +

    由于模板字符串的大括号内部,就是执行 JavaScript 代码,因此如果大括号内部是一个字符串,将会原样输出。

    +
    1
    2
    `Hello ${"World"}`;
    // "Hello World"
    + +

    模板字符串甚至还能嵌套。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const tmpl = (addrs) => `
    <table>
    ${addrs
    .map(
    (addr) => `
    <tr><td>${addr.first}</td></tr>
    <tr><td>${addr.last}</td></tr>
    `
    )
    .join("")}
    </table>
    `;
    + +

    上面代码中,模板字符串的变量之中,又嵌入了另一个模板字符串,使用方法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const data = [
    { first: "<Jane>", last: "Bond" },
    { first: "Lars", last: "<Croft>" },
    ];

    console.log(tmpl(data));
    // <table>
    //
    // <tr><td><Jane></td></tr>
    // <tr><td>Bond</td></tr>
    //
    // <tr><td>Lars</td></tr>
    // <tr><td><Croft></td></tr>
    //
    // </table>
    + +

    如果需要引用模板字符串本身,在需要时执行,可以像下面这样写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 写法一
    let str = "return " + "`Hello ${name}!`";
    let func = new Function("name", str);
    func("Jack"); // "Hello Jack!"

    // 写法二
    let str = "(name) => `Hello ${name}!`";
    let func = eval.call(null, str);
    func("Jack"); // "Hello Jack!"
    + +

    实例:模板编译

    下面,我们来看一个通过模板字符串,生成正式模板的实例。

    +
    1
    2
    3
    4
    5
    6
    7
    let template = `
    <ul>
    <% for(let i=0; i < data.supplies.length; i++) { %>
    <li><%= data.supplies[i] %></li>
    <% } %>
    </ul>
    `;
    + +

    上面代码在模板字符串之中,放置了一个常规模板。该模板使用<%...%>放置 JavaScript 代码,使用<%= ... %>输出 JavaScript 表达式。

    +

    怎么编译这个模板字符串呢?

    +

    一种思路是将其转换为 JavaScript 表达式字符串。

    +
    1
    2
    3
    4
    5
    6
    7
    echo("<ul>");
    for (let i = 0; i < data.supplies.length; i++) {
    echo("<li>");
    echo(data.supplies[i]);
    echo("</li>");
    }
    echo("</ul>");
    + +

    这个转换使用正则表达式就行了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let evalExpr = /<%=(.+?)%>/g;
    let expr = /<%([\s\S]+?)%>/g;

    template = template
    .replace(evalExpr, "`); \n echo( $1 ); \n echo(`")
    .replace(expr, "`); \n $1 \n echo(`");

    template = "echo(`" + template + "`);";
    + +

    然后,将template封装在一个函数里面返回,就可以了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let script = `(function parse(data){
    let output = "";

    function echo(html){
    output += html;
    }

    ${template}

    return output;
    })`;

    return script;
    + +

    将上面的内容拼装成一个模板编译函数compile

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    function compile(template) {
    const evalExpr = /<%=(.+?)%>/g;
    const expr = /<%([\s\S]+?)%>/g;

    template = template
    .replace(evalExpr, "`); \n echo( $1 ); \n echo(`")
    .replace(expr, "`); \n $1 \n echo(`");

    template = "echo(`" + template + "`);";

    let script = `(function parse(data){
    let output = "";

    function echo(html){
    output += html;
    }

    ${template}

    return output;
    })`;

    return script;
    }
    + +

    compile函数的用法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    let parse = eval(compile(template));
    div.innerHTML = parse({ supplies: ["broom", "mop", "cleaner"] });
    // <ul>
    // <li>broom</li>
    // <li>mop</li>
    // <li>cleaner</li>
    // </ul>
    + +

    标签模板

    模板字符串的功能,不仅仅是上面这些。它可以紧跟在一个函数名后面,该函数将被调用来处理这个模板字符串。这被称为“标签模板”功能(tagged template)。

    +
    1
    2
    3
    alert`123`;
    // 等同于
    alert(123);
    + +

    标签模板其实不是模板,而是函数调用的一种特殊形式。“标签”指的就是函数,紧跟在后面的模板字符串就是它的参数。

    +

    但是,如果模板字符里面有变量,就不是简单的调用了,而是会将模板字符串先处理成多个参数,再调用函数。

    +
    1
    2
    3
    4
    5
    6
    let a = 5;
    let b = 10;

    tag`Hello ${a + b} world ${a * b}`;
    // 等同于
    tag(["Hello ", " world ", ""], 15, 50);
    + +

    上面代码中,模板字符串前面有一个标识名tag,它是一个函数。整个表达式的返回值,就是tag函数处理模板字符串后的返回值。

    +

    函数tag依次会接收到多个参数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function tag(stringArr, value1, value2) {
    // ...
    }

    // 等同于

    function tag(stringArr, ...values) {
    // ...
    }
    + +

    tag函数的第一个参数是一个数组,该数组的成员是模板字符串中那些没有变量替换的部分,也就是说,变量替换只发生在数组的第一个成员与第二个成员之间、第二个成员与第三个成员之间,以此类推。

    +

    tag函数的其他参数,都是模板字符串各个变量被替换后的值。由于本例中,模板字符串含有两个变量,因此tag会接受到value1value2两个参数。

    +

    tag函数所有参数的实际值如下。

    +
      +
    • 第一个参数:['Hello ', ' world ', '']
    • +
    • 第二个参数: 15
    • +
    • 第三个参数:50
    • +
    +

    也就是说,tag函数实际上以下面的形式调用。

    +
    1
    tag(["Hello ", " world ", ""], 15, 50);
    + +

    我们可以按照需要编写tag函数的代码。下面是tag函数的一种写法,以及运行结果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    let a = 5;
    let b = 10;

    function tag(s, v1, v2) {
    console.log(s[0]);
    console.log(s[1]);
    console.log(s[2]);
    console.log(v1);
    console.log(v2);

    return "OK";
    }

    tag`Hello ${a + b} world ${a * b}`;
    // "Hello "
    // " world "
    // ""
    // 15
    // 50
    // "OK"
    + +

    下面是一个更复杂的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    let total = 30;
    let msg = passthru`The total is ${total} (${total * 1.05} with tax)`;

    function passthru(literals) {
    let result = "";
    let i = 0;

    while (i < literals.length) {
    result += literals[i++];
    if (i < arguments.length) {
    result += arguments[i];
    }
    }

    return result;
    }

    msg; // "The total is 30 (31.5 with tax)"
    + +

    上面这个例子展示了,如何将各个参数按照原来的位置拼合回去。

    +

    passthru函数采用 rest 参数的写法如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function passthru(literals, ...values) {
    let output = "";
    let index;
    for (index = 0; index < values.length; index++) {
    output += literals[index] + values[index];
    }

    output += literals[index];
    return output;
    }
    + +

    “标签模板”的一个重要应用,就是过滤 HTML 字符串,防止用户输入恶意内容。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

    function SaferHTML(templateData) {
    let s = templateData[0];
    for (let i = 1; i < arguments.length; i++) {
    let arg = String(arguments[i]);

    // Escape special characters in the substitution.
    s += arg.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");

    // Don't escape special characters in the template.
    s += templateData[i];
    }
    return s;
    }
    + +

    上面代码中,sender变量往往是用户提供的,经过SaferHTML函数处理,里面的特殊字符都会被转义。

    +
    1
    2
    3
    4
    5
    let sender = '<script>alert("abc")</script>'; // 恶意代码
    let message = SaferHTML`<p>${sender} has sent you a message.</p>`;

    message;
    // <p>&lt;script&gt;alert("abc")&lt;/script&gt; has sent you a message.</p>
    + +

    标签模板的另一个应用,就是多语言转换(国际化处理)。

    +
    1
    2
    i18n`Welcome to ${siteName}, you are visitor number ${visitorNumber}!`;
    // "欢迎访问xxx,您是第xxxx位访问者!"
    + +

    模板字符串本身并不能取代 Mustache 之类的模板库,因为没有条件判断和循环处理功能,但是通过标签函数,你可以自己添加这些功能。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // 下面的hashTemplate函数
    // 是一个自定义的模板处理函数
    let libraryHtml = hashTemplate`
    <ul>
    #for book in ${myBooks}
    <li><i>#{book.title}</i> by #{book.author}</li>
    #end
    </ul>
    `;
    + +

    除此之外,你甚至可以使用标签模板,在 JavaScript 语言之中嵌入其他语言。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    jsx`
    <div>
    <input
    ref='input'
    onChange='${this.handleChange}'
    defaultValue='${this.state.value}' />
    ${this.state.value}
    </div>
    `;
    + +

    上面的代码通过jsx函数,将一个 DOM 字符串转为 React 对象。你可以在 Github 找到jsx函数的具体实现

    +

    下面则是一个假想的例子,通过java函数,在 JavaScript 代码之中运行 Java 代码。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    java`
    class HelloWorldApp {
    public static void main(String[] args) {
    System.out.println(“Hello World!”); // Display the string.
    }
    }
    `;
    HelloWorldApp.main();
    + +

    模板处理函数的第一个参数(模板字符串数组),还有一个raw属性。

    +
    1
    2
    console.log`123`;
    // ["123", raw: Array[1]]
    + +

    上面代码中,console.log接受的参数,实际上是一个数组。该数组有一个raw属性,保存的是转义后的原字符串。

    +

    请看下面的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    tag`First line\nSecond line`;

    function tag(strings) {
    console.log(strings.raw[0]);
    // strings.raw[0] 为 "First line\\nSecond line"
    // 打印输出 "First line\nSecond line"
    }
    + +

    上面代码中,tag函数的第一个参数strings,有一个raw属性,也指向一个数组。该数组的成员与strings数组完全一致。比如,strings数组是["First line\nSecond line"],那么strings.raw数组就是["First line\\nSecond line"]。两者唯一的区别,就是字符串里面的斜杠都被转义了。比如,strings.raw 数组会将\n视为\\n两个字符,而不是换行符。这是为了方便取得转义之前的原始模板而设计的。

    +

    String.raw()

    ES6 还为原生的 String 对象,提供了一个raw方法。

    +

    String.raw方法,往往用来充当模板字符串的处理函数,返回一个斜杠都被转义(即斜杠前面再加一个斜杠)的字符串,对应于替换变量后的模板字符串。

    +
    1
    2
    3
    4
    5
    String.raw`Hi\n${2 + 3}!`;
    // 返回 "Hi\\n5!"

    String.raw`Hi\u000A!`;
    // 返回 "Hi\\u000A!"
    + +

    如果原字符串的斜杠已经转义,那么String.raw会进行再次转义。

    +
    1
    2
    String.raw`Hi\\n`;
    // 返回 "Hi\\\\n"
    + +

    String.raw方法可以作为处理模板字符串的基本方法,它会将所有变量替换,而且对斜杠进行转义,方便下一步作为字符串来使用。

    +

    String.raw方法也可以作为正常的函数使用。这时,它的第一个参数,应该是一个具有raw属性的对象,且raw属性的值应该是一个数组。

    +
    1
    2
    3
    4
    5
    String.raw({ raw: "test" }, 0, 1, 2);
    // 't0e1s2t'

    // 等同于
    String.raw({ raw: ["t", "e", "s", "t"] }, 0, 1, 2);
    + +

    作为函数,String.raw的代码实现基本如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    String.raw = function (strings, ...values) {
    let output = "";
    let index;
    for (index = 0; index < values.length; index++) {
    output += strings.raw[index] + values[index];
    }

    output += strings.raw[index];
    return output;
    };
    + +

    模板字符串的限制

    前面提到标签模板里面,可以内嵌其他语言。但是,模板字符串默认会将字符串转义,导致无法嵌入其他语言。

    +

    举例来说,标签模板里面可以嵌入 LaTEX 语言。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function latex(strings) {
    // ...
    }

    let document = latex`
    \newcommand{\fun}{\textbf{Fun!}} // 正常工作
    \newcommand{\unicode}{\textbf{Unicode!}} // 报错
    \newcommand{\xerxes}{\textbf{King!}} // 报错

    Breve over the h goes \u{h}ere // 报错
    `;
    + +

    上面代码中,变量document内嵌的模板字符串,对于 LaTEX 语言来说完全是合法的,但是 JavaScript 引擎会报错。原因就在于字符串的转义。

    +

    模板字符串会将\u00FF\u{42}当作 Unicode 字符进行转义,所以\unicode解析时报错;而\x56会被当作十六进制字符串转义,所以\xerxes会报错。也就是说,\u\x在 LaTEX 里面有特殊含义,但是 JavaScript 将它们转义了。

    +

    为了解决这个问题,ES2018 放松了对标签模板里面的字符串转义的限制。如果遇到不合法的字符串转义,就返回undefined,而不是报错,并且从raw属性上面可以得到原始字符串。

    +
    1
    2
    3
    4
    5
    function tag(strs) {
    strs[0] === undefined;
    strs.raw[0] === "\\unicode and \\u{55}";
    }
    tag`\unicode and \u{55}`;
    + +

    上面代码中,模板字符串原本是应该报错的,但是由于放松了对字符串转义的限制,所以不报错了,JavaScript 引擎将第一个字符设置为undefined,但是raw属性依然可以得到原始字符串,因此tag函数还是可以对原字符串进行处理。

    +

    注意,这种对字符串转义的放松,只在标签模板解析字符串时生效,不是标签模板的场合,依然会报错。

    +
    1
    let bad = `bad escape sequence: \unicode`; // 报错
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%89%A9%E5%B1%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\345\257\271\350\261\241\347\232\204\346\211\251\345\261\225/index.html" "b/posts/ES6-\345\257\271\350\261\241\347\232\204\346\211\251\345\261\225/index.html" new file mode 100644 index 00000000..abc32263 --- /dev/null +++ "b/posts/ES6-\345\257\271\350\261\241\347\232\204\346\211\251\345\261\225/index.html" @@ -0,0 +1,609 @@ +对象的扩展 | JCAlways + + + + + + + + + + + + + +

    对象的扩展

    对象的扩展

    属性的简洁表示法

    ES6 允许直接写入变量和函数,作为对象的属性和方法。这样的书写更加简洁。

    +
    1
    2
    3
    4
    5
    6
    const foo = "bar";
    const baz = { foo };
    baz; // {foo: "bar"}

    // 等同于
    const baz = { foo: foo };
    + +

    上面代码表明,ES6 允许在对象之中,直接写变量。这时,属性名为变量名, 属性值为变量的值。下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function f(x, y) {
    return { x, y };
    }

    // 等同于

    function f(x, y) {
    return { x: x, y: y };
    }

    f(1, 2); // Object {x: 1, y: 2}
    + +

    除了属性简写,方法也可以简写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const o = {
    method() {
    return "Hello!";
    },
    };

    // 等同于

    const o = {
    method: function () {
    return "Hello!";
    },
    };
    + +

    下面是一个实际的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let birth = "2000/01/01";

    const Person = {
    name: "张三",

    //等同于birth: birth
    birth,

    // 等同于hello: function ()...
    hello() {
    console.log("我的名字是", this.name);
    },
    };
    + +

    这种写法用于函数的返回值,将会非常方便。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function getPoint() {
    const x = 1;
    const y = 10;
    return { x, y };
    }

    getPoint();
    // {x:1, y:10}
    + +

    CommonJS 模块输出一组变量,就非常合适使用简洁写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    let ms = {};

    function getItem(key) {
    return key in ms ? ms[key] : null;
    }

    function setItem(key, value) {
    ms[key] = value;
    }

    function clear() {
    ms = {};
    }

    module.exports = { getItem, setItem, clear };
    // 等同于
    module.exports = {
    getItem: getItem,
    setItem: setItem,
    clear: clear,
    };
    + +

    属性的赋值器(setter)和取值器(getter),事实上也是采用这种写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const cart = {
    _wheels: 4,

    get wheels() {
    return this._wheels;
    },

    set wheels(value) {
    if (value < this._wheels) {
    throw new Error("数值太小了!");
    }
    this._wheels = value;
    },
    };
    + +

    注意,简洁写法的属性名总是字符串,这会导致一些看上去比较奇怪的结果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const obj = {
    class() {},
    };

    // 等同于

    var obj = {
    class: function () {},
    };
    + +

    上面代码中,class是字符串,所以不会因为它属于关键字,而导致语法解析报错。

    +

    如果某个方法的值是一个 Generator 函数,前面需要加上星号。

    +
    1
    2
    3
    4
    5
    const obj = {
    *m() {
    yield "hello world";
    },
    };
    + +

    属性名表达式

    JavaScript 定义对象的属性,有两种方法。

    +
    1
    2
    3
    4
    5
    // 方法一
    obj.foo = true;

    // 方法二
    obj["a" + "bc"] = 123;
    + +

    上面代码的方法一是直接用标识符作为属性名,方法二是用表达式作为属性名,这时要将表达式放在方括号之内。

    +

    但是,如果使用字面量方式定义对象(使用大括号),在 ES5 中只能使用方法一(标识符)定义属性。

    +
    1
    2
    3
    4
    var obj = {
    foo: true,
    abc: 123,
    };
    + +

    ES6 允许字面量定义对象时,用方法二(表达式)作为对象的属性名,即把表达式放在方括号内。

    +
    1
    2
    3
    4
    5
    6
    let propKey = "foo";

    let obj = {
    [propKey]: true,
    ["a" + "bc"]: 123,
    };
    + +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let lastWord = "last word";

    const a = {
    "first word": "hello",
    [lastWord]: "world",
    };

    a["first word"]; // "hello"
    a[lastWord]; // "world"
    a["last word"]; // "world"
    + +

    表达式还可以用于定义方法名。

    +
    1
    2
    3
    4
    5
    6
    7
    let obj = {
    ["h" + "ello"]() {
    return "hi";
    },
    };

    obj.hello(); // hi
    + +

    注意,属性名表达式与简洁表示法,不能同时使用,会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 报错
    const foo = 'bar';
    const bar = 'abc';
    const baz = { [foo] };

    // 正确
    const foo = 'bar';
    const baz = { [foo]: 'abc'};
    + +

    注意,属性名表达式如果是一个对象,默认情况下会自动将对象转为字符串[object Object],这一点要特别小心。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const keyA = { a: 1 };
    const keyB = { b: 2 };

    const myObject = {
    [keyA]: "valueA",
    [keyB]: "valueB",
    };

    myObject; // Object {[object Object]: "valueB"}
    + +

    上面代码中,[keyA][keyB]得到的都是[object Object],所以[keyB]会把[keyA]覆盖掉,而myObject最后只有一个[object Object]属性。

    +

    方法的 name 属性

    函数的name属性,返回函数名。对象方法也是函数,因此也有name属性。

    +
    1
    2
    3
    4
    5
    6
    7
    const person = {
    sayName() {
    console.log("hello!");
    },
    };

    person.sayName.name; // "sayName"
    + +

    上面代码中,方法的name属性返回函数名(即方法名)。

    +

    如果对象的方法使用了取值函数(getter)和存值函数(setter),则name属性不是在该方法上面,而是该方法的属性的描述对象的getset属性上面,返回值是方法名前加上getset

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const obj = {
    get foo() {},
    set foo(x) {},
    };

    obj.foo.name;
    // TypeError: Cannot read property 'name' of undefined

    const descriptor = Object.getOwnPropertyDescriptor(obj, "foo");

    descriptor.get.name; // "get foo"
    descriptor.set.name; // "set foo"
    + +

    有两种特殊情况:bind方法创造的函数,name属性返回bound加上原函数的名字;Function构造函数创造的函数,name属性返回anonymous

    +
    1
    2
    3
    4
    5
    6
    new Function().name; // "anonymous"

    var doSomething = function () {
    // ...
    };
    doSomething.bind().name; // "bound doSomething"
    + +

    如果对象的方法是一个 Symbol 值,那么name属性返回的是这个 Symbol 值的描述。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const key1 = Symbol("description");
    const key2 = Symbol();
    let obj = {
    [key1]() {},
    [key2]() {},
    };
    obj[key1].name; // "[description]"
    obj[key2].name; // ""
    + +

    上面代码中,key1对应的 Symbol 值有描述,key2没有。

    +

    Object.is()

    ES5 比较两个值是否相等,只有两个运算符:相等运算符(==)和严格相等运算符(===)。它们都有缺点,前者会自动转换数据类型,后者的NaN不等于自身,以及+0等于-0。JavaScript 缺乏一种运算,在所有环境中,只要两个值是一样的,它们就应该相等。

    +

    ES6 提出“Same-value equality”(同值相等)算法,用来解决这个问题。Object.is就是部署这个算法的新方法。它用来比较两个值是否严格相等,与严格比较运算符(===)的行为基本一致。

    +
    1
    2
    3
    4
    Object.is("foo", "foo");
    // true
    Object.is({}, {});
    // false
    + +

    不同之处只有两个:一是+0不等于-0,二是NaN等于自身。

    +
    1
    2
    3
    4
    5
    +0 === -0; //true
    NaN === NaN; // false

    Object.is(+0, -0); // false
    Object.is(NaN, NaN); // true
    + +

    ES5 可以通过下面的代码,部署Object.is

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Object.defineProperty(Object, "is", {
    value: function (x, y) {
    if (x === y) {
    // 针对+0 不等于 -0的情况
    return x !== 0 || 1 / x === 1 / y;
    }
    // 针对NaN的情况
    return x !== x && y !== y;
    },
    configurable: true,
    enumerable: false,
    writable: true,
    });
    + +

    Object.assign()

    基本用法

    Object.assign方法用于对象的合并,将源对象(source)的所有可枚举属性,复制到目标对象(target)。

    +
    1
    2
    3
    4
    5
    6
    7
    const target = { a: 1 };

    const source1 = { b: 2 };
    const source2 = { c: 3 };

    Object.assign(target, source1, source2);
    target; // {a:1, b:2, c:3}
    + +

    Object.assign方法的第一个参数是目标对象,后面的参数都是源对象。

    +

    注意,如果目标对象与源对象有同名属性,或多个源对象有同名属性,则后面的属性会覆盖前面的属性。

    +
    1
    2
    3
    4
    5
    6
    7
    const target = { a: 1, b: 1 };

    const source1 = { b: 2, c: 2 };
    const source2 = { c: 3 };

    Object.assign(target, source1, source2);
    target; // {a:1, b:2, c:3}
    + +

    如果只有一个参数,Object.assign会直接返回该参数。

    +
    1
    2
    const obj = { a: 1 };
    Object.assign(obj) === obj; // true
    + +

    如果该参数不是对象,则会先转成对象,然后返回。

    +
    1
    typeof Object.assign(2); // "object"
    + +

    由于undefinednull无法转成对象,所以如果它们作为参数,就会报错。

    +
    1
    2
    Object.assign(undefined); // 报错
    Object.assign(null); // 报错
    + +

    如果非对象参数出现在源对象的位置(即非首参数),那么处理规则有所不同。首先,这些参数都会转成对象,如果无法转成对象,就会跳过。这意味着,如果undefinednull不在首参数,就不会报错。

    +
    1
    2
    3
    let obj = { a: 1 };
    Object.assign(obj, undefined) === obj; // true
    Object.assign(obj, null) === obj; // true
    + +

    其他类型的值(即数值、字符串和布尔值)不在首参数,也不会报错。但是,除了字符串会以数组形式,拷贝入目标对象,其他值都不会产生效果。

    +
    1
    2
    3
    4
    5
    6
    const v1 = "abc";
    const v2 = true;
    const v3 = 10;

    const obj = Object.assign({}, v1, v2, v3);
    console.log(obj); // { "0": "a", "1": "b", "2": "c" }
    + +

    上面代码中,v1v2v3分别是字符串、布尔值和数值,结果只有字符串合入目标对象(以字符数组的形式),数值和布尔值都会被忽略。这是因为只有字符串的包装对象,会产生可枚举属性。

    +
    1
    2
    3
    Object(true); // {[[PrimitiveValue]]: true}
    Object(10); // {[[PrimitiveValue]]: 10}
    Object("abc"); // {0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc"}
    + +

    上面代码中,布尔值、数值、字符串分别转成对应的包装对象,可以看到它们的原始值都在包装对象的内部属性[[PrimitiveValue]]上面,这个属性是不会被Object.assign拷贝的。只有字符串的包装对象,会产生可枚举的实义属性,那些属性则会被拷贝。

    +

    Object.assign拷贝的属性是有限制的,只拷贝源对象的自身属性(不拷贝继承属性),也不拷贝不可枚举的属性(enumerable: false)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Object.assign(
    { b: "c" },
    Object.defineProperty({}, "invisible", {
    enumerable: false,
    value: "hello",
    })
    );
    // { b: 'c' }
    + +

    上面代码中,Object.assign要拷贝的对象只有一个不可枚举属性invisible,这个属性并没有被拷贝进去。

    +

    属性名为 Symbol 值的属性,也会被Object.assign拷贝。

    +
    1
    2
    Object.assign({ a: "b" }, { [Symbol("c")]: "d" });
    // { a: 'b', Symbol(c): 'd' }
    + +

    注意点

    (1)浅拷贝

    +

    Object.assign方法实行的是浅拷贝,而不是深拷贝。也就是说,如果源对象某个属性的值是对象,那么目标对象拷贝得到的是这个对象的引用。

    +
    1
    2
    3
    4
    5
    const obj1 = { a: { b: 1 } };
    const obj2 = Object.assign({}, obj1);

    obj1.a.b = 2;
    obj2.a.b; // 2
    + +

    上面代码中,源对象obj1a属性的值是一个对象,Object.assign拷贝得到的是这个对象的引用。这个对象的任何变化,都会反映到目标对象上面。

    +

    (2)同名属性的替换

    +

    对于这种嵌套的对象,一旦遇到同名属性,Object.assign的处理方法是替换,而不是添加。

    +
    1
    2
    3
    4
    const target = { a: { b: "c", d: "e" } };
    const source = { a: { b: "hello" } };
    Object.assign(target, source);
    // { a: { b: 'hello' } }
    + +

    上面代码中,target对象的a属性被source对象的a属性整个替换掉了,而不会得到{ a: { b: 'hello', d: 'e' } }的结果。这通常不是开发者想要的,需要特别小心。

    +

    一些函数库提供Object.assign的定制版本(比如 Lodash 的_.defaultsDeep方法),可以得到深拷贝的合并。

    +

    (3)数组的处理

    +

    Object.assign可以用来处理数组,但是会把数组视为对象。

    +
    1
    2
    Object.assign([1, 2, 3], [4, 5]);
    // [4, 5, 3]
    + +

    上面代码中,Object.assign把数组视为属性名为 0、1、2 的对象,因此源数组的 0 号属性4覆盖了目标数组的 0 号属性1

    +

    (4)取值函数的处理

    +

    Object.assign只能进行值的复制,如果要复制的值是一个取值函数,那么将求值后再复制。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const source = {
    get foo() {
    return 1;
    },
    };
    const target = {};

    Object.assign(target, source);
    // { foo: 1 }
    + +

    上面代码中,source对象的foo属性是一个取值函数,Object.assign不会复制这个取值函数,只会拿到值以后,将这个值复制过去。

    +

    常见用途

    Object.assign方法有很多用处。

    +

    (1)为对象添加属性

    +
    1
    2
    3
    4
    5
    class Point {
    constructor(x, y) {
    Object.assign(this, { x, y });
    }
    }
    + +

    上面方法通过Object.assign方法,将x属性和y属性添加到Point类的对象实例。

    +

    (2)为对象添加方法

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Object.assign(SomeClass.prototype, {
    someMethod(arg1, arg2) {
    ···
    },
    anotherMethod() {
    ···
    }
    });

    // 等同于下面的写法
    SomeClass.prototype.someMethod = function (arg1, arg2) {
    ···
    };
    SomeClass.prototype.anotherMethod = function () {
    ···
    };
    + +

    上面代码使用了对象属性的简洁表示法,直接将两个函数放在大括号中,再使用assign方法添加到SomeClass.prototype之中。

    +

    (3)克隆对象

    +
    1
    2
    3
    function clone(origin) {
    return Object.assign({}, origin);
    }
    + +

    上面代码将原始对象拷贝到一个空对象,就得到了原始对象的克隆。

    +

    不过,采用这种方法克隆,只能克隆原始对象自身的值,不能克隆它继承的值。如果想要保持继承链,可以采用下面的代码。

    +
    1
    2
    3
    4
    function clone(origin) {
    let originProto = Object.getPrototypeOf(origin);
    return Object.assign(Object.create(originProto), origin);
    }
    + +

    (4)合并多个对象

    +

    将多个对象合并到某个对象。

    +
    1
    const merge = (target, ...sources) => Object.assign(target, ...sources);
    + +

    如果希望合并后返回一个新对象,可以改写上面函数,对一个空对象合并。

    +
    1
    const merge = (...sources) => Object.assign({}, ...sources);
    + +

    (5)为属性指定默认值

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const DEFAULTS = {
    logLevel: 0,
    outputFormat: "html",
    };

    function processContent(options) {
    options = Object.assign({}, DEFAULTS, options);
    console.log(options);
    // ...
    }
    + +

    上面代码中,DEFAULTS对象是默认值,options对象是用户提供的参数。Object.assign方法将DEFAULTSoptions合并成一个新对象,如果两者有同名属性,则option的属性值会覆盖DEFAULTS的属性值。

    +

    注意,由于存在浅拷贝的问题,DEFAULTS对象和options对象的所有属性的值,最好都是简单类型,不要指向另一个对象。否则,DEFAULTS对象的该属性很可能不起作用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const DEFAULTS = {
    url: {
    host: "example.com",
    port: 7070,
    },
    };

    processContent({ url: { port: 8000 } });
    // {
    // url: {port: 8000}
    // }
    + +

    上面代码的原意是将url.port改成 8000,url.host不变。实际结果却是options.url覆盖掉DEFAULTS.url,所以url.host就不存在了。

    +

    属性的可枚举性和遍历

    可枚举性

    对象的每个属性都有一个描述对象(Descriptor),用来控制该属性的行为。Object.getOwnPropertyDescriptor方法可以获取该属性的描述对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let obj = { foo: 123 };
    Object.getOwnPropertyDescriptor(obj, "foo");
    // {
    // value: 123,
    // writable: true,
    // enumerable: true,
    // configurable: true
    // }
    + +

    描述对象的enumerable属性,称为”可枚举性“,如果该属性为false,就表示某些操作会忽略当前属性。

    +

    目前,有四个操作会忽略enumerablefalse的属性。

    +
      +
    • for...in循环:只遍历对象自身的和继承的可枚举的属性。
    • +
    • Object.keys():返回对象自身的所有可枚举的属性的键名。
    • +
    • JSON.stringify():只串行化对象自身的可枚举的属性。
    • +
    • Object.assign(): 忽略enumerablefalse的属性,只拷贝对象自身的可枚举的属性。
    • +
    +

    这四个操作之中,前三个是 ES5 就有的,最后一个Object.assign()是 ES6 新增的。其中,只有for...in会返回继承的属性,其他三个方法都会忽略继承的属性,只处理对象自身的属性。实际上,引入“可枚举”(enumerable)这个概念的最初目的,就是让某些属性可以规避掉for...in操作,不然所有内部属性和方法都会被遍历到。比如,对象原型的toString方法,以及数组的length属性,就通过“可枚举性”,从而避免被for...in遍历到。

    +
    1
    2
    3
    4
    5
    Object.getOwnPropertyDescriptor(Object.prototype, "toString").enumerable;
    // false

    Object.getOwnPropertyDescriptor([], "length").enumerable;
    // false
    + +

    上面代码中,toStringlength属性的enumerable都是false,因此for...in不会遍历到这两个继承自原型的属性。

    +

    另外,ES6 规定,所有 Class 的原型的方法都是不可枚举的。

    +
    1
    2
    3
    4
    5
    6
    7
    Object.getOwnPropertyDescriptor(
    class {
    foo() {}
    }.prototype,
    "foo"
    ).enumerable;
    // false
    + +

    总的来说,操作中引入继承的属性会让问题复杂化,大多数时候,我们只关心对象自身的属性。所以,尽量不要用for...in循环,而用Object.keys()代替。

    +

    属性的遍历

    ES6 一共有 5 种方法可以遍历对象的属性。

    +

    (1)for…in

    +

    for...in循环遍历对象自身的和继承的可枚举属性(不含 Symbol 属性)。

    +

    (2)Object.keys(obj)

    +

    Object.keys返回一个数组,包括对象自身的(不含继承的)所有可枚举属性(不含 Symbol 属性)的键名。

    +

    (3)Object.getOwnPropertyNames(obj)

    +

    Object.getOwnPropertyNames返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。

    +

    (4)Object.getOwnPropertySymbols(obj)

    +

    Object.getOwnPropertySymbols返回一个数组,包含对象自身的所有 Symbol 属性的键名。

    +

    (5)Reflect.ownKeys(obj)

    +

    Reflect.ownKeys返回一个数组,包含对象自身的所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

    +

    以上的 5 种方法遍历对象的键名,都遵守同样的属性遍历的次序规则。

    +
      +
    • 首先遍历所有数值键,按照数值升序排列。
    • +
    • 其次遍历所有字符串键,按照加入时间升序排列。
    • +
    • 最后遍历所有 Symbol 键,按照加入时间升序排列。
    • +
    +
    1
    2
    Reflect.ownKeys({ [Symbol()]: 0, b: 0, 10: 0, 2: 0, a: 0 });
    // ['2', '10', 'b', 'a', Symbol()]
    + +

    上面代码中,Reflect.ownKeys方法返回一个数组,包含了参数对象的所有属性。这个数组的属性次序是这样的,首先是数值属性210,其次是字符串属性ba,最后是 Symbol 属性。

    +

    Object.getOwnPropertyDescriptors()

    前面说过,Object.getOwnPropertyDescriptor方法会返回某个对象属性的描述对象(descriptor)。ES2017 引入了Object.getOwnPropertyDescriptors方法,返回指定对象所有自身属性(非继承属性)的描述对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    const obj = {
    foo: 123,
    get bar() {
    return "abc";
    },
    };

    Object.getOwnPropertyDescriptors(obj);
    // { foo:
    // { value: 123,
    // writable: true,
    // enumerable: true,
    // configurable: true },
    // bar:
    // { get: [Function: get bar],
    // set: undefined,
    // enumerable: true,
    // configurable: true } }
    + +

    上面代码中,Object.getOwnPropertyDescriptors方法返回一个对象,所有原对象的属性名都是该对象的属性名,对应的属性值就是该属性的描述对象。

    +

    该方法的实现非常容易。

    +
    1
    2
    3
    4
    5
    6
    7
    function getOwnPropertyDescriptors(obj) {
    const result = {};
    for (let key of Reflect.ownKeys(obj)) {
    result[key] = Object.getOwnPropertyDescriptor(obj, key);
    }
    return result;
    }
    + +

    该方法的引入目的,主要是为了解决Object.assign()无法正确拷贝get属性和set属性的问题。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    const source = {
    set foo(value) {
    console.log(value);
    },
    };

    const target1 = {};
    Object.assign(target1, source);

    Object.getOwnPropertyDescriptor(target1, "foo");
    // { value: undefined,
    // writable: true,
    // enumerable: true,
    // configurable: true }
    + +

    上面代码中,source对象的foo属性的值是一个赋值函数,Object.assign方法将这个属性拷贝给target1对象,结果该属性的值变成了undefined。这是因为Object.assign方法总是拷贝一个属性的值,而不会拷贝它背后的赋值方法或取值方法。

    +

    这时,Object.getOwnPropertyDescriptors方法配合Object.defineProperties方法,就可以实现正确拷贝。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const source = {
    set foo(value) {
    console.log(value);
    },
    };

    const target2 = {};
    Object.defineProperties(target2, Object.getOwnPropertyDescriptors(source));
    Object.getOwnPropertyDescriptor(target2, "foo");
    // { get: undefined,
    // set: [Function: set foo],
    // enumerable: true,
    // configurable: true }
    + +

    上面代码中,两个对象合并的逻辑可以写成一个函数。

    +
    1
    2
    const shallowMerge = (target, source) =>
    Object.defineProperties(target, Object.getOwnPropertyDescriptors(source));
    + +

    Object.getOwnPropertyDescriptors方法的另一个用处,是配合Object.create方法,将对象属性克隆到一个新对象。这属于浅拷贝。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    const clone = Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
    );

    // 或者

    const shallowClone = (obj) =>
    Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
    );
    + +

    上面代码会克隆对象obj

    +

    另外,Object.getOwnPropertyDescriptors方法可以实现一个对象继承另一个对象。以前,继承另一个对象,常常写成下面这样。

    +
    1
    2
    3
    4
    const obj = {
    __proto__: prot,
    foo: 123,
    };
    + +

    ES6 规定__proto__只有浏览器要部署,其他环境不用部署。如果去除__proto__,上面代码就要改成下面这样。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const obj = Object.create(prot);
    obj.foo = 123;

    // 或者

    const obj = Object.assign(Object.create(prot), {
    foo: 123,
    });
    + +

    有了Object.getOwnPropertyDescriptors,我们就有了另一种写法。

    +
    1
    2
    3
    4
    5
    6
    const obj = Object.create(
    prot,
    Object.getOwnPropertyDescriptors({
    foo: 123,
    })
    );
    + +

    Object.getOwnPropertyDescriptors也可以用来实现 Mixin(混入)模式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    let mix = (object) => ({
    with: (...mixins) =>
    mixins.reduce(
    (c, mixin) => Object.create(c, Object.getOwnPropertyDescriptors(mixin)),
    object
    ),
    });

    // multiple mixins example
    let a = { a: "a" };
    let b = { b: "b" };
    let c = { c: "c" };
    let d = mix(c).with(a, b);

    d.c; // "c"
    d.b; // "b"
    d.a; // "a"
    + +

    上面代码返回一个新的对象d,代表了对象ab被混入了对象c的操作。

    +

    出于完整性的考虑,Object.getOwnPropertyDescriptors进入标准以后,以后还会新增Reflect.getOwnPropertyDescriptors方法。

    +

    __proto__属性,Object.setPrototypeOf(),Object.getPrototypeOf()

    JavaScript 语言的对象继承是通过原型链实现的。ES6 提供了更多原型对象的操作方法。

    +

    __proto__属性

    __proto__属性(前后各两个下划线),用来读取或设置当前对象的prototype对象。目前,所有浏览器(包括 IE11)都部署了这个属性。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // es5 的写法
    const obj = {
    method: function() { ... }
    };
    obj.__proto__ = someOtherObj;

    // es6 的写法
    var obj = Object.create(someOtherObj);
    obj.method = function() { ... };
    + +

    该属性没有写入 ES6 的正文,而是写入了附录,原因是__proto__前后的双下划线,说明它本质上是一个内部属性,而不是一个正式的对外的 API,只是由于浏览器广泛支持,才被加入了 ES6。标准明确规定,只有浏览器必须部署这个属性,其他运行环境不一定需要部署,而且新的代码最好认为这个属性是不存在的。因此,无论从语义的角度,还是从兼容性的角度,都不要使用这个属性,而是使用下面的Object.setPrototypeOf()(写操作)、Object.getPrototypeOf()(读操作)、Object.create()(生成操作)代替。

    +

    实现上,__proto__调用的是Object.prototype.__proto__,具体实现如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    Object.defineProperty(Object.prototype, "__proto__", {
    get() {
    let _thisObj = Object(this);
    return Object.getPrototypeOf(_thisObj);
    },
    set(proto) {
    if (this === undefined || this === null) {
    throw new TypeError();
    }
    if (!isObject(this)) {
    return undefined;
    }
    if (!isObject(proto)) {
    return undefined;
    }
    let status = Reflect.setPrototypeOf(this, proto);
    if (!status) {
    throw new TypeError();
    }
    },
    });

    function isObject(value) {
    return Object(value) === value;
    }
    + +

    如果一个对象本身部署了__proto__属性,该属性的值就是对象的原型。

    +
    1
    2
    Object.getPrototypeOf({ __proto__: null });
    // null
    + +

    Object.setPrototypeOf()

    Object.setPrototypeOf方法的作用与__proto__相同,用来设置一个对象的prototype对象,返回参数对象本身。它是 ES6 正式推荐的设置原型对象的方法。

    +
    1
    2
    3
    4
    5
    // 格式
    Object.setPrototypeOf(object, prototype);

    // 用法
    const o = Object.setPrototypeOf({}, null);
    + +

    该方法等同于下面的函数。

    +
    1
    2
    3
    4
    function (obj, proto) {
    obj.__proto__ = proto;
    return obj;
    }
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    let proto = {};
    let obj = { x: 10 };
    Object.setPrototypeOf(obj, proto);

    proto.y = 20;
    proto.z = 40;

    obj.x; // 10
    obj.y; // 20
    obj.z; // 40
    + +

    上面代码将proto对象设为obj对象的原型,所以从obj对象可以读取proto对象的属性。

    +

    如果第一个参数不是对象,会自动转为对象。但是由于返回的还是第一个参数,所以这个操作不会产生任何效果。

    +
    1
    2
    3
    Object.setPrototypeOf(1, {}) === 1; // true
    Object.setPrototypeOf("foo", {}) === "foo"; // true
    Object.setPrototypeOf(true, {}) === true; // true
    + +

    由于undefinednull无法转为对象,所以如果第一个参数是undefinednull,就会报错。

    +
    1
    2
    3
    4
    5
    Object.setPrototypeOf(undefined, {});
    // TypeError: Object.setPrototypeOf called on null or undefined

    Object.setPrototypeOf(null, {});
    // TypeError: Object.setPrototypeOf called on null or undefined
    + +

    Object.getPrototypeOf()

    该方法与Object.setPrototypeOf方法配套,用于读取一个对象的原型对象。

    +
    1
    Object.getPrototypeOf(obj);
    + +

    下面是一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    function Rectangle() {
    // ...
    }

    const rec = new Rectangle();

    Object.getPrototypeOf(rec) === Rectangle.prototype;
    // true

    Object.setPrototypeOf(rec, Object.prototype);
    Object.getPrototypeOf(rec) === Rectangle.prototype;
    // false
    + +

    如果参数不是对象,会被自动转为对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 等同于 Object.getPrototypeOf(Number(1))
    Object.getPrototypeOf(1);
    // Number {[[PrimitiveValue]]: 0}

    // 等同于 Object.getPrototypeOf(String('foo'))
    Object.getPrototypeOf("foo");
    // String {length: 0, [[PrimitiveValue]]: ""}

    // 等同于 Object.getPrototypeOf(Boolean(true))
    Object.getPrototypeOf(true);
    // Boolean {[[PrimitiveValue]]: false}

    Object.getPrototypeOf(1) === Number.prototype; // true
    Object.getPrototypeOf("foo") === String.prototype; // true
    Object.getPrototypeOf(true) === Boolean.prototype; // true
    + +

    如果参数是undefinednull,它们无法转为对象,所以会报错。

    +
    1
    2
    3
    4
    5
    Object.getPrototypeOf(null);
    // TypeError: Cannot convert undefined or null to object

    Object.getPrototypeOf(undefined);
    // TypeError: Cannot convert undefined or null to object
    + +

    super 关键字

    我们知道,this关键字总是指向函数所在的当前对象,ES6 又新增了另一个类似的关键字super,指向当前对象的原型对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const proto = {
    foo: "hello",
    };

    const obj = {
    foo: "world",
    find() {
    return super.foo;
    },
    };

    Object.setPrototypeOf(obj, proto);
    obj.find(); // "hello"
    + +

    上面代码中,对象objfind方法之中,通过super.foo引用了原型对象protofoo属性。

    +

    注意,super关键字表示原型对象时,只能用在对象的方法之中,用在其他地方都会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 报错
    const obj = {
    foo: super.foo,
    };

    // 报错
    const obj = {
    foo: () => super.foo,
    };

    // 报错
    const obj = {
    foo: function () {
    return super.foo;
    },
    };
    + +

    上面三种super的用法都会报错,因为对于 JavaScript 引擎来说,这里的super都没有用在对象的方法之中。第一种写法是super用在属性里面,第二种和第三种写法是super用在一个函数里面,然后赋值给foo属性。目前,只有对象方法的简写法可以让 JavaScript 引擎确认,定义的是对象的方法。

    +

    JavaScript 引擎内部,super.foo等同于Object.getPrototypeOf(this).foo(属性)或Object.getPrototypeOf(this).foo.call(this)(方法)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    const proto = {
    x: "hello",
    foo() {
    console.log(this.x);
    },
    };

    const obj = {
    x: "world",
    foo() {
    super.foo();
    },
    };

    Object.setPrototypeOf(obj, proto);

    obj.foo(); // "world"
    + +

    上面代码中,super.foo指向原型对象protofoo方法,但是绑定的this却还是当前对象obj,因此输出的就是world

    +

    Object.keys(),Object.values(),Object.entries()

    Object.keys()

    ES5 引入了Object.keys方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键名。

    +
    1
    2
    3
    var obj = { foo: "bar", baz: 42 };
    Object.keys(obj);
    // ["foo", "baz"]
    + +

    ES2017 引入了跟Object.keys配套的Object.valuesObject.entries,作为遍历一个对象的补充手段,供for...of循环使用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    let { keys, values, entries } = Object;
    let obj = { a: 1, b: 2, c: 3 };

    for (let key of keys(obj)) {
    console.log(key); // 'a', 'b', 'c'
    }

    for (let value of values(obj)) {
    console.log(value); // 1, 2, 3
    }

    for (let [key, value] of entries(obj)) {
    console.log([key, value]); // ['a', 1], ['b', 2], ['c', 3]
    }
    + +

    Object.values()

    Object.values方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值。

    +
    1
    2
    3
    const obj = { foo: "bar", baz: 42 };
    Object.values(obj);
    // ["bar", 42]
    + +

    返回数组的成员顺序,与本章的《属性的遍历》部分介绍的排列规则一致。

    +
    1
    2
    3
    const obj = { 100: "a", 2: "b", 7: "c" };
    Object.values(obj);
    // ["b", "c", "a"]
    + +

    上面代码中,属性名为数值的属性,是按照数值大小,从小到大遍历的,因此返回的顺序是bca

    +

    Object.values只返回对象自身的可遍历属性。

    +
    1
    2
    const obj = Object.create({}, { p: { value: 42 } });
    Object.values(obj); // []
    + +

    上面代码中,Object.create方法的第二个参数添加的对象属性(属性p),如果不显式声明,默认是不可遍历的,因为p的属性描述对象的enumerable默认是falseObject.values不会返回这个属性。只要把enumerable改成trueObject.values就会返回属性p的值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const obj = Object.create(
    {},
    {
    p: {
    value: 42,
    enumerable: true,
    },
    }
    );
    Object.values(obj); // [42]
    + +

    Object.values会过滤属性名为 Symbol 值的属性。

    +
    1
    2
    Object.values({ [Symbol()]: 123, foo: "abc" });
    // ['abc']
    + +

    如果Object.values方法的参数是一个字符串,会返回各个字符组成的一个数组。

    +
    1
    2
    Object.values("foo");
    // ['f', 'o', 'o']
    + +

    上面代码中,字符串会先转成一个类似数组的对象。字符串的每个字符,就是该对象的一个属性。因此,Object.values返回每个属性的键值,就是各个字符组成的一个数组。

    +

    如果参数不是对象,Object.values会先将其转为对象。由于数值和布尔值的包装对象,都不会为实例添加非继承的属性。所以,Object.values会返回空数组。

    +
    1
    2
    Object.values(42); // []
    Object.values(true); // []
    + +

    Object.entries

    Object.entries方法返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组。

    +
    1
    2
    3
    const obj = { foo: "bar", baz: 42 };
    Object.entries(obj);
    // [ ["foo", "bar"], ["baz", 42] ]
    + +

    除了返回值不一样,该方法的行为与Object.values基本一致。

    +

    如果原对象的属性名是一个 Symbol 值,该属性会被忽略。

    +
    1
    2
    Object.entries({ [Symbol()]: 123, foo: "abc" });
    // [ [ 'foo', 'abc' ] ]
    + +

    上面代码中,原对象有两个属性,Object.entries只输出属性名非 Symbol 值的属性。将来可能会有Reflect.ownEntries()方法,返回对象自身的所有属性。

    +

    Object.entries的基本用途是遍历对象的属性。

    +
    1
    2
    3
    4
    5
    6
    let obj = { one: 1, two: 2 };
    for (let [k, v] of Object.entries(obj)) {
    console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`);
    }
    // "one": 1
    // "two": 2
    + +

    Object.entries方法的另一个用处是,将对象转为真正的Map结构。

    +
    1
    2
    3
    const obj = { foo: "bar", baz: 42 };
    const map = new Map(Object.entries(obj));
    map; // Map { foo: "bar", baz: 42 }
    + +

    自己实现Object.entries方法,非常简单。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // Generator函数的版本
    function* entries(obj) {
    for (let key of Object.keys(obj)) {
    yield [key, obj[key]];
    }
    }

    // 非Generator函数的版本
    function entries(obj) {
    let arr = [];
    for (let key of Object.keys(obj)) {
    arr.push([key, obj[key]]);
    }
    return arr;
    }
    + +

    对象的扩展运算符

    《数组的扩展》一章中,已经介绍过扩展运算符(...)。

    +
    1
    2
    3
    const [a, ...b] = [1, 2, 3];
    a; // 1
    b; // [2, 3]
    + +

    ES2018 将这个运算符引入了对象。

    +

    解构赋值

    对象的解构赋值用于从一个对象取值,相当于将目标对象自身的所有可遍历的(enumerable)、但尚未被读取的属性,分配到指定的对象上面。所有的键和它们的值,都会拷贝到新对象上面。

    +
    1
    2
    3
    4
    let { x, y, ...z } = { x: 1, y: 2, a: 3, b: 4 };
    x; // 1
    y; // 2
    z; // { a: 3, b: 4 }
    + +

    上面代码中,变量z是解构赋值所在的对象。它获取等号右边的所有尚未读取的键(ab),将它们连同值一起拷贝过来。

    +

    由于解构赋值要求等号右边是一个对象,所以如果等号右边是undefinednull,就会报错,因为它们无法转为对象。

    +
    1
    2
    let { x, y, ...z } = null; // 运行时错误
    let { x, y, ...z } = undefined; // 运行时错误
    + +

    解构赋值必须是最后一个参数,否则会报错。

    +
    1
    2
    let { ...x, y, z } = obj; // 句法错误
    let { x, ...y, ...z } = obj; // 句法错误
    + +

    上面代码中,解构赋值不是最后一个参数,所以会报错。

    +

    注意,解构赋值的拷贝是浅拷贝,即如果一个键的值是复合类型的值(数组、对象、函数)、那么解构赋值拷贝的是这个值的引用,而不是这个值的副本。

    +
    1
    2
    3
    4
    let obj = { a: { b: 1 } };
    let { ...x } = obj;
    obj.a.b = 2;
    x.a.b; // 2
    + +

    上面代码中,x是解构赋值所在的对象,拷贝了对象obja属性。a属性引用了一个对象,修改这个对象的值,会影响到解构赋值对它的引用。

    +

    另外,扩展运算符的解构赋值,不能复制继承自原型对象的属性。

    +
    1
    2
    3
    4
    5
    6
    let o1 = { a: 1 };
    let o2 = { b: 2 };
    o2.__proto__ = o1;
    let { ...o3 } = o2;
    o3; // { b: 2 }
    o3.a; // undefined
    + +

    上面代码中,对象o3复制了o2,但是只复制了o2自身的属性,没有复制它的原型对象o1的属性。

    +

    下面是另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const o = Object.create({ x: 1, y: 2 });
    o.z = 3;

    let { x, ...newObj } = o;
    let { y, z } = newObj;
    x; // 1
    y; // undefined
    z; // 3
    + +

    上面代码中,变量x是单纯的解构赋值,所以可以读取对象o继承的属性;变量yz是扩展运算符的解构赋值,只能读取对象o自身的属性,所以变量z可以赋值成功,变量y取不到值。ES6 规定,变量声明语句之中,如果使用解构赋值,扩展运算符后面必须是一个变量名,而不能是一个解构赋值表达式,所以上面代码引入了中间变量newObj,如果写成下面这样会报错。

    +
    1
    2
    let { x, ...{ y, z } } = o;
    // SyntaxError: ... must be followed by an identifier in declaration contexts
    + +

    解构赋值的一个用处,是扩展某个函数的参数,引入其他操作。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    function baseFunction({ a, b }) {
    // ...
    }
    function wrapperFunction({ x, y, ...restConfig }) {
    // 使用 x 和 y 参数进行操作
    // 其余参数传给原始函数
    return baseFunction(restConfig);
    }
    + +

    上面代码中,原始函数baseFunction接受ab作为参数,函数wrapperFunctionbaseFunction的基础上进行了扩展,能够接受多余的参数,并且保留原始函数的行为。

    +

    扩展运算符

    对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中。

    +
    1
    2
    3
    let z = { a: 3, b: 4 };
    let n = { ...z };
    n; // { a: 3, b: 4 }
    + +

    这等同于使用Object.assign方法。

    +
    1
    2
    3
    let aClone = { ...a };
    // 等同于
    let aClone = Object.assign({}, a);
    + +

    上面的例子只是拷贝了对象实例的属性,如果想完整克隆一个对象,还拷贝对象原型的属性,可以采用下面的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 写法一
    const clone1 = {
    __proto__: Object.getPrototypeOf(obj),
    ...obj,
    };

    // 写法二
    const clone2 = Object.assign(Object.create(Object.getPrototypeOf(obj)), obj);

    // 写法三
    const clone3 = Object.create(
    Object.getPrototypeOf(obj),
    Object.getOwnPropertyDescriptors(obj)
    );
    + +

    上面代码中,写法一的__proto__属性在非浏览器的环境不一定部署,因此推荐使用写法二和写法三。

    +

    扩展运算符可以用于合并两个对象。

    +
    1
    2
    3
    let ab = { ...a, ...b };
    // 等同于
    let ab = Object.assign({}, a, b);
    + +

    如果用户自定义的属性,放在扩展运算符后面,则扩展运算符内部的同名属性会被覆盖掉。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let aWithOverrides = { ...a, x: 1, y: 2 };
    // 等同于
    let aWithOverrides = { ...a, ...{ x: 1, y: 2 } };
    // 等同于
    let x = 1,
    y = 2,
    aWithOverrides = { ...a, x, y };
    // 等同于
    let aWithOverrides = Object.assign({}, a, { x: 1, y: 2 });
    + +

    上面代码中,a对象的x属性和y属性,拷贝到新对象后会被覆盖掉。

    +

    这用来修改现有对象部分的属性就很方便了。

    +
    1
    2
    3
    4
    let newVersion = {
    ...previousVersion,
    name: "New Name", // Override the name property
    };
    + +

    上面代码中,newVersion对象自定义了name属性,其他属性全部复制自previousVersion对象。

    +

    如果把自定义属性放在扩展运算符前面,就变成了设置新对象的默认属性值。

    +
    1
    2
    3
    4
    5
    let aWithDefaults = { x: 1, y: 2, ...a };
    // 等同于
    let aWithDefaults = Object.assign({}, { x: 1, y: 2 }, a);
    // 等同于
    let aWithDefaults = Object.assign({ x: 1, y: 2 }, a);
    + +

    与数组的扩展运算符一样,对象的扩展运算符后面可以跟表达式。

    +
    1
    2
    3
    4
    const obj = {
    ...(x > 1 ? { a: 1 } : {}),
    b: 2,
    };
    + +

    如果扩展运算符后面是一个空对象,则没有任何效果。

    +
    1
    2
    {...{}, a: 1}
    // { a: 1 }
    + +

    如果扩展运算符的参数是nullundefined,这两个值会被忽略,不会报错。

    +
    1
    let emptyObject = { ...null, ...undefined }; // 不报错
    + +

    扩展运算符的参数对象之中,如果有取值函数get,这个函数是会执行的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 并不会抛出错误,因为 x 属性只是被定义,但没执行
    let aWithXGetter = {
    ...a,
    get x() {
    throw new Error("not throw yet");
    },
    };

    // 会抛出错误,因为 x 属性被执行了
    let runtimeError = {
    ...a,
    ...{
    get x() {
    throw new Error("throw now");
    },
    },
    };
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%89%A9%E5%B1%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\346\225\260\345\200\274\347\232\204\346\211\251\345\261\225/index.html" "b/posts/ES6-\346\225\260\345\200\274\347\232\204\346\211\251\345\261\225/index.html" new file mode 100644 index 00000000..50c06eee --- /dev/null +++ "b/posts/ES6-\346\225\260\345\200\274\347\232\204\346\211\251\345\261\225/index.html" @@ -0,0 +1,431 @@ +数值的扩展 | JCAlways + + + + + + + + + + + + + +

    数值的扩展

    数值的扩展

    二进制和八进制表示法

    ES6 提供了二进制和八进制数值的新的写法,分别用前缀0b(或0B)和0o(或0O)表示。

    +
    1
    2
    0b111110111 === 503; // true
    0o767 === 503; // true
    + +

    从 ES5 开始,在严格模式之中,八进制就不再允许使用前缀0表示,ES6 进一步明确,要使用前缀0o表示。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 非严格模式
    (function () {
    console.log(0o11 === 011);
    })()(
    // true

    // 严格模式
    function () {
    "use strict";
    console.log(0o11 === 011);
    }
    )(); // Uncaught SyntaxError: Octal literals are not allowed in strict mode.
    + +

    如果要将0b0o前缀的字符串数值转为十进制,要使用Number方法。

    +
    1
    2
    Number("0b111"); // 7
    Number("0o10"); // 8
    + +

    Number.isFinite(), Number.isNaN()

    ES6 在Number对象上,新提供了Number.isFinite()Number.isNaN()两个方法。

    +

    Number.isFinite()用来检查一个数值是否为有限的(finite),即不是Infinity

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Number.isFinite(15); // true
    Number.isFinite(0.8); // true
    Number.isFinite(NaN); // false
    Number.isFinite(Infinity); // false
    Number.isFinite(-Infinity); // false
    Number.isFinite("foo"); // false
    Number.isFinite("15"); // false
    Number.isFinite(true); // false
    + +

    注意,如果参数类型不是数值,Number.isFinite一律返回false

    +

    Number.isNaN()用来检查一个值是否为NaN

    +
    1
    2
    3
    4
    5
    6
    7
    Number.isNaN(NaN); // true
    Number.isNaN(15); // false
    Number.isNaN("15"); // false
    Number.isNaN(true); // false
    Number.isNaN(9 / NaN); // true
    Number.isNaN("true" / 0); // true
    Number.isNaN("true" / "true"); // true
    + +

    如果参数类型不是NaNNumber.isNaN一律返回false

    +

    它们与传统的全局方法isFinite()isNaN()的区别在于,传统方法先调用Number()将非数值的值转为数值,再进行判断,而这两个新方法只对数值有效,Number.isFinite()对于非数值一律返回false, Number.isNaN()只有对于NaN才返回true,非NaN一律返回false

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    isFinite(25); // true
    isFinite("25"); // true
    Number.isFinite(25); // true
    Number.isFinite("25"); // false

    isNaN(NaN); // true
    isNaN("NaN"); // true
    Number.isNaN(NaN); // true
    Number.isNaN("NaN"); // false
    Number.isNaN(1); // false
    + +

    Number.parseInt(), Number.parseFloat()

    ES6 将全局方法parseInt()parseFloat(),移植到Number对象上面,行为完全保持不变。

    +
    1
    2
    3
    4
    5
    6
    7
    // ES5的写法
    parseInt("12.34"); // 12
    parseFloat("123.45#"); // 123.45

    // ES6的写法
    Number.parseInt("12.34"); // 12
    Number.parseFloat("123.45#"); // 123.45
    + +

    这样做的目的,是逐步减少全局性方法,使得语言逐步模块化。

    +
    1
    2
    Number.parseInt === parseInt; // true
    Number.parseFloat === parseFloat; // true
    + +

    Number.isInteger()

    Number.isInteger()用来判断一个数值是否为整数。

    +
    1
    2
    Number.isInteger(25); // true
    Number.isInteger(25.1); // false
    + +

    JavaScript 内部,整数和浮点数采用的是同样的储存方法,所以 25 和 25.0 被视为同一个值。

    +
    1
    2
    Number.isInteger(25); // true
    Number.isInteger(25.0); // true
    + +

    如果参数不是数值,Number.isInteger返回false

    +
    1
    2
    3
    4
    Number.isInteger(); // false
    Number.isInteger(null); // false
    Number.isInteger("15"); // false
    Number.isInteger(true); // false
    + +

    注意,由于 JavaScript 采用 IEEE 754 标准,数值存储为 64 位双精度格式,数值精度最多可以达到 53 个二进制位(1 个隐藏位与 52 个有效位)。如果数值的精度超过这个限度,第 54 位及后面的位就会被丢弃,这种情况下,Number.isInteger可能会误判。

    +
    1
    Number.isInteger(3.0000000000000002); // true
    + +

    上面代码中,Number.isInteger的参数明明不是整数,但是会返回true。原因就是这个小数的精度达到了小数点后 16 个十进制位,转成二进制位超过了 53 个二进制位,导致最后的那个2被丢弃了。

    +

    类似的情况还有,如果一个数值的绝对值小于Number.MIN_VALUE(5E-324),即小于 JavaScript 能够分辨的最小值,会被自动转为 0。这时,Number.isInteger也会误判。

    +
    1
    2
    Number.isInteger(5e-324); // false
    Number.isInteger(5e-325); // true
    + +

    上面代码中,5E-325由于值太小,会被自动转为 0,因此返回true

    +

    总之,如果对数据精度的要求较高,不建议使用Number.isInteger()判断一个数值是否为整数。

    +

    Number.EPSILON

    ES6 在Number对象上面,新增一个极小的常量Number.EPSILON。根据规格,它表示 1 与大于 1 的最小浮点数之间的差。

    +

    对于 64 位浮点数来说,大于 1 的最小浮点数相当于二进制的1.00..001,小数点后面有连续 51 个零。这个值减去 1 之后,就等于 2 的 -52 次方。

    +
    1
    2
    3
    4
    5
    6
    Number.EPSILON === Math.pow(2, -52);
    // true
    Number.EPSILON;
    // 2.220446049250313e-16
    Number.EPSILON.toFixed(20);
    // "0.00000000000000022204"
    + +

    Number.EPSILON实际上是 JavaScript 能够表示的最小精度。误差如果小于这个值,就可以认为已经没有意义了,即不存在误差了。

    +

    引入一个这么小的量的目的,在于为浮点数计算,设置一个误差范围。我们知道浮点数计算是不精确的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    0.1 + 0.2;
    // 0.30000000000000004

    0.1 + 0.2 - 0.3;
    // 5.551115123125783e-17

    (5.551115123125783e-17).toFixed(20);
    // '0.00000000000000005551'
    + +

    上面代码解释了,为什么比较0.1 + 0.20.3得到的结果是false

    +
    1
    0.1 + 0.2 === 0.3; // false
    + +

    Number.EPSILON可以用来设置“能够接受的误差范围”。比如,误差范围设为 2 的-50 次方(即Number.EPSILON * Math.pow(2, 2)),即如果两个浮点数的差小于这个值,我们就认为这两个浮点数相等。

    +
    1
    2
    5.551115123125783e-17 < Number.EPSILON * Math.pow(2, 2);
    // true
    + +

    因此,Number.EPSILON的实质是一个可以接受的最小误差范围。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function withinErrorMargin(left, right) {
    return Math.abs(left - right) < Number.EPSILON * Math.pow(2, 2);
    }

    0.1 + 0.2 === 0.3; // false
    withinErrorMargin(0.1 + 0.2, 0.3); // true

    1.1 + 1.3 === 2.4; // false
    withinErrorMargin(1.1 + 1.3, 2.4); // true
    + +

    上面的代码为浮点数运算,部署了一个误差检查函数。

    +

    安全整数和 Number.isSafeInteger()

    JavaScript 能够准确表示的整数范围在-2^532^53之间(不含两个端点),超过这个范围,无法精确表示这个值。

    +
    1
    2
    3
    4
    5
    6
    7
    Math.pow(2, 53); // 9007199254740992

    9007199254740992; // 9007199254740992
    9007199254740993; // 9007199254740992

    Math.pow(2, 53) === Math.pow(2, 53) + 1;
    // true
    + +

    上面代码中,超出 2 的 53 次方之后,一个数就不精确了。

    +

    ES6 引入了Number.MAX_SAFE_INTEGERNumber.MIN_SAFE_INTEGER这两个常量,用来表示这个范围的上下限。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Number.MAX_SAFE_INTEGER === Math.pow(2, 53) - 1;
    // true
    Number.MAX_SAFE_INTEGER === 9007199254740991;
    // true

    Number.MIN_SAFE_INTEGER === -Number.MAX_SAFE_INTEGER;
    // true
    Number.MIN_SAFE_INTEGER === -9007199254740991;
    // true
    + +

    上面代码中,可以看到 JavaScript 能够精确表示的极限。

    +

    Number.isSafeInteger()则是用来判断一个整数是否落在这个范围之内。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    Number.isSafeInteger("a"); // false
    Number.isSafeInteger(null); // false
    Number.isSafeInteger(NaN); // false
    Number.isSafeInteger(Infinity); // false
    Number.isSafeInteger(-Infinity); // false

    Number.isSafeInteger(3); // true
    Number.isSafeInteger(1.2); // false
    Number.isSafeInteger(9007199254740990); // true
    Number.isSafeInteger(9007199254740992); // false

    Number.isSafeInteger(Number.MIN_SAFE_INTEGER - 1); // false
    Number.isSafeInteger(Number.MIN_SAFE_INTEGER); // true
    Number.isSafeInteger(Number.MAX_SAFE_INTEGER); // true
    Number.isSafeInteger(Number.MAX_SAFE_INTEGER + 1); // false
    + +

    这个函数的实现很简单,就是跟安全整数的两个边界值比较一下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Number.isSafeInteger = function (n) {
    return (
    typeof n === "number" &&
    Math.round(n) === n &&
    Number.MIN_SAFE_INTEGER <= n &&
    n <= Number.MAX_SAFE_INTEGER
    );
    };
    + +

    实际使用这个函数时,需要注意。验证运算结果是否落在安全整数的范围内,不要只验证运算结果,而要同时验证参与运算的每个值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Number.isSafeInteger(9007199254740993);
    // false
    Number.isSafeInteger(990);
    // true
    Number.isSafeInteger(9007199254740993 - 990);
    // true
    9007199254740993 - 990;
    // 返回结果 9007199254740002
    // 正确答案应该是 9007199254740003
    + +

    上面代码中,9007199254740993不是一个安全整数,但是Number.isSafeInteger会返回结果,显示计算结果是安全的。这是因为,这个数超出了精度范围,导致在计算机内部,以9007199254740992的形式储存。

    +
    1
    2
    9007199254740993 === 9007199254740992;
    // true
    + +

    所以,如果只验证运算结果是否为安全整数,很可能得到错误结果。下面的函数可以同时验证两个运算数和运算结果。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function trusty(left, right, result) {
    if (
    Number.isSafeInteger(left) &&
    Number.isSafeInteger(right) &&
    Number.isSafeInteger(result)
    ) {
    return result;
    }
    throw new RangeError("Operation cannot be trusted!");
    }

    trusty(9007199254740993, 990, 9007199254740993 - 990);
    // RangeError: Operation cannot be trusted!

    trusty(1, 2, 3);
    // 3
    + +

    Math 对象的扩展

    ES6 在 Math 对象上新增了 17 个与数学相关的方法。所有这些方法都是静态方法,只能在 Math 对象上调用。

    +

    Math.trunc()

    Math.trunc方法用于去除一个数的小数部分,返回整数部分。

    +
    1
    2
    3
    4
    5
    Math.trunc(4.1); // 4
    Math.trunc(4.9); // 4
    Math.trunc(-4.1); // -4
    Math.trunc(-4.9); // -4
    Math.trunc(-0.1234); // -0
    + +

    对于非数值,Math.trunc内部使用Number方法将其先转为数值。

    +
    1
    2
    3
    4
    Math.trunc("123.456"); // 123
    Math.trunc(true); //1
    Math.trunc(false); // 0
    Math.trunc(null); // 0
    + +

    对于空值和无法截取整数的值,返回NaN

    +
    1
    2
    3
    4
    Math.trunc(NaN); // NaN
    Math.trunc("foo"); // NaN
    Math.trunc(); // NaN
    Math.trunc(undefined); // NaN
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    Math.trunc =
    Math.trunc ||
    function (x) {
    return x < 0 ? Math.ceil(x) : Math.floor(x);
    };
    + +

    Math.sign()

    Math.sign方法用来判断一个数到底是正数、负数、还是零。对于非数值,会先将其转换为数值。

    +

    它会返回五种值。

    +
      +
    • 参数为正数,返回+1
    • +
    • 参数为负数,返回-1
    • +
    • 参数为 0,返回0
    • +
    • 参数为-0,返回-0;
    • +
    • 其他值,返回NaN
    • +
    +
    1
    2
    3
    4
    5
    Math.sign(-5); // -1
    Math.sign(5); // +1
    Math.sign(0); // +0
    Math.sign(-0); // -0
    Math.sign(NaN); // NaN
    + +

    如果参数是非数值,会自动转为数值。对于那些无法转为数值的值,会返回NaN

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Math.sign(""); // 0
    Math.sign(true); // +1
    Math.sign(false); // 0
    Math.sign(null); // 0
    Math.sign("9"); // +1
    Math.sign("foo"); // NaN
    Math.sign(); // NaN
    Math.sign(undefined); // NaN
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Math.sign =
    Math.sign ||
    function (x) {
    x = +x; // convert to a number
    if (x === 0 || isNaN(x)) {
    return x;
    }
    return x > 0 ? 1 : -1;
    };
    + +

    Math.cbrt()

    Math.cbrt方法用于计算一个数的立方根。

    +
    1
    2
    3
    4
    Math.cbrt(-1); // -1
    Math.cbrt(0); // 0
    Math.cbrt(1); // 1
    Math.cbrt(2); // 1.2599210498948734
    + +

    对于非数值,Math.cbrt方法内部也是先使用Number方法将其转为数值。

    +
    1
    2
    Math.cbrt("8"); // 2
    Math.cbrt("hello"); // NaN
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    6
    Math.cbrt =
    Math.cbrt ||
    function (x) {
    var y = Math.pow(Math.abs(x), 1 / 3);
    return x < 0 ? -y : y;
    };
    + +

    Math.clz32()

    JavaScript 的整数使用 32 位二进制形式表示,Math.clz32方法返回一个数的 32 位无符号整数形式有多少个前导 0。

    +
    1
    2
    3
    4
    5
    Math.clz32(0); // 32
    Math.clz32(1); // 31
    Math.clz32(1000); // 22
    Math.clz32(0b01000000000000000000000000000000); // 1
    Math.clz32(0b00100000000000000000000000000000); // 2
    + +

    上面代码中,0 的二进制形式全为 0,所以有 32 个前导 0;1 的二进制形式是0b1,只占 1 位,所以 32 位之中有 31 个前导 0;1000 的二进制形式是0b1111101000,一共有 10 位,所以 32 位之中有 22 个前导 0。

    +

    clz32这个函数名就来自”count leading zero bits in 32-bit binary representation of a number“(计算一个数的 32 位二进制形式的前导 0 的个数)的缩写。

    +

    左移运算符(<<)与Math.clz32方法直接相关。

    +
    1
    2
    3
    4
    5
    Math.clz32(0); // 32
    Math.clz32(1); // 31
    Math.clz32(1 << 1); // 30
    Math.clz32(1 << 2); // 29
    Math.clz32(1 << 29); // 2
    + +

    对于小数,Math.clz32方法只考虑整数部分。

    +
    1
    2
    Math.clz32(3.2); // 30
    Math.clz32(3.9); // 30
    + +

    对于空值或其他类型的值,Math.clz32方法会将它们先转为数值,然后再计算。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Math.clz32(); // 32
    Math.clz32(NaN); // 32
    Math.clz32(Infinity); // 32
    Math.clz32(null); // 32
    Math.clz32("foo"); // 32
    Math.clz32([]); // 32
    Math.clz32({}); // 32
    Math.clz32(true); // 31
    + +

    Math.imul()

    Math.imul方法返回两个数以 32 位带符号整数形式相乘的结果,返回的也是一个 32 位的带符号整数。

    +
    1
    2
    3
    Math.imul(2, 4); // 8
    Math.imul(-1, 8); // -8
    Math.imul(-2, -2); // 4
    + +

    如果只考虑最后 32 位,大多数情况下,Math.imul(a, b)a * b的结果是相同的,即该方法等同于(a * b)|0的效果(超过 32 位的部分溢出)。之所以需要部署这个方法,是因为 JavaScript 有精度限制,超过 2 的 53 次方的值无法精确表示。这就是说,对于那些很大的数的乘法,低位数值往往都是不精确的,Math.imul方法可以返回正确的低位数值。

    +
    1
    (0x7fffffff * 0x7fffffff) | 0; // 0
    + +

    上面这个乘法算式,返回结果为 0。但是由于这两个二进制数的最低位都是 1,所以这个结果肯定是不正确的,因为根据二进制乘法,计算结果的二进制最低位应该也是 1。这个错误就是因为它们的乘积超过了 2 的 53 次方,JavaScript 无法保存额外的精度,就把低位的值都变成了 0。Math.imul方法可以返回正确的值 1。

    +
    1
    Math.imul(0x7fffffff, 0x7fffffff); // 1
    + +

    Math.fround()

    Math.fround方法返回一个数的 32 位单精度浮点数形式。

    +

    对于 32 位单精度格式来说,数值精度是 24 个二进制位(1 位隐藏位与 23 位有效位),所以对于 -224 至 224 之间的整数(不含两个端点),返回结果与参数本身一致。

    +
    1
    2
    3
    Math.fround(0); // 0
    Math.fround(1); // 1
    Math.fround(2 ** 24 - 1); // 16777215
    + +

    如果参数的绝对值大于 224,返回的结果便开始丢失精度。

    +
    1
    2
    Math.fround(2 ** 24); // 16777216
    Math.fround(2 ** 24 + 1); // 16777216
    + +

    Math.fround方法的主要作用,是将 64 位双精度浮点数转为 32 位单精度浮点数。如果小数的精度超过 24 个二进制位,返回值就会不同于原值,否则返回值不变(即与 64 位双精度值一致)。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 未丢失有效精度
    Math.fround(1.125); // 1.125
    Math.fround(7.25); // 7.25

    // 丢失精度
    Math.fround(0.3); // 0.30000001192092896
    Math.fround(0.7); // 0.699999988079071
    Math.fround(1.0000000123); // 1
    + +

    对于 NaNInfinity,此方法返回原值。对于其它类型的非数值,Math.fround 方法会先将其转为数值,再返回单精度浮点数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    Math.fround(NaN); // NaN
    Math.fround(Infinity); // Infinity

    Math.fround("5"); // 5
    Math.fround(true); // 1
    Math.fround(null); // 0
    Math.fround([]); // 0
    Math.fround({}); // NaN
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    Math.fround =
    Math.fround ||
    function (x) {
    return new Float32Array([x])[0];
    };
    + +

    Math.hypot()

    Math.hypot方法返回所有参数的平方和的平方根。

    +
    1
    2
    3
    4
    5
    6
    7
    Math.hypot(3, 4); // 5
    Math.hypot(3, 4, 5); // 7.0710678118654755
    Math.hypot(); // 0
    Math.hypot(NaN); // NaN
    Math.hypot(3, 4, "foo"); // NaN
    Math.hypot(3, 4, "5"); // 7.0710678118654755
    Math.hypot(-3); // 3
    + +

    上面代码中,3 的平方加上 4 的平方,等于 5 的平方。

    +

    如果参数不是数值,Math.hypot方法会将其转为数值。只要有一个参数无法转为数值,就会返回 NaN。

    +

    对数方法

    ES6 新增了 4 个对数相关方法。

    +

    (1) Math.expm1()

    +

    Math.expm1(x)返回 ex - 1,即Math.exp(x) - 1

    +
    1
    2
    3
    Math.expm1(-1); // -0.6321205588285577
    Math.expm1(0); // 0
    Math.expm1(1); // 1.718281828459045
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    Math.expm1 =
    Math.expm1 ||
    function (x) {
    return Math.exp(x) - 1;
    };
    + +

    (2)Math.log1p()

    +

    Math.log1p(x)方法返回1 + x的自然对数,即Math.log(1 + x)。如果x小于-1,返回NaN

    +
    1
    2
    3
    4
    Math.log1p(1); // 0.6931471805599453
    Math.log1p(0); // 0
    Math.log1p(-1); // -Infinity
    Math.log1p(-2); // NaN
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    Math.log1p =
    Math.log1p ||
    function (x) {
    return Math.log(1 + x);
    };
    + +

    (3)Math.log10()

    +

    Math.log10(x)返回以 10 为底的x的对数。如果x小于 0,则返回 NaN。

    +
    1
    2
    3
    4
    5
    Math.log10(2); // 0.3010299956639812
    Math.log10(1); // 0
    Math.log10(0); // -Infinity
    Math.log10(-2); // NaN
    Math.log10(100000); // 5
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    Math.log10 =
    Math.log10 ||
    function (x) {
    return Math.log(x) / Math.LN10;
    };
    + +

    (4)Math.log2()

    +

    Math.log2(x)返回以 2 为底的x的对数。如果x小于 0,则返回 NaN。

    +
    1
    2
    3
    4
    5
    6
    7
    Math.log2(3); // 1.584962500721156
    Math.log2(2); // 1
    Math.log2(1); // 0
    Math.log2(0); // -Infinity
    Math.log2(-2); // NaN
    Math.log2(1024); // 10
    Math.log2(1 << 29); // 29
    + +

    对于没有部署这个方法的环境,可以用下面的代码模拟。

    +
    1
    2
    3
    4
    5
    Math.log2 =
    Math.log2 ||
    function (x) {
    return Math.log(x) / Math.LN2;
    };
    + +

    双曲函数方法

    ES6 新增了 6 个双曲函数方法。

    +
      +
    • Math.sinh(x) 返回x的双曲正弦(hyperbolic sine)
    • +
    • Math.cosh(x) 返回x的双曲余弦(hyperbolic cosine)
    • +
    • Math.tanh(x) 返回x的双曲正切(hyperbolic tangent)
    • +
    • Math.asinh(x) 返回x的反双曲正弦(inverse hyperbolic sine)
    • +
    • Math.acosh(x) 返回x的反双曲余弦(inverse hyperbolic cosine)
    • +
    • Math.atanh(x) 返回x的反双曲正切(inverse hyperbolic tangent)
    • +
    +

    指数运算符

    ES2016 新增了一个指数运算符(**)。

    +
    1
    2
    2 ** 2; // 4
    2 ** 3; // 8
    + +

    这个运算符的一个特点是右结合,而不是常见的左结合。多个指数运算符连用时,是从最右边开始计算的。

    +
    1
    2
    3
    // 相当于 2 ** (3 ** 2)
    2 ** (3 ** 2);
    // 512
    + +

    上面代码中,首先计算的是第二个指数运算符,而不是第一个。

    +

    指数运算符可以与等号结合,形成一个新的赋值运算符(**=)。

    +
    1
    2
    3
    4
    5
    6
    7
    let a = 1.5;
    a **= 2;
    // 等同于 a = a * a;

    let b = 4;
    b **= 3;
    // 等同于 b = b * b * b;
    + +

    注意,V8 引擎的指数运算符与Math.pow的实现不相同,对于特别大的运算结果,两者会有细微的差异。

    +
    1
    2
    3
    4
    5
    Math.pow(99, 99);
    // 3.697296376497263e+197

    99 ** 99;
    // 3.697296376497268e+197
    + +

    上面代码中,两个运算结果的最后一位有效数字是有差异的。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E6%95%B0%E5%80%BC%E7%9A%84%E6%89%A9%E5%B1%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\346\225\260\347\273\204\347\232\204\346\211\251\345\261\225/index.html" "b/posts/ES6-\346\225\260\347\273\204\347\232\204\346\211\251\345\261\225/index.html" new file mode 100644 index 00000000..f8839296 --- /dev/null +++ "b/posts/ES6-\346\225\260\347\273\204\347\232\204\346\211\251\345\261\225/index.html" @@ -0,0 +1,500 @@ +数组的扩展 | JCAlways + + + + + + + + + + + + + +

    数组的扩展

    数组的扩展

    扩展运算符

    含义

    扩展运算符(spread)是三个点(...)。它好比 rest 参数的逆运算,将一个数组转为用逗号分隔的参数序列。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    console.log(...[1, 2, 3])
    // 1 2 3

    console.log(1, ...[2, 3, 4], 5)
    // 1 2 3 4 5

    [...document.querySelectorAll('div')]
    // [<div>, <div>, <div>]
    + +

    该运算符主要用于函数调用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function push(array, ...items) {
    array.push(...items);
    }

    function add(x, y) {
    return x + y;
    }

    const numbers = [4, 38];
    add(...numbers); // 42
    + +

    上面代码中,array.push(...items)add(...numbers)这两行,都是函数的调用,它们的都使用了扩展运算符。该运算符将一个数组,变为参数序列。

    +

    扩展运算符与正常的函数参数可以结合使用,非常灵活。

    +
    1
    2
    3
    function f(v, w, x, y, z) {}
    const args = [0, 1];
    f(-1, ...args, 2, ...[3]);
    + +

    扩展运算符后面还可以放置表达式。

    +
    1
    const arr = [...(x > 0 ? ["a"] : []), "b"];
    + +

    如果扩展运算符后面是一个空数组,则不产生任何效果。

    +
    1
    2
    [...[], 1];
    // [1]
    + +

    替代函数的 apply 方法

    由于扩展运算符可以展开数组,所以不再需要apply方法,将数组转为函数的参数了。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // ES5 的写法
    function f(x, y, z) {
    // ...
    }
    var args = [0, 1, 2];
    f.apply(null, args);

    // ES6的写法
    function f(x, y, z) {
    // ...
    }
    let args = [0, 1, 2];
    f(...args);
    + +

    下面是扩展运算符取代apply方法的一个实际的例子,应用Math.max方法,简化求出一个数组最大元素的写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // ES5 的写法
    Math.max.apply(null, [14, 3, 77]);

    // ES6 的写法
    Math.max(...[14, 3, 77]);

    // 等同于
    Math.max(14, 3, 77);
    + +

    上面代码中,由于 JavaScript 不提供求数组最大元素的函数,所以只能套用Math.max函数,将数组转为一个参数序列,然后求最大值。有了扩展运算符以后,就可以直接用Math.max了。

    +

    另一个例子是通过push函数,将一个数组添加到另一个数组的尾部。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // ES5的 写法
    var arr1 = [0, 1, 2];
    var arr2 = [3, 4, 5];
    Array.prototype.push.apply(arr1, arr2);

    // ES6 的写法
    let arr1 = [0, 1, 2];
    let arr2 = [3, 4, 5];
    arr1.push(...arr2);
    + +

    上面代码的 ES5 写法中,push方法的参数不能是数组,所以只好通过apply方法变通使用push方法。有了扩展运算符,就可以直接将数组传入push方法。

    +

    下面是另外一个例子。

    +
    1
    2
    3
    4
    // ES5
    new (Date.bind.apply(Date, [null, 2015, 1, 1]))();
    // ES6
    new Date(...[2015, 1, 1]);
    + +

    扩展运算符的应用

    (1)复制数组

    +

    数组是复合的数据类型,直接复制的话,只是复制了指向底层数据结构的指针,而不是克隆一个全新的数组。

    +
    1
    2
    3
    4
    5
    const a1 = [1, 2];
    const a2 = a1;

    a2[0] = 2;
    a1; // [2, 2]
    + +

    上面代码中,a2并不是a1的克隆,而是指向同一份数据的另一个指针。修改a2,会直接导致a1的变化。

    +

    ES5 只能用变通方法来复制数组。

    +
    1
    2
    3
    4
    5
    const a1 = [1, 2];
    const a2 = a1.concat();

    a2[0] = 2;
    a1; // [1, 2]
    + +

    上面代码中,a1会返回原数组的克隆,再修改a2就不会对a1产生影响。

    +

    扩展运算符提供了复制数组的简便写法。

    +
    1
    2
    3
    4
    5
    const a1 = [1, 2];
    // 写法一
    const a2 = [...a1];
    // 写法二
    const [...a2] = a1;
    + +

    上面的两种写法,a2都是a1的克隆。

    +

    (2)合并数组

    +

    扩展运算符提供了数组合并的新写法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const arr1 = ["a", "b"];
    const arr2 = ["c"];
    const arr3 = ["d", "e"];

    // ES5 的合并数组
    arr1.concat(arr2, arr3);
    // [ 'a', 'b', 'c', 'd', 'e' ]

    // ES6 的合并数组
    [...arr1, ...arr2, ...arr3];
    // [ 'a', 'b', 'c', 'd', 'e' ]
    + +

    不过,这两种方法都是浅拷贝,使用的时候需要注意。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const a1 = [{ foo: 1 }];
    const a2 = [{ bar: 2 }];

    const a3 = a1.concat(a2);
    const a4 = [...a1, ...a2];

    a3[0] === a1[0]; // true
    a4[0] === a1[0]; // true
    + +

    上面代码中,a3a4是用两种不同方法合并而成的新数组,但是它们的成员都是对原数组成员的引用,这就是浅拷贝。如果修改了原数组的成员,会同步反映到新数组。

    +

    (3)与解构赋值结合

    +

    扩展运算符可以与解构赋值结合起来,用于生成数组。

    +
    1
    2
    3
    4
    // ES5
    a = list[0], rest = list.slice(1)
    // ES6
    [a, ...rest] = list
    + +

    下面是另外一些例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const [first, ...rest] = [1, 2, 3, 4, 5];
    first; // 1
    rest; // [2, 3, 4, 5]

    const [first, ...rest] = [];
    first; // undefined
    rest; // []

    const [first, ...rest] = ["foo"];
    first; // "foo"
    rest; // []
    + +

    如果将扩展运算符用于数组赋值,只能放在参数的最后一位,否则会报错。

    +
    1
    2
    3
    4
    5
    const [...butLast, last] = [1, 2, 3, 4, 5];
    // 报错

    const [first, ...middle, last] = [1, 2, 3, 4, 5];
    // 报错
    + +

    (4)字符串

    +

    扩展运算符还可以将字符串转为真正的数组。

    +
    1
    2
    [..."hello"];
    // [ "h", "e", "l", "l", "o" ]
    + +

    上面的写法,有一个重要的好处,那就是能够正确识别四个字节的 Unicode 字符。

    +
    1
    2
    'x\uD83D\uDE80y'.length // 4
    [...'x\uD83D\uDE80y'].length // 3
    + +

    上面代码的第一种写法,JavaScript 会将四个字节的 Unicode 字符,识别为 2 个字符,采用扩展运算符就没有这个问题。因此,正确返回字符串长度的函数,可以像下面这样写。

    +
    1
    2
    3
    4
    5
    function length(str) {
    return [...str].length;
    }

    length("x\uD83D\uDE80y"); // 3
    + +

    凡是涉及到操作四个字节的 Unicode 字符的函数,都有这个问题。因此,最好都用扩展运算符改写。

    +
    1
    2
    3
    4
    5
    6
    7
    let str = 'x\uD83D\uDE80y';

    str.split('').reverse().join('')
    // 'y\uDE80\uD83Dx'

    [...str].reverse().join('')
    // 'y\uD83D\uDE80x'
    + +

    上面代码中,如果不用扩展运算符,字符串的reverse操作就不正确。

    +

    (5)实现了 Iterator 接口的对象

    +

    任何 Iterator 接口的对象(参阅 Iterator 一章),都可以用扩展运算符转为真正的数组。

    +
    1
    2
    let nodeList = document.querySelectorAll("div");
    let array = [...nodeList];
    + +

    上面代码中,querySelectorAll方法返回的是一个nodeList对象。它不是数组,而是一个类似数组的对象。这时,扩展运算符可以将其转为真正的数组,原因就在于NodeList对象实现了 Iterator 。

    +

    对于那些没有部署 Iterator 接口的类似数组的对象,扩展运算符就无法将其转为真正的数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let arrayLike = {
    0: "a",
    1: "b",
    2: "c",
    length: 3,
    };

    // TypeError: Cannot spread non-iterable object.
    let arr = [...arrayLike];
    + +

    上面代码中,arrayLike是一个类似数组的对象,但是没有部署 Iterator 接口,扩展运算符就会报错。这时,可以改为使用Array.from方法将arrayLike转为真正的数组。

    +

    (6)Map 和 Set 结构,Generator 函数

    +

    扩展运算符内部调用的是数据结构的 Iterator 接口,因此只要具有 Iterator 接口的对象,都可以使用扩展运算符,比如 Map 结构。

    +
    1
    2
    3
    4
    5
    6
    7
    let map = new Map([
    [1, "one"],
    [2, "two"],
    [3, "three"],
    ]);

    let arr = [...map.keys()]; // [1, 2, 3]
    + +

    Generator 函数运行后,返回一个遍历器对象,因此也可以使用扩展运算符。

    +
    1
    2
    3
    4
    5
    6
    7
    const go = function* () {
    yield 1;
    yield 2;
    yield 3;
    };

    [...go()]; // [1, 2, 3]
    + +

    上面代码中,变量go是一个 Generator 函数,执行后返回的是一个遍历器对象,对这个遍历器对象执行扩展运算符,就会将内部遍历得到的值,转为一个数组。

    +

    如果对没有 Iterator 接口的对象,使用扩展运算符,将会报错。

    +
    1
    2
    const obj = { a: 1, b: 2 };
    let arr = [...obj]; // TypeError: Cannot spread non-iterable object
    + +

    Array.from()

    Array.from方法用于将两类对象转为真正的数组:类似数组的对象(array-like object)和可遍历(iterable)的对象(包括 ES6 新增的数据结构 Set 和 Map)。

    +

    下面是一个类似数组的对象,Array.from将它转为真正的数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    let arrayLike = {
    0: "a",
    1: "b",
    2: "c",
    length: 3,
    };

    // ES5的写法
    var arr1 = [].slice.call(arrayLike); // ['a', 'b', 'c']

    // ES6的写法
    let arr2 = Array.from(arrayLike); // ['a', 'b', 'c']
    + +

    实际应用中,常见的类似数组的对象是 DOM 操作返回的 NodeList 集合,以及函数内部的arguments对象。Array.from都可以将它们转为真正的数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // NodeList对象
    let ps = document.querySelectorAll("p");
    Array.from(ps).filter((p) => {
    return p.textContent.length > 100;
    });

    // arguments对象
    function foo() {
    var args = Array.from(arguments);
    // ...
    }
    + +

    上面代码中,querySelectorAll方法返回的是一个类似数组的对象,可以将这个对象转为真正的数组,再使用filter方法。

    +

    只要是部署了 Iterator 接口的数据结构,Array.from都能将其转为数组。

    +
    1
    2
    3
    4
    5
    Array.from("hello");
    // ['h', 'e', 'l', 'l', 'o']

    let namesSet = new Set(["a", "b"]);
    Array.from(namesSet); // ['a', 'b']
    + +

    上面代码中,字符串和 Set 结构都具有 Iterator 接口,因此可以被Array.from转为真正的数组。

    +

    如果参数是一个真正的数组,Array.from会返回一个一模一样的新数组。

    +
    1
    2
    Array.from([1, 2, 3]);
    // [1, 2, 3]
    + +

    值得提醒的是,扩展运算符(...)也可以将某些数据结构转为数组。

    +
    1
    2
    3
    4
    5
    6
    7
    // arguments对象
    function foo() {
    const args = [...arguments];
    }

    // NodeList对象
    [...document.querySelectorAll("div")];
    + +

    扩展运算符背后调用的是遍历器接口(Symbol.iterator),如果一个对象没有部署这个接口,就无法转换。Array.from方法还支持类似数组的对象。所谓类似数组的对象,本质特征只有一点,即必须有length属性。因此,任何有length属性的对象,都可以通过Array.from方法转为数组,而此时扩展运算符就无法转换。

    +
    1
    2
    Array.from({ length: 3 });
    // [ undefined, undefined, undefined ]
    + +

    上面代码中,Array.from返回了一个具有三个成员的数组,每个位置的值都是undefined。扩展运算符转换不了这个对象。

    +

    对于还没有部署该方法的浏览器,可以用Array.prototype.slice方法替代。

    +
    1
    2
    const toArray = (() =>
    Array.from ? Array.from : (obj) => [].slice.call(obj))();
    + +

    Array.from还可以接受第二个参数,作用类似于数组的map方法,用来对每个元素进行处理,将处理后的值放入返回的数组。

    +
    1
    2
    3
    4
    5
    6
    Array.from(arrayLike, (x) => x * x);
    // 等同于
    Array.from(arrayLike).map((x) => x * x);

    Array.from([1, 2, 3], (x) => x * x);
    // [1, 4, 9]
    + +

    下面的例子是取出一组 DOM 节点的文本内容。

    +
    1
    2
    3
    4
    5
    6
    7
    let spans = document.querySelectorAll("span.name");

    // map()
    let names1 = Array.prototype.map.call(spans, (s) => s.textContent);

    // Array.from()
    let names2 = Array.from(spans, (s) => s.textContent);
    + +

    下面的例子将数组中布尔值为false的成员转为0

    +
    1
    2
    Array.from([1, , 2, , 3], (n) => n || 0);
    // [1, 0, 2, 0, 3]
    + +

    另一个例子是返回各种数据的类型。

    +
    1
    2
    3
    4
    5
    function typesOf() {
    return Array.from(arguments, (value) => typeof value);
    }
    typesOf(null, [], NaN);
    // ['object', 'object', 'number']
    + +

    如果map函数里面用到了this关键字,还可以传入Array.from的第三个参数,用来绑定this

    +

    Array.from()可以将各种值转为真正的数组,并且还提供map功能。这实际上意味着,只要有一个原始的数据结构,你就可以先对它的值进行处理,然后转成规范的数组结构,进而就可以使用数量众多的数组方法。

    +
    1
    2
    Array.from({ length: 2 }, () => "jack");
    // ['jack', 'jack']
    + +

    上面代码中,Array.from的第一个参数指定了第二个参数运行的次数。这种特性可以让该方法的用法变得非常灵活。

    +

    Array.from()的另一个应用是,将字符串转为数组,然后返回字符串的长度。因为它能正确处理各种 Unicode 字符,可以避免 JavaScript 将大于\uFFFF的 Unicode 字符,算作两个字符的 bug。

    +
    1
    2
    3
    function countSymbols(string) {
    return Array.from(string).length;
    }
    + +

    Array.of()

    Array.of方法用于将一组值,转换为数组。

    +
    1
    2
    3
    Array.of(3, 11, 8); // [3,11,8]
    Array.of(3); // [3]
    Array.of(3).length; // 1
    + +

    这个方法的主要目的,是弥补数组构造函数Array()的不足。因为参数个数的不同,会导致Array()的行为有差异。

    +
    1
    2
    3
    Array(); // []
    Array(3); // [, , ,]
    Array(3, 11, 8); // [3, 11, 8]
    + +

    上面代码中,Array方法没有参数、一个参数、三个参数时,返回结果都不一样。只有当参数个数不少于 2 个时,Array()才会返回由参数组成的新数组。参数个数只有一个时,实际上是指定数组的长度。

    +

    Array.of基本上可以用来替代Array()new Array(),并且不存在由于参数不同而导致的重载。它的行为非常统一。

    +
    1
    2
    3
    4
    Array.of(); // []
    Array.of(undefined); // [undefined]
    Array.of(1); // [1]
    Array.of(1, 2); // [1, 2]
    + +

    Array.of总是返回参数值组成的数组。如果没有参数,就返回一个空数组。

    +

    Array.of方法可以用下面的代码模拟实现。

    +
    1
    2
    3
    function ArrayOf() {
    return [].slice.call(arguments);
    }
    + +

    数组实例的 copyWithin()

    数组实例的copyWithin方法,在当前数组内部,将指定位置的成员复制到其他位置(会覆盖原有成员),然后返回当前数组。也就是说,使用这个方法,会修改当前数组。

    +
    1
    Array.prototype.copyWithin(target, (start = 0), (end = this.length));
    + +

    它接受三个参数。

    +
      +
    • target(必需):从该位置开始替换数据。如果为负值,表示倒数。
    • +
    • start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
    • +
    • end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
    • +
    +

    这三个参数都应该是数值,如果不是,会自动转为数值。

    +
    1
    2
    [1, 2, 3, 4, 5].copyWithin(0, 3);
    // [4, 5, 3, 4, 5]
    + +

    上面代码表示将从 3 号位直到数组结束的成员(4 和 5),复制到从 0 号位开始的位置,结果覆盖了原来的 1 和 2。

    +

    下面是更多例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // 将3号位复制到0号位
    [1, 2, 3, 4, 5].copyWithin(0, 3, 4)
    // [4, 2, 3, 4, 5]

    // -2相当于3号位,-1相当于4号位
    [1, 2, 3, 4, 5].copyWithin(0, -2, -1)
    // [4, 2, 3, 4, 5]

    // 将3号位复制到0号位
    [].copyWithin.call({length: 5, 3: 1}, 0, 3)
    // {0: 1, 3: 1, length: 5}

    // 将2号位到数组结束,复制到0号位
    let i32a = new Int32Array([1, 2, 3, 4, 5]);
    i32a.copyWithin(0, 2);
    // Int32Array [3, 4, 5, 4, 5]

    // 对于没有部署 TypedArray 的 copyWithin 方法的平台
    // 需要采用下面的写法
    [].copyWithin.call(new Int32Array([1, 2, 3, 4, 5]), 0, 3, 4);
    // Int32Array [4, 2, 3, 4, 5]
    + +

    数组实例的 find() 和 findIndex()

    数组实例的find方法,用于找出第一个符合条件的数组成员。它的参数是一个回调函数,所有数组成员依次执行该回调函数,直到找出第一个返回值为true的成员,然后返回该成员。如果没有符合条件的成员,则返回undefined

    +
    1
    2
    [1, 4, -5, 10].find((n) => n < 0);
    // -5
    + +

    上面代码找出数组中第一个小于 0 的成员。

    +
    1
    2
    3
    [1, 5, 10, 15].find(function (value, index, arr) {
    return value > 9;
    }); // 10
    + +

    上面代码中,find方法的回调函数可以接受三个参数,依次为当前的值、当前的位置和原数组。

    +

    数组实例的findIndex方法的用法与find方法非常类似,返回第一个符合条件的数组成员的位置,如果所有成员都不符合条件,则返回-1

    +
    1
    2
    3
    [1, 5, 10, 15].findIndex(function (value, index, arr) {
    return value > 9;
    }); // 2
    + +

    这两个方法都可以接受第二个参数,用来绑定回调函数的this对象。

    +
    1
    2
    3
    4
    5
    function f(v) {
    return v > this.age;
    }
    let person = { name: "John", age: 20 };
    [10, 12, 26, 15].find(f, person); // 26
    + +

    上面的代码中,find函数接收了第二个参数person对象,回调函数中的this对象指向person对象。

    +

    另外,这两个方法都可以发现NaN,弥补了数组的indexOf方法的不足。

    +
    1
    2
    3
    4
    5
    6
    [NaN]
    .indexOf(NaN)
    // -1

    [NaN].findIndex((y) => Object.is(NaN, y));
    // 0
    + +

    上面代码中,indexOf方法无法识别数组的NaN成员,但是findIndex方法可以借助Object.is方法做到。

    +

    数组实例的 fill()

    fill方法使用给定值,填充一个数组。

    +
    1
    2
    3
    4
    5
    ["a", "b", "c"].fill(7);
    // [7, 7, 7]

    new Array(3).fill(7);
    // [7, 7, 7]
    + +

    上面代码表明,fill方法用于空数组的初始化非常方便。数组中已有的元素,会被全部抹去。

    +

    fill方法还可以接受第二个和第三个参数,用于指定填充的起始位置和结束位置。

    +
    1
    2
    ["a", "b", "c"].fill(7, 1, 2);
    // ['a', 7, 'c']
    + +

    上面代码表示,fill方法从 1 号位开始,向原数组填充 7,到 2 号位之前结束。

    +

    注意,如果填充的类型为对象,那么被赋值的是同一个内存地址的对象,而不是深拷贝对象。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let arr = new Array(3).fill({ name: "Mike" });
    arr[0].name = "Ben";
    arr;
    // [{name: "Ben"}, {name: "Ben"}, {name: "Ben"}]

    let arr = new Array(3).fill([]);
    arr[0].push(5);
    arr;
    // [[5], [5], [5]]
    + +

    数组实例的 entries(),keys() 和 values()

    ES6 提供三个新的方法——entries()keys()values()——用于遍历数组。它们都返回一个遍历器对象(详见《Iterator》一章),可以用for...of循环进行遍历,唯一的区别是keys()是对键名的遍历、values()是对键值的遍历,entries()是对键值对的遍历。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    for (let index of ["a", "b"].keys()) {
    console.log(index);
    }
    // 0
    // 1

    for (let elem of ["a", "b"].values()) {
    console.log(elem);
    }
    // 'a'
    // 'b'

    for (let [index, elem] of ["a", "b"].entries()) {
    console.log(index, elem);
    }
    // 0 "a"
    // 1 "b"
    + +

    如果不使用for...of循环,可以手动调用遍历器对象的next方法,进行遍历。

    +
    1
    2
    3
    4
    5
    let letter = ["a", "b", "c"];
    let entries = letter.entries();
    console.log(entries.next().value); // [0, 'a']
    console.log(entries.next().value); // [1, 'b']
    console.log(entries.next().value); // [2, 'c']
    + +

    数组实例的 includes()

    Array.prototype.includes方法返回一个布尔值,表示某个数组是否包含给定的值,与字符串的includes方法类似。ES2016 引入了该方法。

    +
    1
    2
    3
    4
    [1, 2, 3]
    .includes(2) // true
    [(1, 2, 3)].includes(4) // false
    [(1, 2, NaN)].includes(NaN); // true
    + +

    该方法的第二个参数表示搜索的起始位置,默认为0。如果第二个参数为负数,则表示倒数的位置,如果这时它大于数组长度(比如第二个参数为-4,但数组长度为3),则会重置为从0开始。

    +
    1
    2
    [1, 2, 3].includes(3, 3); // false
    [1, 2, 3].includes(3, -1); // true
    + +

    没有该方法之前,我们通常使用数组的indexOf方法,检查是否包含某个值。

    +
    1
    2
    3
    if (arr.indexOf(el) !== -1) {
    // ...
    }
    + +

    indexOf方法有两个缺点,一是不够语义化,它的含义是找到参数值的第一个出现位置,所以要去比较是否不等于-1,表达起来不够直观。二是,它内部使用严格相等运算符(===)进行判断,这会导致对NaN的误判。

    +
    1
    2
    [NaN].indexOf(NaN);
    // -1
    + +

    includes使用的是不一样的判断算法,就没有这个问题。

    +
    1
    2
    [NaN].includes(NaN);
    // true
    + +

    下面代码用来检查当前环境是否支持该方法,如果不支持,部署一个简易的替代版本。

    +
    1
    2
    3
    4
    5
    const contains = (() =>
    Array.prototype.includes
    ? (arr, value) => arr.includes(value)
    : (arr, value) => arr.some((el) => el === value))();
    contains(["foo", "bar"], "baz"); // => false
    + +

    另外,Map 和 Set 数据结构有一个has方法,需要注意与includes区分。

    +
      +
    • Map 结构的has方法,是用来查找键名的,比如Map.prototype.has(key)WeakMap.prototype.has(key)Reflect.has(target, propertyKey)
    • +
    • Set 结构的has方法,是用来查找值的,比如Set.prototype.has(value)WeakSet.prototype.has(value)
    • +
    +

    数组实例的 flat(),flatMap()

    数组的成员有时还是数组,Array.prototype.flat()用于将嵌套的数组“拉平”,变成一维的数组。该方法返回一个新数组,对原数据没有影响。

    +
    1
    2
    [1, 2, [3, 4]].flat();
    // [1, 2, 3, 4]
    + +

    上面代码中,原数组的成员里面有一个数组,flat()方法将子数组的成员取出来,添加在原来的位置。

    +

    flat()默认只会“拉平”一层,如果想要“拉平”多层的嵌套数组,可以将flat()方法的参数写成一个整数,表示想要拉平的层数,默认为 1。

    +
    1
    2
    3
    4
    5
    6
    [1, 2, [3, [4, 5]]].flat()[
    // [1, 2, 3, [4, 5]]

    (1, 2, [3, [4, 5]])
    ].flat(2);
    // [1, 2, 3, 4, 5]
    + +

    上面代码中,flat()的参数为 2,表示要“拉平”两层的嵌套数组。

    +

    如果不管有多少层嵌套,都要转成一维数组,可以用Infinity关键字作为参数。

    +
    1
    2
    [1, [2, [3]]].flat(Infinity);
    // [1, 2, 3]
    + +

    如果原数组有空位,flat()方法会跳过空位。

    +
    1
    2
    [1, 2, , 4, 5].flat();
    // [1, 2, 4, 5]
    + +

    flatMap()方法对原数组的每个成员执行一个函数(相当于执行Array.prototype.map()),然后对返回值组成的数组执行flat()方法。该方法返回一个新数组,不改变原数组。

    +
    1
    2
    3
    // 相当于 [[2, 4], [3, 6], [4, 8]].flat()
    [2, 3, 4].flatMap((x) => [x, x * 2]);
    // [2, 4, 3, 6, 4, 8]
    + +

    flatMap()只能展开一层数组。

    +
    1
    2
    3
    // 相当于 [[[2]], [[4]], [[6]], [[8]]].flat()
    [1, 2, 3, 4].flatMap((x) => [[x * 2]]);
    // [[2], [4], [6], [8]]
    + +

    上面代码中,遍历函数返回的是一个双层的数组,但是默认只能展开一层,因此flatMap()返回的还是一个嵌套数组。

    +

    flatMap()方法的参数是一个遍历函数,该函数可以接受三个参数,分别是当前数组成员、当前数组成员的位置(从零开始)、原数组。

    +
    1
    2
    3
    arr.flatMap(function callback(currentValue[, index[, array]]) {
    // ...
    }[, thisArg])
    + +

    flatMap()方法还可以有第二个参数,用来绑定遍历函数里面的this

    +

    数组的空位

    数组的空位指,数组的某一个位置没有任何值。比如,Array构造函数返回的数组都是空位。

    +
    1
    Array(3); // [, , ,]
    + +

    上面代码中,Array(3)返回一个具有 3 个空位的数组。

    +

    注意,空位不是undefined,一个位置的值等于undefined,依然是有值的。空位是没有任何值,in运算符可以说明这一点。

    +
    1
    2
    0 in [undefined, undefined, undefined]; // true
    0 in [, , ,]; // false
    + +

    上面代码说明,第一个数组的 0 号位置是有值的,第二个数组的 0 号位置没有值。

    +

    ES5 对空位的处理,已经很不一致了,大多数情况下会忽略空位。

    +
      +
    • forEach(), filter(), reduce(), every()some()都会跳过空位。
    • +
    • map()会跳过空位,但会保留这个值
    • +
    • join()toString()会将空位视为undefined,而undefinednull会被处理成空字符串。
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    // forEach方法
    [,'a'].forEach((x,i) => console.log(i)); // 1

    // filter方法
    ['a',,'b'].filter(x => true) // ['a','b']

    // every方法
    [,'a'].every(x => x==='a') // true

    // reduce方法
    [1,,2].reduce((x,y) => x+y) // 3

    // some方法
    [,'a'].some(x => x !== 'a') // false

    // map方法
    [,'a'].map(x => 1) // [,1]

    // join方法
    [,'a',undefined,null].join('#') // "#a##"

    // toString方法
    [,'a',undefined,null].toString() // ",a,,"
    + +

    ES6 则是明确将空位转为undefined

    +

    Array.from方法会将数组的空位,转为undefined,也就是说,这个方法不会忽略空位。

    +
    1
    2
    Array.from(["a", , "b"]);
    // [ "a", undefined, "b" ]
    + +

    扩展运算符(...)也会将空位转为undefined

    +
    1
    2
    [...["a", , "b"]];
    // [ "a", undefined, "b" ]
    + +

    copyWithin()会连空位一起拷贝。

    +
    1
    [, "a", "b", ,].copyWithin(2, 0); // [,"a",,"a"]
    + +

    fill()会将空位视为正常的数组位置。

    +
    1
    new Array(3).fill("a"); // ["a","a","a"]
    + +

    for...of循环也会遍历空位。

    +
    1
    2
    3
    4
    5
    6
    let arr = [, ,];
    for (let i of arr) {
    console.log(1);
    }
    // 1
    // 1
    + +

    上面代码中,数组arr有两个空位,for...of并没有忽略它们。如果改成map方法遍历,空位是会跳过的。

    +

    entries()keys()values()find()findIndex()会将空位处理成undefined

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // entries()
    [...[,'a'].entries()] // [[0,undefined], [1,"a"]]

    // keys()
    [...[,'a'].keys()] // [0,1]

    // values()
    [...[,'a'].values()] // [undefined,"a"]

    // find()
    [,'a'].find(x => true) // undefined

    // findIndex()
    [,'a'].findIndex(x => true) // 0
    + +

    由于空位的处理规则非常不统一,所以建议避免出现空位。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E6%95%B0%E7%BB%84%E7%9A%84%E6%89%A9%E5%B1%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\346\234\200\346\226\260\346\217\220\346\241\210/index.html" "b/posts/ES6-\346\234\200\346\226\260\346\217\220\346\241\210/index.html" new file mode 100644 index 00000000..4516bccf --- /dev/null +++ "b/posts/ES6-\346\234\200\346\226\260\346\217\220\346\241\210/index.html" @@ -0,0 +1,465 @@ +最新提案 | JCAlways + + + + + + + + + + + + + +

    最新提案

    最新提案

    本章介绍一些尚未进入标准、但很有希望的最新提案。

    +

    do 表达式

    本质上,块级作用域是一个语句,将多个操作封装在一起,没有返回值。

    +
    1
    2
    3
    4
    {
    let t = f();
    t = t * t + 1;
    }
    + +

    上面代码中,块级作用域将两个语句封装在一起。但是,在块级作用域以外,没有办法得到t的值,因为块级作用域不返回值,除非t是全局变量。

    +

    现在有一个提案,使得块级作用域可以变为表达式,也就是说可以返回值,办法就是在块级作用域之前加上do,使它变为do表达式,然后就会返回内部最后执行的表达式的值。

    +
    1
    2
    3
    4
    let x = do {
    let t = f();
    t * t + 1;
    };
    + +

    上面代码中,变量x会得到整个块级作用域的返回值(t * t + 1)。

    +

    do表达式的逻辑非常简单:封装的是什么,就会返回什么。

    +
    1
    2
    3
    4
    5
    // 等同于 <表达式>
    do { <表达式>; }

    // 等同于 <语句>
    do { <语句> }
    + +

    do表达式的好处是可以封装多个语句,让程序更加模块化,就像乐高积木那样一块块拼装起来。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let x = do {
    if (foo()) {
    f();
    } else if (bar()) {
    g();
    } else {
    h();
    }
    };
    + +

    上面代码的本质,就是根据函数foo的执行结果,调用不同的函数,将返回结果赋给变量x。使用do表达式,就将这个操作的意图表达得非常简洁清晰。而且,do块级作用域提供了单独的作用域,内部操作可以与全局作用域隔绝。

    +

    值得一提的是,do表达式在 JSX 语法中非常好用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    return (
    <nav>
    <Home />
    {do {
    if (loggedIn) {
    <LogoutButton />;
    } else {
    <LoginButton />;
    }
    }}
    </nav>
    );
    + +

    上面代码中,如果不用do表达式,就只能用三元判断运算符(?:)。那样的话,一旦判断逻辑复杂,代码就会变得很不易读。

    +

    throw 表达式

    JavaScript 语法规定throw是一个命令,用来抛出错误,不能用于表达式之中。

    +
    1
    2
    // 报错
    console.log(throw new Error());
    + +

    上面代码中,console.log的参数必须是一个表达式,如果是一个throw语句就会报错。

    +

    现在有一个提案,允许throw用于表达式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // 参数的默认值
    function save(filename = throw new TypeError("Argument required")) {}

    // 箭头函数的返回值
    lint(ast, {
    with: () => throw new Error("avoid using 'with' statements."),
    });

    // 条件表达式
    function getEncoder(encoding) {
    const encoder =
    encoding === "utf8"
    ? new UTF8Encoder()
    : encoding === "utf16le"
    ? new UTF16Encoder(false)
    : encoding === "utf16be"
    ? new UTF16Encoder(true)
    : throw new Error("Unsupported encoding");
    }

    // 逻辑表达式
    class Product {
    get id() {
    return this._id;
    }
    set id(value) {
    this._id = value || throw new Error("Invalid value");
    }
    }
    + +

    上面代码中,throw都出现在表达式里面。

    +

    语法上,throw表达式里面的throw不再是一个命令,而是一个运算符。为了避免与throw命令混淆,规定throw出现在行首,一律解释为throw语句,而不是throw表达式。

    +

    链判断运算符

    编程实务中,如果读取对象内部的某个属性,往往需要判断一下该对象是否存在。比如,要读取message.body.user.firstName,安全的写法是写成下面这样。

    +
    1
    2
    3
    4
    5
    6
    const firstName =
    (message &&
    message.body &&
    message.body.user &&
    message.body.user.firstName) ||
    "default";
    + +

    这样的层层判断非常麻烦,因此现在有一个提案,引入了“链判断运算符”(optional chaining operator)?.,简化上面的写法。

    +
    1
    const firstName = message?.body?.user?.firstName || "default";
    + +

    上面代码有三个?.运算符,直接在链式调用的时候判断,左侧的对象是否为nullundefined。如果是的,就不再往下运算,而是返回undefined

    +

    链判断运算符号有三种用法。

    +
      +
    • obj?.prop // 读取对象属性
    • +
    • obj?.[expr] // 同上
    • +
    • func?.(...args) // 函数或对象方法的调用
    • +
    +

    下面是判断函数是否存在的例子。

    +
    1
    iterator.return?.();
    + +

    上面代码中,iterator.return如果有定义,就会调用该方法,否则直接返回undefined

    +

    下面是更多的例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    a?.b;
    // 等同于
    a == null ? undefined : a.b;

    a?.[x];
    // 等同于
    a == null ? undefined : a[x];

    a?.b();
    // 等同于
    a == null ? undefined : a.b();

    a?.();
    // 等同于
    a == null ? undefined : a();
    + +

    使用这个运算符,有几个注意点。

    +

    (1)短路机制

    +
    1
    2
    3
    a?.[++x];
    // 等同于
    a == null ? undefined : a[++x];
    + +

    上面代码中,如果aundefinednull,那么x不会进行递增运算。也就是说,链判断运算符一旦为真,右侧的表达式就不再求值。

    +

    (2)delete 运算符

    +
    1
    2
    3
    delete a?.b;
    // 等同于
    a == null ? undefined : delete a.b;
    + +

    上面代码中,如果aundefinednull,会直接返回undefined,而不会进行delete运算。

    +

    (3)报错场合

    +

    以下写法是禁止,会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 构造函数判断
    new a?.()

    // 运算符右侧是模板字符串
    a?.`{b}`

    // 链判断运算符前后有构造函数或模板字符串
    new a?.b()
    a?.b`{c}`

    // 链运算符用于赋值运算符左侧
    a?.b = c
    + +

    (4)右侧不得为十进制数值

    +

    为了保证兼容以前的代码,允许foo?.3:0被解析成foo ? .3 : 0,因此规定如果?.后面紧跟一个十进制数字,那么?.不再被看成是一个完整的运算符,而会按照三元运算符进行处理,也就是说,那个小数点会归属于后面的十进制数字,形成一个小数。

    +

    直接输入 U+2028 和 U+2029

    JavaScript 字符串允许直接输入字符,以及输入字符的转义形式。举例来说,“中”的 Unicode 码点是 U+4e2d,你可以直接在字符串里面输入这个汉字,也可以输入它的转义形式\u4e2d,两者是等价的。

    +
    1
    "中" === "\u4e2d"; // true
    + +

    但是,JavaScript 规定有 5 个字符,不能在字符串里面直接使用,只能使用转义形式。

    +
      +
    • U+005C:反斜杠(reverse solidus)
    • +
    • U+000D:回车(carriage return)
    • +
    • U+2028:行分隔符(line separator)
    • +
    • U+2029:段分隔符(paragraph separator)
    • +
    • U+000A:换行符(line feed)
    • +
    +

    举例来说,字符串里面不能直接包含反斜杠,一定要转义写成\\或者\u005c

    +

    这个规定本身没有问题,麻烦在于 JSON 格式允许字符串里面直接使用 U+2028(行分隔符)和 U+2029(段分隔符)。这样一来,服务器输出的 JSON 被JSON.parse解析,就有可能直接报错。

    +

    JSON 格式已经冻结(RFC 7159),没法修改了。为了消除这个报错,现在有一个提案,允许 JavaScript 字符串直接输入 U+2028(行分隔符)和 U+2029(段分隔符)。

    +
    1
    const PS = eval("'\u2029'");
    + +

    根据这个提案,上面的代码不会报错。

    +

    注意,模板字符串现在就允许直接输入这两个字符。另外,正则表达式依然不允许直接输入这两个字符,这是没有问题的,因为 JSON 本来就不允许直接包含正则表达式。

    +

    函数的部分执行

    语法

    多参数的函数有时需要绑定其中的一个或多个参数,然后返回一个新函数。

    +
    1
    2
    3
    4
    5
    6
    function add(x, y) {
    return x + y;
    }
    function add7(x) {
    return x + 7;
    }
    + +

    上面代码中,add7函数其实是add函数的一个特殊版本,通过将一个参数绑定为7,就可以从add得到add7

    +
    1
    2
    3
    4
    5
    // bind 方法
    const add7 = add.bind(null, 7);

    // 箭头函数
    const add7 = (x) => add(x, 7);
    + +

    上面两种写法都有些冗余。其中,bind方法的局限更加明显,它必须提供this,并且只能从前到后一个个绑定参数,无法只绑定非头部的参数。

    +

    现在有一个提案,使得绑定参数并返回一个新函数更加容易。这叫做函数的部分执行(partial application)。

    +
    1
    2
    3
    4
    const add = (x, y) => x + y;
    const addOne = add(1, ?);

    const maxGreaterThanZero = Math.max(0, ...);
    + +

    根据新提案,?是单个参数的占位符,...是多个参数的占位符。以下的形式都属于函数的部分执行。

    +
    1
    2
    3
    4
    5
    6
    f(x, ?)
    f(x, ...)
    f(?, x)
    f(..., x)
    f(?, x, ?)
    f(..., x, ...)
    + +

    ?...只能出现在函数的调用之中,并且会返回一个新函数。

    +
    1
    2
    3
    const g = f(?, 1, ...);
    // 等同于
    const g = (x, ...y) => f(x, 1, ...y);
    + +

    函数的部分执行,也可以用于对象的方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    let obj = {
    f(x, y) {
    return x + y;
    },
    };

    const g = obj.f(?, 3);
    g(1); // 4
    + +

    注意点

    函数的部分执行有一些特别注意的地方。

    +

    (1)函数的部分执行是基于原函数的。如果原函数发生变化,部分执行生成的新函数也会立即反映这种变化。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let f = (x, y) => x + y;

    const g = f(?, 3);
    g(1); // 4

    // 替换函数 f
    f = (x, y) => x * y;

    g(1); // 3
    + +

    上面代码中,定义了函数的部分执行以后,更换原函数会立即影响到新函数。

    +

    (2)如果预先提供的那个值是一个表达式,那么这个表达式并不会在定义时求值,而是在每次调用时求值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    let a = 3;
    const f = (x, y) => x + y;

    const g = f(?, a);
    g(1); // 4

    // 改变 a 的值
    a = 10;
    g(1); // 11
    + +

    上面代码中,预先提供的参数是变量a,那么每次调用函数g的时候,才会对a进行求值。

    +

    (3)如果新函数的参数多于占位符的数量,那么多余的参数将被忽略。

    +
    1
    2
    3
    const f = (x, ...y) => [x, ...y];
    const g = f(?, 1);
    g(2, 3, 4); // [2, 1]
    + +

    上面代码中,函数g只有一个占位符,也就意味着它只能接受一个参数,多余的参数都会被忽略。

    +

    写成下面这样,多余的参数就没有问题。

    +
    1
    2
    3
    const f = (x, ...y) => [x, ...y];
    const g = f(?, 1, ...);
    g(2, 3, 4); // [2, 1, 3, 4];
    + +

    (4)...只会被采集一次,如果函数的部分执行使用了多个...,那么每个...的值都将相同。

    +
    1
    2
    3
    const f = (...x) => x;
    const g = f(..., 9, ...);
    g(1, 2, 3); // [1, 2, 3, 9, 1, 2, 3]
    + +

    上面代码中,g定义了两个...占位符,真正执行的时候,它们的值是一样的。

    +

    管道运算符

    Unix 操作系统有一个管道机制(pipeline),可以把前一个操作的值传给后一个操作。这个机制非常有用,使得简单的操作可以组合成为复杂的操作。许多语言都有管道的实现,现在有一个提案,让 JavaScript 也拥有管道机制。

    +

    JavaScript 的管道是一个运算符,写作|>。它的左边是一个表达式,右边是一个函数。管道运算符把左边表达式的值,传入右边的函数进行求值。

    +
    1
    2
    3
    x |> f;
    // 等同于
    f(x);
    + +

    管道运算符最大的好处,就是可以把嵌套的函数,写成从左到右的链式表达式。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    function doubleSay(str) {
    return str + ", " + str;
    }

    function capitalize(str) {
    return str[0].toUpperCase() + str.substring(1);
    }

    function exclaim(str) {
    return str + "!";
    }
    + +

    上面是三个简单的函数。如果要嵌套执行,传统的写法和管道的写法分别如下。

    +
    1
    2
    3
    4
    5
    6
    7
    // 传统的写法
    exclaim(capitalize(doubleSay("hello")));
    // "Hello, hello!"

    // 管道的写法
    "hello" |> doubleSay |> capitalize |> exclaim;
    // "Hello, hello!"
    + +

    管道运算符只能传递一个值,这意味着它右边的函数必须是一个单参数函数。如果是多参数函数,就必须进行柯里化,改成单参数的版本。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    function double(x) {
    return x + x;
    }
    function add(x, y) {
    return x + y;
    }

    let person = { score: 25 };
    person.score |> double |> ((_) => add(7, _));
    // 57
    + +

    上面代码中,add函数需要两个参数。但是,管道运算符只能传入一个值,因此需要事先提供另一个参数,并将其改成单参数的箭头函数_ => add(7, _)。这个函数里面的下划线并没有特别的含义,可以用其他符号代替,使用下划线只是因为,它能够形象地表示这里是占位符。

    +

    管道运算符对于await函数也适用。

    +
    1
    2
    3
    4
    5
    6
    7
    x |> (await f);
    // 等同于
    await f(x);

    const userAge = userId |> (await fetchUserById) |> getAgeFromUser;
    // 等同于
    const userAge = getAgeFromUser(await fetchUserById(userId));
    + +

    数值分隔符

    欧美语言中,较长的数值允许每三位添加一个分隔符(通常是一个逗号),增加数值的可读性。比如,1000可以写作1,000

    +

    现在有一个提案,允许 JavaScript 的数值使用下划线(_)作为分隔符。

    +
    1
    2
    let budget = 1_000_000_000_000;
    budget === 10 ** 12; // true
    + +

    JavaScript 的数值分隔符没有指定间隔的位数,也就是说,可以每三位添加一个分隔符,也可以每一位、每两位、每四位添加一个。

    +
    1
    2
    3
    4
    123_00 === 12_300; // true

    12345_00 === 123_4500; // true
    12345_00 === 1_234_500; // true
    + +

    小数和科学计数法也可以使用数值分隔符。

    +
    1
    2
    3
    4
    // 小数
    0.000_001;
    // 科学计数法
    1e10_000;
    + +

    数值分隔符有几个使用注意点。

    +
      +
    • 不能在数值的最前面(leading)或最后面(trailing)。
    • +
    • 不能两个或两个以上的分隔符连在一起。
    • +
    • 小数点的前后不能有分隔符。
    • +
    • 科学计数法里面,表示指数的eE前后不能有分隔符。
    • +
    +

    下面的写法都会报错。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 全部报错
    3_.141;
    3._141;
    1_e12;
    1e_12;
    123__456;
    _1464301;
    1464301_;
    + +

    除了十进制,其他进制的数值也可以使用分隔符。

    +
    1
    2
    3
    4
    // 二进制
    0b1010_0001_1000_0101;
    // 十六进制
    0xa0_b0_c0;
    + +

    注意,分隔符不能紧跟着进制的前缀0b0B0o0O0x0X

    +
    1
    2
    3
    // 报错
    0_b111111000
    0b_111111000
    + +

    下面三个将字符串转成数值的函数,不支持数值分隔符。主要原因是提案的设计者认为,数值分隔符主要是为了编码时书写数值的方便,而不是为了处理外部输入的数据。

    +
      +
    • Number()
    • +
    • parseInt()
    • +
    • parseFloat()
    • +
    +
    1
    2
    Number("123_456"); // NaN
    parseInt("123_456"); // 123
    + +

    BigInt 数据类型

    简介

    JavaScript 所有数字都保存成 64 位浮点数,这给数值的表示带来了两大限制。一是数值的精度只能到 53 个二进制位(相当于 16 个十进制位),大于这个范围的整数,JavaScript 是无法精确表示的,这使得 JavaScript 不适合进行科学和金融方面的精确计算。二是大于或等于 2 的 1024 次方的数值,JavaScript 无法表示,会返回Infinity

    +
    1
    2
    3
    4
    5
    // 超过 53 个二进制位的数值,无法保持精度
    Math.pow(2, 53) === Math.pow(2, 53) + 1; // true

    // 超过 2 的 1024 次方的数值,无法表示
    Math.pow(2, 1024); // Infinity
    + +

    现在有一个提案,引入了一种新的数据类型 BigInt(大整数),来解决这个问题。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

    +
    1
    2
    3
    4
    const a = 2172141653n;
    const b = 15346349309n;
    a * b; // 33334444555566667777n
    Number(a) * Number(b); // 33334444555566670000
    + +

    为了与 Number 类型区别,BigInt 类型的数据必须使用后缀n表示。

    +
    1
    2
    1234n;
    1n + 2n; // 3n
    + +

    BigInt 同样可以使用各种进制表示,都要加上后缀n

    +
    1
    2
    3
    0b1101n; // 二进制
    0o777n; // 八进制
    0xffn; // 十六进制
    + +

    typeof运算符对于 BigInt 类型的数据返回bigint

    +
    1
    typeof 123n; // 'bigint'
    + +

    BigInt 对象

    JavaScript 原生提供BigInt对象,可以用作构造函数生成 BigInt 类型的数值。转换规则基本与Number()一致,将别的类型的值转为 BigInt。

    +
    1
    2
    3
    4
    BigInt(123); // 123n
    BigInt("123"); // 123n
    BigInt(false); // 0n
    BigInt(true); // 1n
    + +

    BigInt构造函数必须有参数,而且参数必须可以正常转为数值,下面的用法都会报错。

    +
    1
    2
    3
    4
    5
    new BigInt(); // TypeError
    BigInt(undefined); //TypeError
    BigInt(null); // TypeError
    BigInt("123n"); // SyntaxError
    BigInt("abc"); // SyntaxError
    + +

    上面代码中,尤其值得注意字符串123n无法解析成 Number 类型,所以会报错。

    +

    BigInt 对象继承了 Object 提供的实例方法。

    +
      +
    • BigInt.prototype.toLocaleString()
    • +
    • BigInt.prototype.toString()
    • +
    • BigInt.prototype.valueOf()
    • +
    +

    此外,还提供了三个静态方法。

    +
      +
    • BigInt.asUintN(width, BigInt): 对给定的大整数,返回 0 到 2width - 1 之间的大整数形式。
    • +
    • BigInt.asIntN(width, BigInt):对给定的大整数,返回 -2width - 1 到 2width - 1 - 1 之间的大整数形式。
    • +
    • BigInt.parseInt(string[, radix]):近似于Number.parseInt,将一个字符串转换成指定进制的大整数。
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 将一个大整数转为 64 位整数的形式
    const int64a = BigInt.asUintN(64, 12345n);

    // Number.parseInt 与 BigInt.parseInt 的对比
    Number.parseInt("9007199254740993", 10);
    // 9007199254740992
    BigInt.parseInt("9007199254740993", 10);
    // 9007199254740993n
    + +

    上面代码中,由于有效数字超出了最大限度,Number.parseInt方法返回的结果是不精确的,而BigInt.parseInt方法正确返回了对应的大整数。

    +

    对于二进制数组,BigInt 新增了两个类型BigUint64ArrayBigInt64Array,这两种数据类型返回的都是大整数。DataView对象的实例方法DataView.prototype.getBigInt64DataView.prototype.getBigUint64,返回的也是大整数。

    +

    运算

    数学运算方面,BigInt 类型的+-***这四个二元运算符,与 Number 类型的行为一致。除法运算/会舍去小数部分,返回一个整数。

    +
    1
    2
    9n / 5n;
    // 1n
    + +

    几乎所有的 Number 运算符都可以用在 BigInt,但是有两个除外:不带符号的右移位运算符>>>和一元的求正运算符+,使用时会报错。前者是因为>>>运算符是不带符号的,但是 BigInt 总是带有符号的,导致该运算无意义,完全等同于右移运算符>>。后者是因为一元运算符+在 asm.js 里面总是返回 Number 类型,为了不破坏 asm.js 就规定+1n会报错。

    +

    Integer 类型不能与 Number 类型进行混合运算。

    +
    1
    1n + 1.3; // 报错
    + +

    上面代码报错是因为无论返回的是 BigInt 或 Number,都会导致丢失信息。比如(2n**53n + 1n) + 0.5这个表达式,如果返回 BigInt 类型,0.5这个小数部分会丢失;如果返回 Number 类型,有效精度只能保持 53 位,导致精度下降。

    +

    同样的原因,如果一个标准库函数的参数预期是 Number 类型,但是得到的是一个 BigInt,就会报错。

    +
    1
    2
    3
    4
    5
    // 错误的写法
    Math.sqrt(4n); // 报错

    // 正确的写法
    Math.sqrt(Number(4n)); // 2
    + +

    上面代码中,Math.sqrt的参数预期是 Number 类型,如果是 BigInt 就会报错,必须先用Number方法转一下类型,才能进行计算。

    +

    asm.js 里面,|0跟在一个数值的后面会返回一个 32 位整数。根据不能与 Number 类型混合运算的规则,BigInt 如果与|0进行运算会报错。

    +
    1
    1n | 0; // 报错
    + +

    比较运算符(比如>)和相等运算符(==)允许 BigInt 与其他类型的值混合计算,因为这样做不会损失精度。

    +
    1
    2
    3
    4
    0n < 1; // true
    0n < true; // true
    0n == 0; // true
    0n == false; // true
    + +

    同理,精确相等运算符(===)也可以混合使用。

    +
    1
    0n === 0; // false
    + +

    上面代码中,由于0n0的数据类型不同,所以返回false

    +

    大整数可以转为其他数据类型。

    +
    1
    2
    3
    4
    5
    6
    7
    Boolean(0n); // false
    Boolean(1n); // true
    Number(1n); // 1
    String(1n); // "1"

    !0n; // true
    !1n; // false
    + +

    大整数也可以与字符串混合运算。

    +
    1
    "" + 123n; // "123"
    + +

    Math.signbit()

    Math.sign()用来判断一个值的正负,但是如果参数是-0,它会返回-0

    +
    1
    Math.sign(-0); // -0
    + +

    这导致对于判断符号位的正负,Math.sign()不是很有用。JavaScript 内部使用 64 位浮点数(国际标准 IEEE 754)表示数值,IEEE 754 规定第一位是符号位,0表示正数,1表示负数。所以会有两种零,+0是符号位为0时的零值,-0是符号位为1时的零值。实际编程中,判断一个值是+0还是-0非常麻烦,因为它们是相等的。

    +
    1
    +0 === -0; // true
    + +

    目前,有一个提案,引入了Math.signbit()方法判断一个数的符号位是否设置了。

    +
    1
    2
    3
    4
    Math.signbit(2); //false
    Math.signbit(-2); //true
    Math.signbit(0); //false
    Math.signbit(-0); //true
    + +

    可以看到,该方法正确返回了-0的符号位是设置了的。

    +

    该方法的算法如下。

    +
      +
    • 如果参数是NaN,返回false
    • +
    • 如果参数是-0,返回true
    • +
    • 如果参数是负值,返回true
    • +
    • 其他情况返回false
    • +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E6%9C%80%E6%96%B0%E6%8F%90%E6%A1%88/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\346\255\243\345\210\231\347\232\204\346\211\251\345\261\225/index.html" "b/posts/ES6-\346\255\243\345\210\231\347\232\204\346\211\251\345\261\225/index.html" new file mode 100644 index 00000000..5c1f947f --- /dev/null +++ "b/posts/ES6-\346\255\243\345\210\231\347\232\204\346\211\251\345\261\225/index.html" @@ -0,0 +1,419 @@ +正则的扩展 | JCAlways + + + + + + + + + + + + + +

    正则的扩展

    正则的扩展

    RegExp 构造函数

    在 ES5 中,RegExp构造函数的参数有两种情况。

    +

    第一种情况是,参数是字符串,这时第二个参数表示正则表达式的修饰符(flag)。

    +
    1
    2
    3
    var regex = new RegExp("xyz", "i");
    // 等价于
    var regex = /xyz/i;
    + +

    第二种情况是,参数是一个正则表示式,这时会返回一个原有正则表达式的拷贝。

    +
    1
    2
    3
    var regex = new RegExp(/xyz/i);
    // 等价于
    var regex = /xyz/i;
    + +

    但是,ES5 不允许此时使用第二个参数添加修饰符,否则会报错。

    +
    1
    2
    var regex = new RegExp(/xyz/, "i");
    // Uncaught TypeError: Cannot supply flags when constructing one RegExp from another
    + +

    ES6 改变了这种行为。如果RegExp构造函数第一个参数是一个正则对象,那么可以使用第二个参数指定修饰符。而且,返回的正则表达式会忽略原有的正则表达式的修饰符,只使用新指定的修饰符。

    +
    1
    2
    new RegExp(/abc/gi, "i").flags;
    // "i"
    + +

    上面代码中,原有正则对象的修饰符是ig,它会被第二个参数i覆盖。

    +

    字符串的正则方法

    字符串对象共有 4 个方法,可以使用正则表达式:match()replace()search()split()

    +

    ES6 将这 4 个方法,在语言内部全部调用RegExp的实例方法,从而做到所有与正则相关的方法,全都定义在RegExp对象上。

    +
      +
    • String.prototype.match 调用 RegExp.prototype[Symbol.match]
    • +
    • String.prototype.replace 调用 RegExp.prototype[Symbol.replace]
    • +
    • String.prototype.search 调用 RegExp.prototype[Symbol.search]
    • +
    • String.prototype.split 调用 RegExp.prototype[Symbol.split]
    • +
    +

    u 修饰符

    ES6 对正则表达式添加了u修饰符,含义为“Unicode 模式”,用来正确处理大于\uFFFF的 Unicode 字符。也就是说,会正确处理四个字节的 UTF-16 编码。

    +
    1
    2
    /^\uD83D/u.test('\uD83D\uDC2A') // false
    /^\uD83D/.test('\uD83D\uDC2A') // true
    + +

    上面代码中,\uD83D\uDC2A是一个四个字节的 UTF-16 编码,代表一个字符。但是,ES5 不支持四个字节的 UTF-16 编码,会将其识别为两个字符,导致第二行代码结果为true。加了u修饰符以后,ES6 就会识别其为一个字符,所以第一行代码结果为false

    +

    一旦加上u修饰符号,就会修改下面这些正则表达式的行为。

    +

    (1)点字符

    +

    点(.)字符在正则表达式中,含义是除了换行符以外的任意单个字符。对于码点大于0xFFFF的 Unicode 字符,点字符不能识别,必须加上u修饰符。

    +
    1
    2
    3
    4
    var s = '𠮷';

    /^.$/.test(s) // false
    /^.$/u.test(s) // true
    + +

    上面代码表示,如果不添加u修饰符,正则表达式就会认为字符串为两个字符,从而匹配失败。

    +

    (2)Unicode 字符表示法

    +

    ES6 新增了使用大括号表示 Unicode 字符,这种表示法在正则表达式中必须加上u修饰符,才能识别当中的大括号,否则会被解读为量词。

    +
    1
    2
    3
    4
    5
    /\u{61}/.test("a") / // false
    a /
    u.test("a") / // true
    𠮷 /
    u.test("𠮷"); // true
    + +

    上面代码表示,如果不加u修饰符,正则表达式无法识别\u{61}这种表示法,只会认为这匹配 61 个连续的u

    +

    (3)量词

    +

    使用u修饰符后,所有量词都会正确识别码点大于0xFFFF的 Unicode 字符。

    +
    1
    2
    3
    4
    /a{2}/.test('aa') // true
    /a{2}/u.test('aa') // true
    /𠮷{2}/.test('𠮷𠮷') // false
    /𠮷{2}/u.test('𠮷𠮷') // true
    + +

    (4)预定义模式

    +

    u修饰符也影响到预定义模式,能否正确识别码点大于0xFFFF的 Unicode 字符。

    +
    1
    2
    /^\S$/.test('𠮷') // false
    /^\S$/u.test('𠮷') // true
    + +

    上面代码的\S是预定义模式,匹配所有非空白字符。只有加了u修饰符,它才能正确匹配码点大于0xFFFF的 Unicode 字符。

    +

    利用这一点,可以写出一个正确返回字符串长度的函数。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function codePointLength(text) {
    var result = text.match(/[\s\S]/gu);
    return result ? result.length : 0;
    }

    var s = "𠮷𠮷";

    s.length; // 4
    codePointLength(s); // 2
    + +

    (5)i 修饰符

    +

    有些 Unicode 字符的编码不同,但是字型很相近,比如,\u004B\u212A都是大写的K

    +
    1
    2
    3
    /[a-z]/i.test("\u212A") / // false
    [a - z] /
    iu.test("\u212A"); // true
    + +

    上面代码中,不加u修饰符,就无法识别非规范的K字符。

    +

    RegExp.prototype.unicode 属性

    正则实例对象新增unicode属性,表示是否设置了u修饰符。

    +
    1
    2
    3
    4
    5
    const r1 = /hello/;
    const r2 = /hello/u;

    r1.unicode; // false
    r2.unicode; // true
    + +

    上面代码中,正则表达式是否设置了u修饰符,可以从unicode属性看出来。

    +

    y 修饰符

    除了u修饰符,ES6 还为正则表达式添加了y修饰符,叫做“粘连”(sticky)修饰符。

    +

    y修饰符的作用与g修饰符类似,也是全局匹配,后一次匹配都从上一次匹配成功的下一个位置开始。不同之处在于,g修饰符只要剩余位置中存在匹配就可,而y修饰符确保匹配必须从剩余的第一个位置开始,这也就是“粘连”的涵义。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var s = "aaa_aa_a";
    var r1 = /a+/g;
    var r2 = /a+/y;

    r1.exec(s); // ["aaa"]
    r2.exec(s); // ["aaa"]

    r1.exec(s); // ["aa"]
    r2.exec(s); // null
    + +

    上面代码有两个正则表达式,一个使用g修饰符,另一个使用y修饰符。这两个正则表达式各执行了两次,第一次执行的时候,两者行为相同,剩余字符串都是_aa_a。由于g修饰没有位置要求,所以第二次执行会返回结果,而y修饰符要求匹配必须从头部开始,所以返回null

    +

    如果改一下正则表达式,保证每次都能头部匹配,y修饰符就会返回结果了。

    +
    1
    2
    3
    4
    5
    var s = "aaa_aa_a";
    var r = /a+_/y;

    r.exec(s); // ["aaa_"]
    r.exec(s); // ["aa_"]
    + +

    上面代码每次匹配,都是从剩余字符串的头部开始。

    +

    使用lastIndex属性,可以更好地说明y修饰符。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const REGEX = /a/g;

    // 指定从2号位置(y)开始匹配
    REGEX.lastIndex = 2;

    // 匹配成功
    const match = REGEX.exec("xaya");

    // 在3号位置匹配成功
    match.index; // 3

    // 下一次匹配从4号位开始
    REGEX.lastIndex; // 4

    // 4号位开始匹配失败
    REGEX.exec("xaya"); // null
    + +

    上面代码中,lastIndex属性指定每次搜索的开始位置,g修饰符从这个位置开始向后搜索,直到发现匹配为止。

    +

    y修饰符同样遵守lastIndex属性,但是要求必须在lastIndex指定的位置发现匹配。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const REGEX = /a/y;

    // 指定从2号位置开始匹配
    REGEX.lastIndex = 2;

    // 不是粘连,匹配失败
    REGEX.exec("xaya"); // null

    // 指定从3号位置开始匹配
    REGEX.lastIndex = 3;

    // 3号位置是粘连,匹配成功
    const match = REGEX.exec("xaya");
    match.index; // 3
    REGEX.lastIndex; // 4
    + +

    实际上,y修饰符号隐含了头部匹配的标志^

    +
    1
    2
    /b/y.exec("aba");
    // null
    + +

    上面代码由于不能保证头部匹配,所以返回nully修饰符的设计本意,就是让头部匹配的标志^在全局匹配中都有效。

    +

    下面是字符串对象的replace方法的例子。

    +
    1
    2
    const REGEX = /a/gy;
    "aaxa".replace(REGEX, "-"); // '--xa'
    + +

    上面代码中,最后一个a因为不是出现在下一次匹配的头部,所以不会被替换。

    +

    单单一个y修饰符对match方法,只能返回第一个匹配,必须与g修饰符联用,才能返回所有匹配。

    +
    1
    2
    "a1a2a3".match(/a\d/y); // ["a1"]
    "a1a2a3".match(/a\d/gy); // ["a1", "a2", "a3"]
    + +

    y修饰符的一个应用,是从字符串提取 token(词元),y修饰符确保了匹配之间不会有漏掉的字符。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const TOKEN_Y = /\s*(\+|[0-9]+)\s*/y;
    const TOKEN_G = /\s*(\+|[0-9]+)\s*/g;

    tokenize(TOKEN_Y, "3 + 4");
    // [ '3', '+', '4' ]
    tokenize(TOKEN_G, "3 + 4");
    // [ '3', '+', '4' ]

    function tokenize(TOKEN_REGEX, str) {
    let result = [];
    let match;
    while ((match = TOKEN_REGEX.exec(str))) {
    result.push(match[1]);
    }
    return result;
    }
    + +

    上面代码中,如果字符串里面没有非法字符,y修饰符与g修饰符的提取结果是一样的。但是,一旦出现非法字符,两者的行为就不一样了。

    +
    1
    2
    3
    4
    tokenize(TOKEN_Y, "3x + 4");
    // [ '3' ]
    tokenize(TOKEN_G, "3x + 4");
    // [ '3', '+', '4' ]
    + +

    上面代码中,g修饰符会忽略非法字符,而y修饰符不会,这样就很容易发现错误。

    +

    RegExp.prototype.sticky 属性

    y修饰符相匹配,ES6 的正则实例对象多了sticky属性,表示是否设置了y修饰符。

    +
    1
    2
    var r = /hello\d/y;
    r.sticky; // true
    + +

    RegExp.prototype.flags 属性

    ES6 为正则表达式新增了flags属性,会返回正则表达式的修饰符。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // ES5 的 source 属性
    // 返回正则表达式的正文
    /abc/gi.source /
    // "abc"

    // ES6 的 flags 属性
    // 返回正则表达式的修饰符
    abc /
    ig.flags;
    // 'gi'
    + +

    s 修饰符:dotAll 模式

    正则表达式中,点(.)是一个特殊字符,代表任意的单个字符,但是有两个例外。一个是四个字节的 UTF-16 字符,这个可以用u修饰符解决;另一个是行终止符(line terminator character)。

    +

    所谓行终止符,就是该字符表示一行的终结。以下四个字符属于”行终止符“。

    +
      +
    • U+000A 换行符(\n
    • +
    • U+000D 回车符(\r
    • +
    • U+2028 行分隔符(line separator)
    • +
    • U+2029 段分隔符(paragraph separator)
    • +
    +
    1
    2
    /foo.bar/.test("foo\nbar");
    // false
    + +

    上面代码中,因为.不匹配\n,所以正则表达式返回false

    +

    但是,很多时候我们希望匹配的是任意单个字符,这时有一种变通的写法。

    +
    1
    2
    /foo[^]bar/.test("foo\nbar");
    // true
    + +

    这种解决方案毕竟不太符合直觉,ES2018 引入s修饰符,使得.可以匹配任意单个字符。

    +
    1
    /foo.bar/s.test("foo\nbar"); // true
    + +

    这被称为dotAll模式,即点(dot)代表一切字符。所以,正则表达式还引入了一个dotAll属性,返回一个布尔值,表示该正则表达式是否处在dotAll模式。

    +
    1
    2
    3
    4
    5
    6
    7
    const re = /foo.bar/s;
    // 另一种写法
    // const re = new RegExp('foo.bar', 's');

    re.test("foo\nbar"); // true
    re.dotAll; // true
    re.flags; // 's'
    + +

    /s修饰符和多行修饰符/m不冲突,两者一起使用的情况下,.匹配所有字符,而^$匹配每一行的行首和行尾。

    +

    后行断言

    JavaScript 语言的正则表达式,只支持先行断言(lookahead)和先行否定断言(negative lookahead),不支持后行断言(lookbehind)和后行否定断言(negative lookbehind)。ES2018 引入后行断言,V8 引擎 4.9 版(Chrome 62)已经支持。

    +

    ”先行断言“指的是,x只有在y前面才匹配,必须写成/x(?=y)/。比如,只匹配百分号之前的数字,要写成/\d+(?=%)/。”先行否定断言“指的是,x只有不在y前面才匹配,必须写成/x(?!y)/。比如,只匹配不在百分号之前的数字,要写成/\d+(?!%)/

    +
    1
    2
    /\d+(?=%)/.exec('100% of US presidents have been male')  // ["100"]
    /\d+(?!%)/.exec('that’s all 44 of them') // ["44"]
    + +

    上面两个字符串,如果互换正则表达式,就不会得到相同结果。另外,还可以看到,”先行断言“括号之中的部分((?=%)),是不计入返回结果的。

    +

    “后行断言”正好与“先行断言”相反,x只有在y后面才匹配,必须写成/(?<=y)x/。比如,只匹配美元符号之后的数字,要写成/(?<=\$)\d+/。”后行否定断言“则与”先行否定断言“相反,x只有不在y后面才匹配,必须写成/(?<!y)x/。比如,只匹配不在美元符号后面的数字,要写成/(?<!\$)\d+/

    +
    1
    2
    /(?<=\$)\d+/.exec('Benjamin Franklin is on the $100 bill')  // ["100"]
    /(?<!\$)\d+/.exec('it’s is worth about €90') // ["90"]
    + +

    上面的例子中,“后行断言”的括号之中的部分((?<=\$)),也是不计入返回结果。

    +

    下面的例子是使用后行断言进行字符串替换。

    +
    1
    2
    3
    const RE_DOLLAR_PREFIX = /(?<=\$)foo/g;
    "$foo %foo foo".replace(RE_DOLLAR_PREFIX, "bar");
    // '$bar %foo foo'
    + +

    上面代码中,只有在美元符号后面的foo才会被替换。

    +

    “后行断言”的实现,需要先匹配/(?<=y)x/x,然后再回到左边,匹配y的部分。这种“先右后左”的执行顺序,与所有其他正则操作相反,导致了一些不符合预期的行为。

    +

    首先,后行断言的组匹配,与正常情况下结果是不一样的。

    +
    1
    2
    /(?<=(\d+)(\d+))$/.exec('1053') // ["", "1", "053"]
    /^(\d+)(\d+)$/.exec('1053') // ["1053", "105", "3"]
    + +

    上面代码中,需要捕捉两个组匹配。没有“后行断言”时,第一个括号是贪婪模式,第二个括号只能捕获一个字符,所以结果是1053。而“后行断言”时,由于执行顺序是从右到左,第二个括号是贪婪模式,第一个括号只能捕获一个字符,所以结果是1053

    +

    其次,“后行断言”的反斜杠引用,也与通常的顺序相反,必须放在对应的那个括号之前。

    +
    1
    2
    /(?<=(o)d\1)r/.exec('hodor')  // null
    /(?<=\1d(o))r/.exec('hodor') // ["r", "o"]
    + +

    上面代码中,如果后行断言的反斜杠引用(\1)放在括号的后面,就不会得到匹配结果,必须放在前面才可以。因为后行断言是先从左到右扫描,发现匹配以后再回过头,从右到左完成反斜杠引用。

    +

    Unicode 属性类

    ES2018 引入了一种新的类的写法\p{...}\P{...},允许正则表达式匹配符合 Unicode 某种属性的所有字符。

    +
    1
    2
    const regexGreekSymbol = /\p{Script=Greek}/u;
    regexGreekSymbol.test("π"); // true
    + +

    上面代码中,\p{Script=Greek}指定匹配一个希腊文字母,所以匹配π成功。

    +

    Unicode 属性类要指定属性名和属性值。

    +
    1
    \p{UnicodePropertyName=UnicodePropertyValue}
    + +

    对于某些属性,可以只写属性名,或者只写属性值。

    +
    1
    2
    \p{UnicodePropertyName}
    \p{UnicodePropertyValue}
    + +

    \P{…}\p{…}的反向匹配,即匹配不满足条件的字符。

    +

    注意,这两种类只对 Unicode 有效,所以使用的时候一定要加上u修饰符。如果不加u修饰符,正则表达式使用\p\P会报错,ECMAScript 预留了这两个类。

    +

    由于 Unicode 的各种属性非常多,所以这种新的类的表达能力非常强。

    +
    1
    2
    const regex = /^\p{Decimal_Number}+$/u;
    regex.test("𝟏𝟐𝟑𝟜𝟝𝟞𝟩𝟪𝟫𝟬𝟭𝟮𝟯𝟺𝟻𝟼"); // true
    + +

    上面代码中,属性类指定匹配所有十进制字符,可以看到各种字型的十进制字符都会匹配成功。

    +

    \p{Number}甚至能匹配罗马数字。

    +
    1
    2
    3
    4
    5
    // 匹配所有数字
    const regex = /^\p{Number}+$/u;
    regex.test("²³¹¼½¾"); // true
    regex.test("㉛㉜㉝"); // true
    regex.test("ⅠⅡⅢⅣⅤⅥⅦⅧⅨⅩⅪⅫ"); // true
    + +

    下面是其他一些例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 匹配所有空格
    \p{White_Space}

    // 匹配各种文字的所有字母,等同于 Unicode 版的 \w
    [\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

    // 匹配各种文字的所有非字母的字符,等同于 Unicode 版的 \W
    [^\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]

    // 匹配 Emoji
    /\p{Emoji_Modifier_Base}\p{Emoji_Modifier}?|\p{Emoji_Presentation}|\p{Emoji}\uFE0F/gu

    // 匹配所有的箭头字符
    const regexArrows = /^\p{Block=Arrows}+$/u;
    regexArrows.test('←↑→↓↔↕↖↗↘↙⇏⇐⇑⇒⇓⇔⇕⇖⇗⇘⇙⇧⇩') // true
    + +

    具名组匹配

    简介

    正则表达式使用圆括号进行组匹配。

    +
    1
    const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;
    + +

    上面代码中,正则表达式里面有三组圆括号。使用exec方法,就可以将这三组匹配结果提取出来。

    +
    1
    2
    3
    4
    5
    6
    const RE_DATE = /(\d{4})-(\d{2})-(\d{2})/;

    const matchObj = RE_DATE.exec("1999-12-31");
    const year = matchObj[1]; // 1999
    const month = matchObj[2]; // 12
    const day = matchObj[3]; // 31
    + +

    组匹配的一个问题是,每一组的匹配含义不容易看出来,而且只能用数字序号(比如matchObj[1])引用,要是组的顺序变了,引用的时候就必须修改序号。

    +

    ES2018 引入了具名组匹配(Named Capture Groups),允许为每一个组匹配指定一个名字,既便于阅读代码,又便于引用。

    +
    1
    2
    3
    4
    5
    6
    const RE_DATE = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;

    const matchObj = RE_DATE.exec("1999-12-31");
    const year = matchObj.groups.year; // 1999
    const month = matchObj.groups.month; // 12
    const day = matchObj.groups.day; // 31
    + +

    上面代码中,“具名组匹配”在圆括号内部,模式的头部添加“问号 + 尖括号 + 组名”(?<year>),然后就可以在exec方法返回结果的groups属性上引用该组名。同时,数字序号(matchObj[1])依然有效。

    +

    具名组匹配等于为每一组匹配加上了 ID,便于描述匹配的目的。如果组的顺序变了,也不用改变匹配后的处理代码。

    +

    如果具名组没有匹配,那么对应的groups对象属性会是undefined

    +
    1
    2
    3
    4
    5
    const RE_OPT_A = /^(?<as>a+)?$/;
    const matchObj = RE_OPT_A.exec("");

    matchObj.groups.as; // undefined
    "as" in matchObj.groups; // true
    + +

    上面代码中,具名组as没有找到匹配,那么matchObj.groups.as属性值就是undefined,并且as这个键名在groups是始终存在的。

    +

    解构赋值和替换

    有了具名组匹配以后,可以使用解构赋值直接从匹配结果上为变量赋值。

    +
    1
    2
    3
    4
    5
    let {
    groups: { one, two },
    } = /^(?<one>.*):(?<two>.*)$/u.exec("foo:bar");
    one; // foo
    two; // bar
    + +

    字符串替换时,使用$<组名>引用具名组。

    +
    1
    2
    3
    4
    let re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/u;

    "2015-01-02".replace(re, "$<day>/$<month>/$<year>");
    // '02/01/2015'
    + +

    上面代码中,replace方法的第二个参数是一个字符串,而不是正则表达式。

    +

    replace方法的第二个参数也可以是函数,该函数的参数序列如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    "2015-01-02".replace(
    re,
    (
    matched, // 整个匹配结果 2015-01-02
    capture1, // 第一个组匹配 2015
    capture2, // 第二个组匹配 01
    capture3, // 第三个组匹配 02
    position, // 匹配开始的位置 0
    S, // 原字符串 2015-01-02
    groups // 具名组构成的一个对象 {year, month, day}
    ) => {
    let { day, month, year } = groups;
    return `${day}/${month}/${year}`;
    }
    );
    + +

    具名组匹配在原来的基础上,新增了最后一个函数参数:具名组构成的一个对象。函数内部可以直接对这个对象进行解构赋值。

    +

    引用

    如果要在正则表达式内部引用某个“具名组匹配”,可以使用\k<组名>的写法。

    +
    1
    2
    3
    const RE_TWICE = /^(?<word>[a-z]+)!\k<word>$/;
    RE_TWICE.test("abc!abc"); // true
    RE_TWICE.test("abc!ab"); // false
    + +

    数字引用(\1)依然有效。

    +
    1
    2
    3
    const RE_TWICE = /^(?<word>[a-z]+)!\1$/;
    RE_TWICE.test("abc!abc"); // true
    RE_TWICE.test("abc!ab"); // false
    + +

    这两种引用语法还可以同时使用。

    +
    1
    2
    3
    const RE_TWICE = /^(?<word>[a-z]+)!\k<word>!\1$/;
    RE_TWICE.test("abc!abc!abc"); // true
    RE_TWICE.test("abc!abc!ab"); // false
    + +

    String.prototype.matchAll

    如果一个正则表达式在字符串里面有多个匹配,现在一般使用g修饰符或y修饰符,在循环里面逐一取出。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    var regex = /t(e)(st(\d?))/g;
    var string = "test1test2test3";

    var matches = [];
    var match;
    while ((match = regex.exec(string))) {
    matches.push(match);
    }

    matches;
    // [
    // ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"],
    // ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"],
    // ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
    // ]
    + +

    上面代码中,while循环取出每一轮的正则匹配,一共三轮。

    +

    目前有一个提案,增加了String.prototype.matchAll方法,可以一次性取出所有匹配。不过,它返回的是一个遍历器(Iterator),而不是数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const string = "test1test2test3";

    // g 修饰符加不加都可以
    const regex = /t(e)(st(\d?))/g;

    for (const match of string.matchAll(regex)) {
    console.log(match);
    }
    // ["test1", "e", "st1", "1", index: 0, input: "test1test2test3"]
    // ["test2", "e", "st2", "2", index: 5, input: "test1test2test3"]
    // ["test3", "e", "st3", "3", index: 10, input: "test1test2test3"]
    + +

    上面代码中,由于string.matchAll(regex)返回的是遍历器,所以可以用for...of循环取出。相对于返回数组,返回遍历器的好处在于,如果匹配结果是一个很大的数组,那么遍历器比较节省资源。

    +

    遍历器转为数组是非常简单的,使用...运算符和Array.from方法就可以了。

    +
    1
    2
    3
    4
    5
    // 转为数组方法一
    [...string.matchAll(regex)];

    // 转为数组方法二
    Array.from(string.matchAll(regex));
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E6%AD%A3%E5%88%99%E7%9A%84%E6%89%A9%E5%B1%95/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/ES6-\347\274\226\347\250\213\351\243\216\346\240\274/index.html" "b/posts/ES6-\347\274\226\347\250\213\351\243\216\346\240\274/index.html" new file mode 100644 index 00000000..0e58a31b --- /dev/null +++ "b/posts/ES6-\347\274\226\347\250\213\351\243\216\346\240\274/index.html" @@ -0,0 +1,323 @@ +编程风格 | JCAlways + + + + + + + + + + + + + +

    编程风格

    编程风格

    本章探讨如何将 ES6 的新语法,运用到编码实践之中,与传统的 JavaScript 语法结合在一起,写出合理的、易于阅读和维护的代码。

    +

    多家公司和组织已经公开了它们的风格规范,下面的内容主要参考了 Airbnb 公司的 JavaScript 风格规范。

    +

    块级作用域

    (1)let 取代 var

    +

    ES6 提出了两个新的声明变量的命令:letconst。其中,let完全可以取代var,因为两者语义相同,而且let没有副作用。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    "use strict";

    if (true) {
    let x = "hello";
    }

    for (let i = 0; i < 10; i++) {
    console.log(i);
    }
    + +

    上面代码如果用var替代let,实际上就声明了两个全局变量,这显然不是本意。变量应该只在其声明的代码块内有效,var命令做不到这一点。

    +

    var命令存在变量提升效用,let命令没有这个问题。

    +
    1
    2
    3
    4
    5
    6
    "use strict";

    if (true) {
    console.log(x); // ReferenceError
    let x = "hello";
    }
    + +

    上面代码如果使用var替代letconsole.log那一行就不会报错,而是会输出undefined,因为变量声明提升到代码块的头部。这违反了变量先声明后使用的原则。

    +

    所以,建议不再使用var命令,而是使用let命令取代。

    +

    (2)全局常量和线程安全

    +

    letconst之间,建议优先使用const,尤其是在全局环境,不应该设置变量,只应设置常量。

    +

    const优于let有几个原因。一个是const可以提醒阅读程序的人,这个变量不应该改变;另一个是const比较符合函数式编程思想,运算不改变值,只是新建值,而且这样也有利于将来的分布式运算;最后一个原因是 JavaScript 编译器会对const进行优化,所以多使用const,有利于提高程序的运行效率,也就是说letconst的本质区别,其实是编译器内部的处理不同。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // bad
    var a = 1,
    b = 2,
    c = 3;

    // good
    const a = 1;
    const b = 2;
    const c = 3;

    // best
    const [a, b, c] = [1, 2, 3];
    + +

    const声明常量还有两个好处,一是阅读代码的人立刻会意识到不应该修改这个值,二是防止了无意间修改变量值所导致的错误。

    +

    所有的函数都应该设置为常量。

    +

    长远来看,JavaScript 可能会有多线程的实现(比如 Intel 公司的 River Trail 那一类的项目),这时let表示的变量,只应出现在单线程运行的代码中,不能是多线程共享的,这样有利于保证线程安全。

    +

    字符串

    静态字符串一律使用单引号或反引号,不使用双引号。动态字符串使用反引号。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // bad
    const a = "foobar";
    const b = "foo" + a + "bar";

    // acceptable
    const c = `foobar`;

    // good
    const a = "foobar";
    const b = `foo${a}bar`;
    + +

    解构赋值

    使用数组成员对变量赋值时,优先使用解构赋值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const arr = [1, 2, 3, 4];

    // bad
    const first = arr[0];
    const second = arr[1];

    // good
    const [first, second] = arr;
    + +

    函数的参数如果是对象的成员,优先使用解构赋值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // bad
    function getFullName(user) {
    const firstName = user.firstName;
    const lastName = user.lastName;
    }

    // good
    function getFullName(obj) {
    const { firstName, lastName } = obj;
    }

    // best
    function getFullName({ firstName, lastName }) {}
    + +

    如果函数返回多个值,优先使用对象的解构赋值,而不是数组的解构赋值。这样便于以后添加返回值,以及更改返回值的顺序。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    function processInput(input) {
    return [left, right, top, bottom];
    }

    // good
    function processInput(input) {
    return { left, right, top, bottom };
    }

    const { left, right } = processInput(input);
    + +

    对象

    单行定义的对象,最后一个成员不以逗号结尾。多行定义的对象,最后一个成员以逗号结尾。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // bad
    const a = { k1: v1, k2: v2 };
    const b = {
    k1: v1,
    k2: v2,
    };

    // good
    const a = { k1: v1, k2: v2 };
    const b = {
    k1: v1,
    k2: v2,
    };
    + +

    对象尽量静态化,一旦定义,就不得随意添加新的属性。如果添加属性不可避免,要使用Object.assign方法。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    const a = {};
    a.x = 3;

    // if reshape unavoidable
    const a = {};
    Object.assign(a, { x: 3 });

    // good
    const a = { x: null };
    a.x = 3;
    + +

    如果对象的属性名是动态的,可以在创造对象的时候,使用属性表达式定义。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    // bad
    const obj = {
    id: 5,
    name: "San Francisco",
    };
    obj[getKey("enabled")] = true;

    // good
    const obj = {
    id: 5,
    name: "San Francisco",
    [getKey("enabled")]: true,
    };
    + +

    上面代码中,对象obj的最后一个属性名,需要计算得到。这时最好采用属性表达式,在新建obj的时候,将该属性与其他属性定义在一起。这样一来,所有属性就在一个地方定义了。

    +

    另外,对象的属性和方法,尽量采用简洁表达法,这样易于描述和书写。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    var ref = "some value";

    // bad
    const atom = {
    ref: ref,

    value: 1,

    addValue: function (value) {
    return atom.value + value;
    },
    };

    // good
    const atom = {
    ref,

    value: 1,

    addValue(value) {
    return atom.value + value;
    },
    };
    + +

    数组

    使用扩展运算符(…)拷贝数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    const len = items.length;
    const itemsCopy = [];
    let i;

    for (i = 0; i < len; i++) {
    itemsCopy[i] = items[i];
    }

    // good
    const itemsCopy = [...items];
    + +

    使用 Array.from 方法,将类似数组的对象转为数组。

    +
    1
    2
    const foo = document.querySelectorAll(".foo");
    const nodes = Array.from(foo);
    + +

    函数

    立即执行函数可以写成箭头函数的形式。

    +
    1
    2
    3
    (() => {
    console.log("Welcome to the Internet.");
    })();
    + +

    那些需要使用函数表达式的场合,尽量用箭头函数代替。因为这样更简洁,而且绑定了 this。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // bad
    [1, 2, 3].map(function (x) {
    return x * x;
    });

    // good
    [1, 2, 3].map((x) => {
    return x * x;
    });

    // best
    [1, 2, 3].map((x) => x * x);
    + +

    箭头函数取代Function.prototype.bind,不应再用 self/_this/that 绑定 this。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // bad
    const self = this;
    const boundMethod = function (...params) {
    return method.apply(self, params);
    };

    // acceptable
    const boundMethod = method.bind(this);

    // best
    const boundMethod = (...params) => method.apply(this, params);
    + +

    简单的、单行的、不会复用的函数,建议采用箭头函数。如果函数体较为复杂,行数较多,还是应该采用传统的函数写法。

    +

    所有配置项都应该集中在一个对象,放在最后一个参数,布尔值不可以直接作为参数。

    +
    1
    2
    3
    4
    5
    // bad
    function divide(a, b, option = false) {}

    // good
    function divide(a, b, { option = false } = {}) {}
    + +

    不要在函数体内使用 arguments 变量,使用 rest 运算符(…)代替。因为 rest 运算符显式表明你想要获取参数,而且 arguments 是一个类似数组的对象,而 rest 运算符可以提供一个真正的数组。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // bad
    function concatenateAll() {
    const args = Array.prototype.slice.call(arguments);
    return args.join("");
    }

    // good
    function concatenateAll(...args) {
    return args.join("");
    }
    + +

    使用默认值语法设置函数参数的默认值。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    // bad
    function handleThings(opts) {
    opts = opts || {};
    }

    // good
    function handleThings(opts = {}) {
    // ...
    }
    + +

    Map 结构

    注意区分 Object 和 Map,只有模拟现实世界的实体对象时,才使用 Object。如果只是需要key: value的数据结构,使用 Map 结构。因为 Map 有内建的遍历机制。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    let map = new Map(arr);

    for (let key of map.keys()) {
    console.log(key);
    }

    for (let value of map.values()) {
    console.log(value);
    }

    for (let item of map.entries()) {
    console.log(item[0], item[1]);
    }
    + +

    Class

    总是用 Class,取代需要 prototype 的操作。因为 Class 的写法更简洁,更易于理解。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // bad
    function Queue(contents = []) {
    this._queue = [...contents];
    }
    Queue.prototype.pop = function () {
    const value = this._queue[0];
    this._queue.splice(0, 1);
    return value;
    };

    // good
    class Queue {
    constructor(contents = []) {
    this._queue = [...contents];
    }
    pop() {
    const value = this._queue[0];
    this._queue.splice(0, 1);
    return value;
    }
    }
    + +

    使用extends实现继承,因为这样更简单,不会有破坏instanceof运算的危险。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // bad
    const inherits = require("inherits");
    function PeekableQueue(contents) {
    Queue.apply(this, contents);
    }
    inherits(PeekableQueue, Queue);
    PeekableQueue.prototype.peek = function () {
    return this._queue[0];
    };

    // good
    class PeekableQueue extends Queue {
    peek() {
    return this._queue[0];
    }
    }
    + +

    模块

    首先,Module 语法是 JavaScript 模块的标准写法,坚持使用这种写法。使用import取代require

    +
    1
    2
    3
    4
    5
    6
    7
    // bad
    const moduleA = require("moduleA");
    const func1 = moduleA.func1;
    const func2 = moduleA.func2;

    // good
    import { func1, func2 } from "moduleA";
    + +

    使用export取代module.exports

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    // commonJS的写法
    var React = require("react");

    var Breadcrumbs = React.createClass({
    render() {
    return <nav />;
    },
    });

    module.exports = Breadcrumbs;

    // ES6的写法
    import React from "react";

    class Breadcrumbs extends React.Component {
    render() {
    return <nav />;
    }
    }

    export default Breadcrumbs;
    + +

    如果模块只有一个输出值,就使用export default,如果模块有多个输出值,就不使用export defaultexport default与普通的export不要同时使用。

    +

    不要在模块输入中使用通配符。因为这样可以确保你的模块之中,有一个默认输出(export default)。

    +
    1
    2
    3
    4
    5
    // bad
    import * as myObject from "./importModule";

    // good
    import myObject from "./importModule";
    + +

    如果模块默认输出一个函数,函数名的首字母应该小写。

    +
    1
    2
    3
    function makeStyleGuide() {}

    export default makeStyleGuide;
    + +

    如果模块默认输出一个对象,对象名的首字母应该大写。

    +
    1
    2
    3
    4
    5
    const StyleGuide = {
    es6: {},
    };

    export default StyleGuide;
    + +

    ESLint 的使用

    ESLint 是一个语法规则和代码风格的检查工具,可以用来保证写出语法正确、风格统一的代码。

    +

    首先,安装 ESLint。

    +
    1
    $ npm i -g eslint
    + +

    然后,安装 Airbnb 语法规则,以及 import、a11y、react 插件。

    +
    1
    2
    $ npm i -g eslint-config-airbnb
    $ npm i -g eslint-plugin-import eslint-plugin-jsx-a11y eslint-plugin-react
    + +

    最后,在项目的根目录下新建一个.eslintrc文件,配置 ESLint。

    +
    1
    2
    3
    {
    "extends": "eslint-config-airbnb"
    }
    + +

    现在就可以检查,当前项目的代码是否符合预设的规则。

    +

    index.js文件的代码如下。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    var unusued = "I have no purpose!";

    function greet() {
    var message = "Hello, World!";
    alert(message);
    }

    greet();
    + +

    使用 ESLint 检查这个文件,就会报出错误。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    $ eslint index.js
    index.js
    1:1 error Unexpected var, use let or const instead no-var
    1:5 error unusued is defined but never used no-unused-vars
    4:5 error Expected indentation of 2 characters but found 4 indent
    4:5 error Unexpected var, use let or const instead no-var
    5:5 error Expected indentation of 2 characters but found 4 indent

    ✖ 5 problems (5 errors, 0 warnings)
    + +

    上面代码说明,原文件有五个错误,其中两个是不应该使用var命令,而要使用letconst;一个是定义了变量,却没有使用;另外两个是行首缩进为 4 个空格,而不是规定的 2 个空格。

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/ES6-\350\257\273\346\207\202 ECMAScript \350\247\204\346\240\274/index.html" "b/posts/ES6-\350\257\273\346\207\202 ECMAScript \350\247\204\346\240\274/index.html" new file mode 100644 index 00000000..279506ea --- /dev/null +++ "b/posts/ES6-\350\257\273\346\207\202 ECMAScript \350\247\204\346\240\274/index.html" @@ -0,0 +1,420 @@ +读懂 ECMAScript 规格 | JCAlways + + + + + + + + + + + + + +

    读懂 ECMAScript 规格

    读懂 ECMAScript 规格

    概述

    规格文件是计算机语言的官方标准,详细描述语法规则和实现方法。

    +

    一般来说,没有必要阅读规格,除非你要写编译器。因为规格写得非常抽象和精炼,又缺乏实例,不容易理解,而且对于解决实际的应用问题,帮助不大。但是,如果你遇到疑难的语法问题,实在找不到答案,这时可以去查看规格文件,了解语言标准是怎么说的。规格是解决问题的“最后一招”。

    +

    这对 JavaScript 语言很有必要。因为它的使用场景复杂,语法规则不统一,例外很多,各种运行环境的行为不一致,导致奇怪的语法问题层出不穷,任何语法书都不可能囊括所有情况。查看规格,不失为一种解决语法问题的最可靠、最权威的终极方法。

    +

    本章介绍如何读懂 ECMAScript 6 的规格文件。

    +

    ECMAScript 6 的规格,可以在 ECMA 国际标准组织的官方网站(www.ecma-international.org/ecma-262/6.0/)免费下载和在线阅读。

    +

    这个规格文件相当庞大,一共有 26 章,A4 打印的话,足足有 545 页。它的特点就是规定得非常细致,每一个语法行为、每一个函数的实现都做了详尽的清晰的描述。基本上,编译器作者只要把每一步翻译成代码就可以了。这很大程度上,保证了所有 ES6 实现都有一致的行为。

    +

    ECMAScript 6 规格的 26 章之中,第 1 章到第 3 章是对文件本身的介绍,与语言关系不大。第 4 章是对这门语言总体设计的描述,有兴趣的读者可以读一下。第 5 章到第 8 章是语言宏观层面的描述。第 5 章是规格的名词解释和写法的介绍,第 6 章介绍数据类型,第 7 章介绍语言内部用到的抽象操作,第 8 章介绍代码如何运行。第 9 章到第 26 章介绍具体的语法。

    +

    对于一般用户来说,除了第 4 章,其他章节都涉及某一方面的细节,不用通读,只要在用到的时候,查阅相关章节即可。

    +

    术语

    ES6 规格使用了一些专门的术语,了解这些术语,可以帮助你读懂规格。本节介绍其中的几个。

    +

    抽象操作

    所谓”抽象操作“(abstract operations)就是引擎的一些内部方法,外部不能调用。规格定义了一系列的抽象操作,规定了它们的行为,留给各种引擎自己去实现。

    +

    举例来说,Boolean(value)的算法,第一步是这样的。

    +
    +
      +
    1. Let b be ToBoolean(value).
    2. +
    +
    +

    这里的ToBoolean就是一个抽象操作,是引擎内部求出布尔值的算法。

    +

    许多函数的算法都会多次用到同样的步骤,所以 ES6 规格将它们抽出来,定义成”抽象操作“,方便描述。

    +

    Record 和 field

    ES6 规格将键值对(key-value map)的数据结构称为 Record,其中的每一组键值对称为 field。这就是说,一个 Record 由多个 field 组成,而每个 field 都包含一个键名(key)和一个键值(value)。

    +

    [[Notation]]

    ES6 规格大量使用[[Notation]]这种书写法,比如[[Value]][[Writable]][[Get]][[Set]]等等。它用来指代 field 的键名。

    +

    举例来说,obj是一个 Record,它有一个Prototype属性。ES6 规格不会写obj.Prototype,而是写obj.[[Prototype]]。一般来说,使用[[Notation]]这种书写法的属性,都是对象的内部属性。

    +

    所有的 JavaScript 函数都有一个内部属性[[Call]],用来运行该函数。

    +
    1
    F.[[Call]](V, argumentsList)
    + +

    上面代码中,F是一个函数对象,[[Call]]是它的内部方法,F.[[call]]()表示运行该函数,V表示[[Call]]运行时this的值,argumentsList则是调用时传入函数的参数。

    +

    Completion Record

    每一个语句都会返回一个 Completion Record,表示运行结果。每个 Completion Record 有一个[[Type]]属性,表示运行结果的类型。

    +

    [[Type]]属性有五种可能的值。

    +
      +
    • normal
    • +
    • return
    • +
    • throw
    • +
    • break
    • +
    • continue
    • +
    +

    如果[[Type]]的值是normal,就称为 normal completion,表示运行正常。其他的值,都称为 abrupt completion。其中,开发者只需要关注[[Type]]throw的情况,即运行出错;breakcontinuereturn这三个值都只出现在特定场景,可以不用考虑。

    +

    抽象操作的标准流程

    抽象操作的运行流程,一般是下面这样。

    +
    +
      +
    1. Let resultCompletionRecord be AbstractOp().
    2. +
    3. If resultCompletionRecord is an abrupt completion, return resultCompletionRecord.
    4. +
    5. Let result be resultCompletionRecord.[[Value]].
    6. +
    7. return result.
    8. +
    +
    +

    上面的第一步是调用抽象操作AbstractOp(),得到resultCompletionRecord,这是一个 Completion Record。第二步,如果这个 Record 属于 abrupt completion,就将resultCompletionRecord返回给用户。如果此处没有返回,就表示运行结果正常,所得的值存放在resultCompletionRecord.[[Value]]属性。第三步,将这个值记为result。第四步,将result返回给用户。

    +

    ES6 规格将这个标准流程,使用简写的方式表达。

    +
    +
      +
    1. Let result be AbstractOp().
    2. +
    3. ReturnIfAbrupt(result).
    4. +
    5. return result.
    6. +
    +
    +

    这个简写方式里面的ReturnIfAbrupt(result),就代表了上面的第二步和第三步,即如果有报错,就返回错误,否则取出值。

    +

    甚至还有进一步的简写格式。

    +
    +
      +
    1. Let result be ? AbstractOp().
    2. +
    3. return result.
    4. +
    +
    +

    上面流程的?,就代表AbstractOp()可能会报错。一旦报错,就返回错误,否则取出值。

    +

    除了?,ES 6 规格还使用另一个简写符号!

    +
    +
      +
    1. Let result be ! AbstractOp().
    2. +
    3. return result.
    4. +
    +
    +

    上面流程的!,代表AbstractOp()不会报错,返回的一定是 normal completion,总是可以取出值。

    +

    相等运算符

    下面通过一些例子,介绍如何使用这份规格。

    +

    相等运算符(==)是一个很让人头痛的运算符,它的语法行为多变,不符合直觉。这个小节就看看规格怎么规定它的行为。

    +

    请看下面这个表达式,请问它的值是多少。

    +
    1
    0 == null;
    + +

    如果你不确定答案,或者想知道语言内部怎么处理,就可以去查看规格,7.2.12 小节是对相等运算符(==)的描述。

    +

    规格对每一种语法行为的描述,都分成两部分:先是总体的行为描述,然后是实现的算法细节。相等运算符的总体描述,只有一句话。

    +
    +

    “The comparison x == y, where x and y are values, produces true or false.”

    +
    +

    上面这句话的意思是,相等运算符用于比较两个值,返回truefalse

    +

    下面是算法细节。

    +
    +
      +
    1. ReturnIfAbrupt(x).
    2. +
    3. ReturnIfAbrupt(y).
    4. +
    5. If Type(x) is the same as Type(y), then
        +
      1. Return the result of performing Strict Equality Comparison x === y.
      2. +
      +
    6. +
    7. If x is null and y is undefined, return true.
    8. +
    9. If x is undefined and y is null, return true.
    10. +
    11. If Type(x) is Number and Type(y) is String,
      return the result of the comparison x == ToNumber(y).
    12. +
    13. If Type(x) is String and Type(y) is Number,
      return the result of the comparison ToNumber(x) == y.
    14. +
    15. If Type(x) is Boolean, return the result of the comparison ToNumber(x) == y.
    16. +
    17. If Type(y) is Boolean, return the result of the comparison x == ToNumber(y).
    18. +
    19. If Type(x) is either String, Number, or Symbol and Type(y) is Object, then
      return the result of the comparison x == ToPrimitive(y).
    20. +
    21. If Type(x) is Object and Type(y) is either String, Number, or Symbol, then
      return the result of the comparison ToPrimitive(x) == y.
    22. +
    23. Return false.
    24. +
    +
    +

    上面这段算法,一共有 12 步,翻译如下。

    +
    +
      +
    1. 如果x不是正常值(比如抛出一个错误),中断执行。
    2. +
    3. 如果y不是正常值,中断执行。
    4. +
    5. 如果Type(x)Type(y)相同,执行严格相等运算x === y
    6. +
    7. 如果xnullyundefined,返回true
    8. +
    9. 如果xundefinedynull,返回true
    10. +
    11. 如果Type(x)是数值,Type(y)是字符串,返回x == ToNumber(y)的结果。
    12. +
    13. 如果Type(x)是字符串,Type(y)是数值,返回ToNumber(x) == y的结果。
    14. +
    15. 如果Type(x)是布尔值,返回ToNumber(x) == y的结果。
    16. +
    17. 如果Type(y)是布尔值,返回x == ToNumber(y)的结果。
    18. +
    19. 如果Type(x)是字符串或数值或Symbol值,Type(y)是对象,返回x == ToPrimitive(y)的结果。
    20. +
    21. 如果Type(x)是对象,Type(y)是字符串或数值或Symbol值,返回ToPrimitive(x) == y的结果。
    22. +
    23. 返回false
    24. +
    +
    +

    由于0的类型是数值,null的类型是 Null(这是规格4.3.13 小节的规定,是内部 Type 运算的结果,跟typeof运算符无关)。因此上面的前 11 步都得不到结果,要到第 12 步才能得到false

    +
    1
    0 == null; // false
    + +

    数组的空位

    下面再看另一个例子。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const a1 = [undefined, undefined, undefined];
    const a2 = [, , ,];

    a1.length; // 3
    a2.length; // 3

    a1[0]; // undefined
    a2[0]; // undefined

    a1[0] === a2[0]; // true
    + +

    上面代码中,数组a1的成员是三个undefined,数组a2的成员是三个空位。这两个数组很相似,长度都是 3,每个位置的成员读取出来都是undefined

    +

    但是,它们实际上存在重大差异。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    0 in a1; // true
    0 in a2; // false

    a1.hasOwnProperty(0); // true
    a2.hasOwnProperty(0); // false

    Object.keys(a1); // ["0", "1", "2"]
    Object.keys(a2); // []

    a1.map((n) => 1); // [1, 1, 1]
    a2.map((n) => 1); // [, , ,]
    + +

    上面代码一共列出了四种运算,数组a1a2的结果都不一样。前三种运算(in运算符、数组的hasOwnProperty方法、Object.keys方法)都说明,数组a2取不到属性名。最后一种运算(数组的map方法)说明,数组a2没有发生遍历。

    +

    为什么a1a2成员的行为不一致?数组的成员是undefined或空位,到底有什么不同?

    +

    规格的12.2.5 小节《数组的初始化》给出了答案。

    +
    +

    “Array elements may be elided at the beginning, middle or end of the element list. Whenever a comma in the element list is not preceded by an AssignmentExpression (i.e., a comma at the beginning or after another comma), the missing array element contributes to the length of the Array and increases the index of subsequent elements. Elided array elements are not defined. If an element is elided at the end of an array, that element does not contribute to the length of the Array.”

    +
    +

    翻译如下。

    +
    +

    “数组成员可以省略。只要逗号前面没有任何表达式,数组的length属性就会加 1,并且相应增加其后成员的位置索引。被省略的成员不会被定义。如果被省略的成员是数组最后一个成员,则不会导致数组length属性增加。”

    +
    +

    上面的规格说得很清楚,数组的空位会反映在length属性,也就是说空位有自己的位置,但是这个位置的值是未定义,即这个值是不存在的。如果一定要读取,结果就是undefined(因为undefined在 JavaScript 语言中表示不存在)。

    +

    这就解释了为什么in运算符、数组的hasOwnProperty方法、Object.keys方法,都取不到空位的属性名。因为这个属性名根本就不存在,规格里面没说要为空位分配属性名(位置索引),只说要为下一个元素的位置索引加 1。

    +

    至于为什么数组的map方法会跳过空位,请看下一节。

    +

    数组的 map 方法

    规格的22.1.3.15 小节定义了数组的map方法。该小节先是总体描述map方法的行为,里面没有提到数组空位。

    +

    后面的算法描述是这样的。

    +
    +
      +
    1. Let O be ToObject(this value).
    2. +
    3. ReturnIfAbrupt(O).
    4. +
    5. Let len be ToLength(Get(O, "length")).
    6. +
    7. ReturnIfAbrupt(len).
    8. +
    9. If IsCallable(callbackfn) is false, throw a TypeError exception.
    10. +
    11. If thisArg was supplied, let T be thisArg; else let T be undefined.
    12. +
    13. Let A be ArraySpeciesCreate(O, len).
    14. +
    15. ReturnIfAbrupt(A).
    16. +
    17. Let k be 0.
    18. +
    19. Repeat, while k < len
        +
      1. Let Pk be ToString(k).
      2. +
      3. Let kPresent be HasProperty(O, Pk).
      4. +
      5. ReturnIfAbrupt(kPresent).
      6. +
      7. If kPresent is true, then
          +
        1. Let kValue be Get(O, Pk).
        2. +
        3. ReturnIfAbrupt(kValue).
        4. +
        5. Let mappedValue be Call(callbackfn, T, «kValue, k, O»).
        6. +
        7. ReturnIfAbrupt(mappedValue).
        8. +
        9. Let status be CreateDataPropertyOrThrow (A, Pk, mappedValue).
        10. +
        11. ReturnIfAbrupt(status).
        12. +
        +
      8. +
      9. Increase k by 1.
      10. +
      +
    20. +
    21. Return A.
    22. +
    +
    +

    翻译如下。

    +
    +
      +
    1. 得到当前数组的this对象
    2. +
    3. 如果报错就返回
    4. +
    5. 求出当前数组的length属性
    6. +
    7. 如果报错就返回
    8. +
    9. 如果 map 方法的参数callbackfn不可执行,就报错
    10. +
    11. 如果 map 方法的参数之中,指定了this,就让T等于该参数,否则Tundefined
    12. +
    13. 生成一个新的数组A,跟当前数组的length属性保持一致
    14. +
    15. 如果报错就返回
    16. +
    17. 设定k等于 0
    18. +
    19. 只要k小于当前数组的length属性,就重复下面步骤
        +
      1. 设定Pk等于ToString(k),即将K转为字符串
      2. +
      3. 设定kPresent等于HasProperty(O, Pk),即求当前数组有没有指定属性
      4. +
      5. 如果报错就返回
      6. +
      7. 如果kPresent等于true,则进行下面步骤
          +
        1. 设定kValue等于Get(O, Pk),取出当前数组的指定属性
        2. +
        3. 如果报错就返回
        4. +
        5. 设定mappedValue等于Call(callbackfn, T, «kValue, k, O»),即执行回调函数
        6. +
        7. 如果报错就返回
        8. +
        9. 设定status等于CreateDataPropertyOrThrow (A, Pk, mappedValue),即将回调函数的值放入A数组的指定位置
        10. +
        11. 如果报错就返回
        12. +
        +
      8. +
      9. k增加 1
      10. +
      +
    20. +
    21. 返回A
    22. +
    +
    +

    仔细查看上面的算法,可以发现,当处理一个全是空位的数组时,前面步骤都没有问题。进入第 10 步中第 2 步时,kPresent会报错,因为空位对应的属性名,对于数组来说是不存在的,因此就会返回,不会进行后面的步骤。

    +
    1
    2
    3
    4
    5
    const arr = [, , ,];
    arr.map((n) => {
    console.log(n);
    return 1;
    }); // [, , ,]
    + +

    上面代码中,arr是一个全是空位的数组,map方法遍历成员时,发现是空位,就直接跳过,不会进入回调函数。因此,回调函数里面的console.log语句根本不会执行,整个map方法返回一个全是空位的新数组。

    +

    V8 引擎对map方法的实现如下,可以看到跟规格的算法描述完全一致。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function ArrayMap(f, receiver) {
    CHECK_OBJECT_COERCIBLE(this, "Array.prototype.map");

    // Pull out the length so that modifications to the length in the
    // loop will not affect the looping and side effects are visible.
    var array = TO_OBJECT(this);
    var length = TO_LENGTH_OR_UINT32(array.length);
    return InnerArrayMap(f, receiver, array, length);
    }

    function InnerArrayMap(f, receiver, array, length) {
    if (!IS_CALLABLE(f)) throw MakeTypeError(kCalledNonCallable, f);

    var accumulator = new InternalArray(length);
    var is_array = IS_ARRAY(array);
    var stepping = DEBUG_IS_STEPPING(f);
    for (var i = 0; i < length; i++) {
    if (HAS_INDEX(array, i, is_array)) {
    var element = array[i];
    // Prepare break slots for debugger step in.
    if (stepping) %DebugPrepareStepInIfStepping(f);
    accumulator[i] = %_Call(f, receiver, element, i, array);
    }
    }
    var result = new GlobalArray();
    %MoveArrayContents(accumulator, result);
    return result;
    }
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/ES6-%E8%AF%BB%E6%87%82%20ECMAScript%20%E8%A7%84%E6%A0%BC/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/JS-JQuery\345\237\272\347\241\200/index.html" "b/posts/JS-JQuery\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..c89f817b --- /dev/null +++ "b/posts/JS-JQuery\345\237\272\347\241\200/index.html" @@ -0,0 +1,540 @@ +JQuery基础 | JCAlways + + + + + + + + + + + + + +

    JQuery基础

    什么是 Jquery

    +

    官方网站

    +

    jQuery 是 javascript 实现的一个库

    +
    +
      +
    • 优点
        +
      • 跨浏览器兼容
      • +
      • 链式编程,隐式迭代
      • +
      • 简化 DOM 操作,支持事件、样式、动画
      • +
      • 支持插件扩展开发
      • +
      • 开源免费
      • +
      +
    • +
    +

    引入 Jquery

    1
    <script src="https://static.zhangsifan.com/jquery.min.js"></script>
    + +

    入口函数

    1
    2
    3
    4
    5
    6
    7
    8
    $(document).ready(function () {
    // 代码
    });
    // 或者
    $(function () {
    // 代码
    alert(1);
    });
    + +

    jquery 的顶级对象$

    +

    $是 jQuery 的别称,也是 JQuery 的顶级对象

    +
    +

    JQuery 和 DOM 对象相互转换

    用原生 js 获取的对象就是 DOM 对象

    +

    用 JQuery 方法获取的元素就是 JQuery 对象(伪数组)

    +

    DOM 对象转 JQuery 对象

    1
    $(DOM对象);
    + +

    JQuery 对象转 DOM 对象

    1
    2
    3
    // index 是索引号
    $("div")[index];
    $("div").get(index);
    + +

    隐式迭代

    +

    遍历内部 DOM 元素(伪数组的形式存储)的过程就叫隐式迭代

    +
    +

    排他思想

    +

    当前元素设置样式,其余的兄弟元素清除样式。

    +
    +

    事件

    事件注册

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    用法描述
    click ()点击事件
    dblclick ()双击事件
    change ()发生改变
    scrol ()滚动事件
    focus ()获得焦点
    mouseenter()鼠标经过
    hover(移入,移出)发生改变
    +

    事件处理

    +

    可以在元素上绑定一个或多个事件
    事件源.on(事件类型, [委派], 函数);

    +
    +

    多事件多处理程序

    1
    2
    3
    4
    $("div").on({
    click: function () {},
    mouseenter: function () {},
    });
    + +

    事件委派/委托

    1
    2
    3
    $("ul").on("click", "li", function () {
    alert("hello world!");
    });
    + +

    给动态添加的元素添加事件

    1
    2
    3
    $("div").on("click", "p", function () {
    alert("给动态生成的元素绑定事件");
    });
    + +

    解绑事件

    +

    事件源.off()

    +
    +
    1
    2
    3
    4
    5
    6
    // 解除全部事件
    事件源.off();
    // 解除那个事件
    事件源.off("click");
    // 解除事件委派
    事件源.off("click", "li");
    + +

    自动触发事件

    1
    2
    3
    4
    // (事件类型)
    事件源.trigger;
    // (事件类型)
    事件源.triggerHandler;
    + +

    事件对象

    1
    2
    3
    4
    // 阻止冒泡
    e.stopPropagation();
    // 阻止默认行为
    e.preventDefault();
    + +

    常用 API

    选择器

    基础选择器

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称用法描述
    ID 选择器$(“#id”)获取指定 ID 的元素
    全选选择器$(“*“)匹配所有元素
    类选择器$(“.class”)获取同一类名的元素
    标签选择器$(“div”)获取同一标签的元素
    并集选择器$(“div,p”)选取多个元素
    交集选择器$(“li.current”)交集元素
    +
    1
    $("选择器");
    + +

    筛选选择器

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称用法描述
    :first$(‘元素:first’)获取第一个元素
    :last$(‘元素:last’)获取最后一个元素
    :eq(index)$(‘元素:eq(index)’)获取第几个元素
    :odd$(‘元素:odd’)获取奇数的元素
    :even$(‘元素:even’)获取偶数的元素
    :checked$(‘元素:checked’)选中的表单的元素
    +

    筛选方法

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    名称用法描述
    parent()$(‘元素’).parent()查找父集
    parents(“名称”)$(‘元素’).parent(“名称”)查找上级
    children(‘元素’)$(‘元素’).children(‘元素’)查找子集
    find(‘元素’)$(‘元素’).find(‘元素’)后代
    siblings(‘元素’)$(‘元素’).siblings(‘元素’)查找兄弟节点,不包括自己
    nextAll()$(‘元素’).nextAll()查找后面的元素
    prevAll()$(‘元素’).prevAll()查找前面的元素
    hasClass(‘类名’)$(‘元素’).hasClass(‘类名’)判断是否包含类名
    eq(index)$(‘元素’).eq(index)索引方法
    +

    样式操作

    操作 CSS 方法

    1
    2
    3
    4
    5
    6
    // 获取值
    $("div").css("width");
    // 设置值
    $("div").css("width", "200px");
    // 设置多个值
    $("div").css({ width: 200, height: 200 });
    + +

    设置类样式方法

    +

    只会在原先基础上添加或更改,不会对原来的类操作

    +
    +
    1
    2
    3
    4
    5
    6
    // 添加类
    $("div").addClass("big");
    // 删除类
    $("div").removeClass("big");
    // 切换类
    $("div").toggleClass("big");
    + +

    效果(动画)

    动画队列及其停止排队方法

    +

    必须写在动画前面

    +
    +
    1
    .stop();
    + +

    显示隐藏

    1
    2
    3
    4
    5
    6
    7
    8
    // 速度 slow nomal fast 或毫秒数
    // 速度 swing(默认) linear
    // 显示
    show([速度, [切换效果], [回调函数]]);
    // 隐藏
    hide([速度, [切换效果], [回调函数]]);
    // 切换
    toggle([速度, [切换效果], [回调函数]]);
    + +

    滑动

    1
    2
    3
    4
    5
    6
    // 下滑效果
    slideDown([速度, [切换效果], [回调函数]]);
    // 上滑效果
    slideUp([速度, [切换效果], [回调函数]]);
    // 事件切换
    slideToggle([速度, [切换效果], [回调函数]]);
    + +

    淡入淡出

    1
    2
    3
    4
    5
    6
    7
    8
    // 淡入
    fadeIn([速度, [切换效果], [回调函数]]);
    // 淡出
    fadeOut([速度, [切换效果], [回调函数]]);
    // 切换
    fadeToggle([速度, [切换效果], [回调函数]]);
    // 到大某个位置 透明度 0-1之间 和速度必须写
    fadeTo([[速度], 透明度, [切换效果], [回调函数]]);
    + +

    自定义动画

    1
    animate(样式属性, [速度], [切换效果], [回调函数]);
    + +

    属性操作

    设置或获取元素的固有属性

    +

    element.prop(‘属性名’)

    +
    +
    1
    2
    3
    4
    // 获取
    prop("href");
    // 设置
    prop("href", "https://www.zhangsifan.com");
    + +

    自定义属性

    +

    element.attr(‘属性名’)

    +
    +
    1
    2
    3
    4
    // 获取属性值
    attr("属性名");
    // 设置属性值
    attr("属性", "属性值");
    + +

    数据缓存

    +

    数据存放在元素的内存中

    +
    +
    1
    2
    3
    4
    // 获取
    data("属性名");
    // 设置
    data("属性名", "值");
    + +

    文本属性值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 获取元素的内容
    html();
    // 设置元素的内容
    html("内容");
    // 获取元素的文本内容
    text();
    // 设置元素的文本内容
    text("内容");
    // 获取表单的值
    val();
    // 设置表单的值
    val("内容");
    // 保留几位小数
    toFixed(数量);
    + +

    元素操作

    遍历元素

    1
    2
    3
    4
    5
    6
    7
    // 用来遍历元素
    $("元素").each(function (index, item) {
    // item是DOM对象
    $(item).css();
    });
    // 用来遍历数据
    $.each(对象, function (index, element) {});
    + +

    创建元素

    1
    $("<li>我是li</li>");
    + +

    添加元素

    1
    2
    3
    4
    5
    6
    // 内部添加(父子关系)
    元素.append("内容"); // 添加到末尾
    元素.prepend("内容"); // 添加到开头
    // 外部添加(兄弟关系)
    元素.after("内容"); // 后面添加
    元素.before("内容"); // 添加到前面
    + +

    删除元素

    1
    2
    3
    4
    5
    6
    // 删除元素
    元素.remove();
    // 删除标签内容
    元素.empty();
    // 清空元素内容
    元素.html("");
    + +

    尺寸、位置操作

    尺寸

    + + + + + + + + + + + + + + + + + + + + + + +
    语法描述
    width()/height ()获取元素的宽度和高度
    innerWidth()/innerHeight()(包含 padding)
    outerWidth()/outerHeight()(包含 padding,border)
    outerWidth(true)/outerHeight(true)(包含 padding,border,margin)
    +

    位置

    + + + + + + + + + + + + + + + + + + + + + + + + + + +
    语法描述
    offset().top/.left获取距离文档的距离
    offset({top:10,left:30})设置距离文档的距离
    position().top/.left获取距离带有定位父级位置(不能设置)
    scrollTop()获取元素被卷起的头部
    scrollLeft()获取元素被卷起的左侧
    +
    1
    2
    3
    4
    5
    6
    // 返回顶部
    $("body,html")
    .stop()
    .animate({
    scrollTop: 0;
    });
    + +

    其他方法

    JQuery 对象拷贝

    +

    浅拷贝:把原来对象里面的复杂数据类型地址拷贝给目标对象
    深拷贝:把里面的数据完全复制一份给目标对象,有不冲突的属性会合并到一起

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 浅拷贝
    var newObj = {};
    var obj = {
    a: 1,
    name: "dog",
    };
    $.extend(newObj, obj);

    // 深拷贝
    $.extend(true, newObj, obj);
    + +

    多库共存

    1
    var jq = Jquery.noConflict();
    + +

    JQuery 插件

    +

    Jquery 插件库 > jQuery 之家

    +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/JS-JQuery%E5%9F%BA%E7%A1%80/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/JS-JS\345\237\272\347\241\200/index.html" "b/posts/JS-JS\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..76ded1c8 --- /dev/null +++ "b/posts/JS-JS\345\237\272\347\241\200/index.html" @@ -0,0 +1,335 @@ +JS基础 | JCAlways + + + + + + + + + + + + + +

    JS基础

    变量

    运算符

    循环

    for 循环

    1
    2
    3
    for(初始化变量,条件表达式,操作表达式){
    // 循环体
    }
    + +

    双重 for 循环

    +

    外层循环一次,里面循环全部

    +
    +
    1
    2
    3
    4
    5
    6
    for(初始化变量,条件表达式,操作表达式){
    // 循环体
    for(初始化变量,条件表达式,操作表达式){
    // 循环体
    }
    }
    + +

    while 循环

    +

    条件表达式为 true 则循环表达体,否则退出循环

    +
    +
    1
    2
    3
    4
    5
    // 初始化变量
    while (条件表达式) {
    // 循环体
    // 操作表达式
    }
    + +

    do while 循环

    +

    先执行循环体,在判断条件 为 true,继续循环,否则退出循环

    +
    +
    1
    2
    3
    4
    do {
    // 循环体
    // 操作表达式
    } while (条件表达式);
    + +

    continue break

    +

    continue 退出当前循环,继续执行剩余次数的循环
    break 退出整个循环(循环结束)

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // continue
    for (var i = 1; i <= 5; i++) {
    if (i == 3) {
    continue;
    }
    console.log(i);
    }
    // break
    for (var i = 1; i <= 10; i++) {
    if (i == 5) {
    break;
    }
    console.log(i);
    }
    + +

    数组

    创建数组

    new 创建

    1
    var arr = new Array();
    + +

    字面量创建

    1
    2
    var arr = [];
    var arr1 = [1, 2, "3", true];
    + +

    获取数组中的元素

    +

    数组下标从 0 开始

    +
    +
    1
    2
    var arr = [1, 2, "3", true];
    console.log(arr[1]); // 2
    + +

    遍历数组

    1
    2
    3
    4
    var arr = [1, 2, 3, 4, 5];
    for (var i = 0; i <= arr.length - 1; i++) {
    console.log(arr[i]);
    }
    + +

    添加元素

    修改 length

    1
    2
    3
    var arr = [1, 2, 3];
    arr.length = 5;
    console.log(arr); // [1, 2, 3, empty × 2]
    + +

    修改索引号

    1
    2
    3
    var arr = [1, 2, 3];
    arr[3] = 4;
    console.log(arr); // [1, 2, 3, 4]
    + +

    冒泡排序

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var arr = [5, 99, 55, 1, 22, 44, 4, 3, 2, 1];
    for (var i = 0; i <= arr.length - 1; i++) {
    for (var j = 0; j <= arr.length - i - 1; j++) {
    if (arr[j] > arr[j + 1]) {
    var temp = arr[j];
    arr[j] = arr[j + 1];
    arr[j + 1] = temp;
    }
    }
    }
    console.log(arr);
    + +

    函数

    声明函数

    +

    命名规范 以动词开始 小驼峰命名法(单词首字母小写,第二个单词首字母大写)

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    // function 声明(命名函数)
    function 函数名称() {
    // 代码
    }
    // 函数表达式 (匿名函数)
    var 函数名称 = function () {
    // 代码
    };
    + +

    调用函数

    1
    函数名称();
    + +

    函数的参数

    1
    2
    3
    4
    function 函数名 (行参1,形参2...){

    }
    函数名(实参1,实参2...);
    + +

    函数的返回值

    1
    2
    3
    function 函数名称() {
    return 需要返回的结果;
    }
    + +

    arguments 的使用

    +

    arguments 存储了所有的实参-伪数组

    +
    +

    作用域

    +

    可用性代码范围

    +
    +

    全局作用域: 整个 script 标签 或一个单独的 js 文件
    局部作用域: 在函数内部就是局部作用域

    +

    变量的作用域

    全局变量

    +

    在全局作用域下的变量 在全局下可以使用

    +
    +

    局部变量

    +

    在局局作用域下的变量 智能在函数内部使用

    +
    +

    作用域链

    +

    内部函数访问外部函数的变量,采取的是链式查找的方式取值

    +
    +

    对象

    +

    包含属性和方法

    +
    +

    创建对象的方法

    字面量

    1
    2
    3
    4
    5
    6
    7
    var obj = {
    uname: "张三",
    age: 18,
    sayHi:function (){
    console.log('hello)
    }
    };
    + +

    new Object

    1
    2
    3
    4
    5
    var obj = new Object();
    obj.uname = "张三";
    obj.sayHi = function () {
    console.log("hello");
    };
    + +

    构造函数

    +

    构造函数名首字母要大写

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    function 构造函数名(uname, age) {
    this.uname = uname;
    this.age = age;
    this.sayHi = function () {
    console.log("hello");
    };
    }
    var zs = new 构造函数名("张三", 18);
    var ls = new 构造函数名("李四", 12);
    + +

    调用方法

    1
    2
    3
    console.log(obj.uname);
    console.log(obj["uname"]);
    obj.sayHi();
    + +

    遍历对象属性

    1
    2
    3
    4
    for (var key in obj) {
    console.log(key); // 输出属性名
    console.log(obj[key]);
    }
    + +

    new 干了些什么?

      +
    • 开辟一块新的空间
    • +
    • 将 this 指向这个空间
    • +
    • 执行代码,添加属性和方法
    • +
    • 返回这个对象
    • +
    +

    内置对象

    +

    文档地址

    +
    +

    Math

    一般用法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 圆周率
    Math.PI; // 3.14
    // 最大值
    Math.max(10, 20); // 20
    // 最小值
    Math.min(10, 20); // 10
    // 绝对值
    Math.abs(-10); // 10 会把字符串转数字
    // 向下取征
    Math.floor(1.9); // 1
    // 向上取征
    Math.cell(1.1); // 2
    // 四舍五入
    Math.round(1.5); // 2
    + +

    随机数

    1
    2
    3
    4
    function getRandom(min, max) {
    return Math.floor(Math.random() * (max - min + 1)) + min; //含最大值,含最小值
    }
    getRandom(1, 10);
    + +

    Date

    +

    是构造函数 必须 new

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var date = new Date();
    var date = new Date("2019-11-1"); // Fri Nov 01 2019 00:00:00 GMT+0800 (中国标准时间)
    date.getFullYear(); // 年
    date.getMonth() + 1; // 月
    date.getDate(); // 日
    date.getDay(); // 星期 周日返回0
    date.getHours(); // 时
    date.getMinutes(); // 分
    date.getSeconds(); // 秒
    + +

    时间戳

    +

    1970-1-1 0.0.0 到当前时间过了多少毫秒

    +
    +
    1
    2
    3
    4
    5
    6
    7
    var date = new Date();
    console.log(date.valueOf()); // 1582465459329
    console.log(date.getTime()); // 1582465459329
    // ------------------------------------------
    console.log(+new Date()); // 1582465459329
    // H5新增方法--------------------------------
    console.log(Date.now()); // 1582465459329
    + +

    倒计时

    公式

    1
    2
    3
    4
    5
    var times = (inputTime - nowTime) / 1000;
    d = parseInt(总秒数 / 60 / 60 / 24);
    h = parseInt((总秒数 / 60 / 60) % 24);
    m = parseInt((总秒数 / 60) % 60);
    s = parseInt(总秒数 % 60);
    + +

    函数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function conutDown() {
    var inputTime = +new Date("2020-10-1 21:08:03");
    var nowTime = +new Date();
    var times = (inputTime - nowTime) / 1000;
    var d = parseInt(times / 60 / 60 / 24);
    d = d < 10 ? "0" + d : d;
    var h = parseInt((times / 60 / 60) % 24);
    h = h < 10 ? "0" + h : h;
    var m = parseInt((times / 60) % 60);
    m = m < 10 ? "0" + m : m;
    var s = parseInt(times % 60);
    s = s < 10 ? "0" + s : s;
    return d + "天" + h + "时" + m + "分" + s + "秒";
    }
    + +

    Array

    检测是否为数组

    1
    2
    3
    4
    var arr = [];
    console.log(arr instanceof Array); // true
    // H5新增方法--------------------------------
    console.log(Array.isArray(arr)); // true
    + +

    添加删除方法

    1
    2
    3
    4
    5
    var arr = [];
    arr.push(); // 在末尾添加一个或多个元素 返回长度
    arr.unshift(); // 在开头添加一个或多个元素 返回长度
    arr.pop(); // 删除最后一个 返回删除的内容
    arr.shift(); // 删除第一个 返回删除的内容
    + +

    翻转数组

    1
    2
    var arr = [];
    arr.reverse();
    + +

    排序

    1
    2
    3
    4
    var arr = [3, 4, 7, 1];
    arr.sort(function (a, b) {
    return a - b; // a-b升序 b-a 降序
    }); // 1 3 4 7
    + +

    获取索引号

    +

    找不到返回-1

    +
    +
    1
    2
    3
    4
    5
    var arr = ["red", "pink", "blue"];
    // 从前往后
    arr.indexOf("blue"); // 2
    // 从后往前
    arr.lastIndexOf("blue"); // 2
    + +

    转字符串

    1
    2
    3
    4
    var arr = [1, 2, 3];
    arr.toString(); // 1,2,3
    // 括号中是分割符
    arr.join(-); // 1-2-3
    + +

    String

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var str = "nihao,haha";
    // 获取长度
    str.length; // 5
    // 获取某个条件的索引值
    str.indexOf("i"); // 1
    // 根据位置返回字符
    str.charAt(3); // a
    // H5新增方法--------------------------------
    str[3]; // a
    // 拼接字符串
    str.concat("ya"); // nihaoya
    // 截取字符串
    str.substr(2, 2); // ha
    // 替换
    str.replace('n','a') // aihao
    // 转数组
    str.split(,) // ['nihao','haha']
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/JS-JS%E5%9F%BA%E7%A1%80/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/JS-JS\351\253\230\347\272\247/index.html" "b/posts/JS-JS\351\253\230\347\272\247/index.html" new file mode 100644 index 00000000..a9efd06b --- /dev/null +++ "b/posts/JS-JS\351\253\230\347\272\247/index.html" @@ -0,0 +1,210 @@ +JS高级 | JCAlways + + + + + + + + + + + + + +

    JS高级

    标题

    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/JS-JS%E9%AB%98%E7%BA%A7/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/posts/JS-Web Apis/index.html b/posts/JS-Web Apis/index.html new file mode 100644 index 00000000..35dace9a --- /dev/null +++ b/posts/JS-Web Apis/index.html @@ -0,0 +1,440 @@ +Web Apis | JCAlways + + + + + + + + + + + + + +

    Web Apis

    DOM (文档对象模型/Document Object Model)

    获取元素

    通过 ID 获取

    1
    <div id="mybox"></div>
    + +
    1
    var box = document.getElementById("mybox");
    + +

    根据 标签名 获取

    +

    返回的是元素对象集合 以数组的形式存储
    如果要获取每个具体标签对象,通过循环遍历的方式

    +
    +
    1
    2
    3
    4
    5
    6
    7
    <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    </ul>
    + +
    1
    document.getElementsByTagName("li");
    + +

    根据 类名 获得

    1
    <div class="box"></div>
    + +
    1
    document.getElementsByClassName("box");
    + +

    返回指定选择器的第一个

    1
    <div id="nav" class="box"></div>
    + +
    1
    2
    document.querySelector(".box");
    document.querySelector("#nav");
    + +

    返回指定选择器的第一个

    1
    2
    <div class="box"></div>
    <div class="box"></div>
    + +
    1
    document.querySelectorAll(".box");
    + +

    获取 body html 元素

    1
    2
    3
    document.body; // 返回body元素

    document.documentElement; // 返回hhtml元素
    + +

    事件

    +

    事件源 事件类型 处理程序

    +
    +

    绑定事件

    on 的方式

    +

    通过 on 的方式给元素注册事件的时候,注册用一个事件,那么最后的事件会覆盖前面的事件

    +
    +
    1
    2
    3
    document.querySelectorAll(".box").onclick = function () {
    // 处理程序
    };
    + +

    常见的鼠标事件

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    事件名触发条件
    onclick点击事件
    onmouseenter鼠标经过触发
    onmouseleave鼠标离开
    onfocus获得焦点
    onblur失去焦点
    onmousemove鼠标移动触发
    onmouseup鼠标弹起
    onmousedown鼠标按下
    contextmenu上下文菜单
    selectstart开始选中
    +

    常见的键盘事件

    + + + + + + + + + + + + + + + + + + + + + + + +
    事件名触发条件
    oninput输入事件
    onkeydown按下
    onkeyup松开
    onkeypress按下(不能获取系统按键)
    +

    事件监听的方式(多个人使用)

    1
    事件源.addEventListener(事件类型, 处理程序, boolean); // true 捕获效果 / false 冒泡效果
    + +

    移动端触屏事件

    + + + + + + + + + + + + + + + + + + + +
    事件名触发条件
    touchstart按下事件
    touchmove移动事件
    touchend抬起事件
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var div = document.querySelector(".box");
    div.addEventListener("touchstart", function () {
    console.log("按下事件");
    });
    div.addEventListener("touchend", function () {
    console.log("抬起事件");
    });
    div.addEventListener("touchmove", function () {
    console.log("移动事件");
    });
    + +

    删除事件

    on 的方式

    1
    事件源.事件类型 = null;
    + +

    事件监听的方式

    1
    事件源.removeEventListener(事件类型, 处理程序(命名函数));
    + +

    事件对象

    PC 端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    e.stopPropagation(); // 阻止冒泡
    e.cancelBubble = true; // 阻止冒泡
    e.target; // 真正的事件源
    e.target; // 获取正在执行事件的类型
    e.key; //键盘按键的名字
    e.keyCode; //键盘按键的值
    //从HTML的可视区域左上角开始计算
    e.clientX;
    e.clientY;
    //从当前元素的左上边开始计算
    e.offsetX;
    e.offsetY;
    //从页面的可视区域左上角开始计算
    e.pageX;
    e.pageY;
    //从整个屏幕的左上角开始计算
    e.screenX;
    e.screenX;
    + +

    手机端 TouchEvent

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 获取位于移动设备的屏幕上的手指信息(伪数组)
    e.touches;
    // 获取距离视口的位置
    e.touches.clientX;
    // 获取距离页面的坐标
    e.touches.pageX;
    // 获取距离整个屏幕的坐标
    e.touches.screenX;
    // 获取元素身上的手指信息
    e.targetTouches;
    // 离开屏幕的信息
    e.changedTouches;
    + +

    操作元素

    操作元素内容

    获取 写 元素内容

    1
    2
    element.innerText;
    element.innerHTML; // 推荐使用
    + +

    操作表单

    1
    2
    element.value; // 取值
    element.value = ""; // 赋值
    + +

    属性

    1
    2
    3
    4
    element.type = 值; // input类型
    element.checked = 值; // 复选框
    element.selected = 值; // 下拉框
    element.disabled = 值; // 禁用
    + +

    操作元素样式

    行内样式

    1
    element.style.css属性名 = 值;
    + +

    添加类名

    1
    element.className = "类名";
    + +

    操作属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    element.属性; // 获取内置属性
    element.属性 = 值; // 设置内置属性
    element.getAttribute("属性"); // 可以获取自定义属性的值
    element.setAttribute("属性", 值); // 可以设置自定义属性的值
    element.removeAttribute("属性"); // 可以删除自定义属性的值
    // H5API 规范 'data-属性名称'
    // 通过H5的方式获取只能获取到自定义属性
    element.dataset.属性名;
    element.dataset.属性名 = 值;
    + +

    节点操作

    +

    页面中所有的内容都是节点

    +
    +

    查找元素

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    node.parentNode; // 获得子元素的父节点
    node.children; // 获得父元素的字元素
    // 获取第一个元素
    node.children[0];
    node.firstElementChild; // ↓获取第一个元素
    // 获取最后一个元素
    node.children[element.children.length - 1];
    node.lastElementChild; // ↓获取最后一个元素
    // 获取当前标签下一个兄弟元素
    node.nextElementSibling;
    // 获取当前标签上一个兄弟元素
    node.previousElementSibling;
    + +

    创建节点

    +

    先创建 在添加

    +
    +
    1
    2
    3
    4
    5
    // 创建元素节点
    document.createElement("标签名");
    // 添加(插入)节点
    node.appendChild("子元素");
    node.insertBefore("子元素", 谁的前面);
    + +

    删除节点

    1
    node.removeChild("节点");
    + +

    复制节点

    +

    先克隆 在添加

    +
    +
    1
    node.cloneNode(boolean); // true(复制标签复制内容) / false(复制标签)
    + +

    BOM (浏览器对象模型/Browser Object Model)

    常见事件

    +

    只能写一次,多个会加载最后一个

    +
    +
    1
    2
    3
    4
    5
    6
    // 窗口加载事件
    window.onload = function () {};
    document.addEventListener("DOMContentLoaded", function () {});
    // 窗口大小变化
    window.onresize = function () {};
    document.addEventListener("resize", function () {});
    + +

    定时器

    setTimeout()

    +

    延时多少时间后执行

    +
    +
    1
    2
    3
    4
    // 开启定时器
    setTimeout(function () {}, time);
    // 取消定时器
    clearTimeout(定时器编号);
    + +

    setInterval()

    +

    每隔一段时间执行一次

    +
    +
    1
    2
    3
    4
    // 开启定时器
    setInterval(function () {}, time);
    // 取消定时器
    clearInterval(定时器编号);
    + +

    this 指向问题

      +
    • 在全局下,this 指向 window
    • +
    • 在对象的方法下,this 指向调用对象
    • +
    • 构造函数中,this 指向新的对象
    • +
    • apply 调用 this,指向 apply 的第一个参数
    • +
    +

    location 对象

    +

    获取或设置窗体 URL

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    // 通过程序的方法实现页面跳转
    location.href = "路径";
    // 获取页面路径
    location.href;
    // 在新窗口打开页面
    window.open(页面路径);
    // 委派 实现页面跳转
    location.assign(页面路径);
    // 替换 跳转页面
    location.replace(路径);
    // 重新加载页面
    location.reload(boolean); // true 需要去服务器中查询数据 然后将服务器中新数据显示出来 / false 本地缓存中重新查询数据,将数据显示出来
    // 获取域名/ip
    location.host;
    // 获取域名名称
    location.hostname;
    // 获取路径的名字
    location.pathname;
    // 端口
    location.port;
    // 获取协议类型
    location.protocol;
    // 查询条件值
    location.search;
    // 设置临时图片资源
    window.URL.createObjectURL(url);
    + +
    1
    navigator.userAgent; // 获取用户浏览器信息
    + +

    history 对象

    1
    2
    3
    history.back(); //后退
    history.forward(); // 前进
    history.go(); // 前进或后退 1 前进1 / -1 后退1
    + +

    获取元素位置和大小

    offset

    +

    获取元素距离带有定位父元素的距离

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    // 元素在网页中的水平距离
    对象名.offsetLeft;
    // 元素在网页中的垂直距离
    对象名.offsetTop;
    // 元素在网页中的宽度(实际大小)
    对象名.offsetWidth;
    // 元素在网页中的高度(实际大小)
    对象名.offsetHeight;
    + +

    clien

    1
    2
    3
    4
    5
    6
    7
    8
    // 获取元素的左边框
    对象名.clientLeft;
    // 获取元素的上边框
    对象名.clientTop;
    // 获取元素的宽度(除去边框)
    对象名.clientWidth;
    // 获取元素的高度(除去边框)
    对象名.clientHeight;
    + +

    scrool

    +

    滚动

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 水平方向滚动出去的距离
    对象名.scroolLeft;
    // 垂直方向滚动出去的距离
    对象名.scrollTop;
    // 获取元素的宽度(包括超出去的部分)
    对象名.scrollWidth;
    // 获取元素的高度(包括超出去的部分)
    对象名.scrollHeight;

    window.pageYOffset; // 页面被卷起的高度
    + +

    立即执行函数

    +

    不需要调用 立马能够执行

    +
    +
    1
    (function () {})();
    + +

    click 延时解决方案

      +
    • 禁用缩放
    • +
    +
    1
    <meta name="viewport" content="user-scalable=no" />
    + +
      +
    • 利用 touch 事件自己封装

      +
      +

      触摸记录当前时间 离开记录离开时间 如果小于 150ms 并且没有滑动 那么就是点击

      +
      +
    • +
    • fastclick 插件

      +
    • +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/JS-Web%20Apis/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Node-Express\346\241\206\346\236\266/index.html" "b/posts/Node-Express\346\241\206\346\236\266/index.html" new file mode 100644 index 00000000..c5ae4287 --- /dev/null +++ "b/posts/Node-Express\346\241\206\346\236\266/index.html" @@ -0,0 +1,273 @@ +Express框架 | JCAlways + + + + + + + + + + + + + +

    Express框架

    Express 介绍

      +
    • Express 是一个基于 Node.js 平台,快速、开放、极简的 web 开发框架
    • +
    • Express 是一个第三方模块,有丰富的 API 支持,强大而灵活的中间件特性
    • +
    • Express 不对 Node.js 已有的特性进行二次抽象,只是在它之上扩展了 Web 应用所需的基本功能
    • +
    • 链接 +
    • +
    +

    Hello World

      +
    • 下载 Express 包
    • +
    +
    1
    2
    npm init -y
    npm i express
    + +
      +
    • 代码
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 0. 加载 Express
    const express = require("express");
    // 1. 调用 express() 得到一个 app
    // 类似于 http.createServer()
    const app = express();
    // 2. 设置请求对应的处理函数
    // 当客户端以 GET 方法请求 / 的时候就会调用第二个参数:请求处理函数
    app.get("/", (req, res) => {
    res.send("hello world");
    });
    // 3. 监听端口号,启动 Web 服务
    app.listen(3000, () => console.log("app listening on port 3000!"));
    + +

    托管静态资源

    产考文档

    +

    让用户直接访问静态资源是一个 web 服务器最基本的功能。

    +
    1
    2
    http://localhost:3000/1.png http://localhost:3000/css/style.css
    http://localhost:3000/js/index.js
    + +

    例如,如上 url 分别是请求一张图片,一份样式文件,一份 js 代码。我们实现的 web 服务器需要能够直接返回这些文件的内容给客户端浏览器。

    +

    忽略前缀

    此时,所有放在 public 下的内容可以直接访问,注意,此时在 url 中并不需要出现 public 这级目录

    +
      +
    • 在 public 下新建 index.html。可以直接访问到。
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 0. 加载 Express
    const express = require("express");

    // 1. 调用 express() 得到一个 app
    // 类似于 http.createServer()
    const app = express();

    // 2. 设置请求对应的处理函数
    app.use(express.static("public"));

    // 3. 监听端口号,启动 Web 服务
    app.listen(3000, () => console.log("app listening on port 3000!"));
    + +

    限制前缀

    这意味着想要访问 public 下的内容,必须要在请求 url 中加上/public

    +
    1
    2
    // 限制访问前缀
    app.use("/public", express.static("public"));
    + +

    路由

    产考文档

    +
    +

    路由(Routing)是由一个 URL(或者叫路径标识)和一个特定的 HTTP 方法(GET、POST 等)组成的,涉及到应用如何处理响应客户端请求。每一个路由都可以有一个或者多个处理器函数,当匹配到路由时,这些个函数将被执行。

    +
    +

    设置状态码

    1
    res.status(200).json({ name: "abc" });
    + +

    Get

    无参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const express = require("express");
    const app = express();
    app.get("/get", function (req, res) {
    // 直接返回对象
    res.json({ name: "abc" });
    });
    app.listen("8088", () => {
    console.log("8088");
    });
    + +

    有参数

    express 框架会自动收集 get 参数,并保存在 req 对象的query属性中。我们直接来获取即可。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const express = require("express");
    const app = express();
    app.get("/get", function (req, res) {
    // 直接返回对象
    console.log(req.query);

    res.send({ name: "abc" });
    });
    app.listen("8088", () => {
    console.log("8088");
    });
    + +

    Post

    无参数

    1
    2
    3
    4
    5
    6
    7
    const app = express();
    app.post("/post", function (req, res) {
    res.send({ name: "abc" });
    });
    app.listen("8088", () => {
    console.log("8088");
    });
    + +

    普通键值对参数

    获取 post 普通键值对数据,要通过第三方模块body-parser来解析。

    +
    1
    npm install body-parser
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 1. 引入包
    const bodyParser = require("body-parser");

    // 2. 使用包
    app.use(bodyParser.urlencoded({ extended: false }));

    app.post("/add", function (req, res) {
    //3. 可以通过req.body来获取post传递的键值对
    res.json(req.body);
    });
    + +

    文件上传

    如果 post 涉及文件上传操作,则会要额外使用multer这个包来获取上传的信息。

    +
    1
    npm install multer
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 1. 引入包
    const multer = require("multer");
    // 2. 配置
    const upload = multer({ dest: "uploads/" }); // 上传的文件会保存在这个目录下
    // uploads表示一个目录名,你也可以设置成其它的

    // 3. 使用
    // 这个路由使用第二个参数 .upload.single表示单文件上传, 'cover' 表示要上传的文件在本次上次数据中的键名。类似于<input type="file" name='cover'/>

    app.post("postfile", upload.single("cover"), function (req, res) {});
    + +

    常见问题

    node 中的异步问题

    例如

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var fs = require("fs");
    function getMime() {
    //1
    fs.readFile("mime.json", function (err, data) {
    //console.log(data.toString());
    return data; //3
    });
    //2
    //return '123';
    }
    console.log(getMime()); /*由于异步操作没有拿到数据,如何解决,通过异步操作*/
    + +

    解决

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    var fs = require("fs");
    function getMime(callback) {
    fs.readFile("mime.json", function (err, data) {
    callback(data);
    });
    }
    getMime(function (result) {
    console.log(result.toString());
    });
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Node-Express%E6%A1%86%E6%9E%B6/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Node-MySQL\344\275\277\347\224\250/index.html" "b/posts/Node-MySQL\344\275\277\347\224\250/index.html" new file mode 100644 index 00000000..9958ab13 --- /dev/null +++ "b/posts/Node-MySQL\344\275\277\347\224\250/index.html" @@ -0,0 +1,223 @@ +Node操作MySQL | JCAlways + + + + + + + + + + + + + +

    Node操作MySQL

    数据库的增删改查

    1
    insert into 表名(字段名1,字段名2,......) values(值1,值2,......)
    + +

    1
    delete  from 表名  where 删除条件
    + +

    1
    update 表名 set 字段1=值1, 字段2=值2,...  where 修改条件
    + +

    1
    SELECT  字段名1, 字段名2, .....  FROM 表名	WHERE <条件表达式>
    + +

    Node.js 中使用 MySQL

    初始化文件夹

    1
    npm init -y
    + +

    安装插件

    1
    npm i mysql
    + +

    创建 server.js 将代码复制进去

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    var mysql = require("mysql");
    var connection = mysql.createConnection({
    host: "localhost",
    user: "root",
    password: "root",
    database: "test", //你要连接数据库的名字
    });

    connection.connect();
    let sql = ``; //sql查询语句
    connection.query(sql, function (error, results, fields) {
    if (error) throw error;
    console.log(results);
    });

    connection.end();
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Node-MySQL%E4%BD%BF%E7%94%A8/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Node-Node\344\270\255\347\232\204\344\274\232\350\257\235\346\212\200\346\234\257/index.html" "b/posts/Node-Node\344\270\255\347\232\204\344\274\232\350\257\235\346\212\200\346\234\257/index.html" new file mode 100644 index 00000000..ff82ae9d --- /dev/null +++ "b/posts/Node-Node\344\270\255\347\232\204\344\274\232\350\257\235\346\212\200\346\234\257/index.html" @@ -0,0 +1,238 @@ +Node中的会话技术 | JCAlways + + + + + + + + + + + + + +

    Node中的会话技术

    会话技术

    有很多的网站都有登录的功能:

    +
    1
    2
    3
    |--login.html (登录页)
    |--index.html(主页)
    |--vip.html(设置页)
    + +

    实际开发中,我们必须解决:

    +
      +
    1. 页面之间的状态共享问题:例如用户从 login.html 页面登陆之后,再去访问 index.html 或者 setting.html 页面时,应该还是能够获取用户的登陆信息。
    2. +
    3. 权限控制。必须先 login 成功,之后才访问 vip.html。
    4. +
    +

    由于 http 是无状态的,就是无记忆的,对于 HTTP 协议而言,无状态同样指每次 request 请求之间是相互独立的,当前请求并不会记录上一次请求信息。每次请求都是独立的,没有关联的,所以服务器和客户端都不知道是否是登录过的。

    +

    什么是会话控制

    会话控制就是用来弥补 http 无记忆的缺陷的一种技术。它能够将数据持久化(保存数据)的保存在客户端(浏览器)或者服务器端,从而让浏览器和服务器进行多次数据交换时,产生连续性。

    +

    每一次的请求和响应都知道对方是谁

    +

    1566131019358.png

    +

    会话控制的分类

    Cookie–将数据保存到客户端(浏览器)(饼干,甜点)

      +
    • 从服务器端向客户端浏览器留下信息;
    • +
    • 浏览器每次访问服务器时都带上这些信息(自动携带 cookie 是浏览器的特点);
    • +
    +

    节省服务器空间,缺点不安全。不要保存敏感信息。

    +

    session– 将数据保存到服务器端

    session 原理

      +
    • 服务器端会为每个用户(浏览器)各自保存一个 session(文件)。当服务器保存 session 之后,会以 cookie 的形式告诉浏览器,你的 session 编号是哪一个。它把 session 号返回给了浏览器,而把真实的数据保存在服务器。
    • +
    • 下次再来访问服务器的时候,浏览器就会带着它自己的 session 号去访问,服务器根据 session 号就可以找到对应的 session 了。
    • +
    +

    session 优缺点

    优点是安全,缺点需要服务器空间, 是一种最常见的解决方案。

    +

    小结- 跨域带 cookie

    如果是发跨域的 ajax 请求需要带上 cookie 的话,要处理如下:

    +

    后端

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    app.all("*", function (req, res, next) {
    console.log(
    `${Date.now()}:来自${req.connection.remoteAddress} 访问了 ${req.method}-${
    req.url
    }。参数是:${req.query},携带cookie:${req.headers.cookie}`
    );
    res.header("Access-Control-Allow-Origin", req.headers.origin); //需要显示设置来源
    res.header(
    "Access-Control-Allow-Headers",
    "Origin, X-Requested-With, Content-Type, Accept"
    );
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Access-Control-Allow-Credentials", true); //带cookies
    next();
    });
    + +

    前端

    +
    1
    2
    3
    4
    5
    6
    7
    8
    $.ajax({
    crossDomain: true,
    xhrFields: {
    withCredentials: true,
    },
    url,
    type,
    });
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Node-Node%E4%B8%AD%E7%9A%84%E4%BC%9A%E8%AF%9D%E6%8A%80%E6%9C%AF/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Node-Node\344\270\255\347\232\204\350\267\250\345\237\237/index.html" "b/posts/Node-Node\344\270\255\347\232\204\350\267\250\345\237\237/index.html" new file mode 100644 index 00000000..1a9a00bf --- /dev/null +++ "b/posts/Node-Node\344\270\255\347\232\204\350\267\250\345\237\237/index.html" @@ -0,0 +1,336 @@ +Node中的跨域 | JCAlways + + + + + + + + + + + + + +

    Node中的跨域

    什么是跨域

    不同源的AJAX就是跨域

    +

    协议,域名 ,端口相同就是同源

    +

    问题演示

    chrome跨域插件.gif

    +

    浏览器向 web 服务器发起 http 请求时 ,如果同时满足以下三个条件时,就会出现跨域问题,从而导致 ajax 请求失败:

    +
      +
    • 你的浏览器多管闲事了。跨域问题出现的基本原因是浏览器出于安全性的考虑——同源策略:ajax 请求必须是同源,封杀了你跨域请求。可以安装一个浏览器插件allow-control-allow-origin来测试一下。 如果使用 postman 软件(它不是浏览器)来发请求,就不会有这个问题了。

      +
    • +
    • 你的请求是 xhr 请求。就是常说的 ajax 请求。浏览器请求图片资源,js 文件,css 文件是可以跨域的(不是 ajax 请求)

      +
    • +
    • 发出请求不符合同源策略要求。

      +
        +
      • 同源是指:协议相同域名相同端口相同。即发 ajax 请求的所在的页面 与 所请求的接口的 url 必须是同源的。

        +

        以下就是不同源的:

        +

        http://127.0.0.1:5500/message_front/index.html 请求http://localhost:8080/msg

        +
      • +
      • 在前后端分离开发的场景下,前端的页面和后端的服务经常是分开部署的,所以跨域访问的情况是比较常见的。

        +

        什么是跨域.png

        +
      • +
      +

      注意,错误是发生在浏览器端的。请求是可以正常从浏览器发到服务器端,服务器也可以处理请求,只是返回到浏览器端时出错了。

      +
    • +
    +

    实现跨域的解决方案–JSONP

    JSONP 简介

    JSON with Padding,是一种借助于 script 标签发送跨域请求的技巧。

    +

    原理:

    +
      +
    • script 的 src 标签可以请求外部的 js 文件,它是可以发跨域请求的。
    • +
    • 借助 script 标签 src 请求服务端上的接口
    • +
    • 服务端的接口返回 JavaScript 脚本,并附上要返回的数据。
    • +
    +

    它其实并不是 ajax 请求

    +

    实现步骤

    让 script 标签的 src 指向一个接口

    +

    前端:让 script 标签的 src 指向一个后端接口的地址;

    +

    后端:接口的返回值是一个 js 函数调用语句

    +
    +

    前端页面

    +
    1
    2
    <script src="接口地址"></script>
    后果 从这个接口中返回内容会当作js代码去执行
    + +

    注意:

    +
      +
    • script 标签中的 src 会指向一个后端接口的地址。由于 script 标签并不会导致跨域问题,所以这里的请求是可以正常发送和接收的。
    • +
    • 与我们之前理解的 src 指向某个具体的.js 文件不同,我们只需要确保 src 所指向的地址的返回内容是 js 代码就行了,而不必要一定是.js 文件。
    • +
    +

    后端接口

    +
    1
    2
    3
    4
    5
    6
    7
    8
    const express = require('express');
    const app = express();
    app.get('/gettime', (req, res) => {
    res.end(`alert(1)`);
    })
    app.listen(3000, () => {
    console.log('你可以通过http://localhost:3000来访问...');
    });
    + +

    注意:

    +
      +
    • 后端接口的返回值是一个特殊的字符串: 一个刻意拼写的 js 函数调用语句
    • +
    +

    传递函数名到后端

    +

    前端:让 script 标签的 src 指向一个后端接口的地址,并附加函数名;

    +

    后端:接口的返回值是一个 js 函数调用语句

    +

    目标:当请求成功时,执行前端指定的函数

    +
    +

    前端页面

    +
    1
    2
    3
    4
    5
    6
    <script>
    function fn() {
    console.log();
    }
    </script>
    <script src="http://localhost:3000/gettime?callback=fn"></script>
    + +

    注意:

    +
      +
    • 在前端自己定义一个函数,把函数名传给后端
    • +
    • 使用 get 方式传参,并且参数名是 callback。这个参数名要与后端保持一致。
    • +
    +

    后端接口

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    const express = require('express');
    const app = express();
    app.get('/gettime', (req, res) => {
    let { callback } = req.query;
    res.end(`${callback}()`);
    })
    app.listen(3000, () => {
    console.log('你可以通过http://localhost:3000来访问...');
    });
    + +

    注意:

    +
      +
    • 后端接口接收函数名,并返回一个字符串,其内容是函数调用语句
    • +
    +

    后端回传数据

    +

    前端:让 script 标签的 src 指向一个后端接口的地址,并附加函数名;

    +

    后端:接口的返回值是一个 js 函数调用语句,并附加实参;

    +

    目标:当请求成功时,执行前端指定的函数

    +
    +

    前端页面

    +
    1
    2
    3
    4
    5
    6
    <script>
    function dosomething(rs) {
    console.log(rs);
    }
    </script>
    <script src="http://localhost:3000/getTime?callback=dosomething"></script>
    + +

    注意:

    +
      +
    • script 标签中的 src 会指向一个后端接口的地址。由于 script 标签并不会导致跨域问题,所以这里的请求是可以常发送的。
    • +
    • 把前端的函数名传给后端
    • +
    +

    后端接口

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const express = require('express');
    const app = express();
    app.get('/gettime', (req, res) => {
    let { callback } = req.query;
    let data = JSON.stringfy( {a:1,b:2} )
    res.end(`${callback}(${data})`);
    })
    app.listen(3000, () => {
    console.log('你可以通过http://localhost:3000来访问...');
    });

    + +

    注意:

    +
      +
    • 接收函数名,组装一个特殊的字符串:函数调用语名
    • +
    • 把要回传的参数转成字符串,并嵌入返回值,当作函数的实参。
    • +
    +

    Jquery-封装的 JSONP 前端代码

    jquery 中的 ajax 已经封装好了的 jsonp 方式,你可以直接使用。具体来说就是给 ajax 请求添加一个 dataType 属性,其值为”jsonp”。注意前后端都需要改动代码。示例如下:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    $.ajax({
    type: "GET",
    url: "http://localhost:4000/getTime",
    success: function (result) {
    console.log(result);
    },
    dataType: "jsonp", // 必须要指定dataType为jsonp
    });
    + +

    express 框架中的 JSONP 后端代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    const express = require('express');
    const app = express();
    app.get('/gettime', (req, res) => {
    let data = {a:1,b:2}
    res.jsonp(data)
    })
    app.listen(3000, () => {
    console.log('你可以通过http://localhost:3000来访问...');
    });
    + +

    完整的 JSONP 代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>html页面</title>
    </head>
    <body>
    <div class="container">
    <h1>jsonp</h1>
    <div>需要后端接口的配合:http://localhost:3005/jsonp</div>
    <pre>
    //--后端测试代码如下
    const express = require('express');
    const app = express()

    // 留言板接口 -- 获取所有数据
    app.get('/jsonp', (req, res) => {
    var { callback } = req.query;

    res.setHeader('content-type', 'application/javascript');

    res.end(callback + '({a:1,b:2})');
    });

    app.listen(3000,()=>{})
    </pre>
    </div>
    <script>
    function buildCallBackFunction(options, callbackFunName) {
    window[callbackFunName] = function (result) {
    options.success(result);
    window[callbackFunName] = null;
    delete window[callbackFunName];
    };
    }
    function buildParam(options) {
    var params = options.params;
    if (!params) {
    return "";
    }
    if (typeof params === "object") {
    var arr = [];
    for (var p in params) {
    arr.push(`p=${params[p]}`);
    }
    return arr.join("&");
    } else if (typeof params === "string") {
    return params;
    } else {
    return "";
    }
    }

    function buildScript(url) {
    var script = document.createElement("script");
    script.setAttribute("src", url);
    script.onload = function () {
    document.getElementsByTagName("head")[0].removeChild(script);
    };
    document.getElementsByTagName("head")[0].appendChild(script);
    }

    function json(options) {
    var { url, params, success } = options;
    var callbackFunName = "callback_" + Date.now();
    var params = buildParam(options);
    params += !params
    ? "callback=" + callbackFunName
    : "&callback=" + callbackFunName;
    url += "?" + params;
    buildCallBackFunction(options, callbackFunName);
    buildScript(url);
    }

    json({
    url: "http://localhost:3005/jsonp/jsonp",
    // params: 'a=1&b=2',
    params: { a: 1, b: 2 },
    success: function (result) {
    console.log(result);
    },
    });
    </script>
    </body>
    </html>
    + +

    实现跨域的解决方案–CORS

    CORS 是一个 W3C 标准,全称是”跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10(ie8 通过 XDomainRequest 能支持 CORS)。

    +

    参考文档

    +

    通过在被请求的路由中设置 header 头,可以实现跨域。

    +
    1
    2
    3
    4
    5
    6
    7
    app.get("/time", (req, res) => {
    // // 允许任意源访问,不安全
    // res.setHeader('Access-Control-Allow-Origin', '*')
    // 允许指定源访问
    res.setHeader("Access-Control-Allow-Origin", "http://www.xxx.com");
    res.send(Date.now().toString());
    });
    + +
      +
    • 这种方案无需客户端作出任何变化(客户端不用改代码),就当跨域问题不存在一样。
    • +
    • 服务端响应的时候添加一个 Access-Control-Allow-Origin 的响应头
    • +
    • 如果 ajax 请求中还附加了 cookie,则还需要设置一句:res.setHeader('Access-Control-Allow-Credentials', 'true');
    • +
    +

    自行下载使用 npm cors https://www.npmjs.com/package/cors

    +

    jsonp 和 CORS 的对比

    jsonp:

    +
      +
    • 不是 ajax

      +
    • +
    • 只能使用get方式传参

      +
    • +
    • 兼容性好

      +
    • +
    +

    cors:

    +
      +
    • 就是 ajax

      +
    • +
    • 支持各种方式的请求(post,get….)

      +
    • +
    • 浏览器的支持不好

      +
    • +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Node-Node%E4%B8%AD%E7%9A%84%E8%B7%A8%E5%9F%9F/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Node-Node\345\237\272\347\241\200/index.html" "b/posts/Node-Node\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..7fe2c2fc --- /dev/null +++ "b/posts/Node-Node\345\237\272\347\241\200/index.html" @@ -0,0 +1,409 @@ +Node基础 | JCAlways + + + + + + + + + + + + + +

    Node基础

    安装 Node.js

    软件下载地址

    +

    NPM

    介绍

      +
    • CommonJS 包规范是理论,NPM 是其中一 种实践。
    • +
    • 对于 Node 而言,NPM 帮助其完成了第三 方模块的发布、安装和依赖等。借助 NPM, Node 与第三方模块之间形成了很好的一个 生态系统。
    • +
    +

    NPM 命令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    npm -v  # 查看版本
    npm # 帮助说明
    npm search 包名 # 搜索模块包
    npm install 包名 # 在当前目录下安装包
    npm install -g 包名 # 全局模式安装包
    npm remove 包名 # 删除一个模块
    npm install 文件路径 # 从本地安装
    npm install 包名 -registry=地址 # 从镜像源安装
    npm config set registry 地址 # 设置镜像源
    + +

    Hello World

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 引入http模块
    const http = require("http");
    // 创建服务
    const server = http.createServer((req, res) => {
    console.log(`有人访问了本服务器`);
    res.end("<h1>hello world!");
    });
    // 启动服务
    server.listen(8081, () => {
    console.log("服务器启动成功,请在http://localhost:8081中访问....");
    });
    + +

    模块化

      +
    • 核心模块 +
    • +
    • 自定义模块
        +
      • 程序员自己写的模块。就相当于我们在学习 js 时的自定义函数。
      • +
      +
    • +
    • 第三方模块
        +
      • 其他程序员写好的模块。nodejs 生态提供了一个专门的工具 npm 来管理第三方模块,后面我们会专门讲到。
      • +
      • 相当于别人写好的函数或者库。例如我们前面学习的 JQuery 库,arttemplate 等。
      • +
      +
    • +
    +

    Node 的全局变量

    1
    2
    __filename; // 当前文件的绝对路径
    __dirname; // 当前文件所在目录的绝对路径
    + +

    fs 模块(文件系统)

    +

    文档地址

    +
    +

    fs 模块中的常用方法

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    API作用备注
    fs.access(path, callback)判断路径是否存在
    fs.appendFile(file, data, callback)向文件中追加内容
    fs.copyFile(src, callback)复制文件
    fs.mkdir(path, callback)创建目录
    fs.readDir(path, callback)读取目录列表
    fs.rename(oldPath, newPath, callback)重命名文件/目录
    fs.rmdir(path, callback)删除目录只能删除空目录
    fs.stat(path, callback)获取文件/目录信息
    fs.unlink(path, callback)删除文件
    fs.watch(filename[, options][, listener])监视文件/目录
    fs.watchFile(filename[, options], listener)监视文件
    fs.existsSync(absolutePath)判断路径是否存在
    +

    读取文件

      +
    • fs.readFile
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 引入fs模块
    const fs = require("fs");
    fs.readFile("./12.txt", "utf8", (err, data) => {
    // 读出文件自动被调用
    if (err) {
    console.log(err);
    return;
    }
    console.log(data);
    });
    + +
      +
    • fs.readFileSync
    • +
    +
    1
    2
    3
    4
    // 引入fs模块
    const fs = require("fs");
    let data = fs.readFileSync("./1.txt");
    console.log(data.toString());
    + +

    写入文件

    覆盖写入

      +
    • fs.writeFile
    • +
    +
    1
    2
    3
    4
    5
    // 引入fs模块
    const fs = require("fs");
    fs.writeFile("2.txt", "我是文本", (err) => {
    console.log(err);
    });
    + +
      +
    • fs.writeFileSync
    • +
    +
    1
    2
    3
    // 引入fs模块
    const fs = require("fs");
    fs.writeFileSync("1.txt", "我是文本");
    + +

    追加写入

      +
    • fs.appendFile
    • +
    +
    1
    2
    3
    4
    5
    // 引入fs模块
    const fs = require("fs");
    fs.appendFile("1.txt", "我是文本", (err) => {
    console.log(err);
    });
    + +
      +
    • fs.appendFileSync
    • +
    +
    1
    2
    3
    // 引入fs模块
    const fs = require("fs");
    fs.appendFileSync("1.txt", "我是文本");
    + +

    path 模块(路径)

    +

    文档地址

    +
    +

    path 模块其它方法列表

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    方法作用
    path.basename(path[, ext])获取返回 path 的最后一部分(文件名)
    path.dirname(path)返回目录名
    path.extname(path)返回路径中文件的扩展名(包含.)
    path.format(pathObject)将一个对象格式化为一个路径字符串
    path.join([…paths])拼接路径
    path.parse(path)把路径字符串解析成对象的格式
    path.resolve([…paths])基于当前工作目录拼接路径
    +

    http 模块

    搭建 web 服务器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 引入fs模块
    const http = require("http");
    /**
    * req - 本次请求的信息
    * res - 本次响应的信息
    */
    const server = http.createServer((req, res) => {
    res.setHeader("content-type", "text/html;charset=utf-8"); // 设置响应头
    res.end("helloworld");
    console.log(req.url); // 客户端浏览器本次请求的地址
    });

    server.listen(8080, function () {
    console.log("success");
    });
    + +

    req-请求对象

      +
    • req.url。客户端浏览器本次请求的地址
    • +
    • req.method。 获取请求行中的请求方法
    • +
    • req.headers。 获取请求
    • +
    +

    res-响应对象

      +
    • res.end()

      +
        +
      • 设置响应体。把把本次的处理结果返回给客户端浏览器
      • +
      • 如果不写这一句,则客户端浏览器永远收不到响应
      • +
      +
    • +
    • res.setHeader() 设置响应头,比如设置响应体的编码

      +

      res.setHeader('content-type', 'text/html;charset=utf-8');

      +
    • +
    • res.statusCode 设置状态码

      +

      res.statusCode=500

      +
    • +
    +

    为不同的文件类型设置不同的 Content-Type

    通过使用 res 对象中的 setHeader 方法,我们可以设置 content-type 这个响应头。这个响应头的作用是告诉浏览器,本次响应的内容是什么格式的内容。以方便浏览器进行处理。

    +

    常见的几中文件类型及 content-type 如下。

    +
      +
    • .html:res.setHeader(‘content-type’, ‘text/html;charset=utf8’)
    • +
    • .css:res.setHeader(‘content-type’, ‘text/css;charset=utf8’)
    • +
    • .js:res.setHeader(‘content-type’, ‘application/javascript’)
    • +
    • .png:res.setHeader(‘content-type’, ‘image/png’)
    • +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Node-Node%E5%9F%BA%E7%A1%80/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/Node-Promise/index.html b/posts/Node-Promise/index.html new file mode 100644 index 00000000..7e4baba7 --- /dev/null +++ b/posts/Node-Promise/index.html @@ -0,0 +1,238 @@ +Promise | JCAlways + + + + + + + + + + + + + +

    Promise

    Promise 构造器

    Promise 是一个构造器,用来创建 Promise 类型的对象 。就好像 Array 是一个构造器,用来创建数组。

    +

    格式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var p1 = new Promise(function (resolve, reject) {
    alert(1);
    // 在某一个时刻去调用
    // resolve():把p1的状态由 pending -----> resolved
    // reject() :把p1的状态由 pending -----> rejected
    console.log(222);
    resolve(456);
    });
    console.dir(p1);
    + +

    三种状态

    pending

    pending,”行将发生的”。相当于是一个初始状态。

    +
    1
    2
    3
    4
    var p = new Promise((ok, err) => {
    console.info("发呆.....");
    });
    console.dir(p);
    + +

    resolved

    创建 Promise 对象时,在实参函数中调用了 ok 方法。

    +
    1
    2
    3
    4
    5
    var p = new Promise((ok, err) => {
    console.info("发呆.....");
    ok();
    });
    console.dir(p);
    + +

    rejected

    创建 Promise 对象时,调用 error 方法。

    +
    1
    2
    3
    4
    5
    var p = new Promise((ok, err) => {
    console.info("发呆.....");
    err();
    });
    console.dir(p);
    + +

    状态是可转化

    1
    2
    pending ----->ok()    --> resolved;
    pending ----->error()---> rejected ;
    + +
    +

    状态转换是不可逆的。一旦从 pending —> resolved(或者是 rejected),就不可能再回到 pending。也不能由 resolved 变成 rejected,或者反过来。 (这一点与水不同,水可以在液态,固态,气态三种状态中相互转换)。

    +
    +

    promise 的值 promisevalue

    一个 promise 对象除了状态之外,还有 promsievalue,在构造器中,这个值在调用 resolve 和 reject 方法时,自动传入。

    +

    例如:

    +
    1
    2
    3
    4
    5
    var p = new Promise( (resolve,reject) => { resolve(123); } );
    // 此时,prommise对象p的状态是 resolved,值是123。

    var p = new Promise( (resolve,reject) => { reject(1); } );
    // 此时,prommise对象p的状态是 rejected,值是1。
    + +

    单独来看 promiseValue 似乎没有什么意义,它的使用场景在于结合 promise 对象的实例方法一起来用来。

    +

    Promise 实例的方法

    then()

      +
    • 如果 promise 对象的状态是 resolved,则promisec对象.then()会自动调用第一个函数;
    • +
    • 如果 promise 对象的状态是 rejected,则promisec对象.then()会自动调用第二个函数,如果此时 then 方法并没有设置第二个参数,就会报错; 这种情况的处理方法在后面介绍
    • +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    var p = new Promise((resolve, reject) => {
    resolve();
    });

    p.then(
    () => {
    console.info("then,成功");
    },
    () => {
    console.info("then,失败");
    }
    );
    //--------------------------------------------------------------------------------------
    var p = new Promise((resolve, reject) => {
    reject();
    });
    p.then(
    () => {
    console.info("then,成功");
    },
    () => {
    console.info("then,失败");
    }
    );
    + +

    catch()

    finally()

    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Node-Promise/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/React-React\345\237\272\347\241\200/index.html" "b/posts/React-React\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..d6337f23 --- /dev/null +++ "b/posts/React-React\345\237\272\347\241\200/index.html" @@ -0,0 +1,244 @@ +React基础 | JCAlways + + + + + + + + + + + + + +

    React基础

    +

    React:用于构建用户界面的 JavaScript 库(框架)
    英文网站 > 中文网站

    +

    特征:

    +
      +
    • 声明式视图
        +
      • 对于声明式组件,当数据变更的时候,React 低层负责高效更新。这种方式代码更加可预见并且更容易调试。
      • +
      +
    • +
    • 组件化
        +
      • 封装管理数据的组件,通过组合的方式实现复杂的 UI,组件的逻辑采用 js 实现而不是模板,这样可以保持数据在 DOM 之外。
      • +
      +
    • +
    • 一次学习,随处编写
        +
      • React 可以进行服务端渲染,也可以用于移动 APP 开发(React Native)
      • +
      +
    • +
    +
    +

    Hello World

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <!-- 1.导入相关库文件 -->
    <script src="https://static.zhangsifan.com/react.js"></script>
    <script src="https://static.zhangsifan.com/react-dom.js"></script>
    <script src="https://static.zhangsifan.com/babel.min.js"></script>
    </head>
    <body>
    <!-- 2.添加一个容器 -->
    <div id="app"></div>
    <!-- 3.基于React实现业务 -->
    <script type="text/babel">
    let content = <h1>Hello World</h1>;
    ReactDOM.render(content, document.getElementById("app"));
    </script>
    </body>
    </html>
    + +

    开发工具

    VS Code 插件

      +
    • JS JSX Snippets
    • +
    • jsx-beautify
    • +
    • Live Server
    • +
    +
    1
    2
    3
    4
    5
    "emmet.triggerExpansionOnTab": true,
    "emmet.includeLanguages": {
    "javascript": "javascriptreact",
    "wxml": "html"
    },
    + +

    JSX 基础语法

    JSX 是什么

    1
    let element = <h1>Hello World</h1>;
    + +

    JSX 元素中动态插入数据

    1
    2
    let name = "世界";
    let element = <h1>你好,{name}</h1>;
    + +

    JSX 设置动态属性

    1
    2
    3
    4
    5
    6
    let info = "bt";
    let element = (
    <h1 className="title" abc={info}>
    你好,世界
    </h1>
    );
    + +

    JSX 可以包含子元素

    1
    2
    3
    4
    5
    6
    let element = (
    <div>
    <h1>你好</h1>
    <h1>世界</h1>
    </div>
    );
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/React-React%E5%9F%BA%E7%A1%80/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/TS-TS\345\237\272\347\241\200 /index.html" "b/posts/TS-TS\345\237\272\347\241\200 /index.html" new file mode 100644 index 00000000..4c0c98e6 --- /dev/null +++ "b/posts/TS-TS\345\237\272\347\241\200 /index.html" @@ -0,0 +1,235 @@ +TS基础 | JCAlways + + + + + + + + + + + + + +

    TS基础

    初识 TypeScript

    TypeScript 的介绍

    TypeScript 是一种由微软开发的开源、跨平台的编程语言。它是 JavaScript 的超集,最终会被编译为 JavaScript 代码。

    +

    2012 年 10 月,微软发布了首个公开版本的 TypeScript,2013 年 6 月 19 日,在经历了一个预览版之后微软正式发布了正式版 TypeScript

    +

    TypeScript 的作者是安德斯·海尔斯伯格,C#的首席架构师。它是开源和跨平台的编程语言。

    +

    TypeScript 扩展了 JavaScript 的语法,所以任何现有的 JavaScript 程序可以运行在 TypeScript 环境中。

    +

    TypeScript 是为大型应用的开发而设计,并且可以编译为 JavaScript。

    +

    TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持**,它由 Microsoft 开发,代码开源于 GitHub 上

    +

    TypeScript 是 JavaScript 的一个超集,主要提供了类型系统和对 ES6+ 的支持,它由 Microsoft 开发,代码开源于 GitHub (opens new window)上

    +

    TypeScript 的特点

    TypeScript 主要有 3 大特点:

    +
      +
    • 始于 JavaScript,归于 JavaScript
      TypeScript 可以编译出纯净、 简洁的 JavaScript 代码,并且可以运行在任何浏览器上、Node.js 环境中和任何支持 ECMAScript 3(或更高版本)的 JavaScript 引擎中。

      +
    • +
    • 强大的类型系统
      类型系统允许 JavaScript 开发者在开发 JavaScript 应用程序时使用高效的开发工具和常用操作比如静态检查和代码重构。

      +
    • +
    • 先进的 JavaScript
      TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。

      +
    • +
    +

    安装 TypeScript

    命令行运行如下命令,全局安装 TypeScript:

    +
    1
    npm install -g typescript
    + +

    安装完成后,在控制台运行如下命令,检查安装是否成功:

    +
    1
    tsc -V
    + +

    HelloWorld

    1
    2
    3
    4
    5
    function sayHi(str) {
    return "Hello" + str;
    }
    let username = "World";
    console.log(sayHi(username));
    + +

    手动编译代码

    1
    tsc helloworld.ts
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/TS-TS%E5%9F%BA%E7%A1%80%20/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/Vue-MVVM\345\216\237\347\220\206/index.html" "b/posts/Vue-MVVM\345\216\237\347\220\206/index.html" new file mode 100644 index 00000000..28276cf3 --- /dev/null +++ "b/posts/Vue-MVVM\345\216\237\347\220\206/index.html" @@ -0,0 +1,381 @@ +MVVM原理 | JCAlways + + + + + + + + + + + + + +

    MVVM原理

    MVVM 原理

    常见的面试问题:

    +
      +
    • Vue 数据绑定的原理?
    • +
    • MVVM 数据绑定的原理?
    • +
    • Vue 双向数据绑定的原理?
    • +
    • Vue 数据响应式原理?
    • +
    • 数据响应式原理?
    • +
    +

    MVVM

    +

    当前比较流行的前端框架都是采用的 MVVM 的方式:

    +

    什么是 MVVM?

    +

    简单一句话:数据驱动视图。

    +

    介绍

    感受 MVVM

      +
    • 传统的 DOM 操作方式
    • +
    • 模板引擎方式
    • +
    • 数据驱动视图方式(MVVM)
    • +
    +

    什么是 MVVM

    +

    简单一句话:数据驱动视图

    +
    +

    MVVM

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <!-- 视图 -->
    <template>
    <div>{{ message }}</div>
    </template>

    <!--
    - 把普通的 JavaScript 对象和视图 DOM 之间建立了一种映射关系:
    - 数据的改变影响视图
    - 视图(表单元素)的改变影响数据
    -->

    <script>
    // Model 普通数据对象
    export default {
    data() {
    return {
    message: "Hello World",
    };
    },
    };
    </script>
    + +
      +
    • Model(M):普通的 JavaScript 对象,例如 Vue 实例中的 data
        +
      • 普通数据
      • +
      +
    • +
    • View(V):视图
        +
      • HTML DOM 模板
      • +
      +
    • +
    • ViewModel(VM):Vue 实例
        +
      • 负责数据和视图的更新
      • +
      • 它是 Model 数据 和 View 视图通信的一个桥梁
      • +
      +
    • +
    +

    JavaScript 数据劫持

      +
    • 数据劫持?
    • +
    • Observer 数据观察
    • +
    • 数据拦截器
    • +
    +

    如何实现修改一个对象成员就修改了 DOM?

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    const data = {
    message: "Hello World",
    };

    // 修改数据
    data.message = "hello";

    // ? 如何知道数据发生改变了呢

    // 当数据改变了操作 DOM
    document.querySelector("xxx").style.xxx = "xxx";
    + +

    答案是:JavaScript 数据劫持,或者说是 JavaScript 对象属性拦截器。

    +

    什么是数据劫持(属性拦截器)?

    +

    说白了就是:观察数据的变化。

    +
      +
    • Object.defineProperty
        +
      • ECMAScript 5 中的一个 API
      • +
      • Vue 1 和 Vue 2 中使用的都是 Object.defineProperty
      • +
      +
    • +
    • Proxy
        +
      • ECMAScript 6 中的一个 API
      • +
      • 即将升级的 Vue 3 会升级使用 Proxy
      • +
      • Proxy 比 Object.defineProperty 性能要更好
      • +
      +
    • +
    +

    Object.defineProperty

    +

    参考资料:

    + +
    +

    Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

    +

    语法

    1
    Object.defineProperty(obj, prop, descriptor);
    + +

    参数:

    +
      +
    • obj 要在其上定义属性的对象。

      +
    • +
    • prop 要定义或修改的属性的名称。

      +
    • +
    • descriptor 将被定义或修改的属性描述符。

      +
    • +
    +

    返回值:

    +

    被传递给函数的对象。

    +

    描述符

      +
    • configurable

      +

      当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false

      +
    • +
    • enumerable

      +

      当且仅当该属性的enumerabletrue时,该属性才能够出现在对象的枚举属性中。默认为 false

      +
    • +
    • value

      +

      该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined

      +
    • +
    • writable

      +

      当且仅当该属性的writabletrue时,value才能被赋值运算符改变。默认为 false

      +
    • +
    • get

      +

      一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。

      +

      默认为 undefined

      +
    • +
    • set

      +

      一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

      +

      默认为 undefined

      +
    • +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    configurableenumerablevaluewritablegetset
    数据描述符YesYesYesYesNoNo
    存取描述符YesYesNoNoYesYes
    +

    如果一个描述符不具有 value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value 或 writable)和(get 或 set)关键字,将会产生一个异常。

    +

    示例

    需求:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    const data = {
    name: "张三",
    age: 18,
    };

    // data.name 被访问了
    data.name;

    // data.name 被改变了
    data.name = xxx;

    // data.age 被改变了
    data.age = xxx;
    + +

    实现:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    const data = {};

    let _name = "";
    let _age = 0;

    // 在 data 对象中添加一个属性 name
    Object.defineProperty(data, "name", {
    configurable: false,
    enumerable: true,
    // 监听属性的修改
    set(value) {
    _name = value;
    },
    // 监听属性的读取
    get() {
    return _name;
    },
    });

    // 在 data 对象中添加一个属性 age
    Object.defineProperty(data, "age", {
    configurable: false,
    enumerable: true,
    // 监听属性的修改
    set(value) {
    _age = value;
    },
    // 监听属性的读取
    get() {
    return _age;
    },
    });
    + +

    事件发布/订阅

      +
    • 观察者模式
    • +
    • 发布/订阅模式
    • +
    +
    1
    2
    3
    4
    5
    // 监听一个自定义事件
    bus.$on("事件类型", 处理函数);

    // 发布事件
    bus.$emit("事件类型", 处理函数);
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    function EventEmitter() {
    // 存储所有订阅的消息处理函数
    this.subs = {
    // 事件类型: [处理函数, 处理函数...]
    // a: [],
    };
    }

    EventEmitter.prototype.$on = function (eventType, callback) {
    this.subs[eventType] = this.subs[eventType] || [];
    this.subs[eventType].push(callback);
    };

    // 参数中的 ... 表示函数的剩余(rest)参数
    // 它会把所有参数放到一个数组中
    EventEmitter.prototype.$emit = function (eventType, ...args) {
    const subs = this.subs[eventType];
    if (subs) {
    subs.forEach((callback) => {
    callback(...args);
    });
    }
    };
    + +

    DOM 操作

    +

    原理实现

    示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8" />
    <title>MVVM原理分析</title>
    </head>
    <body>
    <div id="app">
    <h3 v-text="msg"><span>哈哈...</span></h3>
    <input type="text" v-model="msg" />
    <button v-on:click="sayHi">按钮</button>
    </div>
    <script src="https://gcore.jsdelivr.net/npm/vue/dist/vue.js"></script>
    <script>
    var vm = new Vue({
    el: "#app",
    data: {
    msg: "学习MVVM原现分析!",
    },
    methods: {
    sayHi() {
    this.msg = "修改了数据";
    },
    },
    });
    </script>
    </body>
    </html>
    + +

    VM 模型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    function Vue(options) {
    // 保存初始化时传入的参数
    this.$options = options;

    // 检测是一个 dom 还是 选择器
    this.$el =
    typeof options.el === "string"
    ? document.querySelector(options.el)
    : options.el;

    // 保存初始化时定义的数据
    this.$data = options.data || {};
    // 保存初始化时的定义的方法
    this.$methods = options.methods || {};

    // 像 vue 一样,直接通过实例访问 data 中的数据
    Object.keys(this.$data).forEach((key) => {
    // 代理数据
    Object.defineProperty(this, key, {
    configurable: false,
    enumerable: true,
    get() {
    console.log("get from vue...");
    return this.$data[key];
    },
    set(val) {
    console.log("set from vue...");
    this.$data[key] = val;
    },
    });
    });

    // 数据拦截,监听数据的访问
    new Observe(this.$data);

    // 编译模板,找到所有需要监听的数据
    new Compile(this.$el, this);
    }
    + +

    数据劫持

    劫持 VM 模型中初始的数据,监听数据的访问

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    function Observe(data) {
    // 只对对象数据设置劫持
    if (!data || typeof data !== "object") return;

    // 保存待劫持的数据
    this.data = data;

    // 监视数据的变化
    Object.keys(data).forEach((key) => {
    this.walk(key, data[key]);
    });
    }

    Observe.prototype.walk = function (key, val) {
    Object.defineProperty(this.data, key, {
    configurable: false,
    enumerable: true,
    set(newVal) {
    if (newVal === val) return;
    val = newVal;
    // 发布,通过数据已经改变了
    watcher.$emit(key, newVal);
    },
    get() {
    return val;
    },
    });
    };
    + +

    编译模板

    对 el 所对应的 DOM 节点的所有节点进行遍历操作,查找出所以包含指令或插值的节点,然后进行订阅监听,实现 DOM 的更新。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    // 订阅/发布
    const watcher = new Watcher();

    // 编译模板
    function Compile(el, vm) {
    this.vm = vm;

    // 必须为元素节点
    if (el.nodeType !== 1) return;

    // 编译模板
    this.compileElement(el);
    }

    Compile.prototype.compileElement = function (el) {
    // 获取子节点
    let childNodes = el.childNodes;

    // 没有子节点
    if (!childNodes) return;

    Array.from(childNodes).forEach((node) => {
    let text = node.textContent,
    // 查找模板中所有的插值
    reg = /(\{\{(.*)\}\})/;

    // 是文本节点,并且包含 {{}}
    if (node.nodeType === 3 && reg.test(text)) {
    // 匹配单元
    node.textContent = text.replace(RegExp.$1, this.vm[RegExp.$2]);

    // 监听{{}}中的数据
    watcher.$on(RegExp.$2, (newVal) => {
    node.textContent = text.replace(RegExp.$1, newVal);
    });
    // 元素节点
    } else if (node.nodeType === 1) {
    // 查找所以 v- 开头的指令
    this.compile(node);
    }

    // 递归查子节点
    this.compileElement(node);
    });
    };

    Compile.prototype.compile = function (node) {
    // 获取元素节点上的所有属性
    let attrs = node.attributes;

    Array.from(attrs).forEach((attr) => {
    let attrName = attr.name;

    // 检测元素节点上是不包含一些指令如 v-text v-html 等
    if (attrName.indexOf("v-") === 0) {
    let exp = attr.value;
    let dir = attrName.slice(2);

    // 删除 v- 开头的属性
    node.removeAttribute(attrName);

    // 检测指令是否以 v-on 开头
    if (dir.indexOf("on") === 0) {
    let type = dir.split(":")[1],
    handler = this.vm.$methods[exp].bind(this.vm);

    // 添加事件监听
    return node.addEventListener(type, handler);
    }

    // 除了 v-on 外的其它指令
    directives[dir] && directives[dir](node, exp, this.vm);

    // 监听
    watcher.$on(exp, (newVal) => {
    directives[dir] && directives[dir](node, exp, this.vm);
    });
    }
    });
    };
    + +

    订阅/发布

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    function Watcher(sub) {
    this.subs = {};
    }

    Watcher.prototype.$on = function (sub, cb) {
    this.subs[sub] = this.subs[sub] || [];
    this.subs[sub].push(cb);
    };

    Watcher.prototype.$emit = function (sub, newVal) {
    this.subs[sub].forEach((cb) => {
    cb(newVal);
    });
    };
    + +

    推荐阅读

    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Vue-MVVM%E5%8E%9F%E7%90%86/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Vue-Vue-Cli\347\232\204\344\275\277\347\224\250/index.html" "b/posts/Vue-Vue-Cli\347\232\204\344\275\277\347\224\250/index.html" new file mode 100644 index 00000000..1a4b97a0 --- /dev/null +++ "b/posts/Vue-Vue-Cli\347\232\204\344\275\277\347\224\250/index.html" @@ -0,0 +1,434 @@ +Vue-Cli的使用 | JCAlways + + + + + + + + + + + + + +

    Vue-Cli的使用

    Vue-Cli

    +

    官方网站

    +
    +
    +

    vue-cli是一个辅助开发工具=> 代码编译 + 样式 + 语法校验 + 输出设置 + 其他 …

    +

    作用: 可以为开发者提供一个**标准的项目开发结构** 和配置 开发者不需要再关注

    +
    +

    安装 vue-cli

    解决powershell禁止运行脚本

    +
    1
    set-ExecutionPolicy RemoteSigned
    + +

    采用 npm 的方式安装

    1
    npm i -g @vue/cli
    + +

    采用 cmpn 的方式安装

    +

    cnpm 官网

    +
    +
    1
    2
    3
    npm install -g cnpm --registry=https://registry.npm.taobao.org

    cnpm i -g @vue/cli
    + +

    查看安装版本号

    1
    2
    3
    vue -V
    //或者
    vue --version
    + +

    vue-cli 2.0 补丁

    1
    npm install -g @vue/cli-init
    + +

    初始化项目

    创建 2.0 项目

    1
    2
    3
    4
    5
    6
    7
    8
    #  heroes 创建的项目名称
    vue init webpack-simple 项目名称
    # 切换到当前目录
    cd heroes
    # 安装依赖
    npm install
    # 在开发模式下 启动运行项目
    npm run dev
    + +

    创建 4.0 项目

    1
    2
    3
    4
    5
    6
    # 4.0下创建项目
    vue create 项目名称 // create(创建) 为关键字
    # 切换到当前目录
    cd heroes
    # 在开发模式下 启动运行项目
    npm run serve
    + +

    在 src 目录创建文件夹

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    ├─api #接口
    ├─assets #静态资源
    ├─components #公用组件
    ├─directive #指令
    ├─filter #过滤器
    ├─router #路由
    ├─utils #工具函数
    ├─styles #全局样式
    └─views # 路由级别组件
    └─App.vue # 根组件
    └─main.js # 入口文件
    + +

    以上结构参考

    +

    常用 NPM 插件

    +

    NPM 官网

    +

    导航

    + +
    +

    Element-UI

    +

    官方网站

    +
    +

    安装

    1
    npm i element-ui -S
    + +

    导入

    main.js 中写入以下内容:

    +
    1
    2
    3
    4
    5
    ...
    import ElementUI from 'element-ui';
    import 'element-ui/lib/theme-chalk/index.css';
    ...
    Vue.use(ElementUI);
    + +

    使用

    +

    参考官方文档

    +
    +

    Vant

    +

    官方网站

    +
    +

    安装

    1
    npm i vant -S
    + +

    导入

    main.js中导入

    +
    1
    2
    3
    4
    5
    ...
    import Vant from 'vant';
    import 'vant/lib/index.css';
    ...
    Vue.use(Vant);
    + +

    使用

    +

    参考官方文档

    +
    +

    安装 rem 适配插件

    1
    npm install postcss-pxtorem --save-dev
    + +
    1
    npm i -S amfe-flexible
    + +

    postcss.config.js中写入如下代码

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    module.exports = {
    plugins: {
    autoprefixer: {},
    // 配置计算rem值得插件
    "postcss-pxtorem": {
    // 按照37.5的基准值来换算rem单位
    // vat默认最佳显示状态在iphone 6 宽度375px
    rootValue: 37.5,
    propList: ["*"],
    },
    },
    };
    + +

    main.js中写入

    +
    1
    import "amfe-flexible";
    + +

    Vue-Router

    +

    官方网站

    +

    实现原理: vue-router通过hash与History interface两种方式实现前端路由

    +
    +

    安装

    1
    npm install vue-router
    + +

    导入

    src/router/index.js中写入以下内容:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import Vue from "vue";
    import VueRouter from "vue-router";

    Vue.use(VueRouter);
    const router = new VueRouter({
    routes: [
    {
    path: "",
    component: null,
    },
    ],
    });

    export default router;
    + +

    main.js中写入以下内容:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    ...
    import router from '@/router'
    ...
    new Vue({
    router,
    render: h => h(App)
    }).$mount('#app')

    + +

    使用

    推荐使用路由懒加载方式导入

    +  + +
    +

    参考笔记

    +
    +

    路由懒加载

    1
    const xxx = () => import("@/views/xxx");
    + +

    导航守卫

    +

    官方文档

    +

    使用场景:

    +
      +
    • 前置守卫

      +
        +
      • 判断用户是否达到某些条件,不满足条件强制重定向到一个指定的地址
      • +
      +
    • +
    • 后置守卫

      +
        +
      • 待编辑
      • +
      +
    • +
    +
    +
    前置守卫

    src/router/index.js中写入以下内容:

    +
    1
    2
    3
    4
    5
    6
    7
    8
    ...
    router.beforeEach((to, from, next) => {
    // to 跳转目标路由对象
    // from 从哪里跳过来的路由对象
    // next() 放行 next('地址') 拦截到哪里
    next()
    })
    ...
    + +
    后置守卫
    1
    2
    3
    4
    5
    ...
    router.afterEach((to, from) => {
    // ...
    })
    ...
    + +

    VueX

    +

    官方网站

    +
    +

    安装

    1
    npm i vuex
    + +

    导入和使用

    +

    查看专栏

    +
    +

    Axios

    +

    参考文档

    +

    实现原理:XMLHttpRequest对象

    +

    优点:

    +
      +
    • 支持 promise
    • +
    • 能拦截请求和响应
    • +
    • 能转换请求和响应数据
    • +
    • 自动转换 JSON 数据
    • +
    • 能取消请求
    • +
    +

    Axios 源码深度剖析

    +
    +

    安装

    1
    npm install axios
    + +

    导入

    src/api/index.js中写入以下内容

    +
    1
    2
    import axios from "axios";
    export default axios;
    + +

    main.js中写入以下内容

    +
    1
    2
    3
    4
    ...
    import axios from '@/api'
    Vue.prototype.$axios = axios
    ...
    + +

    使用

    设置基准地址

    1
    axios.defaults.baseURL = "http://ttapi.research.itcast.cn/mp/v1_0/";
    + +

    处理数字最大安全值

    +

    使用 JSON-bigint 解决,使用方法

    +
    +

    请求拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    axios.interceptors.request.use(
    function (config) {
    // 在发送请求之前做些什么
    return config;
    },
    function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
    }
    );
    + +

    响应拦截器

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    axios.interceptors.response.use(
    function (response) {
    // 对响应数据做点什么
    return response;
    },
    function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
    }
    );
    + +

    Echarts

    +

    官方文档

    +
    +

    安装

    1
    npm install echarts
    + +

    导入与使用

    在要使用的vue文件中写入以下内容

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <template>
    <div>
    <!-- 2.准备DOM容器,具备高宽 -->
    <div id="main" style="width: 600px;height:400px;"></div>
    </div>
    </template>

    <script>
    //1.导入echarts
    import echarts from "echarts";
    export default {
    mounted() {
    //3.初始化echarts
    const dom = this.$refs.dom;
    const myEcharts = echarts.init(dom);
    //4.配置和数据
    const option = {};
    //5.使用
    myEcharts.setOption(option);
    },
    };
    </script>

    <style></style>
    + +

    使用

    +

    option的属性产考此页

    +
    +

    jsonp

    +

    官方网站

    +
    +

    安装

    1
    npm i jsonp
    + +

    导入与使用

    1
    2
    3
    import jsonp from "jsonp";

    jsonp("", (err, data) => {});
    + +

    qs

    +

    官方网站

    +
    +

    安装

    1
    npm i qs
    + +

    导入和使用

    1
    2
    3
    import qs from "qs";

    var str = qs.stringify(obj);
    + +

    json-bigint

    +

    官方网站

    +
    +

    安装

    1
    npm i json-bigint
    + +

    导入与使用

    负责axios的 js 文件中写入以下内容

    +
    1
    import axios from "axios";
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    axios.defaults.transformResponse = [
    (data) => {
    try {
    return JSONBIG.parse(data);
    } catch (e) {
    return data;
    }
    },
    ];
    + +

    moment

    +

    官方网站

    +
    +

    安装

    1
    npm i moment
    + +

    导入与使用

    dayjs

    +

    官方网站

    +
    +

    安装

    1
    npm i dayjs
    + +

    导入与使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import dayjs from "dayjs";
    import relativeTime from "dayjs/plugin/relativeTime";
    import "dayjs/locale/zh-cn";
    dayjs.extend(relativeTime);

    const relTime = (time) => {
    return dayjs().locale("zh-cn").from(time);
    };

    export default {
    install(Vue) {
    Vue.filter("relTime", relTime);
    },
    };
    + +

    生成文件

    在根目录创建vue.config.js

    +
    1
    2
    3
    module.exports = {
    publicPath: "./",
    };
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Vue-Vue-Cli%E7%9A%84%E4%BD%BF%E7%94%A8/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git a/posts/Vue-Vuex/index.html b/posts/Vue-Vuex/index.html new file mode 100644 index 00000000..8f7c97f8 --- /dev/null +++ b/posts/Vue-Vuex/index.html @@ -0,0 +1,306 @@ +VueX | JCAlways + + + + + + + + + + + + + +

    VueX

    VueX 介绍

    +

    官方网站
    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。(管理数据共享的工具)

    +
    +

    看图结论:

    +
      +
    • state 管理数据,管理的数据是响应式的,当数据改变时驱动视图更新。
    • +
    • mutations 更新数据,state 中的数据只能使用 mutations 去改变数据。
    • +
    • actions 请求数据,响应成功后把数据提交给 mutations
    • +
    +

    初始化功能

    安装

    1
    npm i vuex --save
    + +

    新建 store.js 文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // 初始化一个vuex的实例(数据仓库) 导出即可
    import Vue from "vue";
    import Vuex from "vuex";
    // 使用安装
    Vue.use(Vuex);
    // 初始化
    const store = new Vuex.Store({
    state: null,
    mutations: null,
    actions: null,
    });
    export default store;
    + +

    在 main.js 配置

    1
    2
    3
    4
    5
    6
    7
    ...
    import store from '@/store'

    new Vue({
    store,
    render: h => h(App)
    }).$mount("#app");
    + +

    state (管理数据)

    普通写法

    store.js文件下写入

    +
    1
    2
    3
    4
    5
    6
    const store = new Vuex.Store({
    state: {
    // 管理数据
    count: 0,
    },
    });
    + +

    在组件获取state的数据:原始用法插值表达式

    +
    1
    <p>{{ $store.state.count }}</p>
    + +

    计算属性写法

    1
    <p>{{ count }}</p>
    + +

    常规写法

    +

    正常写法

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    export default {
    ...
    computed: {
    count: function() {
    return this.$store.state.count;
    }
    }
    };
    + +
    +

    缩写

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    export default {
    ...
    computed: {
    count() {
    return this.$store.state.count;
    }
    }
    };
    + +

    不能使用箭头函数 this指向的不是vue实例

      +### mapState + +

    导入

    1
    import { mapState } from "vuex";
    + +

    使用:mapState(对象)

    +

    基本写法

    +
    +
    1
    2
    3
    4
    5
    computed: mapState({
    count: function (state) {
    return state.count;
    },
    });
    + +
    +

    箭头函数写法

    +
    +
    1
    2
    3
    computed: mapState({
    count: (state) => state.count,
    });
    + +
    +

    vuex 提供写法

    +
    +
    1
    2
    3
    computed: mapState({
    count: "count",
    });
    + +
    +

    当你的计算属性,需要依赖 vuex 中的数据,同时,依赖组件中 data 的数据

    +
    +
    1
    2
    3
    4
    5
    computed: mapState({
    myCount(state) {
    return state.count + 233;
    },
    });
    + +

    使用:mapState(数组)

    1
    computed: mapState(["count"]);
    + +

    如果组件自己有计算属性,state 的字段映射成计算属性

    1
    2
    3
    4
    5
    6
    computed: {
    myCount() {
    return 1;
    },
    ...mapState(["count"])
    }
    + +

    mutations (修改数据)

    常规写法

    store.js文件下写入

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const store = new Vuex.Store({
    ...
    mutations: {
    //state:state中的值
    //payload:传入的值
    increment(state, payload) {
    state.count = state.count + payload.num;
    }
    }
    });
    + +

    要使用的组件文件下写入

    +
    1
    2
    3
    4
    5
    6
    7
    methods: {
    fn() {
    //第一项 函数名
    //第二项 传入的参数
    this.$store.commit("increment", { num: 22 });
    }
    }
    + +

    mapMutations

    要使用的组件文件下写入

    +
    +

    函数要传的参数在调用处的括号里传入

    +
    +
    1
    <button @click="increment({num:123})">点我</button>
    + +
    1
    2
    3
    methods: {
    ...mapMutations(["increment"])
    }
    + +

    actions (异步获取数据)

    常规写法

    store.js文件下写入

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    actions: {
    getData({ commit }, num) {
    //模拟拿到后端数据
    window.setTimeout(() => {
    const data = num;
    commit("add", data);
    }, 2000);
    }
    }
    + +

    要使用的组件文件下写入

    +
    1
    2
    3
    getData() {
    this.$store.dispatch("getData", 10086);
    }
    + +

    mapActions

    要使用的组件文件下写入

    +
    +

    函数要传的参数在调用处的括号里传入

    +
    +
    1
    <button @click="getData(123)">点我</button>
    + +
    1
    2
    3
    methods: {
    ...mapActions(["getData"])
    }
    + +

    modules

    +

    在 store 全局数据 是可以分模块化管理的

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    modules:{
    m1:{
    namespaced:true, // 开启命名空间
    state:{
    count:1
    }
    }
    }
    + +
    1
    <h2>{{$store.state.m1.count}}</h2>
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Vue-Vuex/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    \ No newline at end of file diff --git "a/posts/Vue-Vue\345\237\272\347\241\200/index.html" "b/posts/Vue-Vue\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..286f03b2 --- /dev/null +++ "b/posts/Vue-Vue\345\237\272\347\241\200/index.html" @@ -0,0 +1,859 @@ +Vue基础 | JCAlways + + + + + + + + + + + + + +

    Vue基础

    Vue 是什么之前端现状

    +
      +
    • Vue 是一个优秀的**前端框架** 国内大多数中小型公司都在用,

      +
    • +
    • 开发者按照 Vue 的**规范**进行开发 => 不按照规范开发 就会报错

      +
    • +
    +
      +
    1. 和 DOM**解耦** Vue 框架 显示数据 获取数据都不再通过 dom 对象
    2. +
    3. 适应当前**SPA**的项目开发 => single page application 单页应用程序 =>只有一个 html 页面
    4. +
    5. 传统网站开发 一般来说 需求不大
    6. +
    7. 当下各种新框架都采用了**类Vue或者类React**的语法去作 为主语法, 微信小程序/MpVue/uni-app
    8. +
    +
    +

    Vue 特点

    +
      +
    1. 响应式数据 数据驱动视图 可以让我们只关注数据 Vue 中数据是响应式的 => 数据变化 => 视图一定变化
    2. +
    3. MVVM 双向绑定 => 数据 <=> 视图 , 数据变化 ==>视图变化 ,视图变化 => 数据变化
    4. +
    +
      +
    • model—模型,数据对象(data)
    • +
    • view—视图,模板页面
    • +
    • viewmodel**(vm)**—视图模型(Vue 的实例)
    • +
    +
      +
    1. **指令**增强了 html 功能 新特性 ,Vue 扩展了 html 标签的功能 用指令, /angular ng-
    2. +
    3. 组件化开发 复用代码 => SPA => 10 页面 => 10 个组件
    4. +
    +
    +

    相关链接

    +

    Vue 官方文档 >Vue 开源项目汇总 >Vue.js 中文社区
    所有关于 Vue 的问题都可以通过**查阅文档**解决

    +
    +

    如何安装 Vue.js

    采用本地文件引入的方式 直接下载 在 script 标签中引入

    +

    开发版本 > 生产版本

    +
    +

    第一个程序–Hello World

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <!--1. 设置Vue实例管理的视图 -->
    <div id="app">
    <!-- 5.在<div id="app"></div>中通过{{}}中使用data中的数据 -->
    {{name}}
    </div>
    <!-- 2.引入js -->
    <script src="./vue.js"></script>
    <script>
    // 3.实例化Vue对象
    var vm = new Vue({
    // 4填入实例选项
    el: "#app",建立视图和vue对象一一对应
    data: {
    //数据
    name: "hello world"
    },
    //方法
    methods: {
    fn() {
    console.log("helloworld")
    }
    }
    });
    </script>
    + +

    实例选项

    el

    +
      +
    • 作用:当前Vue实例所管理的html视图 在视图之外 ,就不能应用 Vue 特性
    • +
    • 值:通常是 id 选择器(或者是一个 HTMLElement 实例)
    • +
    • 不要让 el 所管理的视图是 html 或者 body!
    • +
    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    //使用id选择器
    el: "#app",
    //类选择器
    el: ".div",
    //DOM对象
    el: document.getElementById('app'),
    //错误示范
    //el: document.getElementById('body'),
    //错误示范
    //el: document.getElementById('html'),
    //保存信息:vue.js:634 [Vue warn]: Do not mount Vue to <html> or <body> - mount to normal elements instead.
    + +

    data

    +
      +
    • Vue 实例的数据对象,是响应式数据(数据驱动视图) 数据变化 => 视图变化
    • +
    • 可以通过 vm.$data 访问原始数据对象 =>Vue 框架给所有的属性都加了$特殊符号
    • +
    • Vue 实例 **vm**也代理了 data 对象上所有的属性,因此访问 vm.a 等价于访问 vm.$data.a
    • +
    • 视图中绑定的数据必须**显式**的初始化到 data 中
    • +
    • 数据对象的更新方式 直接 采用 实例.属性 = 值
    • +
    +
    +
    1
    2
    3
    4
    5
    data: {
    name: "helloworld",
    flag: true,
    arr: [1, 2, 3, 4, 5],
    }
    + +

    methods

    +
      +
    • methods 是一个对象
    • +
    • 可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用
    • +
    • 方法中的 this 自动绑定为 Vue 实例。
    • +
    • methods 中所有的方法 同样也被代理到了 Vue 实例对象上,都可通过 this 访问
    • +
    • Vue 实例代理了 data 中所有属性,代理了 methods 方法 ,定义属性或者方法时 要考虑**重名**问题
    • +
    • 注意,**不应该使用箭头函数来定义 method 函数** (例如 plus: () => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this 将不会按照期望指向 Vue 实例,this.a 将是 undefined
    • +
    +
    +
    1
    2
    3
    4
    5
    6
    methods: {
    fn() {
    console.log("helloworld")
    return "123";
    }
    },
    + +

    created

    mounted

    插值表达式

    +

    官方文档

    +
    +
    +

    作用:会将绑定的数据实时的显示出来:

    +

    形式: 通过 **{{ 插值表达式 }}**包裹的形式

    +

    通过任何方式修改所绑定的数据,所显示的数据都会被实时替换(响应式数据)

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    <!-- js表达式 -->
    <p>{{ 1 + 2 + 3 }}</p>
    <p>{{ 1 > 2 }}</p>
    <!-- name为data中的数据 -->
    <p>{{ name + ':消息' }}</p>
    <!-- count 为data中的数据 -->
    <p>{{ count === 1 }}</p>
    <!-- count 为data中的数据 -->
    <p>{{ count === 1 ? "成立" : "不成立" }}</p>

    <!-- 方法调用 -->
    <!-- fn为methods中的方法 -->
    <p>{{ fn() }}</p>
    + +

    系统指令

    +

    官方文档

    +
    +
    +
      +
    • 指令 (Directives) 是带有 v- 前缀的特殊特性。 对于 html 标签的功能扩展,一个指令对应一个功能
    • +
    • 指令特性的值预期是**单个 JavaScript 表达式**(v-for 是例外情况,稍后我们再讨论)。
    • +
    • 指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。
    • +
    • 指令位置: 起始标签
    • +
    • 语法 v-指令=“表达式” 如果 表达式想要是一个字符串 就必须这样写(用单引号包裹) v-指令=**" '字符串' "**,否则会被当做一个 data 数据中的变量
    • +
    +
    +
    1
    2
    3
    4
    5
    <p v-text="name"></p>
    // 将name这个变量(data中定义的变量)给了v-text指令
    <p v-text="name"></p>
    <p title="name"></p>
    // 将name这个字符串给了title属性
    + +

    v-text 和 v-html

    +
    +

    很像 innerText 和 innerHTML

    +
    +
      +
    • v-text:更新标签中的内容

      +
    • +
    • v-text 和插值表达式的区别

      +
        +
      • v-text 更新**整个**标签中的内容
      • +
      • 插值表达式: 更新标签中**局部**的内容
      • +
      +
    • +
    • v-html:更新标签中的内容/标签

      +
    • +
    • 可以渲染内容中的 HTML 标签

      +
    • +
    • 注意:尽量避免使用,容易造成危险 (XSS 跨站脚本攻击)

      +
    • +
    +
    +
    1
    2
    <p v-text="name"></p>
    <p v-html="nameHtml"></p>
    + +

    v-if 和 v-show–条件渲染

    +

    官方文档

    +
    +
    +
      +
    • 场景: 需要根据条件决定 元素是否显示 使用以上指令

      +
    • +
    • 使用: v-if 和 v-show 后面的表达式返回的布尔值 来决定 该元素显示隐藏

      +
    • +
    • 注意 : v-if 是直接决定元素 的 添加 或者删除 而 v-show 只是根据样式来决定 显示隐藏

      +
    • +
    +

    v-if  有更高的切换开销,而  v-show  有更高的初始渲染开销。

    +

    因此,如果需要非常频繁地切换,则使用  v-show  较好;

    +

    如果在运行时条件很少改变,则使用  v-if  较好。

    +

    如果 切换频繁 前者 开销更大

    +
    +

    扩展 如果 有多个元素需要根据条件进行渲染,怎么办?我们可以用一个**div标签包裹多个元素**,

    +

    但是这样的话 我们相当于**多了一个div标签,我们可以采用一个template**标签,来解决这个问题,template 标签不会产生任何实质的标签在页面上,并且能完成相应的功能

    +
    1
    2
    3
    4
    <p v-if="showMessage">v-if</p>
    //直接添加删除标签
    <p v-show="showMessage">v-show</p>
    //使用display显示和隐藏
    + +

    v-on–事件处理

    +

    官方文档

    +
    +
    +
      +
    • 场景: 使用 v-on 指令给元素绑定事件
    • +
    • 使用: 绑定 v-on:事件名.修饰符=”方法名” 可使用 @事件名=”方法名的方式”
    • +
    • 注意 方法名 中 可以采用$event 的方式传形参 也可以直接写事件名 默认第一个参数为 event 事件参数
    • +
    • 如果只写方法名 不写括号 =>方法中默认传入的第一个参数就是事件参数 =>event
    • +
    • 修饰符(可不写)
    • +
    +
      +
    • .once - 只触发一次回调。
    • +
    • .prevent - 调用 event.preventDefault()
    • +
    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    <div id="app">
    <input v-on:input="fn($event)" type="text" />
    <button v-on:click="fn2($event)"></button>
    <input @input="fn($event)" type="text" />
    <button @click="fn2($event)"></button>
    </div>
    <script src="./vue.js"></script>
    <script>
    var vm = new Vue({
    el: "#app",
    data: {
    name: "helloworld"
    },
    methods: {
    fn(e) {
    console.log(e.target.value, this.name);
    }
    fn2(){
    console.log(this.name);
    }
    }
    });
    </script>
    + +

    v-for–列表渲染

    +

    官方文档

    +
    +

    v-for-数组

    +

    目标:掌握 v-for 循环数组的用法

    +
      +
    • 根据一组数组或对象的选项列表进行渲染。
    • +
    • v-for  指令需要使用  item in items 或者 item of items  形式的特殊语法,
    • +
    • items  是源数据数组 /对象
    • +
    • 循环生成谁,就在谁的标签上写v-for指令
    • +
    +

    当要渲染相似的标签结构时用 v-for

    +
    1
    2
    3
    4
    5
    6
    7
    item in
    items(
    // item为当前遍历属性数组项的值
    item,
    index
    ) in
    items; //item为当前遍历属性数组项的值 index为数组的索引
    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <div id="app">
    <ul>
    <li v-for="item in list">{{item}}</li>
    </ul>
    </div>
    <script src="./vue.js"></script>
    <script>
    var vm = new Vue({
    el: "#app",
    data: {
    list: [1, 2, 3],
    },
    methods: {},
    });
    </script>
    + +

    v-for-对象

    1
    2
    3
    4
    基本语法;
    item in items;
    两个参数(item, key) in items;
    三个参数(item, key, index) in items;
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    <div id="app">
    <ul>
    <li v-for="(item,key,index) in person">{{key+':'+item+':'+index}}</li>
    </ul>
    </div>
    <script src="./vue.js"></script>
    <script>
    var vm = new Vue({
    el: "#app",
    data: {
    person: {
    name: "张三",
    sex: "男",
    age: "18"
    }
    },
    methods: {}
    });
    </script>
    + +

    v-for-key

    +

    目标: 掌握在 v-for 循环中给循环项赋值 key

    +
      +
    • 场景:列表数据变动会导致 视图列表重新更新 为了 提升性能 方便更新 需要提供 一个属性 key

      +
    • +
    • 使用: 通常是给列表数据中的唯一值 也可以用索引值

      +
    • +
    +
    +
    1
    2
    3
    <li v-for="(item,key,index) in person" :key="index">
    {{key+':'+item+':'+index}}
    </li>
    + +

    当 v-if 和 v-for 相遇

    +

    v-for 的优先级大于 v-if ,所有 v-if 才能使用 v-for 的变量

    +
    +
    1
    2
    3
    4
    5
    6
    7
    <li
    v-text="item"
    @click="fn(item)"
    v-if="item>0"
    v-for="(item,key,index) in person"
    :key="index"
    ></li>
    + +

    v-bind–Class 与 Style 绑定

    +

    官方文档

    +
    +

    v-bind–绑定一般属性

    +

    作用:绑定标签上的任何属性
    场景:当标签上的属性是变量/动态/需要改变的
    语法:属性=”表达式”

    +
    +
    1
    2
    3
    4
    5
    <img :src="src" alt="">
    data: {
    id: 'test',
    src: 'http://pic37.nipic.com/20140113/8800276_184927469000_2.png'
    },
    + +

    v-bind–绑定 Class-对象用法

    +

    语法:class=”{ class 名称”: 布尔值 }”

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    <p :class="{class名称:show}">内容</p>
    var vm = new Vue({
    el: "#app",
    data: {
    show: false
    },
    methods: {}
    });
    + +

    v-bind–绑定 Class-数组用法

    +

    语法:class=”[class 变量 1,class 变量 2..]”

    +
    +
    1
    2
    3
    4
    <p class="default" :class="list"></p>
    data: {
    list: ['primary', 'danger', 'info']
    },
    + +

    v-bind–绑定 style-对象用法

    +

    语法:style=”{css 属性名: 变量}”

    +
    +
    1
    2
    3
    4
    <p style="color: red;" :style="{fontSize:fsValue}">文本</p>
    data: {
    fsValue: '30px',
    },
    + +

    v-bind–绑定 Style-数组用法

    +

    语法:style=”[对象 1,对象 2…]”

    +
    +
    1
    2
    3
    4
    5
    <p style="color:red" :style="[obj]">文本</p>
    obj: {
    fontSize: "48px",
    fontWeight: "bold"
    }
    + +

    v-model–表单输入绑定

    +

    官方文档

    +
    +
    +

    特点: 双向数据绑定

    +
      +
    • 数据发生变化可以更新到界面
    • +
    • 通过界面可以更改数据
    • +
    • v-model  会忽略所有表单元素的  valuecheckedselected  特性的初始值而总是将 Vue 实例的数据作为数据来源。应该在  data选项中声明初始值。
    • +
    +
    +
    1
    2
    3
    4
    5
    <input v-model="name" type="text">
    <p>{{name}}</p>
    data: {
    name: "张三"
    },
    + +
    +

    语法糖

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    <p>{{name}}</p>
    //不使用方法
    <input type="text" :value="name" @input="name=$event.target.value">
    //使用方法
    <input type="text" :value="name" @input="fn">
    //语法糖效果
    <input type="text" v-model="name">
    data: {
    name: "",
    },
    methods: {
    fn(event) {
    this.name = event.target.value;
    }
    }
    +
    +

    v-cloak–防止页面模板闪屏现象

    +

    官方文档

    +
    +
    +

    解决页面初次渲染时 页面模板闪屏现象

    +
    +
    1
    2
    3
    4
    5
    6
    <style>
    [v-cloak] {
    display: none;
    }
    </style>
    <div v-cloak id="app"></div>
    + +

    v-once–使得所在元素只渲染一次

    +

    官方文档

    +
    +
    +

    使得所在元素只渲染一次 使用场景:静态化数据

    +
    +
    1
    2
    3
    4
    5
    <p>{{name}}</p>
    <p v-once>{{name}}</p>
    data: {
    name: "文本"
    },
    + +

    vue 过滤器

    +

    官方文档

    +
    +
    +

    data 中的数据格式(日期格式/货币格式/大小写等)需要数据格式化时,可以采用过滤器进行过滤 => 过滤器函数 => 对于 data 中数据的格式化处理

    +
    +

    使用过滤器

    1
    2
    3
    4
    <!-- 在双花括号中 -->
    <p>{{ name | 过滤器名称|过滤器名称2 }}</p>
    <!-- 在v-bind中 -->
    <p v-bind:id="name | 过滤器名称|过滤器名称2 "></p>
    + +

    定义过滤器

    +

    全局过滤器

    +
    +
    1
    2
    3
    4
    Vue.filter("过滤器名称", function (value) {
    return value;
    });
    var vm = new Vue({});
    + +
    +

    局部过滤器

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var vm = new Vue({
    ...
    filters:{
    //正常写法
    过滤器名称:function(value){
    return value;
    },
    //简写
    过滤器名称 (value){
    return value;
    }
    }
    });
    + +

    ref 操作 dom

    1
    <button ref="自定义名称">按钮</button> this.$refs.自定义名称.value;
    + +

    自定义指令

    +

    官方文档

    +
    +
    +

    需要对普通 DOM 元素进行操作,这时候就会用到自定义指令

    +
    +

    使用自定义指令

    1
    <p v-自定义指令名称></p>
    + +

    全局自定义指令

    1
    2
    3
    4
    5
    6
    Vue.directive("自定义指令名称",{
    //obj是DOM对象
    inserted(obj,params)=>{
    //TODO
    }
    });
    + +

    局部自定义指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    var vm = new Vue({
    ...
    directives:{
    自定义指令名称:{
    inserted:function(obj,params){
    //TODO
    }
    }
    }
    });
    + +

    计算属性

    +

    官方文档

    +
    +
    +

    当表达式过于复杂的情况下 可以采用计算属性 对于任何复杂逻辑都可以采用计算属性

    +
      +
    • 说明: 计算属性的值 依赖 数据对象中的值 数据对象发生改变 => 计算属性发生改变=> 视图改变
    • +
    • 计算属性**必须有返回值** 相当于对插值表达式**逻辑的一次封装**
    • +
    • methods 方法和计算属性的区别
    • +
    • 1 使用时 方法必须写括号**()**
    • +
    • 2 计算属性有**缓存机制** => 计算属性依赖 data 中的数据 => 第一次执行计算属性完毕之后,计算属性会将计算结果放入缓存 => 第二次执行计算属性时 => 发现计算属性依赖的数据没有变化 => 从缓存中获取缓存,不再执行计算属性逻辑
    • +
    • 计算属性可以抽提复杂的逻辑 /还比方法的效率更高
    • +
    +
    +

    使用计算属性

    1
    <p>{{计算属性名}}</p>
    + +

    定义计算属性

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var vm = new Vue({
    ...
    computed:{
    // this指向vm实例
    //正常写法
    计算属性名:function (){
    return //TODO
    },
    //简写
    计算属性名(){
    return //TODO
    }
    }
    });
    + +

    json-server 工具的使用

    +

    官方文档

    +
    +

    安装

    1
    npm i -g json-server
    + +

    使用

    +

    新建一个 json 文件 db.json 并创建内容

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    {
    "表名": [
    {
    "id": 1,
    "name": "小米"
    },
    {
    "id": 2,
    "name": "苹果"
    }
    ],
    "表名1": [
    {
    "id": 1
    }
    ]
    }
    + +
    +

    在相应的目录运行如下命令

    +
    +
    1
    json-server --watch db.json
    + +

    RESTFUL 的接口规则

    +
      +
    • RESTful 是一套接口设计规范
    • +
    • 用**不同的请求类型发送同样一个请求标识** 所对应的处理是不同的
    • +
    • 同样的请求标识(同一个地址) =>不同的请求类型 => get/post/put/delete
    • +
    • 通过 Http 请求的不同类型(POST/DELETE/PUT/GET)来判断是什么业务操作(CRUD )
    • +
    • CRUD => 增删改查
    • +
    • json-server 应用了 RESTful 规范
    • +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    HTTP 方法数据处理说明
    POSTCreate新增一个没有 id 的资源
    GETRead取得一个资源
    PUTUpdate更新一个资源。或新增一个含 id 资源(如果 id 不存在)
    DELETEDelete删除一个资源
    +
    +
      +
    • 查询数据 GET /brands 获取 db.json 下 brands 对应的所有数据 列表

      +
    • +
    • GET /brands/1 查询 id=1 数据 单条

      +
    • +
    • 删除数据 DELETE /brands/1 删除 id=1 数据

      +
    • +
    • 修改数据 PUT /brands/1 请求体对象

      +
    • +
    • 上传/添加 POST /brands 请求体对象

      +
    • +
    • 查询 GET /brands?title_like=关键字 -> 模糊搜索

      +
    • +
    +
    +

    使用 axios 插件发送网络请求

    +

    promise 是一种对于 ajax**回调地狱**的一种 异形封装,它让代码看上去更优雅

    +
    +

    安装

    +

    script 标签导入 点击下载

    +
    +
    1
    <script src="https://unpkg.com/axios/dist/axios.min.js"></script>
    + +
    +

    npm 方式安装

    +
    +
    1
    npm install axios
    + +

    使用

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    axios.get("url").then(function(data) {
    console.log(data.data);
    });
    axios.delete("url").then(function(data) {
    console.log(data.data);
    });
    axios
    .post("url", {
    name: "",
    date: new Date(),
    })
    .then(function(data) {
    console.log(data.data);
    });
    axios
    .put("url", {
    name: "",
    })
    .then(function(data) {
    console.log(data.data);
    });
    -----------------------axios({
    url,
    data,
    method: "get/post/delete/put",
    });
    + +

    watch-监听

    +

    官方文档

    +
    +
    +

    watch 选项就是 Vue 实例的选项 watch:{}

    +

    那个data中的属性发生改变就监听谁

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    new Vue(
    ...
    watch:{
    //正常写法
    data中的属性名:function (newValue,oldValue){
    //this指向Vue实例对象
    },
    //简写
    data中的属性名(newValue,oldValue){

    }
    }
    );

    + +

    组件

    +

    官方文档

    +
    +
    +

    特点:是一个特殊的Vue实例

    +

    每个组件都是独立

    +

    和 Vue 实例相似之处: data/methods/computed/watch 等一应俱全 Vue 实例有的 组件基本都有

    +

    组件没有 el ,但是有 template => 组件页面结构

    +

    注意 值得注意的是 data 和 Vue 实例的区别为 组件中 data 为一个函数 没有 el 选项

    +

    组件的 data 是一个带**返回值的函数** => 因为组件的数据是独立的,data => 返回一个新数据

    +

    template 代表其**页面结构** (有且只要一个根元素)

    +

    每个组件都是**独立**的 运行作用域 数据 逻辑没有任何关联

    +

    template:有且只有一个根元素

    +

    data: 数据管理=>函数=>带返回值的函数=>{}

    +
    +
    +

    错误提示:

    +
    1
    vue.js620 [Vue warn] Do not use built-in or reserved HTML elements as component id menu
    + +

    不能使用内置或者保留的HTML或者组件作为组件id

    +

    意思就是你使用的组件名的名称跟系统(vue)的内置属性名冲突了,所以创建失败,最好的方法就是换个名字。

    +
    +

    使用组件

    1
    <组件名称></组件名称>
    + +

    全局组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Vue.component(组件名称, {
    template: `模板结构`,
    data() {
    return {
    name: "",
    };
    },
    methods: {},
    });
    + +

    局部组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var obj = {
    template:`模板结构`,
    data(){
    return {
    name:"",
    }
    },
    methods:{

    }
    }
    new Vue({
    ...
    components:{
    组件名称:obj,
    }
    })
    + +

    组件嵌套

    +

    在父模板中使用其他组件的标签

    +

    一旦形成组件嵌套,就会形成父子关系

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    new Vue({
    ...
    components:{
    1:{
    template:,
    componeents:{
    2:{
    template:,
    }
    }
    }
    }

    })
    + +

    父组件给子组件传值 Props

    +
      +
    • 定义属性 给谁传值 就给标签写传递的属性
    • +
    • 接收属性 谁用属性谁接收
    • +
    • 使用它 Vue 实例代理了所有的data属性/methods方法/计算属性/props属性
    • +
    +
    +
    1
    <abc-d :test="name"></abc-d>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var vm = new Vue({
    ...
    data:{
    name:"北京"
    },
    components:{
    "abc-d":`
    <div>{{test}}</div>`,
    props:["test"]
    }
    })
    + +

    子组件给父组件传值(自定义事件)

    +

    可通过子组件中触发$emit事件

    +
    +
    1
    <abc-d @clickfn="fn2"></abc-d>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    var vm = new Vue({
    ...
    data:{
    newlist:""
    },
    methods: {
    fn2(list) {
    this.newlist = list;
    }
    },
    components:{
    "abc-d":{
    template: `<div @click="fn">{{cityname}}</div>`,
    props:["list"],
    methods:{
    fn(){
    this.$emit("clickfn",this.list)
    }
    }
    }
    }
    })
    + +

    非父子传值

    +
      +
    • 在 A 组件绑定自定义事件,接收触发事件的数据。
    • +
    • 在 B 组件触发自定义事件,提交数据即可。
    • +
    • 补充:谁绑定的事件只有有谁触发。
    • +
    • 使用 C 专门负责绑定事件。
    • +
    +
    +

    src的根目录下创建eventBus.js 写入如下代码

    +
    1
    2
    import Vue from "vue";
    export default new Vue();
    + +

    创建两个组件

    +

    com-a.vue

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    <template>
    <div>我是A组件-----------------{{str}}</div>
    </template>

    <script>
    import eventBus from "@/eventBus";
    export default {
    data() {
    return {
    str: "",
    };
    },
    created() {
    eventBus.$on("toa", (data) => {
    this.str = data;
    });
    },
    };
    </script>

    <style></style>
    + +

    com-b.vue

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <template>
    <div>
    我是B组件
    <button @click="fn">传值到A组件</button>
    </div>
    </template>

    <script>
    import eventBus from '@/eventBus'
    export default {
    data () {
    return {
    msg: 'b的数据'
    }
    },
    methods: {
    fn () {
    eventBus.$emit('toa', this.msg)
    }
    }
    }
    </script>

    <style>
    </style>
    + +

    动态组件

    +

    官方文档

    +
      +
    • activated – 激活组件的钩子
    • +
    • deactivated – 离开组件的钩子
    • +
    +
    +

    一级路由处理方法

    Props

    +
      +
    • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
    • +
    • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
    • +
    • max - 数字。最多可以缓存多少组件实例。
    • +
    +

    二级路由处理方法

      +
    • 并非所有组件要做缓存,所以按需缓存
        +
      • 两层路由组件,根据路由规则信息,决定 keep-alive 是否包裹 router-view
      • +
      +
    • +
    +

    在有需要缓存的 vue 文件中写入

    +
    1
    2
    3
    4
    <keep-alive>
    <router-view v-if="$route.meta.keepAlive"></router-view>
    </keep-alive>
    <router-view v-if="!$route.meta.keepAlive"></router-view>
    + +

    router.js文件下写入

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const routes = [
    {
    path: "/user/profile",
    name: "user-profile",
    component: UserProfile,
    meta: {
    keepAlive: true,
    },
    },
    ];
    + +

    单页应用-SPA

    +
      +
    • 传统模式 每个页面及其内容都需要从服务器一次次请求 如果网络差, 体验则会感觉很慢
    • +
    • spa 模式, 第一次加载 会将所有的资源都请求到页面 模块之间切换不会再请求服务器
    • +
    +
    +

    特点

    +
      +
    • 优点
        +
      • 用户体验好
      • +
      +
    • +
    • 缺点
        +
      • 首屏加载慢
      • +
      • 不利于 SEO
      • +
      +
    • +
    +
    +

    实现原理

    +

    通过页面的锚链接来实现spa

    +

    hash(锚链接)位于链接地址#之后

    +

    hash 值的改变**不会触发**页面刷新

    +

    hash 值是 url 地址的一部分,会存储在页面地址上 我们可以获取到

    +

    可以通过**事件监听**hash 值得改变

    +

    拿到了 hash 值,就可以根据不同的 hash 值进行不同的**模块切换**

    +
    +

    路由

    +

    官方文档

    +
    +

    js 实现路由

    +

    采用 hash 值改变的特性来进行前端路由切换

    +
    +
    1
    2
    3
    4
    5
    <!-- 定义导航 -->
    <a href="#bj">北京</a>
    <a href="#sh">上海</a>
    <!-- 定义一个容器 -->
    <div id="container"></div>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <script>
    window.onhashchange = function(){
    var path = window.location.hash.substr(1);
    var dom = document.getElementById("container");
    switch(path){
    case "bj":
    dom.innerText = "北京";
    break;
    case "sh":
    dom.innerText = "上海";
    break;
    default:
    break;
    }
    }
    </script>
    + +

    vue-router-插件

    +

    官方文档

    +
    +

    下载安装 / CDN

    通过 script 引入

    点击下载

    +

    通过 npm 安装

    1
    npm install vue-router
    + +

    使用 VueRouter

    +

    使用步骤

    +
      +
    • 引入 vue-router.js
    • +
    • 定义导航
    • +
    • 定义容器
    • +
    • 实例化一个 VueRouter 对象
    • +
    • 配置路由表=>实例化对象中配置路由表
    • +
    • 发生关系 要讲
    • +
    +
    +
    1
    2
    3
    <router-link to="/bj">北京</router-link>
    <router-link to="/sh">上海</router-link>
    <router-view></router-view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    <script src="/path/to/vue.js"></script>
    <script src="/path/to/vue-router.js"></script>

    var router = new VueRouter({
    routes:[{
    path:"/",
    component:{
    template:`<div>北京</div>`
    }
    },{
    path:"/bj",
    component:{
    template:`<div>北京</div>`
    }
    },{
    path:"/sh",
    component:{
    template:`<div>上海</div>`
    }
    }]
    });
    var vm = new Vue({
    ...
    router
    })
    + +

    动态路由

    +

    官方文档

    +
    +
    +
      +
    • 定义路由产生path:"/brand/:id"
    • +
    • 传递路由参数
    • +
    • 通过this.$route.params.id获取参数
    • +
    +
    + + + + + + + + + + + + + + + + + + +
    路由规则匹配路径$route.params
    /user/:username/user/evan{ username: 'evan' }
    /user/:username/post/:post_id/user/evan/post/123{ username: 'evan', post_id: '123' }
    +
    1
    2
    3
    4
    <router-link to="/user/北京/1">北京</router-link>
    <router-link to="/user/上海/2">上海</router-link>
    <router-link to="/user/天津/3">天津</router-link>
    <router-view></router-view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    var router = new VueRouter({
    routes: [
    {
    path: "/user/:city/:id",
    component: {
    template: `<div>{{$route.params.city}}{{$route.params.id}}</div>`,
    },
    },
    ],
    });
    var vm = new Vue({
    ...router,
    });
    + +

    to–命名路由

    +

    官方文档

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 字符串 -->
    <router-link to:"/bj"></router-link>
    <!-- 变量 -->
    <router-link :to:"path"></router-link>
    <!-- 对象 -->
    <router-link :to:"{path:'/user'}"></router-link>
    <!-- 对象-name -->
    <router-link to:"{name:'user',params:{name:123}}"></router-link>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    var router = new VueRouter({
    routes:[{
    path:"/bj",
    component:{
    template:`<div>我是北京,北京欢迎你</div>`,
    }
    },{
    path:"",
    component:{
    template:`<div>我是上海,上海欢迎你{{$route.params.city}}</div>`
    }
    }]
    });
    var vm = new Vue({
    ...
    data:{
    path:"/sh"
    }
    router
    })
    + +

    重定向

    +

    官方文档

    +
    +
    +

    希望某个页面被强制中转,
    拦截谁就在谁的组件上写redirect:

    +
    +
    1
    2
    3
    <router-link to="/bj">北京</router-link>
    <router-link to="/sh">上海</router-link>
    <router-view></router-view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    var router = new VueRouter({
    routes: [
    {
    path: "/bj",
    component: {
    template: `<div>Welcome To BeiJing!<br></div>`,
    },
    },
    {
    path: "/sh",
    redirect: "/sz",
    component: {
    template: `<div>Welcome To ShangHai!<br></div>`,
    },
    },
    {
    path: "/sz",
    component: {
    template: `<div>Welcome To shenzhen!<br>由于航空管制,已飞到深圳</div>`,
    },
    },
    ],
    });
    var vm = new Vue({
    ...router,
    });
    + +

    编程式的导航

    +

    官方文档

    +
    +
    +

    跳转不同的组件 不仅仅可以用**router-link** 还可以采用**代码行为**

    +

    (Vue 实例)this.$router 可以拿到当前路由对象的实例

    +

    **router-linkpush**方法都是追加历史记录

    +

    $router的方法如下

    +

    push–追加记录

    +

    replace–替换记录

    +

    go–前进或后退

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    //追加记录
    this.$router.push("/sh");
    //追加记录
    this.$router.push({ path: "/sh" });
    //替换记录
    this.$router.replace("/sh");
    //前进或后退
    this.$router.go(-2);
    + +

    激活样式

    +

    设置激活 class 样式

    +

    router-link-active 是一个固定的 class => 该 class 默认值就是 router-link-active,可以变,

    +

    linkActiveClass => 改变 router-link 的激活样式的 class

    +
    +
    1
    2
    3
    4
    5
    <style>
    .router-link-active {
    /* CSS样式 */
    }
    </style>
    + +
    1
    2
    3
    4
    5
    6
    7
    var router = new VueRouter({
    //改变路由激活的Class样式
    linkActiveClass:"",
    routes:[{
    ...
    }]
    })
    + +

    嵌套路由

    +

    官方文档

    +
    +
    +

    如果存在组件嵌套,就需要提供多个视图容器<router-view></router-view>

    +
    +
    1
    2
    3
    <router-link to="/bj">北京</router-link>
    <router-link to="/sh">上海</router-link>
    <router-view></router-view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    var router = new VueRouter({
    routes: [
    {
    path: "/1",
    //二级路由表
    children: [
    {
    path: "",
    componten: {},
    },
    {
    path: "/1/2",
    componten: {},
    },
    ],
    component: {
    template: `<div>
    <router-link to="/bj">北京</router-link>
    <router-link to="/sh">上海</router-link>
    <router-view></router-view>
    </div>`,
    },
    },
    ],
    });
    + +

    过渡动效

    +

    官方文档

    +
    +
    +

    采用了 v-if 或 v-show

    +

    是基本的动态组件,所以我们可以用 组件给它添加一些过渡效果:

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    <style>
    /* 最终样式 */
    .mydiv {
    width: 200px;
    height: 200px;
    }
    /* 进入动画 */
    .abc-enter,
    /* 离开动画 */
    .abc-leave-to {
    width: 0;
    height: 0;
    }
    /* 进入动画 */
    .abc-enter-active,
    /* 离开动画 */
    .abc-leave-active {
    transition: all 1s;
    }
    </style>

    <transition name="abc">
    <div class="mydiv" v-if="show"></div>
    </transition>
    + +
    1
    2
    3
    4
    5
    6
    var vm = new Vue({
    ...
    data:{
    show:false,
    }
    })
    + +

    Vue 的生命周期

    +

    点击查看

    +
    +

    使用 Vue-Cli 开发项目

    +

    点击查看

    +
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Vue-Vue%E5%9F%BA%E7%A1%80/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    目录
    1. 1. Vue 是什么之前端现状
    2. 2. Vue 特点
    3. 3. 相关链接
    4. 4. 如何安装 Vue.js
      1. 4.1. 采用本地文件引入的方式 直接下载 在 script 标签中引入
    5. 5. 第一个程序–Hello World
    6. 6. 实例选项
      1. 6.1. el
      2. 6.2. data
      3. 6.3. methods
      4. 6.4. created
      5. 6.5. mounted
    7. 7. 插值表达式
    8. 8. 系统指令
      1. 8.1. v-text 和 v-html
      2. 8.2. v-if 和 v-show–条件渲染
      3. 8.3. v-on–事件处理
      4. 8.4. v-for–列表渲染
        1. 8.4.1. v-for-数组
        2. 8.4.2. v-for-对象
      5. 8.5. v-for-key
      6. 8.6. 当 v-if 和 v-for 相遇
      7. 8.7. v-bind–Class 与 Style 绑定
        1. 8.7.1. v-bind–绑定一般属性
        2. 8.7.2. v-bind–绑定 Class-对象用法
        3. 8.7.3. v-bind–绑定 Class-数组用法
        4. 8.7.4. v-bind–绑定 style-对象用法
        5. 8.7.5. v-bind–绑定 Style-数组用法
      8. 8.8. v-model–表单输入绑定
      9. 8.9. v-cloak–防止页面模板闪屏现象
      10. 8.10. v-once–使得所在元素只渲染一次
    9. 9. vue 过滤器
      1. 9.1. 使用过滤器
      2. 9.2. 定义过滤器
    10. 10. ref 操作 dom
    11. 11. 自定义指令
      1. 11.1. 使用自定义指令
      2. 11.2. 全局自定义指令
      3. 11.3. 局部自定义指令
    12. 12. 计算属性
      1. 12.1. 使用计算属性
      2. 12.2. 定义计算属性
    13. 13. json-server 工具的使用
      1. 13.1. 安装
      2. 13.2. 使用
    14. 14. RESTFUL 的接口规则
    15. 15. 使用 axios 插件发送网络请求
      1. 15.1. 安装
      2. 15.2. 使用
    16. 16. watch-监听
    17. 17. 组件
      1. 17.1. 使用组件
      2. 17.2. 全局组件
      3. 17.3. 局部组件
      4. 17.4. 组件嵌套
      5. 17.5. 父组件给子组件传值 Props
      6. 17.6. 子组件给父组件传值(自定义事件)
      7. 17.7. 非父子传值
      8. 17.8. 动态组件
        1. 17.8.1. 一级路由处理方法
        2. 17.8.2. 二级路由处理方法
    18. 18. 单页应用-SPA
      1. 18.1. 特点
      2. 18.2. 实现原理
    19. 19. 路由
      1. 19.1. js 实现路由
      2. 19.2. vue-router-插件
        1. 19.2.1. 下载安装 / CDN
          1. 19.2.1.1. 通过 script 引入
          2. 19.2.1.2. 通过 npm 安装
      3. 19.3. 使用 VueRouter
    20. 20. 动态路由
      1. 20.0.1. to–命名路由
      2. 20.0.2. 重定向
      3. 20.0.3. 编程式的导航
      4. 20.0.4. 激活样式
      5. 20.0.5. 嵌套路由
  • 21. 过渡动效
  • 22. Vue 的生命周期
  • 23. 使用 Vue-Cli 开发项目
  • 最新文章
    \ No newline at end of file diff --git "a/posts/Vue-vue\347\232\204\351\222\251\345\255\220\345\207\275\346\225\260/index.html" "b/posts/Vue-vue\347\232\204\351\222\251\345\255\220\345\207\275\346\225\260/index.html" new file mode 100644 index 00000000..bae4272f --- /dev/null +++ "b/posts/Vue-vue\347\232\204\351\222\251\345\255\220\345\207\275\346\225\260/index.html" @@ -0,0 +1,215 @@ +Vue的钩子函数 | JCAlways + + + + + + + + + + + + + +

    Vue的钩子函数

    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Vue-vue%E7%9A%84%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/Wechat-Uni-App\345\274\200\345\217\221\351\241\271\347\233\256/index.html" "b/posts/Wechat-Uni-App\345\274\200\345\217\221\351\241\271\347\233\256/index.html" new file mode 100644 index 00000000..7d10f21c --- /dev/null +++ "b/posts/Wechat-Uni-App\345\274\200\345\217\221\351\241\271\347\233\256/index.html" @@ -0,0 +1,254 @@ +Uni-App开发项目 | JCAlways + + + + + + + + + + + + + +

    Uni-App开发项目

    uni-app—官方网站

    +

    uni-app 是一个使用 Vue.js 开发所有前端应用的框架,开发者编写一套代码,可发布到 iOS、Android、H5、以及各种小程序(微信/支付宝/百度/头条/QQ/钉钉)等多个平台。
    即使不跨端,uni-app同时也是更好的小程序开发框架。

    +
    +

    使用 uni-app 创建项目

    环境安装

    1
    npm install -g @vue/cli
    + +

    创建 uni-app

    1
    vue create -p dcloudio/uni-preset-vue myapp
    + +

    配置 AppID

    manifest.json中填入 appid

    +

    运行并发布 uni-app

    1
    npm run dev:%PLATFORM%
    + +

    %PLATFORM% 可取值如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    平台
    h5H5
    mp-alipay支付宝小程序
    mp-baidu百度小程序
    mp-weixin微信小程序
    mp-toutiao头条小程序
    mp-qqqq 小程序
    +

    导入 less

    1
    npm i less less-loader --save
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Wechat-Uni-App%E5%BC%80%E5%8F%91%E9%A1%B9%E7%9B%AE/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/posts/Wechat-\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\237\272\347\241\200/index.html" "b/posts/Wechat-\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\237\272\347\241\200/index.html" new file mode 100644 index 00000000..e69261b8 --- /dev/null +++ "b/posts/Wechat-\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217\345\237\272\347\241\200/index.html" @@ -0,0 +1,1199 @@ +微信小程序基础 | JCAlways + + + + + + + + + + + + + +

    微信小程序基础

    微信小程序开发准备

    编辑器

    VSCode 下载地址

    +

    微信小程序开发工具 下载地址

    +

    官方 API 文档

    官方文档

    +

    VSCode 推荐安装的插件

    minapp

    +

    小程序的结构目录

    小程序文件结构和传统 web 对比

    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    结构传统 web微信小程序
    结构HTMLWXML
    样式CSSWXSS
    逻辑JavaScriptJavaScript
    配置JSON
    +

    基本的项目目录

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    │  app.js              # 全局的js(入口文件)
    │ app.json # 全局的配置文件
    │ app.wxss # 全局的样式文件
    │ project.config.json # 整个项目的描述文件 类似node中的package.json

    ├─pages # 小程序对应的页面的目录
    │ ├─index # 首页
    │ │ index.js # js文件
    │ │ index.json # 配置文件
    │ │ index.wxml # 类似html
    │ │ index.wxss # 样式文件 类似CSS
    │ │
    │ └─logs # 子页面
    │ logs.js
    │ logs.json
    │ logs.wxml
    │ logs.wxss

    └─utils # 自己封装的工具函数
    util.js
    + +

    配置介绍

    注意:配置文件中不能出现注释

    +  + +

    全局配置 app.json

    +

    通过 app.json 文件对小程序进行全局配置,如页面文件的路径、窗口表现、设置网络超时时间、设置多 tab 等。
    官方文档

    +
    +

    app.json 配置清单

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型必填描述
    pagesString Array设置页面路径
    windowObject设置默认窗口表现
    tabBarObject设置底部 tab 表现
    networkTimeoutObject设置网络超时时间
    debugBoolean设置是否开启调试模式
    +

    pages-(页面路径)

    +

    用于指定小程序由哪些页面组成,每一项都对应一个页面的 路径(含文件名) 信息。文件名不需要写文件后缀,框架会自动去寻找对于位置的 .json, .js, .wxml, .wxss 四个文件进行处理。
    官方文档

    +
    +
    1
    2
    3
    {
    "pages": ["pages/index/index", "pages/detail/detail"]
    }
    + +

    window-(默认窗口表现)

    +

    用于设置小程序的状态栏、导航条、标题、窗口背景色。
    官方文档

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值描述
    navigationBarBackgroundColorHexColor#000000导航栏背景颜色,如”#000000”
    navigationBarTextStyleStringwhite导航栏标题颜色,仅支持 black/white
    navigationBarTitleTextString导航栏标题文字内容
    backgroundColorHexColor#ffffff窗口的背景色
    +
    1
    2
    3
    4
    5
    6
    7
    8
    {
    "window": {
    "navigationBarBackgroundColor": "#66ccff",
    "navigationBarTitleText": "名称",
    "navigationBarTextStyle": "white",
    "backgroundColor": "#F0F0F0"
    }
    }
    + +

    tabBar-( 底部 tab 栏的表现 )

    +

    如果小程序是一个多 tab 应用(客户端窗口的底部或顶部有 tab 栏可以切换页面),可以通过 tabBar 配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。
    官方文档

    +
    +

    对象类型,配置项指定 tab 栏的表现,以及 tab 切换时显示的对应页面。

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型必填默认值描述
    colorHexColortab 上的文字默认颜色
    selectedColorHexColortab 上的文字选中时的颜色
    backgroundColorHexColortab 的背景色
    borderStyleStringblacktabbar 上边框的颜色, 仅支持 black/white
    listArraytab 的列表,最少 2 个、最多 5 个
    positionStringbottomtab 的位置 可选值 bottom、top
    +

    其中 list 接受一个数组,数组中的每个项都是一个对象,其属性值如下:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型必填描述
    pagePathString页面路径,必须在 pages 中先定义
    textStringtab 上按钮文字
    iconPathString图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px,当 postion 为 top 时,此参数无效,不支持网络图片
    selectedIconPathString选中时的图片路径,icon 大小限制为 40kb,建议尺寸为 81px * 81px ,当 postion 为 top 时,此参数无效
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    "tabBar": {
    "color": "#D78B09",
    "selectedColor": "#FFF",
    "backgroundColor": "#FECA49",
    "borderStyle": "white",
    "list": [
    {
    "text": "首页",
    "pagePath": "pages/index/index",
    "iconPath": "icons/home-default.png",
    "selectedIconPath": "icons/home-active.png"
    },
    {
    "text": "日志",
    "pagePath": "pages/logs/logs",
    "iconPath": "icons/cards-default.png",
    "selectedIconPath": "icons/cards-active.png"
    }
    ]
    }
    + +

    页面配置 page.json

    +

    每个页面可以有不同的表现,通过 pages 目录下的 .json 文件,如 logs.json ,实现页面的局部配置。但是只能设置 app.json 中的 window 配置项的内容,页面中配置项会覆盖 app.json 的 window 中相同的配置项。
    官方文档

    +
    +

    常用配置

    + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型描述
    navigationBarTitleTextHexColor导航栏标题文字内容
    navigationBarBackgroundColorHexColor导航栏背景颜色
    navigationBarTextStyleString字体颜色 只支持black / white
    +
    1
    2
    3
    4
    5
    {
    "navigationBarTitleText": "页面标题",
    "navigationBarBackgroundColor": "#6cf",
    "navigationBarTextStyle": "white"
    }
    + +

    静态资源

    +

    小程序打包体积不允许超过 2M
    通过配置 project.config.json 文件,可以忽略某些文件(如图片等)以减少预览发布资源所占空间的大小。

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    "packOptions": {
    "ignore": [
    {
    "type": "folder",
    "value": "static"
    }
    ]
    }
    + +

    字体图标

    +

    在小程序中可以像网页一样使用字体图标,并且使用方式基本一致。唯一的不同在于小程序中字体图标所引入的字体文件路径为网络路径,且必须为 https 协议。

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @font-face {
    font-family: "icomoon";
    /* wxss 不支持本地资源(图片、字体) */ /* 服务器地址需为 https 协议 */
    src: url("https://botue.github.io/85qi/fonts/icomoon.eot?lzaqut");
    src: url("https://botue.github.io/85qi/fonts/icomoon.eot?lzaqut#iefix") format("embedded-opentype"),
    url("https://botue.github.io/85qi/fonts/icomoon.ttf?lzaqut") format("truetype"),
    url("https://botue.github.io/85qi/fonts/icomoon.woff?lzaqut") format("woff"),
    url("https://botue.github.io/85qi/fonts/icomoon.svg?lzaqut#icomoon") format("svg");
    font-weight: normal;
    font-style: normal;
    }
    + +

    视图层

    数据绑定

    所谓数据绑定是指数据与页面中组件的关联关系。使用 Mustache 语法(双大括号)将数据变量包起来

    +

    简单数据—-官方文档

    数据绑定使用 Mustache 语法(双大括号)将变量包起来,可以作用于:

    +

    内容

    +
    1
    <view> {{ message }} </view>
    + +
    1
    2
    3
    4
    5
    Page({
    data: {
    message: "Hello MINA!",
    },
    });
    + +

    组件属性(需要在双引号之内)

    +
    1
    <view id="item-{{id}}"> </view>
    + +
    1
    2
    3
    4
    5
    Page({
    data: {
    id: 0,
    },
    });
    + +

    控制属性(需要在双引号之内)

    +
    1
    <view wx:if="{{condition}}">文本</view>
    + +
    1
    2
    3
    4
    5
    Page({
    data: {
    condition: true,
    },
    });
    + +

    关键字(需要在双引号之内)

    +
    1
    <checkbox checked="{{false}}"> </checkbox>
    + +

    特别注意:不要直接写 `checked="false"`,其计算结果是一个字符串,转成 boolean 类型后代表真值。

    +  + +

    复杂数据

    1
    2
    3
    <text
    >我叫{{user.name}},我今年{{user.age}}岁了,我在学习{{courses[0]}}课程。</text
    >
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    Page({
    // 通过 data 属性,初始化页面中用到的数据
    data: {
    user: {
    name: "小明",
    age: 16,
    },
    courses: ["wxml", "wxss", "javascript"],
    },
    });
    + +

    运算

    1
    <text>{{a}} + {{b}} = {{a + b}}</text> <text>{{flag ? '是': '否'}}</text>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    Page({
    // 通过 data 属性,初始化页面中用到的数据
    data: {
    a: 10,
    b: 5,
    flag: true,
    },
    });
    + +

    列表渲染

    +

    将数组数据遍历绑定到组件中。通过 wx:for 控制属(类似 vue 中的指令)性实现。
    项的变量名默认为item 可以通过wx:for-item="value"修改变量名
    下标变量名默认为index 可以通过wx:for-index="key"修改变量名 0
    通过 block 可以将多个组件元素“包”到一起进行渲染,block 只是提供一种结构,并不会被渲染到页面中。一般这样做的目的是可以将多个组件按统一的逻辑进行控制。

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <view>
    <view wx:for="{{arr}}" wx:key="{{index}}">{{index}}:{{item}}</view>
    <view wx:for="{{person}}" wx:key="{{index}}">{{index}}:{{item}}</view>
    <view
    wx:for="{{person}}"
    wx:key="{{index}}"
    wx:for-index="key"
    wx:for-item="value"
    >
    {{key}}:{{value}}
    </view>
    <!-- block最终不会变成真正的dom元素 -->
    <block wx:for="{{arr}}" wx:key="{{index}}">{{index}}:{{item}}</block>
    </view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    Page({
    data: {
    arr: ["苹果", "香蕉", "菠萝", "火龙果"],
    person: {
    name: "小张",
    age: 22,
    },
    },
    });
    + +

    条件渲染

    +

    根据条件控制是否渲染某个(些)组件,通过 wx:if 属性实现。

    +
    +

    基本用法

    1
    2
    3
    <view wx:if="{{true}}">
    <text>锄禾日当午</text>
    </view>
    + +

    多分支

    1
    2
    3
    <view wx:if="{{view == '小明'}}">小明</view>
    <view wx:elif="{{view == '小张'}}">小张</view>
    <view wx:else="{{view == '小李'}}">小李</view>
    + +
    1
    2
    3
    4
    5
    Page({
    data: {
    view: "小张",
    },
    });
    + +

    hidden—显示/隐藏

    为小程序组件添加 hidden 属性也可以控制组件是否显示,其效果类似于 vue 中的 v-show,它与 wx:if 的不同之处是 wx:if 通过添加/移除节点实现元素的显示/隐藏,而 hidden 是对过样式 display 属性实现的。

    +
    1
    2
    3
    4
    5
    6
    <view hidden="{{true}}">
    <text>hidden=true</text>
    </view>
    <view hidden="{{false}}">
    <text>hidden=false</text>
    </view>
    + +

    小程序中组件属性的值如果为布尔类型时,只要包含这个属性即为 true,要表达 false 时,需要通过 { { } } 表达,原因是 { { } } 中的内容为被小程序当成表达式解析,所以 hidden="{{false}}"会被解析成数据类型的布尔类型,而如果写成 hidden=”false” 则将 false 当成字符串解析。

    +

    样式 WXSS

    +

    wxss 是一套样式语言,用于描述 WXML 的样式组件

    +
    +

    数据

    获取 data 的值

    +
    1
    this.data.name;
    + +

    修改 data 数据

    +
    1
    2
    3
    this.setData({
    name: "张三",
    });
    + +

    事件对象

    +
    1
    2
    3
    in(e){
    console.log(e)
    }
    + +

    获取输入框的值是通过事件源对象获取的e.detail.value

    +

    传参要动过自定义属性传参

    +

    发送请求

    小程序不支持XMLHTTPRequest,$.ajax,axios

    +

    开发者所请求的

    +
    1
    2
    3
    4
    5
    6
    7
    wx.request({
    url: "",
    method: "",
    data: {},
    success() {},
    fail() {},
    });
    + +

    事件

    事件绑定

    +

    使用bind事件名称="事件回调"或者bind:事件类型="事件回调"

    +
    +
    1
    2
    3
    <button bind:tap="getTime">获取时间</button>
    <!-- 或者 -->
    <button bindtap="getTime">获取时间</button>
    + +

    事件冒泡

    +

    盒子嵌套的事件会触发事件冒泡.
    里面的事件先触发,随后外面的事件触发

    +
    +
    1
    2
    3
    <view class="father" bind:tap="father">
    <view class="son" bind:tap="son"></view>
    </view>
    + +

    阻止事件冒泡

    +

    实现监听的同时,阻止冒泡
    使用 catch:事件名称="事件回调"或者catch事件名称="事件回调"

    +
    +
    1
    2
    3
    4
    5
    <view class="father" bind:tap="father">
    <view class="son" catch:tap="son"></view>
    <!-- 或者 -->
    <view class="son" catchtap="son"></view>
    </view>
    + +

    事件捕获

    +

    点击里面盒子,外面的事件先执行,里面的事件在执行
    使用capture-bind:事件名称="事件回调"

    +
    +
    1
    2
    3
    <view class="father" capture-bind:tap="father">
    <view class="son" capture-bind:tap="son"></view>
    </view>
    + +

    阻止捕获

    +

    在哪里阻止,里面的事件就不会执行
    使用capture-catch:事件名称="事件回调"

    +
    +
    1
    2
    3
    <view class="father" capture-catch:tap="father">
    <view class="son" capture-bind:tap="son"></view>
    </view>
    + +

    事件互斥

    +

    有 mut 的互斥
    使用mut-bind:事件名称="事件回调"

    +
    +
    1
    2
    3
    4
    5
    <view class="box" bind:tap="fn">
    <view class="father" mut-bind:tap="father">
    <view class="son" mut-bind:tap="son"></view>
    </view>
    </view>
    + +

    事件回调

    1
    2
    3
    fn(e){
    console.log(e)
    }
    + +

    生命周期

    +

    生命周期就是函数,只是会自己执行

    +
    +

    APP—应用级别

    onLaunch

    +

    小程序启动时会自动执行该函数(只会执行一次)

    +
    +
    1
    2
    3
    4
    5
    App({
    onLaunch() {
    console.log("小程序启动会执行");
    },
    });
    + +

    onShow

    +

    小程序前台运行时会自动执行

    +
    +
    1
    2
    3
    4
    5
    App({
    onShow() {
    console.log("小程序在前台会执行");
    },
    });
    + +

    onHide

    +

    小程序后台运行时会自动执行

    +
    +
    1
    2
    3
    4
    5
    App({
    onHide() {
    console.log("小程序在后台会执行");
    },
    });
    + +

    onError

    +

    小程序错误会自动执行

    +
    +
    1
    2
    3
    4
    5
    App({
    onError() {
    console.log("小程序错误会执行");
    },
    });
    + +

    onPageNotFound

    +

    小程序启动时,页面找不到会自动执行

    +
    +
    1
    2
    3
    4
    5
    App({
    onPageNotFound() {
    console.log("页面找不到呀!!!!");
    },
    });
    + +

    Page—页面级别

    onLoad

    +

    当前页面加载时会自动加载该函数 (只会执行一次)

    +
    +
    1
    2
    3
    4
    5
    Page({
    onLoad() {
    console.log("页面加载会调用");
    },
    });
    + +

    onShow

    +

    当前页面加载完毕,显示时会自动加载该函数

    +
    +
    1
    2
    3
    4
    5
    Page({
    onShow() {
    console.log("页面加载完毕,显示会调用");
    },
    });
    + +

    onReady

    +

    当前页面初次渲染时,显示时会自动加载该函数 (只会执行一次)

    +
    +
    1
    2
    3
    4
    5
    Page({
    onReady() {
    console.log("页面渲染完成会调用");
    },
    });
    + +

    onHide

    +

    页面隐藏会调用该函数

    +
    +
    1
    2
    3
    4
    5
    Page({
    onHide() {
    console.log("页面隐藏会调用");
    },
    });
    + +

    场景值—官方文档

    +

    场景值用来描述用户进入小程序的路径
    只能通过App.js下的onLaunchonShow的事件回调来获取

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    App({
    onLaunch(e) {
    // 只会执行一次
    console.log(e.scene);
    },
    // 或者
    onShow(e) {
    console.log(e.scene);
    },
    });
    + +

    地址参数

    +

    使用 navigator 的地址穿参,在被传的 JS 文件调用 onLoad 的回调获取

    +
    +
    1
    <navigator url="../index3/index?a=1&b=2">跳转到下个页面</navigator>
    + +
    1
    2
    3
    4
    5
    Page({
    onLoad(e) {
    console.log(e);
    },
    });
    + +

    组件

    view—官方文档

    +

    类似 div

    +
    + + + + + + + + + + + + + + + +
    属性名类型默认值说明
    hover-classstringnone指定按下去的样式类。当 hover-class="none" 时,没有点击态效果
    +
    1
    2
    3
    4
    <!-- WXML -->
    <view hover-class="hover_class">view</view>
    <!-- WXSS -->
    .hover_class{ background-color: red }
    + +

    scroll-view—官方文档

    +

    可滚动视图区域

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    scroll-xbooleanfalse允许横向滚动
    scroll-ybooleanfalse允许纵向滚动
    bindscrolltouppereventhandle滚动到顶部/左边时触发
    bindscrolltolowereventhandle滚动到底部/右边时触发
    +
    1
    2
    3
    4
    5
    <scroll-view scroll-y style="height:400rpx ;width:200rpx;border:1px solid red">
    <view>文是文本</view>
    ...
    <view>文是文本</view>
    </scroll-view>
    + +

    text —官方文档

    +

    显示普通文本的标签 text 只能嵌套 text

    +
    + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    selectablebooleanfalse文本是否可选
    decodebooleandalse是否解码
    +
    1
    <text class="" selectable="false" space="false" decode="false">text</text>
    + +

    image—官方文档

    +

    图片标签 image 组件默认宽度 320px 高度 240ppx 不支持背景图片的写法 > image组件中二维码/小程序码图片不支持长按识别。仅在wx.previewImage中支持长按识别

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    srcString图片资源地址
    modestringscaleToFill图片裁剪、缩放的模式参数见官方文档
    lazy-loadbooleanfalse图片懒加载,在即将进入一定范围(上下三屏)时才开始加载
    +

    mode 的合法值

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    说明
    scaleToFill缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素
    aspectFit缩放模式,保持纵横比缩放图片,使图片的长边能完全显示出来。也就是说,可以完整地将图片显示出来。
    aspectFill缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。也就是说,图片通常只在水平或垂直方向是完整的,另一个方向将会发生截取。
    widthFix缩放模式,宽度不变,高度自动变化,保持原图宽高比不变
    top裁剪模式,不缩放图片,只显示图片的顶部区域
    bottom裁剪模式,不缩放图片,只显示图片的底部区域
    center裁剪模式,不缩放图片,只显示图片的中间区域
    left裁剪模式,不缩放图片,只显示图片的左边区域
    right裁剪模式,不缩放图片,只显示图片的右边区域
    top left裁剪模式,不缩放图片,只显示图片的左上边区域
    top right裁剪模式,不缩放图片,只显示图片的右上边区域
    bottom left裁剪模式,不缩放图片,只显示图片的左下边区域
    bottom right裁剪模式,不缩放图片,只显示图片的右下边区域
    +
    1
    2
    3
    4
    5
    6
    <image
    class=""
    src="../../images/private.png"
    mode="aspectFit|aspectFill|widthFix"
    lazy-load="false"
    ></image>
    + +

    swiper(轮播图)—官方文档

    +

    微信内置的轮播图插件swiper的默认高度是150rpx

    +

    swiper 的高度=swiper 的宽 * 原图的高 / 原图的宽

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    indicator-dotsbooleanfalse是否显示面板指示点
    indicator-colorcolorrgba(0, 0, 0, .3)指示点颜色
    indicator-active-colorcolor#000000当前选中的指示点颜色
    autoplaybooleanfalse是否自动切换
    intervalnumber5000自动切换时间间隔
    circularbooleanfalse是否采用衔接滑动
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    <swiper
    autoplay
    circular
    indicator-dots
    indicator-color="#ccc"
    indicator-active-color="#6cf"
    >
    <swiper-item>
    <image class="swiper-image " src="../../images/01.jpg " />
    </swiper-item>
    <swiper-item>
    <image class="swiper-image " src="../../images/02.jpg " />
    </swiper-item>
    <swiper-item>
    <image class="swiper-image " src="../../images/03.jpg " />
    </swiper-item>
    <swiper-item>
    <image class="swiper-image " src="../../images/04.jpg " />
    </swiper-item>
    </swiper>
    + +
    +

    导航组件 类似超链接标签

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    targetStringseif在哪个目标上发生跳转,默认当前小程序
    urlString当前小程序内的跳转链接
    open-typeStringnavigate跳转方式(见下表)
    +

    open-type 的有效值:

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    说明
    navigate保留当前页面,跳转到页面的某页 但是不能跳转到 tabbar 页面
    redirect关闭当前页面,跳转到页面的某页 但是不能跳转到 tabbar 页面
    switchTab跳转到 tabbar 页面 关闭其他菲 tabbar 页面
    reLaunch关闭所有页面,打开到页面的某个页面
    navigateBack关闭当前页面,返回上一页面或多级页面
    exit退出小程序,target="miniProgram"时生效
    +
    1
    2
    3
    4
    5
    6
    7
    8
    <!-- 保留当前页面,跳转到页面的某页 但是不能跳转到tabbar页面 -->
    <navigator url="../swiper/index" open-type="navigate">swiper</navigator>
    <!-- 关闭当前页面,跳转到页面的某页 但是不能跳转到tabbar页面 -->
    <navigator url="../swiper/index" open-type="redirect">swiper</navigator>
    类型 跳转到tabbar页面 关闭其他菲tabbar页面-->
    <navigator url="../index/index" open-type="switchTab">swiper</navigator>
    <!-- 关闭所有页面,打开到页面的某个页面 -->
    <navigator url="../swiper/index" open-type="reLaunch">swiper</navigator>
    + +

    audio—官方文档

    +

    音频组件
    mp3 音乐链接来自[刘志进实验室]

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    idstringaudio 组件的唯一标识符
    srcstring要播放音频的资源地址
    loopbooleanfalse是否循环播放
    controlsbooleanfalse是否显示默认控件
    +
    1
    2
    3
    4
    5
    6
    7
    8
    <audio
    id="my"
    src="{{audioSrc}}"
    poster="{{audioPoster}}"
    name="{{audioName}}"
    author="{{audioAuthor}}"
    controls
    ></audio>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    Page({
    data: {
    audioPoster:
    "http://p1.music.126.net/ka7kZIHdviNfYO9lqBaOEQ==/109951163906385177.jpg?param=300x300",
    audioName: "前世今生",
    audioAuthor: "十三里夏天,龚子笑",
    audioSrc:
    "http://m10.music.126.net/20191130104652/2466ef3c0dc4c3a159cf970e0f16c14b/ymusic/075e/025c/520c/049c0bb5455612be6aebef319191d974.mp3",
    },
    onReady: function (e) {
    // 使用 wx.createAudioContext 获取 audio 上下文 context
    this.audioCtx = wx.createAudioContext("my");
    // 强制播放
    this.audioCtx.play();
    },
    });
    + +

    video—官方文档

    +

    视频组件

    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    属性类型默认值说明
    srcString要播放视频的资源地址
    durationnumber指定视频时长 /s
    controlsbooleantrue是否显示默认播放控件(播放/暂停按钮、播放进度、时间)
    autoplaybooleanfalse是否自动播放
    loopbooleanfalse是否循环播放
    mutedbooleanfalse是否静音播放
    +
    1
    2
    3
    4
    5
    6
    <video
    src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400"
    loop
    autoplay
    controls
    ></video>
    + +

    rich-text—官方文档

    +

    富文本

    +
    +
    1
    <rich-text nodes="{{html}}"></rich-text>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    Page({
    data: {
    nodes: [
    {
    name: "div",
    attrs: {
    class: "div_class",
    style: "line-height: 60px; color: red;",
    },
    children: [
    {
    type: "text",
    text: "Hello&nbsp;World!",
    },
    ],
    },
    ],
    },
    });
    + +

    button—官方文档

    +

    按钮

    +
    +

    web-view—官方文档

    +

    承载网页的容器。会自动铺满整个小程序页面,个人类型的小程序暂不支持使用

    +
    + + + + + + + + + + + + + +
    属性类型说明
    srcStringwebview 指向网页的链接。可打开关联的公众号的文章,其它网页需登录小程序管理后台配置业务域名。
    +
    1
    <web-view src="https://zhangsifan.com"></web-view>
    + +

    自定义组件

    新建自定义组件

    +

    根目录==>components==>新建 Component

    +
    +

    目录结构

    1
    2
    3
    4
    5
    6
    ├── components .................................................. 组件目录
    | ├── header
    | | ├── index.js ............................................ 组件header业务逻辑
    | | ├── index.json .......................................... 组件header配置
    | | ├── index.wxml .......................................... 组件header布局结构
    | | └── index.wxss .......................................... 组件header布局样式
    + +

    导入自定义组件

    1
    2
    3
    4
    5
    6
    7
    // pages/index/index.json
    {
    usingComponents: {
    // 导入自定义组件
    header: "../../components/header/index";
    }
    }
    + +
    1
    2
    3
    4
    5
    <!-- pages/index/index.wxml -->
    <view class="box">
    <!-- 应用自定义组件 -->
    <header />
    </view>
    + +

    父组件给子组件传值

    +

    父组件将数据传给子组件时,通过子组件定义的属性实现

    +
    +

    父组件代码

    +
    1
    <myheader list="{{list}}"></myheader>
    + +
    1
    2
    3
    4
    5
    Page({
    data: {
    list: [1, 2, 3, 4, 5],
    },
    });
    + +

    子组件代码

    +
    1
    <view wx:for="{{list}}" wx:key="*this">{{item}}</view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Component({
    /**
    * 组件的属性列表
    */
    properties: {
    list: {
    type: Array,
    value: [],
    },
    },
    });
    + +

    子组件给父组件传值

    +

    子组件将数据传给父组件时通过自定义事件实现
    父组件定义一个事件
    子组件来触发父组件自定义的事件

    +
    +

    父组件代码

    +
    1
    2
    3
    <myheader bind:myevent="mycallback" list="{{list}}"></myheader>
    <view>{{student.name}}</view>
    <view>{{student.age}}</view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    Page({
    data: {
    student: {},
    },
    // 自定义事件的回调函数
    mycallback(e) {
    console.log(e.detail);
    this.setData({
    student: e.detail,
    });
    },
    });
    + +

    子组件代码

    +
    1
    2
    <view wx:for="{{list}}" wx:key="*this">{{item}}</view>
    <view bind:tap="mmm">点我传值</view>
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    Component({
    /**
    * 组件的属性列表
    */
    properties: {
    list: {
    type: Array,
    value: [],
    },
    },

    /**
    * 组件的初始数据
    */
    data: {
    msg: "我是自定义组件的参数",
    student: {
    name: "小张",
    gender: "男",
    age: 16,
    },
    },

    /**
    * 组件的方法列表
    */
    methods: {
    mmm() {
    this.triggerEvent("myevent", this.data.student);
    },
    },
    });
    + +

    API

    +

    Application Program Interface 官方文档

    +
    +

    消息提示框—官方文档

    1
    2
    3
    4
    5
    wx.showToast({
    title: "成功", // 标题
    icon: "loading", // 图标 有效值 success loading none
    duration: 2000, // 提示的延迟时间
    });
    + +

    Loading 提示框—官方文档

    +

    显示 loading 提示框。需主动调用 wx.hideLoading 才能关闭提示框

    +
    +
    1
    2
    3
    4
    5
    6
    wx.showLoading({
    title: "标题",
    });
    setTimeout(() => {
    wx.hideLoading();
    }, 3000);
    + +

    确认对话框—官方文档

    +

    显示模态对话框

    +
    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    wx.showModal({
    title: "大标题",
    content: "小标题",
    success(res) {
    if (res.confirm) {
    console.log("确定");
    } else if (res.cancel) {
    console.log("取消");
    }
    },
    });
    + +

    选择框—官方文档

    +

    显示操作菜单

    +
    +
    1
    2
    3
    4
    5
    6
    wx.showActionSheet({
    itemList: ["拍照", "从相册获取"],
    success(res) {
    console.log(res.tapIndex); // 用户点击的按钮序号,从上到下的顺序,从0开始
    },
    });
    + +

    拍照—官方文档

    +

    从本地相册选择图片或使用相机拍照

    +
    +
    1
    2
    3
    4
    5
    wx.chooseImage({
    success(res) {
    console.log(res.tempFiles[0].path);
    },
    });
    + +

    文件上传接口—官方文档

    1
    2
    3
    4
    5
    6
    wx.uploadFile({
    url: "",
    filePath: null,
    name: "",
    success(res) {},
    });
    + +

    下拉刷新—官方文档

    1
    2
    3
    4
    async onPullDownRefresh() {
    await this.getFloorList();
    wx.stopPullDownRefresh();
    }
    + +

    监听页面是否快到底部

    1
    2
    3
    4
    5
    Page({
    onReachBottom() {
    console.log("快到底部了");
    },
    });
    + +

    分享小程序—官方文档

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Page({
    onShareAppMessage() {
    return {
    title: "标题:瞧一瞧,看一看啦",
    path: "/pages/index/index?id=888",
    imageUrl: "https://res.wx.qq.com/wxdoc/dist/assets/img/0.4cb08bb4.jpg",
    };
    },
    });
    + +

    模块化

    +

    小程序遵循的是类似 CommonJS 的规范。

    +
    +

    规范

    1
    2
    3
    4
    5
    // pages/index/test.js
    function abc(arg) {
    console.log("我是模块的函数,传入的参数为", arg);
    }
    module.exports.abc = abc;
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // pages/index/index.js
    const mk = require("./test.js");
    Page({
    data: {
    msg: "233",
    },
    onLoad: function (options) {
    mk.abc(this.data.msg);
    },
    });
    + +

    npm

    +

    小程序默认不支持 npm 的模块,必须经过构建后才可以使用
    微信开发工具==>工具==>构建 npm==>详情==>本地管理==>使用 npm

    +
    +
    1
    2
    3
    4
    # 初始化npm
    npm init -y
    # 安装模块
    npm install mime
    + +
    1
    2
    3
    // pages/index/index.js
    // 当通过开发工具进行构建后,才可以将 npm 模块导入,这时导入的是 miniprogram_npm 中的模块
    const mime = require("mime");
    + +

    文件作用域

    +

    在 JavaScript 文件中声明的变量和函数只在该文件中有效;不同的文件中可以声明相同名字的变量和函数,不会互相影响。
    通过全局函数 getApp 可以获取全局的应用实例,如果需要全局的数据可以在 App() 中设置如:

    +
    +
    1
    2
    3
    4
    5
    6
    7
    //app.js
    App({
    // 定义全局数据
    name: "小张",
    // 定义生命周期
    onLaunch() {},
    });
    + +
    1
    2
    3
    4
    5
    6
    7
    // pages/index/index.js
    const app = getApp();
    Page({
    data: {
    name: app.name,
    },
    });
    + +

    WXS—官方文档

    WXS.jpg

    +

    由上图我们可以知道 JsCore(Javascript)和 界面(WXML、WXSS)是互相隔离的,它们之间的通信是通过 Native(微信)中转实现的。

    +
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    Page({
    data: {
    msg: "学习小程序!",
    },

    foo: function () {
    console.log("wxml无法调用该函数...");
    },

    sayHi: function () {
    console.log("你好,小程序!");
    },
    });
    + +
    1
    2
    3
    4
    5
    6
    7
    8
    <view class="msg">{{msg}}</view>
    <!-- 将sayHi注册为事件回调,当事件触发时会被调用 -->
    <button type="primary" bind:tap="sayHi">打招呼</button>
    <!-- 直接调用函数,无效!!! -->
    <view class="demo">{{foo()}}</view>

    <!-- 由于无法直接调用函数,故下面的写法是不允许的!!! -->
    <button type="primary" bind:tap="sayHi()">打招呼</button>
    + +

    基本用法

    +

    视图层和逻辑层的隔离性给开发带来了不便,通过 WXS 可以解决这个问题,WXS(WeiXin Script)是小程序的一套脚本语言,结合 WXML,可以构建出页面的结构。
    WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致,但类似!!!!WXS 声明变量只能使用 var 、变量名不能为 $、通过 getDate 获取时间对象等,这些都是与 Javascript 不一致的方面。

    +
    +

    内联式

    1
    2
    3
    4
    5
    <view>{{m1.sayHi()}}</view>
    <wxs module="m1">
    var name = '小明'; function sayHi(){ console.log('Hi') } //
    将内部封装的功能导出 module.exports.sayHi = sayHi
    </wxs>
    + +

    引入式

    1
    2
    3
    4
    5
    // wxs/m1.wxs
    module.exports.name = "小张";
    module.exports.sayHello = function (name) {
    return "你好!" + name + "!";
    };
    + +
    1
    2
    3
    4
    5
    <!-- pages/index/index.wxml -->
    <view>{{m2.name}}</view>
    <view>{{m2.sayHello('小王')}}</view>

    <wxs module="m2" src="../../wxs/m2.wxs"></wxs>
    + +

    语法

    +

    WXS 一般是结合 WXML 使用的,它通过被用来格式展示数据,类似于 Vue 中过滤器的功能。

    +
    +
    1
    2
    3
    4
    <!-- 将 now 时间戳传入 date.format 方法 -->
    <view class="now">{{date.format(now)}}</view>
    <!-- 模块 date 暴露了 format 方法 -->
    <wxs module="date" src="../../wxs/date.wxs"></wxs>
    + +
    1
    2
    3
    4
    Page({
    // 获取当前时间(时间戳)
    now: Date.now(),
    });
    + +
    1
    2
    3
    4
    5
    6
    module.exports.format = function (timestamp) {
    // wxs 中通过 getDate 函数获得时间对象
    var d = getDate(timestamp);
    // 返回值会在 wxml 中被渲染展示
    return d.getFullYear() + '年' + (d.getMonth() + 1) + '月' d.getDate() + '日';
    }
    + +

    其他

    Vant Weapp—官方网站

    +

    Vant Weapp 是移动端 Vue 组件库 Vant 的小程序版本,两者基于相同的视觉规范,提供一致的 API 接口,助力开发者快速搭建小程序应用。

    +
    +

    安装

    1
    npm i @vant/weapp -S --production
    + +

    构建 npm

    +

    构建和使用方法已在上面讲过,就不在重复讲述了

    +
    +

    导入组件

    1
    2
    3
    4
    5
    6
    // app.json
    {
    "usingComponents": {
    "van-button": "@vant/weapp/button"
    }
    }
    + +
    1
    2
    <!--index.wxml-->
    <van-button type="primary">按钮</van-button>
    + +

    F2—官方网站

    +

    F2 是一个专注于移动,开箱即用的可视化解决方案,完美支持 H5 环境同时兼容多种环境(Node, 小程序,Weex),完备的图形语法理论,满足你的各种可视化需求,专业的移动设计指引为你带来最佳的移动端图表体验。

    +
    +

    安装

    1
    npm i @antv/f2-canvas
    + +

    构建 npm

    +

    构建和使用方法已在上面讲过,就不在重复讲述了

    +
    +

    导入组件

    1
    2
    3
    4
    5
    6
    // pages/index/index.json
    {
    "usingComponents": {
    "ff-canvas": "@antv/f2-canvas"
    }
    }
    + +
    1
    2
    3
    4
    <!--pages/index/index.wxml-->
    <view class="container">
    <ff-canvas id="column-dom" canvas-id="column" opts="{{ opts }}"></ff-canvas>
    </view>
    + +
    1
    2
    3
    4
    5
    /* pages/index/index.wxss */
    ff-canvas {
    width: 750rpx;
    height: 640rpx;
    }
    + +

    绘制图表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    import F2 from "@antv/wx-f2";

    let chart = null;

    function initChart(canvas, width, height, F2) {
    // 图表数据(也可通过请求后端获得)
    const data = [
    { year: "1951 年", sales: 38 },
    { year: "1952 年", sales: 52 },
    { year: "1956 年", sales: 61 },
    { year: "1957 年", sales: 145 },
    { year: "1958 年", sales: 48 },
    { year: "1959 年", sales: 38 },
    { year: "1960 年", sales: 38 },
    { year: "1962 年", sales: 38 },
    ];

    // 实例化图表
    chart = new F2.Chart({
    el: canvas,
    width,
    height,
    });

    // 配置图表数据
    chart.source(data, {
    sales: {
    tickCount: 5,
    },
    });

    chart.interval().position("year*sales");
    chart.render();
    return chart;
    }

    Page({
    data: {
    // 图表参数
    opts: {
    onInit: initChart,
    },
    },
    });
    + +

    日历

    +

    一个非常好用的小程序日历组件。

    +
    +

    下载并解压缩,拷贝 calendar 文件到小程序项目下

    +

    导入组件

    1
    2
    3
    4
    5
    6
    <!--pages/index/index.json-->
    {
    "usingComponents": {
    "calendar": "/components/calendar/index"
    }
    }
    + +

    在 wxml 中引入组件

    +
    1
    2
    <!--pages/index/index.wxml-->
    <calendar />
    +
    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/Wechat-%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%9F%BA%E7%A1%80/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    目录
    1. 1. 微信小程序开发准备
      1. 1.1. 编辑器
      2. 1.2. 官方 API 文档
      3. 1.3. VSCode 推荐安装的插件
    2. 2. 小程序的结构目录
      1. 2.1. 小程序文件结构和传统 web 对比
      2. 2.2. 基本的项目目录
    3. 3. 配置介绍
      1. 3.1. 全局配置 app.json
        1. 3.1.1. app.json 配置清单
          1. 3.1.1.1. pages-(页面路径)
          2. 3.1.1.2. window-(默认窗口表现)
          3. 3.1.1.3. tabBar-( 底部 tab 栏的表现 )
      2. 3.2. 页面配置 page.json
      3. 3.3. 静态资源
      4. 3.4. 字体图标
    4. 4. 视图层
      1. 4.1. 数据绑定
        1. 4.1.1. 简单数据—-官方文档
        2. 4.1.2. 复杂数据
        3. 4.1.3. 运算
      2. 4.2. 列表渲染
      3. 4.3. 条件渲染
        1. 4.3.1. 基本用法
        2. 4.3.2. 多分支
        3. 4.3.3. hidden—显示/隐藏
    5. 5. 样式 WXSS
      1. 5.1. 数据
      2. 5.2. 发送请求
    6. 6. 事件
      1. 6.1. 事件绑定
      2. 6.2. 事件冒泡
        1. 6.2.1. 阻止事件冒泡
      3. 6.3. 事件捕获
        1. 6.3.1. 阻止捕获
      4. 6.4. 事件互斥
      5. 6.5. 事件回调
    7. 7. 生命周期
      1. 7.1. APP—应用级别
        1. 7.1.1. onLaunch
        2. 7.1.2. onShow
        3. 7.1.3. onHide
        4. 7.1.4. onError
        5. 7.1.5. onPageNotFound
      2. 7.2. Page—页面级别
        1. 7.2.1. onLoad
        2. 7.2.2. onShow
        3. 7.2.3. onReady
        4. 7.2.4. onHide
      3. 7.3. 场景值—官方文档
      4. 7.4. 地址参数
    8. 8. 组件
      1. 8.1. view—官方文档
      2. 8.2. scroll-view—官方文档
      3. 8.3. text —官方文档
      4. 8.4. image—官方文档
      5. 8.5. swiper(轮播图)—官方文档
      6. 8.6. navigator(a 标签)—官方文档
      7. 8.7. audio—官方文档
      8. 8.8. video—官方文档
      9. 8.9. rich-text—官方文档
      10. 8.10. button—官方文档
      11. 8.11. web-view—官方文档
    9. 9. 自定义组件
      1. 9.1. 新建自定义组件
        1. 9.1.1. 目录结构
        2. 9.1.2. 导入自定义组件
      2. 9.2. 父组件给子组件传值
      3. 9.3. 子组件给父组件传值
    10. 10. API
      1. 10.1. 消息提示框—官方文档
      2. 10.2. Loading 提示框—官方文档
      3. 10.3. 确认对话框—官方文档
      4. 10.4. 选择框—官方文档
      5. 10.5. 拍照—官方文档
      6. 10.6. 文件上传接口—官方文档
      7. 10.7. 下拉刷新—官方文档
      8. 10.8. 监听页面是否快到底部
      9. 10.9. 分享小程序—官方文档
    11. 11. 模块化
      1. 11.1. 规范
      2. 11.2. npm
      3. 11.3. 文件作用域
    12. 12. WXS—官方文档
      1. 12.1. 基本用法
        1. 12.1.1. 内联式
        2. 12.1.2. 引入式
      2. 12.2. 语法
    13. 13. 其他
      1. 13.1. Vant Weapp—官方网站
        1. 13.1.1. 安装
        2. 13.1.2. 构建 npm
        3. 13.1.3. 导入组件
      2. 13.2. F2—官方网站
        1. 13.2.1. 安装
        2. 13.2.2. 构建 npm
        3. 13.2.3. 导入组件
        4. 13.2.4. 绘制图表
    14. 14. 日历
      1. 14.1. 导入组件
    最新文章
    \ No newline at end of file diff --git a/posts/navs/index.html b/posts/navs/index.html new file mode 100644 index 00000000..b8cb8ff0 --- /dev/null +++ b/posts/navs/index.html @@ -0,0 +1,225 @@ +文章导航 | JCAlways + + + + + + + + + + + + + +

    文章导航

    文章作者: JCAlways
    文章链接: https://blog.zhangsifan.com/posts/navs/
    版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 JCAlways
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 00000000..12d7fadf --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: +Sitemap: https://blog.zhangsifan.com/sitemap.txt +Sitemap: https://blog.zhangsifan.com/sitemap.xml +Sitemap: https://blog.zhangsifan.com/atom.xml diff --git a/service-worker.js b/service-worker.js new file mode 100644 index 00000000..1d5d84ae --- /dev/null +++ b/service-worker.js @@ -0,0 +1,2 @@ +if(!self.define){let e,i={};const a=(a,d)=>(a=new URL(a+".js",d).href,i[a]||new Promise((i=>{if("document"in self){const e=document.createElement("script");e.src=a,e.onload=i,document.head.appendChild(e)}else e=a,importScripts(a),i()})).then((()=>{let e=i[a];if(!e)throw new Error(`Module ${a} didn’t register its module`);return e})));self.define=(d,r)=>{const s=e||("document"in self?document.currentScript.src:"")||location.href;if(i[s])return;let c={};const f=e=>a(e,s),l={module:{uri:s},exports:c,require:f};i[s]=Promise.all(d.map((e=>l[e]||f(e)))).then((e=>(r(...e),c)))}}define(["./workbox-1f84e78b"],(function(e){"use strict";self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"about/index.html",revision:"67a7301713d96bd6497ebb27e029a5c0"},{url:"archives/2019/08/index.html",revision:"cd249fbfa12f31f4fb1e484a2f08be59"},{url:"archives/2019/09/index.html",revision:"7eb76ec2220fe3488ee8f7aa87a773be"},{url:"archives/2019/10/index.html",revision:"13da105ffe77fb23d058b0c5145a41d5"},{url:"archives/2019/10/page/2/index.html",revision:"a602e37a370cfd3f669b36cb648d2e7f"},{url:"archives/2019/10/page/3/index.html",revision:"120b78dde4beaffbd78a396da3a13c0f"},{url:"archives/2019/10/page/4/index.html",revision:"950256098ba968d9041cc232c00533c5"},{url:"archives/2019/10/page/5/index.html",revision:"4f838c81b2d270c11b38c53439913fa2"},{url:"archives/2019/11/index.html",revision:"ae9a868bf8c96b4b17e453a32744a7bc"},{url:"archives/2019/12/index.html",revision:"7888c923e3ce1d0484bdcbd5eadc0876"},{url:"archives/2019/index.html",revision:"8660f4889d493f3643c1baad89c92052"},{url:"archives/2019/page/2/index.html",revision:"d14da38d4fe8bcd0a7fe75a9e17a6faa"},{url:"archives/2019/page/3/index.html",revision:"858d92fd441d762a8ee3d00c7cb92a6e"},{url:"archives/2019/page/4/index.html",revision:"da1921f6a8fd4518a15f23a33f191597"},{url:"archives/2019/page/5/index.html",revision:"98b51c3d3302cf29aa8ba16ae970d557"},{url:"archives/2019/page/6/index.html",revision:"2b2ec7e4de3d523ca66c726a280468cb"},{url:"archives/2020/01/index.html",revision:"035888dd7afa7b5740f31b46b0af9d94"},{url:"archives/2020/index.html",revision:"4c77c6b621bfc50ceb509eb3a3616b4c"},{url:"archives/2021/03/index.html",revision:"81f8f47d1213d9b555a710cf5a729118"},{url:"archives/2021/index.html",revision:"1cf499ac877ccfa39dbb0ad042697a61"},{url:"archives/2022/09/index.html",revision:"b704ddc66d753691955e6c713ab56997"},{url:"archives/2022/10/index.html",revision:"e24ca35991dda72a62eb8da6cc8f47d7"},{url:"archives/2022/index.html",revision:"0cfe97e31b27a24a40ef0cf659eb9205"},{url:"archives/2023/03/index.html",revision:"005faf25a63756c7306c4aaac61a838d"},{url:"archives/2023/05/index.html",revision:"97becc39257904b0993a569d9677b030"},{url:"archives/2023/08/index.html",revision:"f6f1fcf36cbaf7bfc4d02718b43275c0"},{url:"archives/2023/index.html",revision:"d9adf975c46be6b913ffd462efa7ff4b"},{url:"archives/2024/04/index.html",revision:"5366361f680ba7331855fc64195423ca"},{url:"archives/2024/09/index.html",revision:"e1b351cdd1aa6f456b686cc0aab746b2"},{url:"archives/2024/10/index.html",revision:"8459a58275745b70472e7d242d57a370"},{url:"archives/2024/index.html",revision:"a40f8a8b12221039c2d7cc330fb01025"},{url:"archives/index.html",revision:"d22172987b018da149a7d1ca1a8996f4"},{url:"archives/page/2/index.html",revision:"5e79acf801be5c28c63792d287d26200"},{url:"archives/page/3/index.html",revision:"8e842f7fd003ef2952590d598b3641ea"},{url:"archives/page/4/index.html",revision:"43331ceb05f9d5caf5fd45a0f67dbf1c"},{url:"archives/page/5/index.html",revision:"aa7218c5fc8f23de08a1f4a4e806fc4f"},{url:"archives/page/6/index.html",revision:"c4423c4c74ddfdc966341a63e5af2a13"},{url:"archives/page/7/index.html",revision:"748478fd417028a2d0f3911ddd46fd61"},{url:"archives/page/8/index.html",revision:"00d3dfd9a820bb309742fe44d7bc7b3f"},{url:"assets/algolia/algoliasearch.js",revision:"d5d2500bfe8443b42baaefe4996ee532"},{url:"assets/algolia/algoliasearch.min.js",revision:"9c5e51e57e2b1d888950bf4cb5708c49"},{url:"assets/algolia/algoliasearchLite.js",revision:"ce9b0e62645c036a143f639b92e7789f"},{url:"assets/algolia/algoliasearchLite.min.js",revision:"c2d71f042c879659dbc97f8853b62f21"},{url:"books/index.html",revision:"1c202859cec2a6a691ff4c953cb95cb2"},{url:"categories/index.html",revision:"49c6fd0a0b0e6fb9d61765618d0ba58c"},{url:"categories/JavaScript/ES6/index.html",revision:"de9e350d0c14ec99f5a26ff67ad62ce7"},{url:"categories/JavaScript/ES6/page/2/index.html",revision:"d8f6e7fbc3af46c96935b25a1560384d"},{url:"categories/JavaScript/ES6/page/3/index.html",revision:"dcfbebdc180a75cb536a88e2ee02fcab"},{url:"categories/JavaScript/ES6/page/4/index.html",revision:"50381808634b2cffb45835600eafb3ed"},{url:"categories/JavaScript/index.html",revision:"434b2074fdb0e60b5a61e1120a00b601"},{url:"categories/JavaScript/JQuery/index.html",revision:"90fd7c447e24147fa725512e52b5463e"},{url:"categories/JavaScript/Node-js/index.html",revision:"2d124d9281202075186b00ca223e8e97"},{url:"categories/JavaScript/page/2/index.html",revision:"dd0fdb82860f301c8beecd44b6b6d22d"},{url:"categories/JavaScript/page/3/index.html",revision:"73f5e6a9dff60f1a6b01ea7d58c0d295"},{url:"categories/JavaScript/page/4/index.html",revision:"58be8420d809f3a0f3e78da139dfda37"},{url:"categories/JavaScript/page/5/index.html",revision:"9655be749c2300f4ff9d8268612b9bf5"},{url:"categories/JavaScript/React-js/index.html",revision:"6e9fe336a1c3b543284b4314a9e1fcb4"},{url:"categories/JavaScript/Vue-js/index.html",revision:"e422672ee9223d688f9f39ef92ff2857"},{url:"categories/TypeScript/index.html",revision:"7818951aaf12a8a604721816cb6ae35d"},{url:"categories/Web-Office/index.html",revision:"a6fe5faf559ab4d7acffe82017a0eca0"},{url:"categories/使用文档/Element-Plus/index.html",revision:"a784cb40ac2a77630d152d815b438e22"},{url:"categories/使用文档/index.html",revision:"9aee0ac69b8e2f2a2033f373915716f7"},{url:"categories/地图类/index.html",revision:"90109de970d58b3036990bf81dcb82d0"},{url:"categories/开发工具/index.html",revision:"d8bdfcb77e8c08e37b85a1a986b7c31b"},{url:"categories/微信小程序/index.html",revision:"0a89fb324e98bcf9761692c30cbe16ce"},{url:"categories/技术文档/index.html",revision:"a2375a13101ee82d103a64192219c832"},{url:"categories/辅助类/index.html",revision:"15bbf5fdbe58e01f4d75204dc5cd9ed4"},{url:"categories/验证类/index.html",revision:"80cee76429b52c752546b3b2041d1085"},{url:"css/index.css",revision:"26001e2bf323af5735f62ba7d92ff9a2"},{url:"css/var.css",revision:"d41d8cd98f00b204e9800998ecf8427e"},{url:"games/index.html",revision:"2e19b1e7ff1ea76416c6fc7fb13c50fd"},{url:"img/404.jpg",revision:"4ef3cfb882b6dd4128da4c8745e9a507"},{url:"img/butterfly-icon.png",revision:"28fa72a4d9b2feea4bb643a12facb7fb"},{url:"img/error-page.png",revision:"7ade9a88a5ced2c311e69b0b16680591"},{url:"img/friend_404.gif",revision:"68af0be9d22722e74665ef44dd532ba8"},{url:"index.html",revision:"6ad504d8b8968bc195b05d274b505e5b"},{url:"js/main.js",revision:"f93d9674fa0a266eefc65e92b21778be"},{url:"js/search/algolia.js",revision:"75e66239aa7a33ad0218f92e08021a64"},{url:"js/search/local-search.js",revision:"3a22c1b24d57711a7c0566aa2cecf98e"},{url:"js/tw_cn.js",revision:"accbc2ce08ee93a7bc3bc2199f4d0cfd"},{url:"js/utils.js",revision:"8d3507831ac63b0d5fc9c22bc1e87957"},{url:"link/index.html",revision:"1a2c1202efa69f4108e74977b47894d9"},{url:"movies/index.html",revision:"ce55b892705ad264b8d3ae9d18bcd1e8"},{url:"page/2/index.html",revision:"a02901f471bdeb47ea4fbcd40f2ac3c9"},{url:"page/3/index.html",revision:"4458dba67d4a389fefcf77314d680566"},{url:"page/4/index.html",revision:"67914aa2475881ad169a92aff47222f5"},{url:"page/5/index.html",revision:"a75aa813ebe203df39a35d89a432f95c"},{url:"page/6/index.html",revision:"f3b58482363a238cb4c3a840f3a53d5f"},{url:"page/7/index.html",revision:"e53ef4d28937953b9295b26227a8bded"},{url:"page/8/index.html",revision:"9170d7f5958e856cf6a5c5da24ce2059"},{url:"posts/20220903nr/index.html",revision:"36d93e450d7096b4359174e1c13e727f"},{url:"posts/20220903sg/index.html",revision:"2978acb961e48525338f3cf107df689d"},{url:"posts/20220905ap/index.html",revision:"97352372dc06dbafe12decfb2d5d8c96"},{url:"posts/20220909px/index.html",revision:"9e64a1604693c1cc5b2d3e558c68253a"},{url:"posts/20220915up/index.html",revision:"80d2e13b5dfb231a29135b6c7d2ad47d"},{url:"posts/20221010bg/index.html",revision:"bf8418d832d847b90f2e0945c1634d7f"},{url:"posts/20221021ov/index.html",revision:"d0d649e725647514ef7cc92089716595"},{url:"posts/20230329gt/index.html",revision:"f9b9a5ac864e232c797912c55e6fc022"},{url:"posts/20230330gt/index.html",revision:"520b5132c45f6a78c55f284006aeaded"},{url:"posts/20230505tr/index.html",revision:"de87e635525df4ebf19b332f2ab520a8"},{url:"posts/20230806pa/index.html",revision:"3ee391bda109b3c22e6b11847497cd1f"},{url:"posts/20240417re/index.html",revision:"92da64848fa9e06314674981e08c77a5"},{url:"posts/20240924wg/index.html",revision:"4ec4070fa6922eed3271532ff2a758fb"},{url:"posts/20241017dv/index.html",revision:"4e0664a7f1bf1fe9f23bf1a29d429c79"},{url:"posts/DOC-24道JavaScript算法/index.html",revision:"6ffd4bb90334c519d74da70449114831"},{url:"posts/DOC-ESLint/index.html",revision:"271db73b939b7136c53b8da43f7d8f4d"},{url:"posts/DOC-Hexo博客搭建流程/index.html",revision:"821e67660ec86bf28ff119596f920040"},{url:"posts/DOC-JavaScript常见问题/index.html",revision:"a6c5a1090c29ac2f5a1ae73efbe95cfa"},{url:"posts/DOC-Jquery的常见问题/index.html",revision:"2594c47c6aabf76beaba4c65b10b0c5e"},{url:"posts/DOC-Xmind笔记汇总/index.html",revision:"584dffeaeafa23565c39ae1028ca3ff6"},{url:"posts/ES6-ArrayBuffer/index.html",revision:"0d2668172c632c3bd34da21802b2b305"},{url:"posts/ES6-async 函数/index.html",revision:"8096a3cafc8235cd823b5925eef77c87"},{url:"posts/ES6-Class 的基本语法/index.html",revision:"33f9d4a973c541e60599fb64f230c2ab"},{url:"posts/ES6-Class 的继承/index.html",revision:"1b588eea3f2d0d8eb7cccac0e4bf99e9"},{url:"posts/ES6-ECMAScript 6 简介/index.html",revision:"10e7d654ffc39515c26181603974f39d"},{url:"posts/ES6-Generator 函数的异步应用/index.html",revision:"9bbd6b0171c07ce1140542bf7466f1be"},{url:"posts/ES6-Generator 函数的语法/index.html",revision:"90eaeb6e59b585c4cfbef2409f3dd930"},{url:"posts/ES6-Iterator 和 for...of 循环/index.html",revision:"320e554efe4bdcc820be85f33fb0d4e0"},{url:"posts/ES6-let 和 const 命令/index.html",revision:"c748d85237dae88edb7cee9b135547fa"},{url:"posts/ES6-Mixin/index.html",revision:"9852d40309a6ae65fbfcd71405de62dd"},{url:"posts/ES6-Module 的加载实现/index.html",revision:"cdfcc48f7b3003379c4143fef08125a4"},{url:"posts/ES6-Module 的语法/index.html",revision:"ab7c4caa9c7e53ee1ae6339a75b195fc"},{url:"posts/ES6-Promise 对象/index.html",revision:"a02747dd4d42147be0a926cce5e8bb01"},{url:"posts/ES6-Proxy/index.html",revision:"ed711551580045996b6e90880b940636"},{url:"posts/ES6-Reflect/index.html",revision:"35bd1aae653bdb285665030e127e54a4"},{url:"posts/ES6-Set 和 Map 数据结构/index.html",revision:"8ead6716d8a204ac1c0a8af2e6348793"},{url:"posts/ES6-SIMD/index.html",revision:"3f1151a95a81850021bc6b14e7baaeef"},{url:"posts/ES6-Symbol/index.html",revision:"b1ef6d31d6a07ac33fc38e0795fda7ba"},{url:"posts/ES6-修饰器/index.html",revision:"e50ae841cc203bff1d1c1df995b76d93"},{url:"posts/ES6-最新提案/index.html",revision:"4088a894bc5106bc342be7186caa752f"},{url:"posts/ES6-函数式编程/index.html",revision:"56c4e2a7cf4946031bae50cd9cc9068e"},{url:"posts/ES6-函数的扩展/index.html",revision:"c61f2e0136f56289fd8c73e2623adb94"},{url:"posts/ES6-参考链接/index.html",revision:"6a980857800fd73c99bc269adee30caa"},{url:"posts/ES6-变量的解构赋值/index.html",revision:"62478778208a11a85a4ea356cbd2624f"},{url:"posts/ES6-字符串的扩展/index.html",revision:"82fa67191b8362c2c893972f223a990e"},{url:"posts/ES6-对象的扩展/index.html",revision:"1187a0e674dd1a146e2fb8764a194617"},{url:"posts/ES6-数值的扩展/index.html",revision:"9fbe7c50475d39fd027c3640419874a7"},{url:"posts/ES6-数组的扩展/index.html",revision:"eedec0578b12b03a85ce3cb376c88825"},{url:"posts/ES6-正则的扩展/index.html",revision:"588475e33f3f81935ba58609a78e7c95"},{url:"posts/ES6-编程风格/index.html",revision:"dc51aa4fcaf8ff1d328d1483c7af8c1b"},{url:"posts/ES6-读懂 ECMAScript 规格/index.html",revision:"c722a242bf6de54f91ffa9eb3243d3c7"},{url:"posts/JS-JQuery基础/index.html",revision:"087e496da04d8d99d6c51e36e9a23849"},{url:"posts/JS-JS基础/index.html",revision:"80e48c68b6d859a7414f9d090c976cb9"},{url:"posts/JS-JS高级/index.html",revision:"9535f3c19b165291ff2955bab6bb5586"},{url:"posts/JS-Web Apis/index.html",revision:"4565c180f7f237bbb91e99565ea2508c"},{url:"posts/navs/index.html",revision:"25fa2347fd40a1094c3d931716aff6ea"},{url:"posts/Node-Express框架/index.html",revision:"34d379a3830b4a0a106d3ba7b6706670"},{url:"posts/Node-MySQL使用/index.html",revision:"601560b47f9064b88f384e61756d6671"},{url:"posts/Node-Node中的会话技术/index.html",revision:"d46421cf6a7badae2e155abb3ac10209"},{url:"posts/Node-Node中的跨域/index.html",revision:"4113ec8404bc77186b3e1a1076bedaf8"},{url:"posts/Node-Node基础/index.html",revision:"817bd60122d6effe48526eb7e4440c79"},{url:"posts/Node-Promise/index.html",revision:"3fc4f2e7d790417cbc0ffd05ec7740e0"},{url:"posts/React-React基础/index.html",revision:"dd6f38fd35025856800b4337e2e8516e"},{url:"posts/TS-TS基础 /index.html",revision:"6b6870bffa717a8583643c6f6868796e"},{url:"posts/Vue-MVVM原理/index.html",revision:"7f1f2ef9ba3571532eb4f6d63902aaa7"},{url:"posts/Vue-Vue-Cli的使用/index.html",revision:"e64f1bf76bfd007dd9784782b98157e1"},{url:"posts/Vue-Vuex/index.html",revision:"260990089f85887d312d101ecd359b8e"},{url:"posts/Vue-Vue基础/index.html",revision:"f7732330f3665f8973755385bee6d00f"},{url:"posts/Vue-vue的钩子函数/index.html",revision:"67ffd1152b7122c1d2b8d62c3edbb182"},{url:"posts/Wechat-Uni-App开发项目/index.html",revision:"1b0159ef79315f40677762f2bf9509bd"},{url:"posts/Wechat-微信小程序基础/index.html",revision:"0f72eff7f1eb07424b6f2f72b88fedec"},{url:"songs/index.html",revision:"b68f582c2e76a042e53242fd8be854be"},{url:"tags/Element-Plus/index.html",revision:"cd390d4053aca31bab5e7ef9144cf73a"},{url:"tags/ES6/index.html",revision:"ef5c42bdc374c35f5429bf843bb4d8c7"},{url:"tags/ES6/page/2/index.html",revision:"06b5004f931f88bd3403294ccdda4aa5"},{url:"tags/ES6/page/3/index.html",revision:"bf7ea4dfcd1afc96577f72c313dae3e1"},{url:"tags/ES6/page/4/index.html",revision:"32f300b4647bdf1aaf211ebadaa1f48f"},{url:"tags/Fingerprintjs/index.html",revision:"db0330b4f02074cbc830af142db3f51f"},{url:"tags/GeeTest/index.html",revision:"81d36cb99ba99f7043e9b1129f5bf02e"},{url:"tags/Hexo/index.html",revision:"1af4ce9d138480bccc5ccad92f50b7fe"},{url:"tags/index.html",revision:"7572869dc1468c371d1507e1f5439a61"},{url:"tags/JavaScript/index.html",revision:"f9e822bd3528f891efd805f9fe2326e3"},{url:"tags/kkFileViews/index.html",revision:"242e2bfdeacb40a86baf7d1fb84c7bb4"},{url:"tags/Node-js/index.html",revision:"3446851d8c37753dcb85b13af69e154b"},{url:"tags/PageSpy/index.html",revision:"6bf0d057977111cef368d14b6d1d1406"},{url:"tags/React-jjs/index.html",revision:"852b3d50ba07e7d8a10c3cc356cbfbff"},{url:"tags/reCAPTCHA/index.html",revision:"c8c5884b68a43f2fc5254793668d6ef2"},{url:"tags/Tauri/index.html",revision:"ef4349acfeb5695254679b37c339fbb9"},{url:"tags/TypeScript/index.html",revision:"6e3965d25b5ccdbb9967c7a25e320b07"},{url:"tags/VSCode/index.html",revision:"de3442579e5b1ee515ca53b8029f39d1"},{url:"tags/Vue-js/index.html",revision:"292729bc94101500cea04878b0451b17"},{url:"tags/WPS-Office/index.html",revision:"41fa9d5beeba28589ade2c17066451d9"},{url:"tags/在线预览/index.html",revision:"e21b4afcdc8dfe77bc57d2edc526e68a"},{url:"tags/导航/index.html",revision:"b61c8999e4ced9c536f0e102f9f71f38"},{url:"tags/微信小程序/index.html",revision:"f8d35f4a16d66628e6b56a9e99117986"},{url:"tags/拖拽/index.html",revision:"1881919acc302bce51f0180dbf14bbbb"},{url:"tags/文件夹上传/index.html",revision:"7fbce6393b287548640c069aced20bf3"},{url:"tags/笔记/index.html",revision:"f4908130ca0e0086a341a4dbb0e33ec8"},{url:"tags/通行秘钥/index.html",revision:"3b963e17e0c32d3e15b76f0649677396"},{url:"tags/高德地图/index.html",revision:"9735a9819511559e925f1be465fa3ee9"}],{})})); +//# sourceMappingURL=service-worker.js.map diff --git a/service-worker.js.map b/service-worker.js.map new file mode 100644 index 00000000..6e48e6ac --- /dev/null +++ b/service-worker.js.map @@ -0,0 +1 @@ +{"version":3,"file":"service-worker.js","sources":["../../../../../tmp/ef3f1988c8923d34fa1254048536e03b/service-worker.js"],"sourcesContent":["import {clientsClaim as workbox_core_clientsClaim} from '/home/runner/work/jcalways.github.io/jcalways.github.io/node_modules/workbox-core/clientsClaim.mjs';\nimport {precacheAndRoute as workbox_precaching_precacheAndRoute} from '/home/runner/work/jcalways.github.io/jcalways.github.io/node_modules/workbox-precaching/precacheAndRoute.mjs';/**\n * Welcome to your Workbox-powered service worker!\n *\n * You'll need to register this file in your web app.\n * See https://goo.gl/nhQhGp\n *\n * The rest of the code is auto-generated. Please don't update this file\n * directly; instead, make changes to your Workbox build configuration\n * and re-run your build process.\n * See https://goo.gl/2aRDsh\n */\n\n\n\n\n\n\n\n\nself.skipWaiting();\n\nworkbox_core_clientsClaim();\n\n\n/**\n * The precacheAndRoute() method efficiently caches and responds to\n * requests for URLs in the manifest.\n * See https://goo.gl/S9QRab\n */\nworkbox_precaching_precacheAndRoute([\n {\n \"url\": \"about/index.html\",\n \"revision\": \"67a7301713d96bd6497ebb27e029a5c0\"\n },\n {\n \"url\": \"archives/2019/08/index.html\",\n \"revision\": \"cd249fbfa12f31f4fb1e484a2f08be59\"\n },\n {\n \"url\": \"archives/2019/09/index.html\",\n \"revision\": \"7eb76ec2220fe3488ee8f7aa87a773be\"\n },\n {\n \"url\": \"archives/2019/10/index.html\",\n \"revision\": \"13da105ffe77fb23d058b0c5145a41d5\"\n },\n {\n \"url\": \"archives/2019/10/page/2/index.html\",\n \"revision\": \"a602e37a370cfd3f669b36cb648d2e7f\"\n },\n {\n \"url\": \"archives/2019/10/page/3/index.html\",\n \"revision\": \"120b78dde4beaffbd78a396da3a13c0f\"\n },\n {\n \"url\": \"archives/2019/10/page/4/index.html\",\n \"revision\": \"950256098ba968d9041cc232c00533c5\"\n },\n {\n \"url\": \"archives/2019/10/page/5/index.html\",\n \"revision\": \"4f838c81b2d270c11b38c53439913fa2\"\n },\n {\n \"url\": \"archives/2019/11/index.html\",\n \"revision\": \"ae9a868bf8c96b4b17e453a32744a7bc\"\n },\n {\n \"url\": \"archives/2019/12/index.html\",\n \"revision\": \"7888c923e3ce1d0484bdcbd5eadc0876\"\n },\n {\n \"url\": \"archives/2019/index.html\",\n \"revision\": \"8660f4889d493f3643c1baad89c92052\"\n },\n {\n \"url\": \"archives/2019/page/2/index.html\",\n \"revision\": \"d14da38d4fe8bcd0a7fe75a9e17a6faa\"\n },\n {\n \"url\": \"archives/2019/page/3/index.html\",\n \"revision\": \"858d92fd441d762a8ee3d00c7cb92a6e\"\n },\n {\n \"url\": \"archives/2019/page/4/index.html\",\n \"revision\": \"da1921f6a8fd4518a15f23a33f191597\"\n },\n {\n \"url\": \"archives/2019/page/5/index.html\",\n \"revision\": \"98b51c3d3302cf29aa8ba16ae970d557\"\n },\n {\n \"url\": \"archives/2019/page/6/index.html\",\n \"revision\": \"2b2ec7e4de3d523ca66c726a280468cb\"\n },\n {\n \"url\": \"archives/2020/01/index.html\",\n \"revision\": \"035888dd7afa7b5740f31b46b0af9d94\"\n },\n {\n \"url\": \"archives/2020/index.html\",\n \"revision\": \"4c77c6b621bfc50ceb509eb3a3616b4c\"\n },\n {\n \"url\": \"archives/2021/03/index.html\",\n \"revision\": \"81f8f47d1213d9b555a710cf5a729118\"\n },\n {\n \"url\": \"archives/2021/index.html\",\n \"revision\": \"1cf499ac877ccfa39dbb0ad042697a61\"\n },\n {\n \"url\": \"archives/2022/09/index.html\",\n \"revision\": \"b704ddc66d753691955e6c713ab56997\"\n },\n {\n \"url\": \"archives/2022/10/index.html\",\n \"revision\": \"e24ca35991dda72a62eb8da6cc8f47d7\"\n },\n {\n \"url\": \"archives/2022/index.html\",\n \"revision\": \"0cfe97e31b27a24a40ef0cf659eb9205\"\n },\n {\n \"url\": \"archives/2023/03/index.html\",\n \"revision\": \"005faf25a63756c7306c4aaac61a838d\"\n },\n {\n \"url\": \"archives/2023/05/index.html\",\n \"revision\": \"97becc39257904b0993a569d9677b030\"\n },\n {\n \"url\": \"archives/2023/08/index.html\",\n \"revision\": \"f6f1fcf36cbaf7bfc4d02718b43275c0\"\n },\n {\n \"url\": \"archives/2023/index.html\",\n \"revision\": \"d9adf975c46be6b913ffd462efa7ff4b\"\n },\n {\n \"url\": \"archives/2024/04/index.html\",\n \"revision\": \"5366361f680ba7331855fc64195423ca\"\n },\n {\n \"url\": \"archives/2024/09/index.html\",\n \"revision\": \"e1b351cdd1aa6f456b686cc0aab746b2\"\n },\n {\n \"url\": \"archives/2024/10/index.html\",\n \"revision\": \"8459a58275745b70472e7d242d57a370\"\n },\n {\n \"url\": \"archives/2024/index.html\",\n \"revision\": \"a40f8a8b12221039c2d7cc330fb01025\"\n },\n {\n \"url\": \"archives/index.html\",\n \"revision\": \"d22172987b018da149a7d1ca1a8996f4\"\n },\n {\n \"url\": \"archives/page/2/index.html\",\n \"revision\": \"5e79acf801be5c28c63792d287d26200\"\n },\n {\n \"url\": \"archives/page/3/index.html\",\n \"revision\": \"8e842f7fd003ef2952590d598b3641ea\"\n },\n {\n \"url\": \"archives/page/4/index.html\",\n \"revision\": \"43331ceb05f9d5caf5fd45a0f67dbf1c\"\n },\n {\n \"url\": \"archives/page/5/index.html\",\n \"revision\": \"aa7218c5fc8f23de08a1f4a4e806fc4f\"\n },\n {\n \"url\": \"archives/page/6/index.html\",\n \"revision\": \"c4423c4c74ddfdc966341a63e5af2a13\"\n },\n {\n \"url\": \"archives/page/7/index.html\",\n \"revision\": \"748478fd417028a2d0f3911ddd46fd61\"\n },\n {\n \"url\": \"archives/page/8/index.html\",\n \"revision\": \"00d3dfd9a820bb309742fe44d7bc7b3f\"\n },\n {\n \"url\": \"assets/algolia/algoliasearch.js\",\n \"revision\": \"d5d2500bfe8443b42baaefe4996ee532\"\n },\n {\n \"url\": \"assets/algolia/algoliasearch.min.js\",\n \"revision\": \"9c5e51e57e2b1d888950bf4cb5708c49\"\n },\n {\n \"url\": \"assets/algolia/algoliasearchLite.js\",\n \"revision\": \"ce9b0e62645c036a143f639b92e7789f\"\n },\n {\n \"url\": \"assets/algolia/algoliasearchLite.min.js\",\n \"revision\": \"c2d71f042c879659dbc97f8853b62f21\"\n },\n {\n \"url\": \"books/index.html\",\n \"revision\": \"1c202859cec2a6a691ff4c953cb95cb2\"\n },\n {\n \"url\": \"categories/index.html\",\n \"revision\": \"49c6fd0a0b0e6fb9d61765618d0ba58c\"\n },\n {\n \"url\": \"categories/JavaScript/ES6/index.html\",\n \"revision\": \"de9e350d0c14ec99f5a26ff67ad62ce7\"\n },\n {\n \"url\": \"categories/JavaScript/ES6/page/2/index.html\",\n \"revision\": \"d8f6e7fbc3af46c96935b25a1560384d\"\n },\n {\n \"url\": \"categories/JavaScript/ES6/page/3/index.html\",\n \"revision\": \"dcfbebdc180a75cb536a88e2ee02fcab\"\n },\n {\n \"url\": \"categories/JavaScript/ES6/page/4/index.html\",\n \"revision\": \"50381808634b2cffb45835600eafb3ed\"\n },\n {\n \"url\": \"categories/JavaScript/index.html\",\n \"revision\": \"434b2074fdb0e60b5a61e1120a00b601\"\n },\n {\n \"url\": \"categories/JavaScript/JQuery/index.html\",\n \"revision\": \"90fd7c447e24147fa725512e52b5463e\"\n },\n {\n \"url\": \"categories/JavaScript/Node-js/index.html\",\n \"revision\": \"2d124d9281202075186b00ca223e8e97\"\n },\n {\n \"url\": \"categories/JavaScript/page/2/index.html\",\n \"revision\": \"dd0fdb82860f301c8beecd44b6b6d22d\"\n },\n {\n \"url\": \"categories/JavaScript/page/3/index.html\",\n \"revision\": \"73f5e6a9dff60f1a6b01ea7d58c0d295\"\n },\n {\n \"url\": \"categories/JavaScript/page/4/index.html\",\n \"revision\": \"58be8420d809f3a0f3e78da139dfda37\"\n },\n {\n \"url\": \"categories/JavaScript/page/5/index.html\",\n \"revision\": \"9655be749c2300f4ff9d8268612b9bf5\"\n },\n {\n \"url\": \"categories/JavaScript/React-js/index.html\",\n \"revision\": \"6e9fe336a1c3b543284b4314a9e1fcb4\"\n },\n {\n \"url\": \"categories/JavaScript/Vue-js/index.html\",\n \"revision\": \"e422672ee9223d688f9f39ef92ff2857\"\n },\n {\n \"url\": \"categories/TypeScript/index.html\",\n \"revision\": \"7818951aaf12a8a604721816cb6ae35d\"\n },\n {\n \"url\": \"categories/Web-Office/index.html\",\n \"revision\": \"a6fe5faf559ab4d7acffe82017a0eca0\"\n },\n {\n \"url\": \"categories/使用文档/Element-Plus/index.html\",\n \"revision\": \"a784cb40ac2a77630d152d815b438e22\"\n },\n {\n \"url\": \"categories/使用文档/index.html\",\n \"revision\": \"9aee0ac69b8e2f2a2033f373915716f7\"\n },\n {\n \"url\": \"categories/地图类/index.html\",\n \"revision\": \"90109de970d58b3036990bf81dcb82d0\"\n },\n {\n \"url\": \"categories/开发工具/index.html\",\n \"revision\": \"d8bdfcb77e8c08e37b85a1a986b7c31b\"\n },\n {\n \"url\": \"categories/微信小程序/index.html\",\n \"revision\": \"0a89fb324e98bcf9761692c30cbe16ce\"\n },\n {\n \"url\": \"categories/技术文档/index.html\",\n \"revision\": \"a2375a13101ee82d103a64192219c832\"\n },\n {\n \"url\": \"categories/辅助类/index.html\",\n \"revision\": \"15bbf5fdbe58e01f4d75204dc5cd9ed4\"\n },\n {\n \"url\": \"categories/验证类/index.html\",\n \"revision\": \"80cee76429b52c752546b3b2041d1085\"\n },\n {\n \"url\": \"css/index.css\",\n \"revision\": \"26001e2bf323af5735f62ba7d92ff9a2\"\n },\n {\n \"url\": \"css/var.css\",\n \"revision\": \"d41d8cd98f00b204e9800998ecf8427e\"\n },\n {\n \"url\": \"games/index.html\",\n \"revision\": \"2e19b1e7ff1ea76416c6fc7fb13c50fd\"\n },\n {\n \"url\": \"img/404.jpg\",\n \"revision\": \"4ef3cfb882b6dd4128da4c8745e9a507\"\n },\n {\n \"url\": \"img/butterfly-icon.png\",\n \"revision\": \"28fa72a4d9b2feea4bb643a12facb7fb\"\n },\n {\n \"url\": \"img/error-page.png\",\n \"revision\": \"7ade9a88a5ced2c311e69b0b16680591\"\n },\n {\n \"url\": \"img/friend_404.gif\",\n \"revision\": \"68af0be9d22722e74665ef44dd532ba8\"\n },\n {\n \"url\": \"index.html\",\n \"revision\": \"6ad504d8b8968bc195b05d274b505e5b\"\n },\n {\n \"url\": \"js/main.js\",\n \"revision\": \"f93d9674fa0a266eefc65e92b21778be\"\n },\n {\n \"url\": \"js/search/algolia.js\",\n \"revision\": \"75e66239aa7a33ad0218f92e08021a64\"\n },\n {\n \"url\": \"js/search/local-search.js\",\n \"revision\": \"3a22c1b24d57711a7c0566aa2cecf98e\"\n },\n {\n \"url\": \"js/tw_cn.js\",\n \"revision\": \"accbc2ce08ee93a7bc3bc2199f4d0cfd\"\n },\n {\n \"url\": \"js/utils.js\",\n \"revision\": \"8d3507831ac63b0d5fc9c22bc1e87957\"\n },\n {\n \"url\": \"link/index.html\",\n \"revision\": \"1a2c1202efa69f4108e74977b47894d9\"\n },\n {\n \"url\": \"movies/index.html\",\n \"revision\": \"ce55b892705ad264b8d3ae9d18bcd1e8\"\n },\n {\n \"url\": \"page/2/index.html\",\n \"revision\": \"a02901f471bdeb47ea4fbcd40f2ac3c9\"\n },\n {\n \"url\": \"page/3/index.html\",\n \"revision\": \"4458dba67d4a389fefcf77314d680566\"\n },\n {\n \"url\": \"page/4/index.html\",\n \"revision\": \"67914aa2475881ad169a92aff47222f5\"\n },\n {\n \"url\": \"page/5/index.html\",\n \"revision\": \"a75aa813ebe203df39a35d89a432f95c\"\n },\n {\n \"url\": \"page/6/index.html\",\n \"revision\": \"f3b58482363a238cb4c3a840f3a53d5f\"\n },\n {\n \"url\": \"page/7/index.html\",\n \"revision\": \"e53ef4d28937953b9295b26227a8bded\"\n },\n {\n \"url\": \"page/8/index.html\",\n \"revision\": \"9170d7f5958e856cf6a5c5da24ce2059\"\n },\n {\n \"url\": \"posts/20220903nr/index.html\",\n \"revision\": \"36d93e450d7096b4359174e1c13e727f\"\n },\n {\n \"url\": \"posts/20220903sg/index.html\",\n \"revision\": \"2978acb961e48525338f3cf107df689d\"\n },\n {\n \"url\": \"posts/20220905ap/index.html\",\n \"revision\": \"97352372dc06dbafe12decfb2d5d8c96\"\n },\n {\n \"url\": \"posts/20220909px/index.html\",\n \"revision\": \"9e64a1604693c1cc5b2d3e558c68253a\"\n },\n {\n \"url\": \"posts/20220915up/index.html\",\n \"revision\": \"80d2e13b5dfb231a29135b6c7d2ad47d\"\n },\n {\n \"url\": \"posts/20221010bg/index.html\",\n \"revision\": \"bf8418d832d847b90f2e0945c1634d7f\"\n },\n {\n \"url\": \"posts/20221021ov/index.html\",\n \"revision\": \"d0d649e725647514ef7cc92089716595\"\n },\n {\n \"url\": \"posts/20230329gt/index.html\",\n \"revision\": \"f9b9a5ac864e232c797912c55e6fc022\"\n },\n {\n \"url\": \"posts/20230330gt/index.html\",\n \"revision\": \"520b5132c45f6a78c55f284006aeaded\"\n },\n {\n \"url\": \"posts/20230505tr/index.html\",\n \"revision\": \"de87e635525df4ebf19b332f2ab520a8\"\n },\n {\n \"url\": \"posts/20230806pa/index.html\",\n \"revision\": \"3ee391bda109b3c22e6b11847497cd1f\"\n },\n {\n \"url\": \"posts/20240417re/index.html\",\n \"revision\": \"92da64848fa9e06314674981e08c77a5\"\n },\n {\n \"url\": \"posts/20240924wg/index.html\",\n \"revision\": \"4ec4070fa6922eed3271532ff2a758fb\"\n },\n {\n \"url\": \"posts/20241017dv/index.html\",\n \"revision\": \"4e0664a7f1bf1fe9f23bf1a29d429c79\"\n },\n {\n \"url\": \"posts/DOC-24道JavaScript算法/index.html\",\n \"revision\": \"6ffd4bb90334c519d74da70449114831\"\n },\n {\n \"url\": \"posts/DOC-ESLint/index.html\",\n \"revision\": \"271db73b939b7136c53b8da43f7d8f4d\"\n },\n {\n \"url\": \"posts/DOC-Hexo博客搭建流程/index.html\",\n \"revision\": \"821e67660ec86bf28ff119596f920040\"\n },\n {\n \"url\": \"posts/DOC-JavaScript常见问题/index.html\",\n \"revision\": \"a6c5a1090c29ac2f5a1ae73efbe95cfa\"\n },\n {\n \"url\": \"posts/DOC-Jquery的常见问题/index.html\",\n \"revision\": \"2594c47c6aabf76beaba4c65b10b0c5e\"\n },\n {\n \"url\": \"posts/DOC-Xmind笔记汇总/index.html\",\n \"revision\": \"584dffeaeafa23565c39ae1028ca3ff6\"\n },\n {\n \"url\": \"posts/ES6-ArrayBuffer/index.html\",\n \"revision\": \"0d2668172c632c3bd34da21802b2b305\"\n },\n {\n \"url\": \"posts/ES6-async 函数/index.html\",\n \"revision\": \"8096a3cafc8235cd823b5925eef77c87\"\n },\n {\n \"url\": \"posts/ES6-Class 的基本语法/index.html\",\n \"revision\": \"33f9d4a973c541e60599fb64f230c2ab\"\n },\n {\n \"url\": \"posts/ES6-Class 的继承/index.html\",\n \"revision\": \"1b588eea3f2d0d8eb7cccac0e4bf99e9\"\n },\n {\n \"url\": \"posts/ES6-ECMAScript 6 简介/index.html\",\n \"revision\": \"10e7d654ffc39515c26181603974f39d\"\n },\n {\n \"url\": \"posts/ES6-Generator 函数的异步应用/index.html\",\n \"revision\": \"9bbd6b0171c07ce1140542bf7466f1be\"\n },\n {\n \"url\": \"posts/ES6-Generator 函数的语法/index.html\",\n \"revision\": \"90eaeb6e59b585c4cfbef2409f3dd930\"\n },\n {\n \"url\": \"posts/ES6-Iterator 和 for...of 循环/index.html\",\n \"revision\": \"320e554efe4bdcc820be85f33fb0d4e0\"\n },\n {\n \"url\": \"posts/ES6-let 和 const 命令/index.html\",\n \"revision\": \"c748d85237dae88edb7cee9b135547fa\"\n },\n {\n \"url\": \"posts/ES6-Mixin/index.html\",\n \"revision\": \"9852d40309a6ae65fbfcd71405de62dd\"\n },\n {\n \"url\": \"posts/ES6-Module 的加载实现/index.html\",\n \"revision\": \"cdfcc48f7b3003379c4143fef08125a4\"\n },\n {\n \"url\": \"posts/ES6-Module 的语法/index.html\",\n \"revision\": \"ab7c4caa9c7e53ee1ae6339a75b195fc\"\n },\n {\n \"url\": \"posts/ES6-Promise 对象/index.html\",\n \"revision\": \"a02747dd4d42147be0a926cce5e8bb01\"\n },\n {\n \"url\": \"posts/ES6-Proxy/index.html\",\n \"revision\": \"ed711551580045996b6e90880b940636\"\n },\n {\n \"url\": \"posts/ES6-Reflect/index.html\",\n \"revision\": \"35bd1aae653bdb285665030e127e54a4\"\n },\n {\n \"url\": \"posts/ES6-Set 和 Map 数据结构/index.html\",\n \"revision\": \"8ead6716d8a204ac1c0a8af2e6348793\"\n },\n {\n \"url\": \"posts/ES6-SIMD/index.html\",\n \"revision\": \"3f1151a95a81850021bc6b14e7baaeef\"\n },\n {\n \"url\": \"posts/ES6-Symbol/index.html\",\n \"revision\": \"b1ef6d31d6a07ac33fc38e0795fda7ba\"\n },\n {\n \"url\": \"posts/ES6-修饰器/index.html\",\n \"revision\": \"e50ae841cc203bff1d1c1df995b76d93\"\n },\n {\n \"url\": \"posts/ES6-最新提案/index.html\",\n \"revision\": \"4088a894bc5106bc342be7186caa752f\"\n },\n {\n \"url\": \"posts/ES6-函数式编程/index.html\",\n \"revision\": \"56c4e2a7cf4946031bae50cd9cc9068e\"\n },\n {\n \"url\": \"posts/ES6-函数的扩展/index.html\",\n \"revision\": \"c61f2e0136f56289fd8c73e2623adb94\"\n },\n {\n \"url\": \"posts/ES6-参考链接/index.html\",\n \"revision\": \"6a980857800fd73c99bc269adee30caa\"\n },\n {\n \"url\": \"posts/ES6-变量的解构赋值/index.html\",\n \"revision\": \"62478778208a11a85a4ea356cbd2624f\"\n },\n {\n \"url\": \"posts/ES6-字符串的扩展/index.html\",\n \"revision\": \"82fa67191b8362c2c893972f223a990e\"\n },\n {\n \"url\": \"posts/ES6-对象的扩展/index.html\",\n \"revision\": \"1187a0e674dd1a146e2fb8764a194617\"\n },\n {\n \"url\": \"posts/ES6-数值的扩展/index.html\",\n \"revision\": \"9fbe7c50475d39fd027c3640419874a7\"\n },\n {\n \"url\": \"posts/ES6-数组的扩展/index.html\",\n \"revision\": \"eedec0578b12b03a85ce3cb376c88825\"\n },\n {\n \"url\": \"posts/ES6-正则的扩展/index.html\",\n \"revision\": \"588475e33f3f81935ba58609a78e7c95\"\n },\n {\n \"url\": \"posts/ES6-编程风格/index.html\",\n \"revision\": \"dc51aa4fcaf8ff1d328d1483c7af8c1b\"\n },\n {\n \"url\": \"posts/ES6-读懂 ECMAScript 规格/index.html\",\n \"revision\": \"c722a242bf6de54f91ffa9eb3243d3c7\"\n },\n {\n \"url\": \"posts/JS-JQuery基础/index.html\",\n \"revision\": \"087e496da04d8d99d6c51e36e9a23849\"\n },\n {\n \"url\": \"posts/JS-JS基础/index.html\",\n \"revision\": \"80e48c68b6d859a7414f9d090c976cb9\"\n },\n {\n \"url\": \"posts/JS-JS高级/index.html\",\n \"revision\": \"9535f3c19b165291ff2955bab6bb5586\"\n },\n {\n \"url\": \"posts/JS-Web Apis/index.html\",\n \"revision\": \"4565c180f7f237bbb91e99565ea2508c\"\n },\n {\n \"url\": \"posts/navs/index.html\",\n \"revision\": \"25fa2347fd40a1094c3d931716aff6ea\"\n },\n {\n \"url\": \"posts/Node-Express框架/index.html\",\n \"revision\": \"34d379a3830b4a0a106d3ba7b6706670\"\n },\n {\n \"url\": \"posts/Node-MySQL使用/index.html\",\n \"revision\": \"601560b47f9064b88f384e61756d6671\"\n },\n {\n \"url\": \"posts/Node-Node中的会话技术/index.html\",\n \"revision\": \"d46421cf6a7badae2e155abb3ac10209\"\n },\n {\n \"url\": \"posts/Node-Node中的跨域/index.html\",\n \"revision\": \"4113ec8404bc77186b3e1a1076bedaf8\"\n },\n {\n \"url\": \"posts/Node-Node基础/index.html\",\n \"revision\": \"817bd60122d6effe48526eb7e4440c79\"\n },\n {\n \"url\": \"posts/Node-Promise/index.html\",\n \"revision\": \"3fc4f2e7d790417cbc0ffd05ec7740e0\"\n },\n {\n \"url\": \"posts/React-React基础/index.html\",\n \"revision\": \"dd6f38fd35025856800b4337e2e8516e\"\n },\n {\n \"url\": \"posts/TS-TS基础 /index.html\",\n \"revision\": \"6b6870bffa717a8583643c6f6868796e\"\n },\n {\n \"url\": \"posts/Vue-MVVM原理/index.html\",\n \"revision\": \"7f1f2ef9ba3571532eb4f6d63902aaa7\"\n },\n {\n \"url\": \"posts/Vue-Vue-Cli的使用/index.html\",\n \"revision\": \"e64f1bf76bfd007dd9784782b98157e1\"\n },\n {\n \"url\": \"posts/Vue-Vuex/index.html\",\n \"revision\": \"260990089f85887d312d101ecd359b8e\"\n },\n {\n \"url\": \"posts/Vue-Vue基础/index.html\",\n \"revision\": \"f7732330f3665f8973755385bee6d00f\"\n },\n {\n \"url\": \"posts/Vue-vue的钩子函数/index.html\",\n \"revision\": \"67ffd1152b7122c1d2b8d62c3edbb182\"\n },\n {\n \"url\": \"posts/Wechat-Uni-App开发项目/index.html\",\n \"revision\": \"1b0159ef79315f40677762f2bf9509bd\"\n },\n {\n \"url\": \"posts/Wechat-微信小程序基础/index.html\",\n \"revision\": \"0f72eff7f1eb07424b6f2f72b88fedec\"\n },\n {\n \"url\": \"songs/index.html\",\n \"revision\": \"b68f582c2e76a042e53242fd8be854be\"\n },\n {\n \"url\": \"tags/Element-Plus/index.html\",\n \"revision\": \"cd390d4053aca31bab5e7ef9144cf73a\"\n },\n {\n \"url\": \"tags/ES6/index.html\",\n \"revision\": \"ef5c42bdc374c35f5429bf843bb4d8c7\"\n },\n {\n \"url\": \"tags/ES6/page/2/index.html\",\n \"revision\": \"06b5004f931f88bd3403294ccdda4aa5\"\n },\n {\n \"url\": \"tags/ES6/page/3/index.html\",\n \"revision\": \"bf7ea4dfcd1afc96577f72c313dae3e1\"\n },\n {\n \"url\": \"tags/ES6/page/4/index.html\",\n \"revision\": \"32f300b4647bdf1aaf211ebadaa1f48f\"\n },\n {\n \"url\": \"tags/Fingerprintjs/index.html\",\n \"revision\": \"db0330b4f02074cbc830af142db3f51f\"\n },\n {\n \"url\": \"tags/GeeTest/index.html\",\n \"revision\": \"81d36cb99ba99f7043e9b1129f5bf02e\"\n },\n {\n \"url\": \"tags/Hexo/index.html\",\n \"revision\": \"1af4ce9d138480bccc5ccad92f50b7fe\"\n },\n {\n \"url\": \"tags/index.html\",\n \"revision\": \"7572869dc1468c371d1507e1f5439a61\"\n },\n {\n \"url\": \"tags/JavaScript/index.html\",\n \"revision\": \"f9e822bd3528f891efd805f9fe2326e3\"\n },\n {\n \"url\": \"tags/kkFileViews/index.html\",\n \"revision\": \"242e2bfdeacb40a86baf7d1fb84c7bb4\"\n },\n {\n \"url\": \"tags/Node-js/index.html\",\n \"revision\": \"3446851d8c37753dcb85b13af69e154b\"\n },\n {\n \"url\": \"tags/PageSpy/index.html\",\n \"revision\": \"6bf0d057977111cef368d14b6d1d1406\"\n },\n {\n \"url\": \"tags/React-jjs/index.html\",\n \"revision\": \"852b3d50ba07e7d8a10c3cc356cbfbff\"\n },\n {\n \"url\": \"tags/reCAPTCHA/index.html\",\n \"revision\": \"c8c5884b68a43f2fc5254793668d6ef2\"\n },\n {\n \"url\": \"tags/Tauri/index.html\",\n \"revision\": \"ef4349acfeb5695254679b37c339fbb9\"\n },\n {\n \"url\": \"tags/TypeScript/index.html\",\n \"revision\": \"6e3965d25b5ccdbb9967c7a25e320b07\"\n },\n {\n \"url\": \"tags/VSCode/index.html\",\n \"revision\": \"de3442579e5b1ee515ca53b8029f39d1\"\n },\n {\n \"url\": \"tags/Vue-js/index.html\",\n \"revision\": \"292729bc94101500cea04878b0451b17\"\n },\n {\n \"url\": \"tags/WPS-Office/index.html\",\n \"revision\": \"41fa9d5beeba28589ade2c17066451d9\"\n },\n {\n \"url\": \"tags/在线预览/index.html\",\n \"revision\": \"e21b4afcdc8dfe77bc57d2edc526e68a\"\n },\n {\n \"url\": \"tags/导航/index.html\",\n \"revision\": \"b61c8999e4ced9c536f0e102f9f71f38\"\n },\n {\n \"url\": \"tags/微信小程序/index.html\",\n \"revision\": \"f8d35f4a16d66628e6b56a9e99117986\"\n },\n {\n \"url\": \"tags/拖拽/index.html\",\n \"revision\": \"1881919acc302bce51f0180dbf14bbbb\"\n },\n {\n \"url\": \"tags/文件夹上传/index.html\",\n \"revision\": \"7fbce6393b287548640c069aced20bf3\"\n },\n {\n \"url\": \"tags/笔记/index.html\",\n \"revision\": \"f4908130ca0e0086a341a4dbb0e33ec8\"\n },\n {\n \"url\": \"tags/通行秘钥/index.html\",\n \"revision\": \"3b963e17e0c32d3e15b76f0649677396\"\n },\n {\n \"url\": \"tags/高德地图/index.html\",\n \"revision\": \"9735a9819511559e925f1be465fa3ee9\"\n }\n], {});\n\n\n\n\n\n\n\n\n"],"names":["self","skipWaiting","workbox_core_clientsClaim","workbox_precaching_precacheAndRoute","url","revision"],"mappings":"0nBAoBAA,KAAKC,cAELC,EAAAA,eAQAC,EAAAA,iBAAoC,CAClC,CACEC,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,qCACPC,SAAY,oCAEd,CACED,IAAO,qCACPC,SAAY,oCAEd,CACED,IAAO,qCACPC,SAAY,oCAEd,CACED,IAAO,qCACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,sBACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,wBACPC,SAAY,oCAEd,CACED,IAAO,uCACPC,SAAY,oCAEd,CACED,IAAO,8CACPC,SAAY,oCAEd,CACED,IAAO,8CACPC,SAAY,oCAEd,CACED,IAAO,8CACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,2CACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,4CACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,0CACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,gBACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,yBACPC,SAAY,oCAEd,CACED,IAAO,qBACPC,SAAY,oCAEd,CACED,IAAO,qBACPC,SAAY,oCAEd,CACED,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,aACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,cACPC,SAAY,oCAEd,CACED,IAAO,kBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,oBACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,uCACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,iCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,iCACPC,SAAY,oCAEd,CACED,IAAO,uCACPC,SAAY,oCAEd,CACED,IAAO,yCACPC,SAAY,oCAEd,CACED,IAAO,uCACPC,SAAY,oCAEd,CACED,IAAO,8CACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,oCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,+BACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,+BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,wCACPC,SAAY,oCAEd,CACED,IAAO,+BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,2BACPC,SAAY,oCAEd,CACED,IAAO,+BACPC,SAAY,oCAEd,CACED,IAAO,wBACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,mCACPC,SAAY,oCAEd,CACED,IAAO,iCACPC,SAAY,oCAEd,CACED,IAAO,+BACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,iCACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,sCACPC,SAAY,oCAEd,CACED,IAAO,kCACPC,SAAY,oCAEd,CACED,IAAO,mBACPC,SAAY,oCAEd,CACED,IAAO,+BACPC,SAAY,oCAEd,CACED,IAAO,sBACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,gCACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,oCAEd,CACED,IAAO,kBACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,8BACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,0BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,4BACPC,SAAY,oCAEd,CACED,IAAO,wBACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,yBACPC,SAAY,oCAEd,CACED,IAAO,yBACPC,SAAY,oCAEd,CACED,IAAO,6BACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,oCAEd,CACED,IAAO,qBACPC,SAAY,oCAEd,CACED,IAAO,wBACPC,SAAY,oCAEd,CACED,IAAO,qBACPC,SAAY,oCAEd,CACED,IAAO,wBACPC,SAAY,oCAEd,CACED,IAAO,qBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,oCAEd,CACED,IAAO,uBACPC,SAAY,qCAEb,CAAA"} \ No newline at end of file diff --git a/sitemap.txt b/sitemap.txt new file mode 100644 index 00000000..612d2d78 --- /dev/null +++ b/sitemap.txt @@ -0,0 +1,117 @@ +https://blog.zhangsifan.com/posts/ES6-%E8%AF%BB%E6%87%82%20ECMAScript%20%E8%A7%84%E6%A0%BC/ +https://blog.zhangsifan.com/posts/JS-JQuery%E5%9F%BA%E7%A1%80/ +https://blog.zhangsifan.com/posts/JS-JS%E5%9F%BA%E7%A1%80/ +https://blog.zhangsifan.com/posts/JS-JS%E9%AB%98%E7%BA%A7/ +https://blog.zhangsifan.com/posts/JS-Web%20Apis/ +https://blog.zhangsifan.com/posts/Node-Express%E6%A1%86%E6%9E%B6/ +https://blog.zhangsifan.com/posts/Node-MySQL%E4%BD%BF%E7%94%A8/ +https://blog.zhangsifan.com/posts/Node-Node%E4%B8%AD%E7%9A%84%E4%BC%9A%E8%AF%9D%E6%8A%80%E6%9C%AF/ +https://blog.zhangsifan.com/posts/Node-Node%E4%B8%AD%E7%9A%84%E8%B7%A8%E5%9F%9F/ +https://blog.zhangsifan.com/posts/Node-Node%E5%9F%BA%E7%A1%80/ +https://blog.zhangsifan.com/posts/Node-Promise/ +https://blog.zhangsifan.com/posts/React-React%E5%9F%BA%E7%A1%80/ +https://blog.zhangsifan.com/posts/TS-TS%E5%9F%BA%E7%A1%80%20/ +https://blog.zhangsifan.com/posts/Vue-MVVM%E5%8E%9F%E7%90%86/ +https://blog.zhangsifan.com/posts/Vue-Vue-Cli%E7%9A%84%E4%BD%BF%E7%94%A8/ +https://blog.zhangsifan.com/posts/Vue-Vuex/ +https://blog.zhangsifan.com/posts/Vue-vue%E7%9A%84%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0/ +https://blog.zhangsifan.com/posts/Wechat-Uni-App%E5%BC%80%E5%8F%91%E9%A1%B9%E7%9B%AE/ +https://blog.zhangsifan.com/posts/Vue-Vue%E5%9F%BA%E7%A1%80/ +https://blog.zhangsifan.com/posts/navs/ +https://blog.zhangsifan.com/posts/Wechat-%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%9F%BA%E7%A1%80/ +https://blog.zhangsifan.com/manifest.json +https://blog.zhangsifan.com/about/index.html +https://blog.zhangsifan.com/categories/index.html +https://blog.zhangsifan.com/link/index.html +https://blog.zhangsifan.com/tags/index.html +https://blog.zhangsifan.com/posts/ES6-ECMAScript%206%20%E7%AE%80%E4%BB%8B/ +https://blog.zhangsifan.com/posts/ES6-Generator%20%E5%87%BD%E6%95%B0%E7%9A%84%E5%BC%82%E6%AD%A5%E5%BA%94%E7%94%A8/ +https://blog.zhangsifan.com/posts/ES6-Generator%20%E5%87%BD%E6%95%B0%E7%9A%84%E8%AF%AD%E6%B3%95/ +https://blog.zhangsifan.com/posts/ES6-Mixin/ +https://blog.zhangsifan.com/posts/ES6-Iterator%20%E5%92%8C%20for...of%20%E5%BE%AA%E7%8E%AF/ +https://blog.zhangsifan.com/posts/ES6-Module%20%E7%9A%84%E5%8A%A0%E8%BD%BD%E5%AE%9E%E7%8E%B0/ +https://blog.zhangsifan.com/posts/ES6-Module%20%E7%9A%84%E8%AF%AD%E6%B3%95/ +https://blog.zhangsifan.com/posts/ES6-Promise%20%E5%AF%B9%E8%B1%A1/ +https://blog.zhangsifan.com/posts/ES6-Proxy/ +https://blog.zhangsifan.com/posts/ES6-Reflect/ +https://blog.zhangsifan.com/posts/ES6-SIMD/ +https://blog.zhangsifan.com/posts/ES6-Set%20%E5%92%8C%20Map%20%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ +https://blog.zhangsifan.com/posts/ES6-Symbol/ +https://blog.zhangsifan.com/posts/ES6-async%20%E5%87%BD%E6%95%B0/ +https://blog.zhangsifan.com/posts/ES6-let%20%E5%92%8C%20const%20%E5%91%BD%E4%BB%A4/ +https://blog.zhangsifan.com/posts/ES6-%E4%BF%AE%E9%A5%B0%E5%99%A8/ +https://blog.zhangsifan.com/posts/ES6-%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/ +https://blog.zhangsifan.com/posts/ES6-%E5%87%BD%E6%95%B0%E7%9A%84%E6%89%A9%E5%B1%95/ +https://blog.zhangsifan.com/posts/ES6-%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC/ +https://blog.zhangsifan.com/posts/ES6-%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5/ +https://blog.zhangsifan.com/posts/ES6-%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%89%A9%E5%B1%95/ +https://blog.zhangsifan.com/posts/ES6-%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%89%A9%E5%B1%95/ +https://blog.zhangsifan.com/posts/ES6-%E6%95%B0%E5%80%BC%E7%9A%84%E6%89%A9%E5%B1%95/ +https://blog.zhangsifan.com/posts/ES6-%E6%95%B0%E7%BB%84%E7%9A%84%E6%89%A9%E5%B1%95/ +https://blog.zhangsifan.com/posts/ES6-%E6%9C%80%E6%96%B0%E6%8F%90%E6%A1%88/ +https://blog.zhangsifan.com/posts/ES6-%E6%AD%A3%E5%88%99%E7%9A%84%E6%89%A9%E5%B1%95/ +https://blog.zhangsifan.com/posts/ES6-%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC/ +https://blog.zhangsifan.com/posts/20220903nr/ +https://blog.zhangsifan.com/posts/20220903sg/ +https://blog.zhangsifan.com/posts/20220905ap/ +https://blog.zhangsifan.com/posts/20220909px/ +https://blog.zhangsifan.com/posts/20220915up/ +https://blog.zhangsifan.com/posts/20221010bg/ +https://blog.zhangsifan.com/posts/20221021ov/ +https://blog.zhangsifan.com/posts/20230329gt/ +https://blog.zhangsifan.com/posts/20230330gt/ +https://blog.zhangsifan.com/posts/20230505tr/ +https://blog.zhangsifan.com/posts/20230806pa/ +https://blog.zhangsifan.com/posts/20240417re/ +https://blog.zhangsifan.com/posts/20240924wg/ +https://blog.zhangsifan.com/posts/20241017dv/ +https://blog.zhangsifan.com/posts/DOC-ESLint/ +https://blog.zhangsifan.com/posts/DOC-24%E9%81%93JavaScript%E7%AE%97%E6%B3%95/ +https://blog.zhangsifan.com/posts/DOC-Hexo%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E6%B5%81%E7%A8%8B/ +https://blog.zhangsifan.com/posts/DOC-JavaScript%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/ +https://blog.zhangsifan.com/posts/DOC-Jquery%E7%9A%84%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/ +https://blog.zhangsifan.com/posts/DOC-Xmind%E7%AC%94%E8%AE%B0%E6%B1%87%E6%80%BB/ +https://blog.zhangsifan.com/posts/ES6-ArrayBuffer/ +https://blog.zhangsifan.com/posts/ES6-Class%20%E7%9A%84%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/ +https://blog.zhangsifan.com/posts/ES6-Class%20%E7%9A%84%E7%BB%A7%E6%89%BF/ +https://blog.zhangsifan.com/ +https://blog.zhangsifan.com/tags/Fingerprintjs/ +https://blog.zhangsifan.com/tags/%E9%AB%98%E5%BE%B7%E5%9C%B0%E5%9B%BE/ +https://blog.zhangsifan.com/tags/Element-Plus/ +https://blog.zhangsifan.com/tags/%E6%8B%96%E6%8B%BD/ +https://blog.zhangsifan.com/tags/%E6%96%87%E4%BB%B6%E5%A4%B9%E4%B8%8A%E4%BC%A0/ +https://blog.zhangsifan.com/tags/WPS-Office/ +https://blog.zhangsifan.com/tags/kkFileViews/ +https://blog.zhangsifan.com/tags/%E5%9C%A8%E7%BA%BF%E9%A2%84%E8%A7%88/ +https://blog.zhangsifan.com/tags/GeeTest/ +https://blog.zhangsifan.com/tags/Tauri/ +https://blog.zhangsifan.com/tags/%E9%80%9A%E8%A1%8C%E7%A7%98%E9%92%A5/ +https://blog.zhangsifan.com/tags/reCAPTCHA/ +https://blog.zhangsifan.com/tags/PageSpy/ +https://blog.zhangsifan.com/tags/VSCode/ +https://blog.zhangsifan.com/tags/JavaScript/ +https://blog.zhangsifan.com/tags/Hexo/ +https://blog.zhangsifan.com/tags/%E7%AC%94%E8%AE%B0/ +https://blog.zhangsifan.com/tags/ES6/ +https://blog.zhangsifan.com/tags/Node-js/ +https://blog.zhangsifan.com/tags/React-jjs/ +https://blog.zhangsifan.com/tags/TypeScript/ +https://blog.zhangsifan.com/tags/Vue-js/ +https://blog.zhangsifan.com/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/ +https://blog.zhangsifan.com/tags/%E5%AF%BC%E8%88%AA/ +https://blog.zhangsifan.com/categories/%E9%AA%8C%E8%AF%81%E7%B1%BB/ +https://blog.zhangsifan.com/categories/%E5%9C%B0%E5%9B%BE%E7%B1%BB/ +https://blog.zhangsifan.com/categories/%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3/ +https://blog.zhangsifan.com/categories/Web-Office/ +https://blog.zhangsifan.com/categories/%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3/Element-Plus/ +https://blog.zhangsifan.com/categories/%E8%BE%85%E5%8A%A9%E7%B1%BB/ +https://blog.zhangsifan.com/categories/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/ +https://blog.zhangsifan.com/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E6%A1%A3/ +https://blog.zhangsifan.com/categories/JavaScript/ +https://blog.zhangsifan.com/categories/JavaScript/ES6/ +https://blog.zhangsifan.com/categories/JavaScript/JQuery/ +https://blog.zhangsifan.com/categories/JavaScript/Node-js/ +https://blog.zhangsifan.com/categories/JavaScript/React-js/ +https://blog.zhangsifan.com/categories/TypeScript/ +https://blog.zhangsifan.com/categories/JavaScript/Vue-js/ +https://blog.zhangsifan.com/categories/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/ diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 00000000..bc7e2f90 --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,979 @@ + + + + + https://blog.zhangsifan.com/posts/ES6-%E8%AF%BB%E6%87%82%20ECMAScript%20%E8%A7%84%E6%A0%BC/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/JS-JQuery%E5%9F%BA%E7%A1%80/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/JS-JS%E5%9F%BA%E7%A1%80/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/JS-JS%E9%AB%98%E7%BA%A7/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/JS-Web%20Apis/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Node-Express%E6%A1%86%E6%9E%B6/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Node-MySQL%E4%BD%BF%E7%94%A8/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Node-Node%E4%B8%AD%E7%9A%84%E4%BC%9A%E8%AF%9D%E6%8A%80%E6%9C%AF/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Node-Node%E4%B8%AD%E7%9A%84%E8%B7%A8%E5%9F%9F/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Node-Node%E5%9F%BA%E7%A1%80/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Node-Promise/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/React-React%E5%9F%BA%E7%A1%80/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/TS-TS%E5%9F%BA%E7%A1%80%20/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Vue-MVVM%E5%8E%9F%E7%90%86/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Vue-Vue-Cli%E7%9A%84%E4%BD%BF%E7%94%A8/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Vue-Vuex/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Vue-vue%E7%9A%84%E9%92%A9%E5%AD%90%E5%87%BD%E6%95%B0/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Wechat-Uni-App%E5%BC%80%E5%8F%91%E9%A1%B9%E7%9B%AE/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Vue-Vue%E5%9F%BA%E7%A1%80/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/navs/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/Wechat-%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F%E5%9F%BA%E7%A1%80/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/manifest.json + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/about/index.html + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/categories/index.html + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/link/index.html + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/tags/index.html + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-ECMAScript%206%20%E7%AE%80%E4%BB%8B/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Generator%20%E5%87%BD%E6%95%B0%E7%9A%84%E5%BC%82%E6%AD%A5%E5%BA%94%E7%94%A8/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Generator%20%E5%87%BD%E6%95%B0%E7%9A%84%E8%AF%AD%E6%B3%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Mixin/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Iterator%20%E5%92%8C%20for...of%20%E5%BE%AA%E7%8E%AF/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Module%20%E7%9A%84%E5%8A%A0%E8%BD%BD%E5%AE%9E%E7%8E%B0/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Module%20%E7%9A%84%E8%AF%AD%E6%B3%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Promise%20%E5%AF%B9%E8%B1%A1/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Proxy/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Reflect/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-SIMD/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Set%20%E5%92%8C%20Map%20%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Symbol/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-async%20%E5%87%BD%E6%95%B0/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-let%20%E5%92%8C%20const%20%E5%91%BD%E4%BB%A4/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E4%BF%AE%E9%A5%B0%E5%99%A8/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E5%87%BD%E6%95%B0%E7%9A%84%E6%89%A9%E5%B1%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E5%8F%98%E9%87%8F%E7%9A%84%E8%A7%A3%E6%9E%84%E8%B5%8B%E5%80%BC/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E5%8F%82%E8%80%83%E9%93%BE%E6%8E%A5/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E5%AD%97%E7%AC%A6%E4%B8%B2%E7%9A%84%E6%89%A9%E5%B1%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E5%AF%B9%E8%B1%A1%E7%9A%84%E6%89%A9%E5%B1%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E6%95%B0%E5%80%BC%E7%9A%84%E6%89%A9%E5%B1%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E6%95%B0%E7%BB%84%E7%9A%84%E6%89%A9%E5%B1%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E6%9C%80%E6%96%B0%E6%8F%90%E6%A1%88/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E6%AD%A3%E5%88%99%E7%9A%84%E6%89%A9%E5%B1%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-%E7%BC%96%E7%A8%8B%E9%A3%8E%E6%A0%BC/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20220903nr/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20220903sg/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20220905ap/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20220909px/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20220915up/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20221010bg/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20221021ov/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20230329gt/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20230330gt/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20230505tr/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20230806pa/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20240417re/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20240924wg/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/20241017dv/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/DOC-ESLint/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/DOC-24%E9%81%93JavaScript%E7%AE%97%E6%B3%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/DOC-Hexo%E5%8D%9A%E5%AE%A2%E6%90%AD%E5%BB%BA%E6%B5%81%E7%A8%8B/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/DOC-JavaScript%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/DOC-Jquery%E7%9A%84%E5%B8%B8%E8%A7%81%E9%97%AE%E9%A2%98/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/DOC-Xmind%E7%AC%94%E8%AE%B0%E6%B1%87%E6%80%BB/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-ArrayBuffer/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Class%20%E7%9A%84%E5%9F%BA%E6%9C%AC%E8%AF%AD%E6%B3%95/ + + 2024-12-02 + + monthly + 0.6 + + + + https://blog.zhangsifan.com/posts/ES6-Class%20%E7%9A%84%E7%BB%A7%E6%89%BF/ + + 2024-12-02 + + monthly + 0.6 + + + + + https://blog.zhangsifan.com/ + 2024-12-02 + daily + 1.0 + + + + + https://blog.zhangsifan.com/tags/Fingerprintjs/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E9%AB%98%E5%BE%B7%E5%9C%B0%E5%9B%BE/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/Element-Plus/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E6%8B%96%E6%8B%BD/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E6%96%87%E4%BB%B6%E5%A4%B9%E4%B8%8A%E4%BC%A0/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/WPS-Office/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/kkFileViews/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E5%9C%A8%E7%BA%BF%E9%A2%84%E8%A7%88/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/GeeTest/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/Tauri/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E9%80%9A%E8%A1%8C%E7%A7%98%E9%92%A5/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/reCAPTCHA/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/PageSpy/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/VSCode/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/JavaScript/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/Hexo/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E7%AC%94%E8%AE%B0/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/ES6/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/Node-js/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/React-jjs/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/TypeScript/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/Vue-js/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/tags/%E5%AF%BC%E8%88%AA/ + 2024-12-02 + weekly + 0.2 + + + + + + https://blog.zhangsifan.com/categories/%E9%AA%8C%E8%AF%81%E7%B1%BB/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E5%9C%B0%E5%9B%BE%E7%B1%BB/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/Web-Office/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E4%BD%BF%E7%94%A8%E6%96%87%E6%A1%A3/Element-Plus/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E8%BE%85%E5%8A%A9%E7%B1%BB/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E6%8A%80%E6%9C%AF%E6%96%87%E6%A1%A3/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/JavaScript/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/JavaScript/ES6/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/JavaScript/JQuery/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/JavaScript/Node-js/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/JavaScript/React-js/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/TypeScript/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/JavaScript/Vue-js/ + 2024-12-02 + weekly + 0.2 + + + + https://blog.zhangsifan.com/categories/%E5%BE%AE%E4%BF%A1%E5%B0%8F%E7%A8%8B%E5%BA%8F/ + 2024-12-02 + weekly + 0.2 + + + diff --git a/songs/index.html b/songs/index.html new file mode 100644 index 00000000..059b0b8f --- /dev/null +++ b/songs/index.html @@ -0,0 +1,283 @@ +我听过的音乐 | JCAlways + + + + + + + + + +
    我听过的音乐
    + + + +
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/ES6/index.html b/tags/ES6/index.html new file mode 100644 index 00000000..3f702704 --- /dev/null +++ b/tags/ES6/index.html @@ -0,0 +1,279 @@ +标签: ES6 | JCAlways + + + + + + + + + + + +
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/ES6/page/2/index.html b/tags/ES6/page/2/index.html new file mode 100644 index 00000000..27d6ca21 --- /dev/null +++ b/tags/ES6/page/2/index.html @@ -0,0 +1,279 @@ +标签: ES6 | JCAlways + + + + + + + + + + + +
    标签 - ES6
    2019
    Promise 对象
    Promise 对象
    Proxy
    Proxy
    Reflect
    Reflect
    SIMD
    SIMD
    Set 和 Map 数据结构
    Set 和 Map 数据结构
    Symbol
    Symbol
    async 函数
    async 函数
    let 和 const 命令
    let 和 const 命令
    修饰器
    修饰器
    函数式编程
    函数式编程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/ES6/page/3/index.html b/tags/ES6/page/3/index.html new file mode 100644 index 00000000..25d9e1b4 --- /dev/null +++ b/tags/ES6/page/3/index.html @@ -0,0 +1,279 @@ +标签: ES6 | JCAlways + + + + + + + + + + + +
    标签 - ES6
    2019
    函数的扩展
    函数的扩展
    变量的解构赋值
    变量的解构赋值
    参考链接
    参考链接
    字符串的扩展
    字符串的扩展
    对象的扩展
    对象的扩展
    数值的扩展
    数值的扩展
    数组的扩展
    数组的扩展
    最新提案
    最新提案
    正则的扩展
    正则的扩展
    编程风格
    编程风格
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/ES6/page/4/index.html b/tags/ES6/page/4/index.html new file mode 100644 index 00000000..3380b25f --- /dev/null +++ b/tags/ES6/page/4/index.html @@ -0,0 +1,279 @@ +标签: ES6 | JCAlways + + + + + + + + + + + +
    标签 - ES6
    2019
    读懂 ECMAScript 规格
    读懂 ECMAScript 规格
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/Element-Plus/index.html b/tags/Element-Plus/index.html new file mode 100644 index 00000000..2512a032 --- /dev/null +++ b/tags/Element-Plus/index.html @@ -0,0 +1,279 @@ +标签: Element-Plus | JCAlways + + + + + + + + + + + +
    标签 - Element-Plus
    2022
    vue.draggable vue3-拖拽排序组件
    vue.draggable vue3-拖拽排序组件
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/Fingerprintjs/index.html b/tags/Fingerprintjs/index.html new file mode 100644 index 00000000..befe0575 --- /dev/null +++ b/tags/Fingerprintjs/index.html @@ -0,0 +1,279 @@ +标签: Fingerprintjs | JCAlways + + + + + + + + + + + +
    标签 - Fingerprintjs
    2022
    Fingerprintjs使用教程
    Fingerprintjs使用教程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/GeeTest/index.html b/tags/GeeTest/index.html new file mode 100644 index 00000000..f05c66b5 --- /dev/null +++ b/tags/GeeTest/index.html @@ -0,0 +1,279 @@ +标签: GeeTest | JCAlways + + + + + + + + + + + +
    标签 - GeeTest
    2023
    GeeTest3.0使用教程
    GeeTest3.0使用教程
    GeeTest4.0使用教程
    GeeTest4.0使用教程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/Hexo/index.html b/tags/Hexo/index.html new file mode 100644 index 00000000..5c8649e7 --- /dev/null +++ b/tags/Hexo/index.html @@ -0,0 +1,279 @@ +标签: Hexo | JCAlways + + + + + + + + + + + +
    标签 - Hexo
    2019
    Hexo博客搭建流程
    Hexo博客搭建流程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/JavaScript/index.html b/tags/JavaScript/index.html new file mode 100644 index 00000000..d5142499 --- /dev/null +++ b/tags/JavaScript/index.html @@ -0,0 +1,279 @@ +标签: JavaScript | JCAlways + + + + + + + + + + + +
    标签 - JavaScript
    2019
    Jquery的常见问题
    Jquery的常见问题
    24道JavaScript算法题
    24道JavaScript算法题
    JavaScript常见问题
    JavaScript常见问题
    JS高级
    JS高级
    JQuery基础
    JQuery基础
    Web Apis
    Web Apis
    JS基础
    JS基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/Node-js/index.html b/tags/Node-js/index.html new file mode 100644 index 00000000..c1a6935d --- /dev/null +++ b/tags/Node-js/index.html @@ -0,0 +1,279 @@ +标签: Node.js | JCAlways + + + + + + + + + + + +
    标签 - Node.js
    2019
    Promise
    Promise
    Node操作MySQL
    Node操作MySQL
    Node中的会话技术
    Node中的会话技术
    Node中的跨域
    Node中的跨域
    Express框架
    Express框架
    Node基础
    Node基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/PageSpy/index.html b/tags/PageSpy/index.html new file mode 100644 index 00000000..9365fb9c --- /dev/null +++ b/tags/PageSpy/index.html @@ -0,0 +1,279 @@ +标签: PageSpy | JCAlways + + + + + + + + + + + +
    标签 - PageSpy
    2024
    PageSpy使用教程
    PageSpy使用教程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/React-jjs/index.html b/tags/React-jjs/index.html new file mode 100644 index 00000000..b18296de --- /dev/null +++ b/tags/React-jjs/index.html @@ -0,0 +1,279 @@ +标签: React.jjs | JCAlways + + + + + + + + + + + +
    标签 - React.jjs
    2020
    React基础
    React基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/Tauri/index.html b/tags/Tauri/index.html new file mode 100644 index 00000000..e8f9f289 --- /dev/null +++ b/tags/Tauri/index.html @@ -0,0 +1,279 @@ +标签: Tauri | JCAlways + + + + + + + + + + + +
    标签 - Tauri
    2023
    Tauri使用教程
    Tauri使用教程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/TypeScript/index.html b/tags/TypeScript/index.html new file mode 100644 index 00000000..e214a468 --- /dev/null +++ b/tags/TypeScript/index.html @@ -0,0 +1,279 @@ +标签: TypeScript | JCAlways + + + + + + + + + + + +
    标签 - TypeScript
    2021
    TS基础
    TS基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/VSCode/index.html b/tags/VSCode/index.html new file mode 100644 index 00000000..1c8c2f99 --- /dev/null +++ b/tags/VSCode/index.html @@ -0,0 +1,279 @@ +标签: VSCode | JCAlways + + + + + + + + + + + +
    标签 - VSCode
    2019
    ESLint
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/Vue-js/index.html b/tags/Vue-js/index.html new file mode 100644 index 00000000..ab863559 --- /dev/null +++ b/tags/Vue-js/index.html @@ -0,0 +1,279 @@ +标签: Vue.js | JCAlways + + + + + + + + + + + +
    标签 - Vue.js
    2019
    VueX
    VueX
    Vue的钩子函数
    Vue的钩子函数
    Vue-Cli的使用
    Vue-Cli的使用
    Vue基础
    Vue基础
    MVVM原理
    MVVM原理
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/WPS-Office/index.html b/tags/WPS-Office/index.html new file mode 100644 index 00000000..d9b5dfbd --- /dev/null +++ b/tags/WPS-Office/index.html @@ -0,0 +1,279 @@ +标签: WPS Office | JCAlways + + + + + + + + + + + +
    标签 - WPS Office
    2022
    WPS WEB Office 前端使用教程
    WPS WEB Office 前端使用教程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/index.html b/tags/index.html new file mode 100644 index 00000000..0c970eeb --- /dev/null +++ b/tags/index.html @@ -0,0 +1,281 @@ +标签 | JCAlways + + + + + + + + + + + + + +
    \ No newline at end of file diff --git a/tags/kkFileViews/index.html b/tags/kkFileViews/index.html new file mode 100644 index 00000000..d25dde11 --- /dev/null +++ b/tags/kkFileViews/index.html @@ -0,0 +1,279 @@ +标签: kkFileViews | JCAlways + + + + + + + + + + + +
    标签 - kkFileViews
    2022
    利用 kkFileView 实现在线预览文件
    利用 kkFileView 实现在线预览文件
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/tags/reCAPTCHA/index.html b/tags/reCAPTCHA/index.html new file mode 100644 index 00000000..df8c5203 --- /dev/null +++ b/tags/reCAPTCHA/index.html @@ -0,0 +1,279 @@ +标签: reCAPTCHA | JCAlways + + + + + + + + + + + +
    标签 - reCAPTCHA
    2024
    Google reCAPTCHA使用教程
    Google reCAPTCHA使用教程
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\345\234\250\347\272\277\351\242\204\350\247\210/index.html" "b/tags/\345\234\250\347\272\277\351\242\204\350\247\210/index.html" new file mode 100644 index 00000000..4400da48 --- /dev/null +++ "b/tags/\345\234\250\347\272\277\351\242\204\350\247\210/index.html" @@ -0,0 +1,279 @@ +标签: 在线预览 | JCAlways + + + + + + + + + + + +
    标签 - 在线预览
    2022
    利用 kkFileView 实现在线预览文件
    利用 kkFileView 实现在线预览文件
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\345\257\274\350\210\252/index.html" "b/tags/\345\257\274\350\210\252/index.html" new file mode 100644 index 00000000..6a5f5374 --- /dev/null +++ "b/tags/\345\257\274\350\210\252/index.html" @@ -0,0 +1,279 @@ +标签: 导航 | JCAlways + + + + + + + + + + + +
    标签 - 导航
    2019
    文章导航
    文章导航
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" "b/tags/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" new file mode 100644 index 00000000..6f824b42 --- /dev/null +++ "b/tags/\345\276\256\344\277\241\345\260\217\347\250\213\345\272\217/index.html" @@ -0,0 +1,279 @@ +标签: 微信小程序 | JCAlways + + + + + + + + + + + +
    标签 - 微信小程序
    2019
    Uni-App开发项目
    Uni-App开发项目
    微信小程序基础
    微信小程序基础
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\346\213\226\346\213\275/index.html" "b/tags/\346\213\226\346\213\275/index.html" new file mode 100644 index 00000000..f3aa64e3 --- /dev/null +++ "b/tags/\346\213\226\346\213\275/index.html" @@ -0,0 +1,279 @@ +标签: 拖拽 | JCAlways + + + + + + + + + + + +
    标签 - 拖拽
    2022
    vue.draggable vue3-拖拽排序组件
    vue.draggable vue3-拖拽排序组件
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\346\226\207\344\273\266\345\244\271\344\270\212\344\274\240/index.html" "b/tags/\346\226\207\344\273\266\345\244\271\344\270\212\344\274\240/index.html" new file mode 100644 index 00000000..ead37560 --- /dev/null +++ "b/tags/\346\226\207\344\273\266\345\244\271\344\270\212\344\274\240/index.html" @@ -0,0 +1,279 @@ +标签: 文件夹上传 | JCAlways + + + + + + + + + + + +
    标签 - 文件夹上传
    2022
    Vue3 El-upload 开启上传文件夹功能
    Vue3 El-upload 开启上传文件夹功能
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\347\254\224\350\256\260/index.html" "b/tags/\347\254\224\350\256\260/index.html" new file mode 100644 index 00000000..3ae44de9 --- /dev/null +++ "b/tags/\347\254\224\350\256\260/index.html" @@ -0,0 +1,279 @@ +标签: 笔记 | JCAlways + + + + + + + + + + + +
    标签 - 笔记
    2019
    Xmind笔记汇总
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\351\200\232\350\241\214\347\247\230\351\222\245/index.html" "b/tags/\351\200\232\350\241\214\347\247\230\351\222\245/index.html" new file mode 100644 index 00000000..97c31ee9 --- /dev/null +++ "b/tags/\351\200\232\350\241\214\347\247\230\351\222\245/index.html" @@ -0,0 +1,279 @@ +标签: 通行秘钥 | JCAlways + + + + + + + + + + + +
    标签 - 通行秘钥
    2023
    通行密钥开发 Passkey
    通行密钥开发 Passkey
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git "a/tags/\351\253\230\345\276\267\345\234\260\345\233\276/index.html" "b/tags/\351\253\230\345\276\267\345\234\260\345\233\276/index.html" new file mode 100644 index 00000000..e5e7a9fb --- /dev/null +++ "b/tags/\351\253\230\345\276\267\345\234\260\345\233\276/index.html" @@ -0,0 +1,279 @@ +标签: 高德地图 | JCAlways + + + + + + + + + + + +
    标签 - 高德地图
    2022
    Vue3使用高德地图
    Vue3使用高德地图
    avatar
    JCAlways
    分享开发过程中遇到的问题,以及一些技术文章!
    Follow Me
    公告
    欢迎访问本站!遇到有用的文章记得分享哦!
    \ No newline at end of file diff --git a/workbox-1f84e78b.js b/workbox-1f84e78b.js new file mode 100644 index 00000000..e3dd801c --- /dev/null +++ b/workbox-1f84e78b.js @@ -0,0 +1,2 @@ +define(["exports"],(function(t){"use strict";try{self["workbox:core:7.2.0"]&&_()}catch(t){}const e=(t,...e)=>{let s=t;return e.length>0&&(s+=` :: ${JSON.stringify(e)}`),s};class s extends Error{constructor(t,s){super(e(t,s)),this.name=t,this.details=s}}try{self["workbox:routing:7.2.0"]&&_()}catch(t){}const n=t=>t&&"object"==typeof t?t:{handle:t};class i{constructor(t,e,s="GET"){this.handler=n(e),this.match=t,this.method=s}setCatchHandler(t){this.catchHandler=n(t)}}class r extends i{constructor(t,e,s){super((({url:e})=>{const s=t.exec(e.href);if(s&&(e.origin===location.origin||0===s.index))return s.slice(1)}),e,s)}}class o{constructor(){this.t=new Map,this.i=new Map}get routes(){return this.t}addFetchListener(){self.addEventListener("fetch",(t=>{const{request:e}=t,s=this.handleRequest({request:e,event:t});s&&t.respondWith(s)}))}addCacheListener(){self.addEventListener("message",(t=>{if(t.data&&"CACHE_URLS"===t.data.type){const{payload:e}=t.data,s=Promise.all(e.urlsToCache.map((e=>{"string"==typeof e&&(e=[e]);const s=new Request(...e);return this.handleRequest({request:s,event:t})})));t.waitUntil(s),t.ports&&t.ports[0]&&s.then((()=>t.ports[0].postMessage(!0)))}}))}handleRequest({request:t,event:e}){const s=new URL(t.url,location.href);if(!s.protocol.startsWith("http"))return;const n=s.origin===location.origin,{params:i,route:r}=this.findMatchingRoute({event:e,request:t,sameOrigin:n,url:s});let o=r&&r.handler;const c=t.method;if(!o&&this.i.has(c)&&(o=this.i.get(c)),!o)return;let a;try{a=o.handle({url:s,request:t,event:e,params:i})}catch(t){a=Promise.reject(t)}const h=r&&r.catchHandler;return a instanceof Promise&&(this.o||h)&&(a=a.catch((async n=>{if(h)try{return await h.handle({url:s,request:t,event:e,params:i})}catch(t){t instanceof Error&&(n=t)}if(this.o)return this.o.handle({url:s,request:t,event:e});throw n}))),a}findMatchingRoute({url:t,sameOrigin:e,request:s,event:n}){const i=this.t.get(s.method)||[];for(const r of i){let i;const o=r.match({url:t,sameOrigin:e,request:s,event:n});if(o)return i=o,(Array.isArray(i)&&0===i.length||o.constructor===Object&&0===Object.keys(o).length||"boolean"==typeof o)&&(i=void 0),{route:r,params:i}}return{}}setDefaultHandler(t,e="GET"){this.i.set(e,n(t))}setCatchHandler(t){this.o=n(t)}registerRoute(t){this.t.has(t.method)||this.t.set(t.method,[]),this.t.get(t.method).push(t)}unregisterRoute(t){if(!this.t.has(t.method))throw new s("unregister-route-but-not-found-with-method",{method:t.method});const e=this.t.get(t.method).indexOf(t);if(!(e>-1))throw new s("unregister-route-route-not-registered");this.t.get(t.method).splice(e,1)}}let c;const a={googleAnalytics:"googleAnalytics",precache:"precache-v2",prefix:"workbox",runtime:"runtime",suffix:"undefined"!=typeof registration?registration.scope:""},h=t=>[a.prefix,t,a.suffix].filter((t=>t&&t.length>0)).join("-"),u=t=>t||h(a.precache),l=t=>t||h(a.runtime);function f(t,e){const s=e();return t.waitUntil(s),s}try{self["workbox:precaching:7.2.0"]&&_()}catch(t){}function w(t){if(!t)throw new s("add-to-cache-list-unexpected-type",{entry:t});if("string"==typeof t){const e=new URL(t,location.href);return{cacheKey:e.href,url:e.href}}const{revision:e,url:n}=t;if(!n)throw new s("add-to-cache-list-unexpected-type",{entry:t});if(!e){const t=new URL(n,location.href);return{cacheKey:t.href,url:t.href}}const i=new URL(n,location.href),r=new URL(n,location.href);return i.searchParams.set("__WB_REVISION__",e),{cacheKey:i.href,url:r.href}}class d{constructor(){this.updatedURLs=[],this.notUpdatedURLs=[],this.handlerWillStart=async({request:t,state:e})=>{e&&(e.originalRequest=t)},this.cachedResponseWillBeUsed=async({event:t,state:e,cachedResponse:s})=>{if("install"===t.type&&e&&e.originalRequest&&e.originalRequest instanceof Request){const t=e.originalRequest.url;s?this.notUpdatedURLs.push(t):this.updatedURLs.push(t)}return s}}}class p{constructor({precacheController:t}){this.cacheKeyWillBeUsed=async({request:t,params:e})=>{const s=(null==e?void 0:e.cacheKey)||this.h.getCacheKeyForURL(t.url);return s?new Request(s,{headers:t.headers}):t},this.h=t}}let y;async function g(t,e){let n=null;if(t.url){n=new URL(t.url).origin}if(n!==self.location.origin)throw new s("cross-origin-copy-response",{origin:n});const i=t.clone(),r={headers:new Headers(i.headers),status:i.status,statusText:i.statusText},o=e?e(r):r,c=function(){if(void 0===y){const t=new Response("");if("body"in t)try{new Response(t.body),y=!0}catch(t){y=!1}y=!1}return y}()?i.body:await i.blob();return new Response(c,o)}function R(t,e){const s=new URL(t);for(const t of e)s.searchParams.delete(t);return s.href}class m{constructor(){this.promise=new Promise(((t,e)=>{this.resolve=t,this.reject=e}))}}const v=new Set;try{self["workbox:strategies:7.2.0"]&&_()}catch(t){}function q(t){return"string"==typeof t?new Request(t):t}class U{constructor(t,e){this.u={},Object.assign(this,e),this.event=e.event,this.l=t,this.p=new m,this.R=[],this.m=[...t.plugins],this.v=new Map;for(const t of this.m)this.v.set(t,{});this.event.waitUntil(this.p.promise)}async fetch(t){const{event:e}=this;let n=q(t);if("navigate"===n.mode&&e instanceof FetchEvent&&e.preloadResponse){const t=await e.preloadResponse;if(t)return t}const i=this.hasCallback("fetchDidFail")?n.clone():null;try{for(const t of this.iterateCallbacks("requestWillFetch"))n=await t({request:n.clone(),event:e})}catch(t){if(t instanceof Error)throw new s("plugin-error-request-will-fetch",{thrownErrorMessage:t.message})}const r=n.clone();try{let t;t=await fetch(n,"navigate"===n.mode?void 0:this.l.fetchOptions);for(const s of this.iterateCallbacks("fetchDidSucceed"))t=await s({event:e,request:r,response:t});return t}catch(t){throw i&&await this.runCallbacks("fetchDidFail",{error:t,event:e,originalRequest:i.clone(),request:r.clone()}),t}}async fetchAndCachePut(t){const e=await this.fetch(t),s=e.clone();return this.waitUntil(this.cachePut(t,s)),e}async cacheMatch(t){const e=q(t);let s;const{cacheName:n,matchOptions:i}=this.l,r=await this.getCacheKey(e,"read"),o=Object.assign(Object.assign({},i),{cacheName:n});s=await caches.match(r,o);for(const t of this.iterateCallbacks("cachedResponseWillBeUsed"))s=await t({cacheName:n,matchOptions:i,cachedResponse:s,request:r,event:this.event})||void 0;return s}async cachePut(t,e){const n=q(t);var i;await(i=0,new Promise((t=>setTimeout(t,i))));const r=await this.getCacheKey(n,"write");if(!e)throw new s("cache-put-with-no-response",{url:(o=r.url,new URL(String(o),location.href).href.replace(new RegExp(`^${location.origin}`),""))});var o;const c=await this.q(e);if(!c)return!1;const{cacheName:a,matchOptions:h}=this.l,u=await self.caches.open(a),l=this.hasCallback("cacheDidUpdate"),f=l?await async function(t,e,s,n){const i=R(e.url,s);if(e.url===i)return t.match(e,n);const r=Object.assign(Object.assign({},n),{ignoreSearch:!0}),o=await t.keys(e,r);for(const e of o)if(i===R(e.url,s))return t.match(e,n)}(u,r.clone(),["__WB_REVISION__"],h):null;try{await u.put(r,l?c.clone():c)}catch(t){if(t instanceof Error)throw"QuotaExceededError"===t.name&&await async function(){for(const t of v)await t()}(),t}for(const t of this.iterateCallbacks("cacheDidUpdate"))await t({cacheName:a,oldResponse:f,newResponse:c.clone(),request:r,event:this.event});return!0}async getCacheKey(t,e){const s=`${t.url} | ${e}`;if(!this.u[s]){let n=t;for(const t of this.iterateCallbacks("cacheKeyWillBeUsed"))n=q(await t({mode:e,request:n,event:this.event,params:this.params}));this.u[s]=n}return this.u[s]}hasCallback(t){for(const e of this.l.plugins)if(t in e)return!0;return!1}async runCallbacks(t,e){for(const s of this.iterateCallbacks(t))await s(e)}*iterateCallbacks(t){for(const e of this.l.plugins)if("function"==typeof e[t]){const s=this.v.get(e),n=n=>{const i=Object.assign(Object.assign({},n),{state:s});return e[t](i)};yield n}}waitUntil(t){return this.R.push(t),t}async doneWaiting(){let t;for(;t=this.R.shift();)await t}destroy(){this.p.resolve(null)}async q(t){let e=t,s=!1;for(const t of this.iterateCallbacks("cacheWillUpdate"))if(e=await t({request:this.request,response:e,event:this.event})||void 0,s=!0,!e)break;return s||e&&200!==e.status&&(e=void 0),e}}class L{constructor(t={}){this.cacheName=l(t.cacheName),this.plugins=t.plugins||[],this.fetchOptions=t.fetchOptions,this.matchOptions=t.matchOptions}handle(t){const[e]=this.handleAll(t);return e}handleAll(t){t instanceof FetchEvent&&(t={event:t,request:t.request});const e=t.event,s="string"==typeof t.request?new Request(t.request):t.request,n="params"in t?t.params:void 0,i=new U(this,{event:e,request:s,params:n}),r=this.U(i,s,e);return[r,this.L(r,i,s,e)]}async U(t,e,n){let i;await t.runCallbacks("handlerWillStart",{event:n,request:e});try{if(i=await this._(e,t),!i||"error"===i.type)throw new s("no-response",{url:e.url})}catch(s){if(s instanceof Error)for(const r of t.iterateCallbacks("handlerDidError"))if(i=await r({error:s,event:n,request:e}),i)break;if(!i)throw s}for(const s of t.iterateCallbacks("handlerWillRespond"))i=await s({event:n,request:e,response:i});return i}async L(t,e,s,n){let i,r;try{i=await t}catch(r){}try{await e.runCallbacks("handlerDidRespond",{event:n,request:s,response:i}),await e.doneWaiting()}catch(t){t instanceof Error&&(r=t)}if(await e.runCallbacks("handlerDidComplete",{event:n,request:s,response:i,error:r}),e.destroy(),r)throw r}}class b extends L{constructor(t={}){t.cacheName=u(t.cacheName),super(t),this.C=!1!==t.fallbackToNetwork,this.plugins.push(b.copyRedirectedCacheableResponsesPlugin)}async _(t,e){const s=await e.cacheMatch(t);return s||(e.event&&"install"===e.event.type?await this.O(t,e):await this.N(t,e))}async N(t,e){let n;const i=e.params||{};if(!this.C)throw new s("missing-precache-entry",{cacheName:this.cacheName,url:t.url});{const s=i.integrity,r=t.integrity,o=!r||r===s;n=await e.fetch(new Request(t,{integrity:"no-cors"!==t.mode?r||s:void 0})),s&&o&&"no-cors"!==t.mode&&(this.k(),await e.cachePut(t,n.clone()))}return n}async O(t,e){this.k();const n=await e.fetch(t);if(!await e.cachePut(t,n.clone()))throw new s("bad-precaching-response",{url:t.url,status:n.status});return n}k(){let t=null,e=0;for(const[s,n]of this.plugins.entries())n!==b.copyRedirectedCacheableResponsesPlugin&&(n===b.defaultPrecacheCacheabilityPlugin&&(t=s),n.cacheWillUpdate&&e++);0===e?this.plugins.push(b.defaultPrecacheCacheabilityPlugin):e>1&&null!==t&&this.plugins.splice(t,1)}}b.defaultPrecacheCacheabilityPlugin={cacheWillUpdate:async({response:t})=>!t||t.status>=400?null:t},b.copyRedirectedCacheableResponsesPlugin={cacheWillUpdate:async({response:t})=>t.redirected?await g(t):t};class C{constructor({cacheName:t,plugins:e=[],fallbackToNetwork:s=!0}={}){this.K=new Map,this.T=new Map,this.W=new Map,this.l=new b({cacheName:u(t),plugins:[...e,new p({precacheController:this})],fallbackToNetwork:s}),this.install=this.install.bind(this),this.activate=this.activate.bind(this)}get strategy(){return this.l}precache(t){this.addToCacheList(t),this.j||(self.addEventListener("install",this.install),self.addEventListener("activate",this.activate),this.j=!0)}addToCacheList(t){const e=[];for(const n of t){"string"==typeof n?e.push(n):n&&void 0===n.revision&&e.push(n.url);const{cacheKey:t,url:i}=w(n),r="string"!=typeof n&&n.revision?"reload":"default";if(this.K.has(i)&&this.K.get(i)!==t)throw new s("add-to-cache-list-conflicting-entries",{firstEntry:this.K.get(i),secondEntry:t});if("string"!=typeof n&&n.integrity){if(this.W.has(t)&&this.W.get(t)!==n.integrity)throw new s("add-to-cache-list-conflicting-integrities",{url:i});this.W.set(t,n.integrity)}if(this.K.set(i,t),this.T.set(i,r),e.length>0){const t=`Workbox is precaching URLs without revision info: ${e.join(", ")}\nThis is generally NOT safe. Learn more at https://bit.ly/wb-precache`;console.warn(t)}}}install(t){return f(t,(async()=>{const e=new d;this.strategy.plugins.push(e);for(const[e,s]of this.K){const n=this.W.get(s),i=this.T.get(e),r=new Request(e,{integrity:n,cache:i,credentials:"same-origin"});await Promise.all(this.strategy.handleAll({params:{cacheKey:s},request:r,event:t}))}const{updatedURLs:s,notUpdatedURLs:n}=e;return{updatedURLs:s,notUpdatedURLs:n}}))}activate(t){return f(t,(async()=>{const t=await self.caches.open(this.strategy.cacheName),e=await t.keys(),s=new Set(this.K.values()),n=[];for(const i of e)s.has(i.url)||(await t.delete(i),n.push(i.url));return{deletedURLs:n}}))}getURLsToCacheKeys(){return this.K}getCachedURLs(){return[...this.K.keys()]}getCacheKeyForURL(t){const e=new URL(t,location.href);return this.K.get(e.href)}getIntegrityForCacheKey(t){return this.W.get(t)}async matchPrecache(t){const e=t instanceof Request?t.url:t,s=this.getCacheKeyForURL(e);if(s){return(await self.caches.open(this.strategy.cacheName)).match(s)}}createHandlerBoundToURL(t){const e=this.getCacheKeyForURL(t);if(!e)throw new s("non-precached-url",{url:t});return s=>(s.request=new Request(t),s.params=Object.assign({cacheKey:e},s.params),this.strategy.handle(s))}}let E;const O=()=>(E||(E=new C),E);class x extends i{constructor(t,e){super((({request:s})=>{const n=t.getURLsToCacheKeys();for(const i of function*(t,{ignoreURLParametersMatching:e=[/^utm_/,/^fbclid$/],directoryIndex:s="index.html",cleanURLs:n=!0,urlManipulation:i}={}){const r=new URL(t,location.href);r.hash="",yield r.href;const o=function(t,e=[]){for(const s of[...t.searchParams.keys()])e.some((t=>t.test(s)))&&t.searchParams.delete(s);return t}(r,e);if(yield o.href,s&&o.pathname.endsWith("/")){const t=new URL(o.href);t.pathname+=s,yield t.href}if(n){const t=new URL(o.href);t.pathname+=".html",yield t.href}if(i){const t=i({url:r});for(const e of t)yield e.href}}(s.url,e)){const e=n.get(i);if(e){return{cacheKey:e,integrity:t.getIntegrityForCacheKey(e)}}}}),t.strategy)}}function N(t){const e=O();!function(t,e,n){let a;if("string"==typeof t){const s=new URL(t,location.href);a=new i((({url:t})=>t.href===s.href),e,n)}else if(t instanceof RegExp)a=new r(t,e,n);else if("function"==typeof t)a=new i(t,e,n);else{if(!(t instanceof i))throw new s("unsupported-route-type",{moduleName:"workbox-routing",funcName:"registerRoute",paramName:"capture"});a=t}(c||(c=new o,c.addFetchListener(),c.addCacheListener()),c).registerRoute(a)}(new x(e,t))}t.clientsClaim=function(){self.addEventListener("activate",(()=>self.clients.claim()))},t.precacheAndRoute=function(t,e){!function(t){O().precache(t)}(t),N(e)}})); +//# sourceMappingURL=workbox-1f84e78b.js.map diff --git a/workbox-1f84e78b.js.map b/workbox-1f84e78b.js.map new file mode 100644 index 00000000..7e551df5 --- /dev/null +++ b/workbox-1f84e78b.js.map @@ -0,0 +1 @@ +{"version":3,"file":"workbox-1f84e78b.js","sources":["node_modules/workbox-core/_version.js","node_modules/workbox-core/_private/logger.js","node_modules/workbox-core/models/messages/messageGenerator.js","node_modules/workbox-core/_private/WorkboxError.js","node_modules/workbox-routing/_version.js","node_modules/workbox-routing/utils/constants.js","node_modules/workbox-routing/utils/normalizeHandler.js","node_modules/workbox-routing/Route.js","node_modules/workbox-routing/RegExpRoute.js","node_modules/workbox-routing/Router.js","node_modules/workbox-routing/utils/getOrCreateDefaultRouter.js","node_modules/workbox-core/_private/cacheNames.js","node_modules/workbox-core/_private/waitUntil.js","node_modules/workbox-precaching/_version.js","node_modules/workbox-precaching/utils/createCacheKey.js","node_modules/workbox-precaching/utils/PrecacheInstallReportPlugin.js","node_modules/workbox-precaching/utils/PrecacheCacheKeyPlugin.js","node_modules/workbox-core/_private/canConstructResponseFromBodyStream.js","node_modules/workbox-core/copyResponse.js","node_modules/workbox-core/_private/cacheMatchIgnoreParams.js","node_modules/workbox-core/_private/Deferred.js","node_modules/workbox-core/models/quotaErrorCallbacks.js","node_modules/workbox-strategies/_version.js","node_modules/workbox-strategies/StrategyHandler.js","node_modules/workbox-core/_private/timeout.js","node_modules/workbox-core/_private/getFriendlyURL.js","node_modules/workbox-core/_private/executeQuotaErrorCallbacks.js","node_modules/workbox-strategies/Strategy.js","node_modules/workbox-precaching/PrecacheStrategy.js","node_modules/workbox-precaching/PrecacheController.js","node_modules/workbox-precaching/utils/getOrCreatePrecacheController.js","node_modules/workbox-precaching/PrecacheRoute.js","node_modules/workbox-precaching/utils/generateURLVariations.js","node_modules/workbox-precaching/utils/removeIgnoredSearchParams.js","node_modules/workbox-precaching/addRoute.js","node_modules/workbox-routing/registerRoute.js","node_modules/workbox-core/clientsClaim.js","node_modules/workbox-precaching/precacheAndRoute.js","node_modules/workbox-precaching/precache.js"],"sourcesContent":["\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:core:7.2.0'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2019 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nconst logger = (process.env.NODE_ENV === 'production'\n ? null\n : (() => {\n // Don't overwrite this value if it's already set.\n // See https://github.com/GoogleChrome/workbox/pull/2284#issuecomment-560470923\n if (!('__WB_DISABLE_DEV_LOGS' in globalThis)) {\n self.__WB_DISABLE_DEV_LOGS = false;\n }\n let inGroup = false;\n const methodToColorMap = {\n debug: `#7f8c8d`,\n log: `#2ecc71`,\n warn: `#f39c12`,\n error: `#c0392b`,\n groupCollapsed: `#3498db`,\n groupEnd: null, // No colored prefix on groupEnd\n };\n const print = function (method, args) {\n if (self.__WB_DISABLE_DEV_LOGS) {\n return;\n }\n if (method === 'groupCollapsed') {\n // Safari doesn't print all console.groupCollapsed() arguments:\n // https://bugs.webkit.org/show_bug.cgi?id=182754\n if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) {\n console[method](...args);\n return;\n }\n }\n const styles = [\n `background: ${methodToColorMap[method]}`,\n `border-radius: 0.5em`,\n `color: white`,\n `font-weight: bold`,\n `padding: 2px 0.5em`,\n ];\n // When in a group, the workbox prefix is not displayed.\n const logPrefix = inGroup ? [] : ['%cworkbox', styles.join(';')];\n console[method](...logPrefix, ...args);\n if (method === 'groupCollapsed') {\n inGroup = true;\n }\n if (method === 'groupEnd') {\n inGroup = false;\n }\n };\n // eslint-disable-next-line @typescript-eslint/ban-types\n const api = {};\n const loggerMethods = Object.keys(methodToColorMap);\n for (const key of loggerMethods) {\n const method = key;\n api[method] = (...args) => {\n print(method, args);\n };\n }\n return api;\n })());\nexport { logger };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { messages } from './messages.js';\nimport '../../_version.js';\nconst fallback = (code, ...args) => {\n let msg = code;\n if (args.length > 0) {\n msg += ` :: ${JSON.stringify(args)}`;\n }\n return msg;\n};\nconst generatorFunction = (code, details = {}) => {\n const message = messages[code];\n if (!message) {\n throw new Error(`Unable to find message for code '${code}'.`);\n }\n return message(details);\n};\nexport const messageGenerator = process.env.NODE_ENV === 'production' ? fallback : generatorFunction;\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { messageGenerator } from '../models/messages/messageGenerator.js';\nimport '../_version.js';\n/**\n * Workbox errors should be thrown with this class.\n * This allows use to ensure the type easily in tests,\n * helps developers identify errors from workbox\n * easily and allows use to optimise error\n * messages correctly.\n *\n * @private\n */\nclass WorkboxError extends Error {\n /**\n *\n * @param {string} errorCode The error code that\n * identifies this particular error.\n * @param {Object=} details Any relevant arguments\n * that will help developers identify issues should\n * be added as a key on the context object.\n */\n constructor(errorCode, details) {\n const message = messageGenerator(errorCode, details);\n super(message);\n this.name = errorCode;\n this.details = details;\n }\n}\nexport { WorkboxError };\n","\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:routing:7.2.0'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * The default HTTP method, 'GET', used when there's no specific method\n * configured for a route.\n *\n * @type {string}\n *\n * @private\n */\nexport const defaultMethod = 'GET';\n/**\n * The list of valid HTTP methods associated with requests that could be routed.\n *\n * @type {Array}\n *\n * @private\n */\nexport const validMethods = [\n 'DELETE',\n 'GET',\n 'HEAD',\n 'PATCH',\n 'POST',\n 'PUT',\n];\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport '../_version.js';\n/**\n * @param {function()|Object} handler Either a function, or an object with a\n * 'handle' method.\n * @return {Object} An object with a handle method.\n *\n * @private\n */\nexport const normalizeHandler = (handler) => {\n if (handler && typeof handler === 'object') {\n if (process.env.NODE_ENV !== 'production') {\n assert.hasMethod(handler, 'handle', {\n moduleName: 'workbox-routing',\n className: 'Route',\n funcName: 'constructor',\n paramName: 'handler',\n });\n }\n return handler;\n }\n else {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(handler, 'function', {\n moduleName: 'workbox-routing',\n className: 'Route',\n funcName: 'constructor',\n paramName: 'handler',\n });\n }\n return { handle: handler };\n }\n};\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { defaultMethod, validMethods } from './utils/constants.js';\nimport { normalizeHandler } from './utils/normalizeHandler.js';\nimport './_version.js';\n/**\n * A `Route` consists of a pair of callback functions, \"match\" and \"handler\".\n * The \"match\" callback determine if a route should be used to \"handle\" a\n * request by returning a non-falsy value if it can. The \"handler\" callback\n * is called when there is a match and should return a Promise that resolves\n * to a `Response`.\n *\n * @memberof workbox-routing\n */\nclass Route {\n /**\n * Constructor for Route class.\n *\n * @param {workbox-routing~matchCallback} match\n * A callback function that determines whether the route matches a given\n * `fetch` event by returning a non-falsy value.\n * @param {workbox-routing~handlerCallback} handler A callback\n * function that returns a Promise resolving to a Response.\n * @param {string} [method='GET'] The HTTP method to match the Route\n * against.\n */\n constructor(match, handler, method = defaultMethod) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(match, 'function', {\n moduleName: 'workbox-routing',\n className: 'Route',\n funcName: 'constructor',\n paramName: 'match',\n });\n if (method) {\n assert.isOneOf(method, validMethods, { paramName: 'method' });\n }\n }\n // These values are referenced directly by Router so cannot be\n // altered by minificaton.\n this.handler = normalizeHandler(handler);\n this.match = match;\n this.method = method;\n }\n /**\n *\n * @param {workbox-routing-handlerCallback} handler A callback\n * function that returns a Promise resolving to a Response\n */\n setCatchHandler(handler) {\n this.catchHandler = normalizeHandler(handler);\n }\n}\nexport { Route };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { Route } from './Route.js';\nimport './_version.js';\n/**\n * RegExpRoute makes it easy to create a regular expression based\n * {@link workbox-routing.Route}.\n *\n * For same-origin requests the RegExp only needs to match part of the URL. For\n * requests against third-party servers, you must define a RegExp that matches\n * the start of the URL.\n *\n * @memberof workbox-routing\n * @extends workbox-routing.Route\n */\nclass RegExpRoute extends Route {\n /**\n * If the regular expression contains\n * [capture groups]{@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp#grouping-back-references},\n * the captured values will be passed to the\n * {@link workbox-routing~handlerCallback} `params`\n * argument.\n *\n * @param {RegExp} regExp The regular expression to match against URLs.\n * @param {workbox-routing~handlerCallback} handler A callback\n * function that returns a Promise resulting in a Response.\n * @param {string} [method='GET'] The HTTP method to match the Route\n * against.\n */\n constructor(regExp, handler, method) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(regExp, RegExp, {\n moduleName: 'workbox-routing',\n className: 'RegExpRoute',\n funcName: 'constructor',\n paramName: 'pattern',\n });\n }\n const match = ({ url }) => {\n const result = regExp.exec(url.href);\n // Return immediately if there's no match.\n if (!result) {\n return;\n }\n // Require that the match start at the first character in the URL string\n // if it's a cross-origin request.\n // See https://github.com/GoogleChrome/workbox/issues/281 for the context\n // behind this behavior.\n if (url.origin !== location.origin && result.index !== 0) {\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`The regular expression '${regExp.toString()}' only partially matched ` +\n `against the cross-origin URL '${url.toString()}'. RegExpRoute's will only ` +\n `handle cross-origin requests if they match the entire URL.`);\n }\n return;\n }\n // If the route matches, but there aren't any capture groups defined, then\n // this will return [], which is truthy and therefore sufficient to\n // indicate a match.\n // If there are capture groups, then it will return their values.\n return result.slice(1);\n };\n super(match, handler, method);\n }\n}\nexport { RegExpRoute };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { defaultMethod } from './utils/constants.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { normalizeHandler } from './utils/normalizeHandler.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport './_version.js';\n/**\n * The Router can be used to process a `FetchEvent` using one or more\n * {@link workbox-routing.Route}, responding with a `Response` if\n * a matching route exists.\n *\n * If no route matches a given a request, the Router will use a \"default\"\n * handler if one is defined.\n *\n * Should the matching Route throw an error, the Router will use a \"catch\"\n * handler if one is defined to gracefully deal with issues and respond with a\n * Request.\n *\n * If a request matches multiple routes, the **earliest** registered route will\n * be used to respond to the request.\n *\n * @memberof workbox-routing\n */\nclass Router {\n /**\n * Initializes a new Router.\n */\n constructor() {\n this._routes = new Map();\n this._defaultHandlerMap = new Map();\n }\n /**\n * @return {Map>} routes A `Map` of HTTP\n * method name ('GET', etc.) to an array of all the corresponding `Route`\n * instances that are registered.\n */\n get routes() {\n return this._routes;\n }\n /**\n * Adds a fetch event listener to respond to events when a route matches\n * the event's request.\n */\n addFetchListener() {\n // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705\n self.addEventListener('fetch', ((event) => {\n const { request } = event;\n const responsePromise = this.handleRequest({ request, event });\n if (responsePromise) {\n event.respondWith(responsePromise);\n }\n }));\n }\n /**\n * Adds a message event listener for URLs to cache from the window.\n * This is useful to cache resources loaded on the page prior to when the\n * service worker started controlling it.\n *\n * The format of the message data sent from the window should be as follows.\n * Where the `urlsToCache` array may consist of URL strings or an array of\n * URL string + `requestInit` object (the same as you'd pass to `fetch()`).\n *\n * ```\n * {\n * type: 'CACHE_URLS',\n * payload: {\n * urlsToCache: [\n * './script1.js',\n * './script2.js',\n * ['./script3.js', {mode: 'no-cors'}],\n * ],\n * },\n * }\n * ```\n */\n addCacheListener() {\n // See https://github.com/Microsoft/TypeScript/issues/28357#issuecomment-436484705\n self.addEventListener('message', ((event) => {\n // event.data is type 'any'\n // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access\n if (event.data && event.data.type === 'CACHE_URLS') {\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const { payload } = event.data;\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`Caching URLs from the window`, payload.urlsToCache);\n }\n const requestPromises = Promise.all(payload.urlsToCache.map((entry) => {\n if (typeof entry === 'string') {\n entry = [entry];\n }\n const request = new Request(...entry);\n return this.handleRequest({ request, event });\n // TODO(philipwalton): TypeScript errors without this typecast for\n // some reason (probably a bug). The real type here should work but\n // doesn't: `Array | undefined>`.\n })); // TypeScript\n event.waitUntil(requestPromises);\n // If a MessageChannel was used, reply to the message on success.\n if (event.ports && event.ports[0]) {\n void requestPromises.then(() => event.ports[0].postMessage(true));\n }\n }\n }));\n }\n /**\n * Apply the routing rules to a FetchEvent object to get a Response from an\n * appropriate Route's handler.\n *\n * @param {Object} options\n * @param {Request} options.request The request to handle.\n * @param {ExtendableEvent} options.event The event that triggered the\n * request.\n * @return {Promise|undefined} A promise is returned if a\n * registered route can handle the request. If there is no matching\n * route and there's no `defaultHandler`, `undefined` is returned.\n */\n handleRequest({ request, event, }) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(request, Request, {\n moduleName: 'workbox-routing',\n className: 'Router',\n funcName: 'handleRequest',\n paramName: 'options.request',\n });\n }\n const url = new URL(request.url, location.href);\n if (!url.protocol.startsWith('http')) {\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`Workbox Router only supports URLs that start with 'http'.`);\n }\n return;\n }\n const sameOrigin = url.origin === location.origin;\n const { params, route } = this.findMatchingRoute({\n event,\n request,\n sameOrigin,\n url,\n });\n let handler = route && route.handler;\n const debugMessages = [];\n if (process.env.NODE_ENV !== 'production') {\n if (handler) {\n debugMessages.push([`Found a route to handle this request:`, route]);\n if (params) {\n debugMessages.push([\n `Passing the following params to the route's handler:`,\n params,\n ]);\n }\n }\n }\n // If we don't have a handler because there was no matching route, then\n // fall back to defaultHandler if that's defined.\n const method = request.method;\n if (!handler && this._defaultHandlerMap.has(method)) {\n if (process.env.NODE_ENV !== 'production') {\n debugMessages.push(`Failed to find a matching route. Falling ` +\n `back to the default handler for ${method}.`);\n }\n handler = this._defaultHandlerMap.get(method);\n }\n if (!handler) {\n if (process.env.NODE_ENV !== 'production') {\n // No handler so Workbox will do nothing. If logs is set of debug\n // i.e. verbose, we should print out this information.\n logger.debug(`No route found for: ${getFriendlyURL(url)}`);\n }\n return;\n }\n if (process.env.NODE_ENV !== 'production') {\n // We have a handler, meaning Workbox is going to handle the route.\n // print the routing details to the console.\n logger.groupCollapsed(`Router is responding to: ${getFriendlyURL(url)}`);\n debugMessages.forEach((msg) => {\n if (Array.isArray(msg)) {\n logger.log(...msg);\n }\n else {\n logger.log(msg);\n }\n });\n logger.groupEnd();\n }\n // Wrap in try and catch in case the handle method throws a synchronous\n // error. It should still callback to the catch handler.\n let responsePromise;\n try {\n responsePromise = handler.handle({ url, request, event, params });\n }\n catch (err) {\n responsePromise = Promise.reject(err);\n }\n // Get route's catch handler, if it exists\n const catchHandler = route && route.catchHandler;\n if (responsePromise instanceof Promise &&\n (this._catchHandler || catchHandler)) {\n responsePromise = responsePromise.catch(async (err) => {\n // If there's a route catch handler, process that first\n if (catchHandler) {\n if (process.env.NODE_ENV !== 'production') {\n // Still include URL here as it will be async from the console group\n // and may not make sense without the URL\n logger.groupCollapsed(`Error thrown when responding to: ` +\n ` ${getFriendlyURL(url)}. Falling back to route's Catch Handler.`);\n logger.error(`Error thrown by:`, route);\n logger.error(err);\n logger.groupEnd();\n }\n try {\n return await catchHandler.handle({ url, request, event, params });\n }\n catch (catchErr) {\n if (catchErr instanceof Error) {\n err = catchErr;\n }\n }\n }\n if (this._catchHandler) {\n if (process.env.NODE_ENV !== 'production') {\n // Still include URL here as it will be async from the console group\n // and may not make sense without the URL\n logger.groupCollapsed(`Error thrown when responding to: ` +\n ` ${getFriendlyURL(url)}. Falling back to global Catch Handler.`);\n logger.error(`Error thrown by:`, route);\n logger.error(err);\n logger.groupEnd();\n }\n return this._catchHandler.handle({ url, request, event });\n }\n throw err;\n });\n }\n return responsePromise;\n }\n /**\n * Checks a request and URL (and optionally an event) against the list of\n * registered routes, and if there's a match, returns the corresponding\n * route along with any params generated by the match.\n *\n * @param {Object} options\n * @param {URL} options.url\n * @param {boolean} options.sameOrigin The result of comparing `url.origin`\n * against the current origin.\n * @param {Request} options.request The request to match.\n * @param {Event} options.event The corresponding event.\n * @return {Object} An object with `route` and `params` properties.\n * They are populated if a matching route was found or `undefined`\n * otherwise.\n */\n findMatchingRoute({ url, sameOrigin, request, event, }) {\n const routes = this._routes.get(request.method) || [];\n for (const route of routes) {\n let params;\n // route.match returns type any, not possible to change right now.\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n const matchResult = route.match({ url, sameOrigin, request, event });\n if (matchResult) {\n if (process.env.NODE_ENV !== 'production') {\n // Warn developers that using an async matchCallback is almost always\n // not the right thing to do.\n if (matchResult instanceof Promise) {\n logger.warn(`While routing ${getFriendlyURL(url)}, an async ` +\n `matchCallback function was used. Please convert the ` +\n `following route to use a synchronous matchCallback function:`, route);\n }\n }\n // See https://github.com/GoogleChrome/workbox/issues/2079\n // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment\n params = matchResult;\n if (Array.isArray(params) && params.length === 0) {\n // Instead of passing an empty array in as params, use undefined.\n params = undefined;\n }\n else if (matchResult.constructor === Object && // eslint-disable-line\n Object.keys(matchResult).length === 0) {\n // Instead of passing an empty object in as params, use undefined.\n params = undefined;\n }\n else if (typeof matchResult === 'boolean') {\n // For the boolean value true (rather than just something truth-y),\n // don't set params.\n // See https://github.com/GoogleChrome/workbox/pull/2134#issuecomment-513924353\n params = undefined;\n }\n // Return early if have a match.\n return { route, params };\n }\n }\n // If no match was found above, return and empty object.\n return {};\n }\n /**\n * Define a default `handler` that's called when no routes explicitly\n * match the incoming request.\n *\n * Each HTTP method ('GET', 'POST', etc.) gets its own default handler.\n *\n * Without a default handler, unmatched requests will go against the\n * network as if there were no service worker present.\n *\n * @param {workbox-routing~handlerCallback} handler A callback\n * function that returns a Promise resulting in a Response.\n * @param {string} [method='GET'] The HTTP method to associate with this\n * default handler. Each method has its own default.\n */\n setDefaultHandler(handler, method = defaultMethod) {\n this._defaultHandlerMap.set(method, normalizeHandler(handler));\n }\n /**\n * If a Route throws an error while handling a request, this `handler`\n * will be called and given a chance to provide a response.\n *\n * @param {workbox-routing~handlerCallback} handler A callback\n * function that returns a Promise resulting in a Response.\n */\n setCatchHandler(handler) {\n this._catchHandler = normalizeHandler(handler);\n }\n /**\n * Registers a route with the router.\n *\n * @param {workbox-routing.Route} route The route to register.\n */\n registerRoute(route) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isType(route, 'object', {\n moduleName: 'workbox-routing',\n className: 'Router',\n funcName: 'registerRoute',\n paramName: 'route',\n });\n assert.hasMethod(route, 'match', {\n moduleName: 'workbox-routing',\n className: 'Router',\n funcName: 'registerRoute',\n paramName: 'route',\n });\n assert.isType(route.handler, 'object', {\n moduleName: 'workbox-routing',\n className: 'Router',\n funcName: 'registerRoute',\n paramName: 'route',\n });\n assert.hasMethod(route.handler, 'handle', {\n moduleName: 'workbox-routing',\n className: 'Router',\n funcName: 'registerRoute',\n paramName: 'route.handler',\n });\n assert.isType(route.method, 'string', {\n moduleName: 'workbox-routing',\n className: 'Router',\n funcName: 'registerRoute',\n paramName: 'route.method',\n });\n }\n if (!this._routes.has(route.method)) {\n this._routes.set(route.method, []);\n }\n // Give precedence to all of the earlier routes by adding this additional\n // route to the end of the array.\n this._routes.get(route.method).push(route);\n }\n /**\n * Unregisters a route with the router.\n *\n * @param {workbox-routing.Route} route The route to unregister.\n */\n unregisterRoute(route) {\n if (!this._routes.has(route.method)) {\n throw new WorkboxError('unregister-route-but-not-found-with-method', {\n method: route.method,\n });\n }\n const routeIndex = this._routes.get(route.method).indexOf(route);\n if (routeIndex > -1) {\n this._routes.get(route.method).splice(routeIndex, 1);\n }\n else {\n throw new WorkboxError('unregister-route-route-not-registered');\n }\n }\n}\nexport { Router };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { Router } from '../Router.js';\nimport '../_version.js';\nlet defaultRouter;\n/**\n * Creates a new, singleton Router instance if one does not exist. If one\n * does already exist, that instance is returned.\n *\n * @private\n * @return {Router}\n */\nexport const getOrCreateDefaultRouter = () => {\n if (!defaultRouter) {\n defaultRouter = new Router();\n // The helpers that use the default Router assume these listeners exist.\n defaultRouter.addFetchListener();\n defaultRouter.addCacheListener();\n }\n return defaultRouter;\n};\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nconst _cacheNameDetails = {\n googleAnalytics: 'googleAnalytics',\n precache: 'precache-v2',\n prefix: 'workbox',\n runtime: 'runtime',\n suffix: typeof registration !== 'undefined' ? registration.scope : '',\n};\nconst _createCacheName = (cacheName) => {\n return [_cacheNameDetails.prefix, cacheName, _cacheNameDetails.suffix]\n .filter((value) => value && value.length > 0)\n .join('-');\n};\nconst eachCacheNameDetail = (fn) => {\n for (const key of Object.keys(_cacheNameDetails)) {\n fn(key);\n }\n};\nexport const cacheNames = {\n updateDetails: (details) => {\n eachCacheNameDetail((key) => {\n if (typeof details[key] === 'string') {\n _cacheNameDetails[key] = details[key];\n }\n });\n },\n getGoogleAnalyticsName: (userCacheName) => {\n return userCacheName || _createCacheName(_cacheNameDetails.googleAnalytics);\n },\n getPrecacheName: (userCacheName) => {\n return userCacheName || _createCacheName(_cacheNameDetails.precache);\n },\n getPrefix: () => {\n return _cacheNameDetails.prefix;\n },\n getRuntimeName: (userCacheName) => {\n return userCacheName || _createCacheName(_cacheNameDetails.runtime);\n },\n getSuffix: () => {\n return _cacheNameDetails.suffix;\n },\n};\n","/*\n Copyright 2020 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * A utility method that makes it easier to use `event.waitUntil` with\n * async functions and return the result.\n *\n * @param {ExtendableEvent} event\n * @param {Function} asyncFn\n * @return {Function}\n * @private\n */\nfunction waitUntil(event, asyncFn) {\n const returnPromise = asyncFn();\n event.waitUntil(returnPromise);\n return returnPromise;\n}\nexport { waitUntil };\n","\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:precaching:7.2.0'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport '../_version.js';\n// Name of the search parameter used to store revision info.\nconst REVISION_SEARCH_PARAM = '__WB_REVISION__';\n/**\n * Converts a manifest entry into a versioned URL suitable for precaching.\n *\n * @param {Object|string} entry\n * @return {string} A URL with versioning info.\n *\n * @private\n * @memberof workbox-precaching\n */\nexport function createCacheKey(entry) {\n if (!entry) {\n throw new WorkboxError('add-to-cache-list-unexpected-type', { entry });\n }\n // If a precache manifest entry is a string, it's assumed to be a versioned\n // URL, like '/app.abcd1234.js'. Return as-is.\n if (typeof entry === 'string') {\n const urlObject = new URL(entry, location.href);\n return {\n cacheKey: urlObject.href,\n url: urlObject.href,\n };\n }\n const { revision, url } = entry;\n if (!url) {\n throw new WorkboxError('add-to-cache-list-unexpected-type', { entry });\n }\n // If there's just a URL and no revision, then it's also assumed to be a\n // versioned URL.\n if (!revision) {\n const urlObject = new URL(url, location.href);\n return {\n cacheKey: urlObject.href,\n url: urlObject.href,\n };\n }\n // Otherwise, construct a properly versioned URL using the custom Workbox\n // search parameter along with the revision info.\n const cacheKeyURL = new URL(url, location.href);\n const originalURL = new URL(url, location.href);\n cacheKeyURL.searchParams.set(REVISION_SEARCH_PARAM, revision);\n return {\n cacheKey: cacheKeyURL.href,\n url: originalURL.href,\n };\n}\n","/*\n Copyright 2020 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * A plugin, designed to be used with PrecacheController, to determine the\n * of assets that were updated (or not updated) during the install event.\n *\n * @private\n */\nclass PrecacheInstallReportPlugin {\n constructor() {\n this.updatedURLs = [];\n this.notUpdatedURLs = [];\n this.handlerWillStart = async ({ request, state, }) => {\n // TODO: `state` should never be undefined...\n if (state) {\n state.originalRequest = request;\n }\n };\n this.cachedResponseWillBeUsed = async ({ event, state, cachedResponse, }) => {\n if (event.type === 'install') {\n if (state &&\n state.originalRequest &&\n state.originalRequest instanceof Request) {\n // TODO: `state` should never be undefined...\n const url = state.originalRequest.url;\n if (cachedResponse) {\n this.notUpdatedURLs.push(url);\n }\n else {\n this.updatedURLs.push(url);\n }\n }\n }\n return cachedResponse;\n };\n }\n}\nexport { PrecacheInstallReportPlugin };\n","/*\n Copyright 2020 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * A plugin, designed to be used with PrecacheController, to translate URLs into\n * the corresponding cache key, based on the current revision info.\n *\n * @private\n */\nclass PrecacheCacheKeyPlugin {\n constructor({ precacheController }) {\n this.cacheKeyWillBeUsed = async ({ request, params, }) => {\n // Params is type any, can't change right now.\n /* eslint-disable */\n const cacheKey = (params === null || params === void 0 ? void 0 : params.cacheKey) ||\n this._precacheController.getCacheKeyForURL(request.url);\n /* eslint-enable */\n return cacheKey\n ? new Request(cacheKey, { headers: request.headers })\n : request;\n };\n this._precacheController = precacheController;\n }\n}\nexport { PrecacheCacheKeyPlugin };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nlet supportStatus;\n/**\n * A utility function that determines whether the current browser supports\n * constructing a new `Response` from a `response.body` stream.\n *\n * @return {boolean} `true`, if the current browser can successfully\n * construct a `Response` from a `response.body` stream, `false` otherwise.\n *\n * @private\n */\nfunction canConstructResponseFromBodyStream() {\n if (supportStatus === undefined) {\n const testResponse = new Response('');\n if ('body' in testResponse) {\n try {\n new Response(testResponse.body);\n supportStatus = true;\n }\n catch (error) {\n supportStatus = false;\n }\n }\n supportStatus = false;\n }\n return supportStatus;\n}\nexport { canConstructResponseFromBodyStream };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { canConstructResponseFromBodyStream } from './_private/canConstructResponseFromBodyStream.js';\nimport { WorkboxError } from './_private/WorkboxError.js';\nimport './_version.js';\n/**\n * Allows developers to copy a response and modify its `headers`, `status`,\n * or `statusText` values (the values settable via a\n * [`ResponseInit`]{@link https://developer.mozilla.org/en-US/docs/Web/API/Response/Response#Syntax}\n * object in the constructor).\n * To modify these values, pass a function as the second argument. That\n * function will be invoked with a single object with the response properties\n * `{headers, status, statusText}`. The return value of this function will\n * be used as the `ResponseInit` for the new `Response`. To change the values\n * either modify the passed parameter(s) and return it, or return a totally\n * new object.\n *\n * This method is intentionally limited to same-origin responses, regardless of\n * whether CORS was used or not.\n *\n * @param {Response} response\n * @param {Function} modifier\n * @memberof workbox-core\n */\nasync function copyResponse(response, modifier) {\n let origin = null;\n // If response.url isn't set, assume it's cross-origin and keep origin null.\n if (response.url) {\n const responseURL = new URL(response.url);\n origin = responseURL.origin;\n }\n if (origin !== self.location.origin) {\n throw new WorkboxError('cross-origin-copy-response', { origin });\n }\n const clonedResponse = response.clone();\n // Create a fresh `ResponseInit` object by cloning the headers.\n const responseInit = {\n headers: new Headers(clonedResponse.headers),\n status: clonedResponse.status,\n statusText: clonedResponse.statusText,\n };\n // Apply any user modifications.\n const modifiedResponseInit = modifier ? modifier(responseInit) : responseInit;\n // Create the new response from the body stream and `ResponseInit`\n // modifications. Note: not all browsers support the Response.body stream,\n // so fall back to reading the entire body into memory as a blob.\n const body = canConstructResponseFromBodyStream()\n ? clonedResponse.body\n : await clonedResponse.blob();\n return new Response(body, modifiedResponseInit);\n}\nexport { copyResponse };\n","/*\n Copyright 2020 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nfunction stripParams(fullURL, ignoreParams) {\n const strippedURL = new URL(fullURL);\n for (const param of ignoreParams) {\n strippedURL.searchParams.delete(param);\n }\n return strippedURL.href;\n}\n/**\n * Matches an item in the cache, ignoring specific URL params. This is similar\n * to the `ignoreSearch` option, but it allows you to ignore just specific\n * params (while continuing to match on the others).\n *\n * @private\n * @param {Cache} cache\n * @param {Request} request\n * @param {Object} matchOptions\n * @param {Array} ignoreParams\n * @return {Promise}\n */\nasync function cacheMatchIgnoreParams(cache, request, ignoreParams, matchOptions) {\n const strippedRequestURL = stripParams(request.url, ignoreParams);\n // If the request doesn't include any ignored params, match as normal.\n if (request.url === strippedRequestURL) {\n return cache.match(request, matchOptions);\n }\n // Otherwise, match by comparing keys\n const keysOptions = Object.assign(Object.assign({}, matchOptions), { ignoreSearch: true });\n const cacheKeys = await cache.keys(request, keysOptions);\n for (const cacheKey of cacheKeys) {\n const strippedCacheKeyURL = stripParams(cacheKey.url, ignoreParams);\n if (strippedRequestURL === strippedCacheKeyURL) {\n return cache.match(cacheKey, matchOptions);\n }\n }\n return;\n}\nexport { cacheMatchIgnoreParams };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * The Deferred class composes Promises in a way that allows for them to be\n * resolved or rejected from outside the constructor. In most cases promises\n * should be used directly, but Deferreds can be necessary when the logic to\n * resolve a promise must be separate.\n *\n * @private\n */\nclass Deferred {\n /**\n * Creates a promise and exposes its resolve and reject functions as methods.\n */\n constructor() {\n this.promise = new Promise((resolve, reject) => {\n this.resolve = resolve;\n this.reject = reject;\n });\n }\n}\nexport { Deferred };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n// Callbacks to be executed whenever there's a quota error.\n// Can't change Function type right now.\n// eslint-disable-next-line @typescript-eslint/ban-types\nconst quotaErrorCallbacks = new Set();\nexport { quotaErrorCallbacks };\n","\"use strict\";\n// @ts-ignore\ntry {\n self['workbox:strategies:7.2.0'] && _();\n}\ncatch (e) { }\n","/*\n Copyright 2020 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { cacheMatchIgnoreParams } from 'workbox-core/_private/cacheMatchIgnoreParams.js';\nimport { Deferred } from 'workbox-core/_private/Deferred.js';\nimport { executeQuotaErrorCallbacks } from 'workbox-core/_private/executeQuotaErrorCallbacks.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { timeout } from 'workbox-core/_private/timeout.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport './_version.js';\nfunction toRequest(input) {\n return typeof input === 'string' ? new Request(input) : input;\n}\n/**\n * A class created every time a Strategy instance instance calls\n * {@link workbox-strategies.Strategy~handle} or\n * {@link workbox-strategies.Strategy~handleAll} that wraps all fetch and\n * cache actions around plugin callbacks and keeps track of when the strategy\n * is \"done\" (i.e. all added `event.waitUntil()` promises have resolved).\n *\n * @memberof workbox-strategies\n */\nclass StrategyHandler {\n /**\n * Creates a new instance associated with the passed strategy and event\n * that's handling the request.\n *\n * The constructor also initializes the state that will be passed to each of\n * the plugins handling this request.\n *\n * @param {workbox-strategies.Strategy} strategy\n * @param {Object} options\n * @param {Request|string} options.request A request to run this strategy for.\n * @param {ExtendableEvent} options.event The event associated with the\n * request.\n * @param {URL} [options.url]\n * @param {*} [options.params] The return value from the\n * {@link workbox-routing~matchCallback} (if applicable).\n */\n constructor(strategy, options) {\n this._cacheKeys = {};\n /**\n * The request the strategy is performing (passed to the strategy's\n * `handle()` or `handleAll()` method).\n * @name request\n * @instance\n * @type {Request}\n * @memberof workbox-strategies.StrategyHandler\n */\n /**\n * The event associated with this request.\n * @name event\n * @instance\n * @type {ExtendableEvent}\n * @memberof workbox-strategies.StrategyHandler\n */\n /**\n * A `URL` instance of `request.url` (if passed to the strategy's\n * `handle()` or `handleAll()` method).\n * Note: the `url` param will be present if the strategy was invoked\n * from a workbox `Route` object.\n * @name url\n * @instance\n * @type {URL|undefined}\n * @memberof workbox-strategies.StrategyHandler\n */\n /**\n * A `param` value (if passed to the strategy's\n * `handle()` or `handleAll()` method).\n * Note: the `param` param will be present if the strategy was invoked\n * from a workbox `Route` object and the\n * {@link workbox-routing~matchCallback} returned\n * a truthy value (it will be that value).\n * @name params\n * @instance\n * @type {*|undefined}\n * @memberof workbox-strategies.StrategyHandler\n */\n if (process.env.NODE_ENV !== 'production') {\n assert.isInstance(options.event, ExtendableEvent, {\n moduleName: 'workbox-strategies',\n className: 'StrategyHandler',\n funcName: 'constructor',\n paramName: 'options.event',\n });\n }\n Object.assign(this, options);\n this.event = options.event;\n this._strategy = strategy;\n this._handlerDeferred = new Deferred();\n this._extendLifetimePromises = [];\n // Copy the plugins list (since it's mutable on the strategy),\n // so any mutations don't affect this handler instance.\n this._plugins = [...strategy.plugins];\n this._pluginStateMap = new Map();\n for (const plugin of this._plugins) {\n this._pluginStateMap.set(plugin, {});\n }\n this.event.waitUntil(this._handlerDeferred.promise);\n }\n /**\n * Fetches a given request (and invokes any applicable plugin callback\n * methods) using the `fetchOptions` (for non-navigation requests) and\n * `plugins` defined on the `Strategy` object.\n *\n * The following plugin lifecycle methods are invoked when using this method:\n * - `requestWillFetch()`\n * - `fetchDidSucceed()`\n * - `fetchDidFail()`\n *\n * @param {Request|string} input The URL or request to fetch.\n * @return {Promise}\n */\n async fetch(input) {\n const { event } = this;\n let request = toRequest(input);\n if (request.mode === 'navigate' &&\n event instanceof FetchEvent &&\n event.preloadResponse) {\n const possiblePreloadResponse = (await event.preloadResponse);\n if (possiblePreloadResponse) {\n if (process.env.NODE_ENV !== 'production') {\n logger.log(`Using a preloaded navigation response for ` +\n `'${getFriendlyURL(request.url)}'`);\n }\n return possiblePreloadResponse;\n }\n }\n // If there is a fetchDidFail plugin, we need to save a clone of the\n // original request before it's either modified by a requestWillFetch\n // plugin or before the original request's body is consumed via fetch().\n const originalRequest = this.hasCallback('fetchDidFail')\n ? request.clone()\n : null;\n try {\n for (const cb of this.iterateCallbacks('requestWillFetch')) {\n request = await cb({ request: request.clone(), event });\n }\n }\n catch (err) {\n if (err instanceof Error) {\n throw new WorkboxError('plugin-error-request-will-fetch', {\n thrownErrorMessage: err.message,\n });\n }\n }\n // The request can be altered by plugins with `requestWillFetch` making\n // the original request (most likely from a `fetch` event) different\n // from the Request we make. Pass both to `fetchDidFail` to aid debugging.\n const pluginFilteredRequest = request.clone();\n try {\n let fetchResponse;\n // See https://github.com/GoogleChrome/workbox/issues/1796\n fetchResponse = await fetch(request, request.mode === 'navigate' ? undefined : this._strategy.fetchOptions);\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`Network request for ` +\n `'${getFriendlyURL(request.url)}' returned a response with ` +\n `status '${fetchResponse.status}'.`);\n }\n for (const callback of this.iterateCallbacks('fetchDidSucceed')) {\n fetchResponse = await callback({\n event,\n request: pluginFilteredRequest,\n response: fetchResponse,\n });\n }\n return fetchResponse;\n }\n catch (error) {\n if (process.env.NODE_ENV !== 'production') {\n logger.log(`Network request for ` +\n `'${getFriendlyURL(request.url)}' threw an error.`, error);\n }\n // `originalRequest` will only exist if a `fetchDidFail` callback\n // is being used (see above).\n if (originalRequest) {\n await this.runCallbacks('fetchDidFail', {\n error: error,\n event,\n originalRequest: originalRequest.clone(),\n request: pluginFilteredRequest.clone(),\n });\n }\n throw error;\n }\n }\n /**\n * Calls `this.fetch()` and (in the background) runs `this.cachePut()` on\n * the response generated by `this.fetch()`.\n *\n * The call to `this.cachePut()` automatically invokes `this.waitUntil()`,\n * so you do not have to manually call `waitUntil()` on the event.\n *\n * @param {Request|string} input The request or URL to fetch and cache.\n * @return {Promise}\n */\n async fetchAndCachePut(input) {\n const response = await this.fetch(input);\n const responseClone = response.clone();\n void this.waitUntil(this.cachePut(input, responseClone));\n return response;\n }\n /**\n * Matches a request from the cache (and invokes any applicable plugin\n * callback methods) using the `cacheName`, `matchOptions`, and `plugins`\n * defined on the strategy object.\n *\n * The following plugin lifecycle methods are invoked when using this method:\n * - cacheKeyWillBeUsed()\n * - cachedResponseWillBeUsed()\n *\n * @param {Request|string} key The Request or URL to use as the cache key.\n * @return {Promise} A matching response, if found.\n */\n async cacheMatch(key) {\n const request = toRequest(key);\n let cachedResponse;\n const { cacheName, matchOptions } = this._strategy;\n const effectiveRequest = await this.getCacheKey(request, 'read');\n const multiMatchOptions = Object.assign(Object.assign({}, matchOptions), { cacheName });\n cachedResponse = await caches.match(effectiveRequest, multiMatchOptions);\n if (process.env.NODE_ENV !== 'production') {\n if (cachedResponse) {\n logger.debug(`Found a cached response in '${cacheName}'.`);\n }\n else {\n logger.debug(`No cached response found in '${cacheName}'.`);\n }\n }\n for (const callback of this.iterateCallbacks('cachedResponseWillBeUsed')) {\n cachedResponse =\n (await callback({\n cacheName,\n matchOptions,\n cachedResponse,\n request: effectiveRequest,\n event: this.event,\n })) || undefined;\n }\n return cachedResponse;\n }\n /**\n * Puts a request/response pair in the cache (and invokes any applicable\n * plugin callback methods) using the `cacheName` and `plugins` defined on\n * the strategy object.\n *\n * The following plugin lifecycle methods are invoked when using this method:\n * - cacheKeyWillBeUsed()\n * - cacheWillUpdate()\n * - cacheDidUpdate()\n *\n * @param {Request|string} key The request or URL to use as the cache key.\n * @param {Response} response The response to cache.\n * @return {Promise} `false` if a cacheWillUpdate caused the response\n * not be cached, and `true` otherwise.\n */\n async cachePut(key, response) {\n const request = toRequest(key);\n // Run in the next task to avoid blocking other cache reads.\n // https://github.com/w3c/ServiceWorker/issues/1397\n await timeout(0);\n const effectiveRequest = await this.getCacheKey(request, 'write');\n if (process.env.NODE_ENV !== 'production') {\n if (effectiveRequest.method && effectiveRequest.method !== 'GET') {\n throw new WorkboxError('attempt-to-cache-non-get-request', {\n url: getFriendlyURL(effectiveRequest.url),\n method: effectiveRequest.method,\n });\n }\n // See https://github.com/GoogleChrome/workbox/issues/2818\n const vary = response.headers.get('Vary');\n if (vary) {\n logger.debug(`The response for ${getFriendlyURL(effectiveRequest.url)} ` +\n `has a 'Vary: ${vary}' header. ` +\n `Consider setting the {ignoreVary: true} option on your strategy ` +\n `to ensure cache matching and deletion works as expected.`);\n }\n }\n if (!response) {\n if (process.env.NODE_ENV !== 'production') {\n logger.error(`Cannot cache non-existent response for ` +\n `'${getFriendlyURL(effectiveRequest.url)}'.`);\n }\n throw new WorkboxError('cache-put-with-no-response', {\n url: getFriendlyURL(effectiveRequest.url),\n });\n }\n const responseToCache = await this._ensureResponseSafeToCache(response);\n if (!responseToCache) {\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`Response '${getFriendlyURL(effectiveRequest.url)}' ` +\n `will not be cached.`, responseToCache);\n }\n return false;\n }\n const { cacheName, matchOptions } = this._strategy;\n const cache = await self.caches.open(cacheName);\n const hasCacheUpdateCallback = this.hasCallback('cacheDidUpdate');\n const oldResponse = hasCacheUpdateCallback\n ? await cacheMatchIgnoreParams(\n // TODO(philipwalton): the `__WB_REVISION__` param is a precaching\n // feature. Consider into ways to only add this behavior if using\n // precaching.\n cache, effectiveRequest.clone(), ['__WB_REVISION__'], matchOptions)\n : null;\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`Updating the '${cacheName}' cache with a new Response ` +\n `for ${getFriendlyURL(effectiveRequest.url)}.`);\n }\n try {\n await cache.put(effectiveRequest, hasCacheUpdateCallback ? responseToCache.clone() : responseToCache);\n }\n catch (error) {\n if (error instanceof Error) {\n // See https://developer.mozilla.org/en-US/docs/Web/API/DOMException#exception-QuotaExceededError\n if (error.name === 'QuotaExceededError') {\n await executeQuotaErrorCallbacks();\n }\n throw error;\n }\n }\n for (const callback of this.iterateCallbacks('cacheDidUpdate')) {\n await callback({\n cacheName,\n oldResponse,\n newResponse: responseToCache.clone(),\n request: effectiveRequest,\n event: this.event,\n });\n }\n return true;\n }\n /**\n * Checks the list of plugins for the `cacheKeyWillBeUsed` callback, and\n * executes any of those callbacks found in sequence. The final `Request`\n * object returned by the last plugin is treated as the cache key for cache\n * reads and/or writes. If no `cacheKeyWillBeUsed` plugin callbacks have\n * been registered, the passed request is returned unmodified\n *\n * @param {Request} request\n * @param {string} mode\n * @return {Promise}\n */\n async getCacheKey(request, mode) {\n const key = `${request.url} | ${mode}`;\n if (!this._cacheKeys[key]) {\n let effectiveRequest = request;\n for (const callback of this.iterateCallbacks('cacheKeyWillBeUsed')) {\n effectiveRequest = toRequest(await callback({\n mode,\n request: effectiveRequest,\n event: this.event,\n // params has a type any can't change right now.\n params: this.params, // eslint-disable-line\n }));\n }\n this._cacheKeys[key] = effectiveRequest;\n }\n return this._cacheKeys[key];\n }\n /**\n * Returns true if the strategy has at least one plugin with the given\n * callback.\n *\n * @param {string} name The name of the callback to check for.\n * @return {boolean}\n */\n hasCallback(name) {\n for (const plugin of this._strategy.plugins) {\n if (name in plugin) {\n return true;\n }\n }\n return false;\n }\n /**\n * Runs all plugin callbacks matching the given name, in order, passing the\n * given param object (merged ith the current plugin state) as the only\n * argument.\n *\n * Note: since this method runs all plugins, it's not suitable for cases\n * where the return value of a callback needs to be applied prior to calling\n * the next callback. See\n * {@link workbox-strategies.StrategyHandler#iterateCallbacks}\n * below for how to handle that case.\n *\n * @param {string} name The name of the callback to run within each plugin.\n * @param {Object} param The object to pass as the first (and only) param\n * when executing each callback. This object will be merged with the\n * current plugin state prior to callback execution.\n */\n async runCallbacks(name, param) {\n for (const callback of this.iterateCallbacks(name)) {\n // TODO(philipwalton): not sure why `any` is needed. It seems like\n // this should work with `as WorkboxPluginCallbackParam[C]`.\n await callback(param);\n }\n }\n /**\n * Accepts a callback and returns an iterable of matching plugin callbacks,\n * where each callback is wrapped with the current handler state (i.e. when\n * you call each callback, whatever object parameter you pass it will\n * be merged with the plugin's current state).\n *\n * @param {string} name The name fo the callback to run\n * @return {Array}\n */\n *iterateCallbacks(name) {\n for (const plugin of this._strategy.plugins) {\n if (typeof plugin[name] === 'function') {\n const state = this._pluginStateMap.get(plugin);\n const statefulCallback = (param) => {\n const statefulParam = Object.assign(Object.assign({}, param), { state });\n // TODO(philipwalton): not sure why `any` is needed. It seems like\n // this should work with `as WorkboxPluginCallbackParam[C]`.\n return plugin[name](statefulParam);\n };\n yield statefulCallback;\n }\n }\n }\n /**\n * Adds a promise to the\n * [extend lifetime promises]{@link https://w3c.github.io/ServiceWorker/#extendableevent-extend-lifetime-promises}\n * of the event event associated with the request being handled (usually a\n * `FetchEvent`).\n *\n * Note: you can await\n * {@link workbox-strategies.StrategyHandler~doneWaiting}\n * to know when all added promises have settled.\n *\n * @param {Promise} promise A promise to add to the extend lifetime promises\n * of the event that triggered the request.\n */\n waitUntil(promise) {\n this._extendLifetimePromises.push(promise);\n return promise;\n }\n /**\n * Returns a promise that resolves once all promises passed to\n * {@link workbox-strategies.StrategyHandler~waitUntil}\n * have settled.\n *\n * Note: any work done after `doneWaiting()` settles should be manually\n * passed to an event's `waitUntil()` method (not this handler's\n * `waitUntil()` method), otherwise the service worker thread my be killed\n * prior to your work completing.\n */\n async doneWaiting() {\n let promise;\n while ((promise = this._extendLifetimePromises.shift())) {\n await promise;\n }\n }\n /**\n * Stops running the strategy and immediately resolves any pending\n * `waitUntil()` promises.\n */\n destroy() {\n this._handlerDeferred.resolve(null);\n }\n /**\n * This method will call cacheWillUpdate on the available plugins (or use\n * status === 200) to determine if the Response is safe and valid to cache.\n *\n * @param {Request} options.request\n * @param {Response} options.response\n * @return {Promise}\n *\n * @private\n */\n async _ensureResponseSafeToCache(response) {\n let responseToCache = response;\n let pluginsUsed = false;\n for (const callback of this.iterateCallbacks('cacheWillUpdate')) {\n responseToCache =\n (await callback({\n request: this.request,\n response: responseToCache,\n event: this.event,\n })) || undefined;\n pluginsUsed = true;\n if (!responseToCache) {\n break;\n }\n }\n if (!pluginsUsed) {\n if (responseToCache && responseToCache.status !== 200) {\n responseToCache = undefined;\n }\n if (process.env.NODE_ENV !== 'production') {\n if (responseToCache) {\n if (responseToCache.status !== 200) {\n if (responseToCache.status === 0) {\n logger.warn(`The response for '${this.request.url}' ` +\n `is an opaque response. The caching strategy that you're ` +\n `using will not cache opaque responses by default.`);\n }\n else {\n logger.debug(`The response for '${this.request.url}' ` +\n `returned a status code of '${response.status}' and won't ` +\n `be cached as a result.`);\n }\n }\n }\n }\n }\n return responseToCache;\n }\n}\nexport { StrategyHandler };\n","/*\n Copyright 2019 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * Returns a promise that resolves and the passed number of milliseconds.\n * This utility is an async/await-friendly version of `setTimeout`.\n *\n * @param {number} ms\n * @return {Promise}\n * @private\n */\nexport function timeout(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\nconst getFriendlyURL = (url) => {\n const urlObj = new URL(String(url), location.href);\n // See https://github.com/GoogleChrome/workbox/issues/2323\n // We want to include everything, except for the origin if it's same-origin.\n return urlObj.href.replace(new RegExp(`^${location.origin}`), '');\n};\nexport { getFriendlyURL };\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { logger } from '../_private/logger.js';\nimport { quotaErrorCallbacks } from '../models/quotaErrorCallbacks.js';\nimport '../_version.js';\n/**\n * Runs all of the callback functions, one at a time sequentially, in the order\n * in which they were registered.\n *\n * @memberof workbox-core\n * @private\n */\nasync function executeQuotaErrorCallbacks() {\n if (process.env.NODE_ENV !== 'production') {\n logger.log(`About to run ${quotaErrorCallbacks.size} ` +\n `callbacks to clean up caches.`);\n }\n for (const callback of quotaErrorCallbacks) {\n await callback();\n if (process.env.NODE_ENV !== 'production') {\n logger.log(callback, 'is complete.');\n }\n }\n if (process.env.NODE_ENV !== 'production') {\n logger.log('Finished running callbacks.');\n }\n}\nexport { executeQuotaErrorCallbacks };\n","/*\n Copyright 2020 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { cacheNames } from 'workbox-core/_private/cacheNames.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { StrategyHandler } from './StrategyHandler.js';\nimport './_version.js';\n/**\n * An abstract base class that all other strategy classes must extend from:\n *\n * @memberof workbox-strategies\n */\nclass Strategy {\n /**\n * Creates a new instance of the strategy and sets all documented option\n * properties as public instance properties.\n *\n * Note: if a custom strategy class extends the base Strategy class and does\n * not need more than these properties, it does not need to define its own\n * constructor.\n *\n * @param {Object} [options]\n * @param {string} [options.cacheName] Cache name to store and retrieve\n * requests. Defaults to the cache names provided by\n * {@link workbox-core.cacheNames}.\n * @param {Array} [options.plugins] [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}\n * to use in conjunction with this caching strategy.\n * @param {Object} [options.fetchOptions] Values passed along to the\n * [`init`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters)\n * of [non-navigation](https://github.com/GoogleChrome/workbox/issues/1796)\n * `fetch()` requests made by this strategy.\n * @param {Object} [options.matchOptions] The\n * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}\n * for any `cache.match()` or `cache.put()` calls made by this strategy.\n */\n constructor(options = {}) {\n /**\n * Cache name to store and retrieve\n * requests. Defaults to the cache names provided by\n * {@link workbox-core.cacheNames}.\n *\n * @type {string}\n */\n this.cacheName = cacheNames.getRuntimeName(options.cacheName);\n /**\n * The list\n * [Plugins]{@link https://developers.google.com/web/tools/workbox/guides/using-plugins}\n * used by this strategy.\n *\n * @type {Array}\n */\n this.plugins = options.plugins || [];\n /**\n * Values passed along to the\n * [`init`]{@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters}\n * of all fetch() requests made by this strategy.\n *\n * @type {Object}\n */\n this.fetchOptions = options.fetchOptions;\n /**\n * The\n * [`CacheQueryOptions`]{@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions}\n * for any `cache.match()` or `cache.put()` calls made by this strategy.\n *\n * @type {Object}\n */\n this.matchOptions = options.matchOptions;\n }\n /**\n * Perform a request strategy and returns a `Promise` that will resolve with\n * a `Response`, invoking all relevant plugin callbacks.\n *\n * When a strategy instance is registered with a Workbox\n * {@link workbox-routing.Route}, this method is automatically\n * called when the route matches.\n *\n * Alternatively, this method can be used in a standalone `FetchEvent`\n * listener by passing it to `event.respondWith()`.\n *\n * @param {FetchEvent|Object} options A `FetchEvent` or an object with the\n * properties listed below.\n * @param {Request|string} options.request A request to run this strategy for.\n * @param {ExtendableEvent} options.event The event associated with the\n * request.\n * @param {URL} [options.url]\n * @param {*} [options.params]\n */\n handle(options) {\n const [responseDone] = this.handleAll(options);\n return responseDone;\n }\n /**\n * Similar to {@link workbox-strategies.Strategy~handle}, but\n * instead of just returning a `Promise` that resolves to a `Response` it\n * it will return an tuple of `[response, done]` promises, where the former\n * (`response`) is equivalent to what `handle()` returns, and the latter is a\n * Promise that will resolve once any promises that were added to\n * `event.waitUntil()` as part of performing the strategy have completed.\n *\n * You can await the `done` promise to ensure any extra work performed by\n * the strategy (usually caching responses) completes successfully.\n *\n * @param {FetchEvent|Object} options A `FetchEvent` or an object with the\n * properties listed below.\n * @param {Request|string} options.request A request to run this strategy for.\n * @param {ExtendableEvent} options.event The event associated with the\n * request.\n * @param {URL} [options.url]\n * @param {*} [options.params]\n * @return {Array} A tuple of [response, done]\n * promises that can be used to determine when the response resolves as\n * well as when the handler has completed all its work.\n */\n handleAll(options) {\n // Allow for flexible options to be passed.\n if (options instanceof FetchEvent) {\n options = {\n event: options,\n request: options.request,\n };\n }\n const event = options.event;\n const request = typeof options.request === 'string'\n ? new Request(options.request)\n : options.request;\n const params = 'params' in options ? options.params : undefined;\n const handler = new StrategyHandler(this, { event, request, params });\n const responseDone = this._getResponse(handler, request, event);\n const handlerDone = this._awaitComplete(responseDone, handler, request, event);\n // Return an array of promises, suitable for use with Promise.all().\n return [responseDone, handlerDone];\n }\n async _getResponse(handler, request, event) {\n await handler.runCallbacks('handlerWillStart', { event, request });\n let response = undefined;\n try {\n response = await this._handle(request, handler);\n // The \"official\" Strategy subclasses all throw this error automatically,\n // but in case a third-party Strategy doesn't, ensure that we have a\n // consistent failure when there's no response or an error response.\n if (!response || response.type === 'error') {\n throw new WorkboxError('no-response', { url: request.url });\n }\n }\n catch (error) {\n if (error instanceof Error) {\n for (const callback of handler.iterateCallbacks('handlerDidError')) {\n response = await callback({ error, event, request });\n if (response) {\n break;\n }\n }\n }\n if (!response) {\n throw error;\n }\n else if (process.env.NODE_ENV !== 'production') {\n logger.log(`While responding to '${getFriendlyURL(request.url)}', ` +\n `an ${error instanceof Error ? error.toString() : ''} error occurred. Using a fallback response provided by ` +\n `a handlerDidError plugin.`);\n }\n }\n for (const callback of handler.iterateCallbacks('handlerWillRespond')) {\n response = await callback({ event, request, response });\n }\n return response;\n }\n async _awaitComplete(responseDone, handler, request, event) {\n let response;\n let error;\n try {\n response = await responseDone;\n }\n catch (error) {\n // Ignore errors, as response errors should be caught via the `response`\n // promise above. The `done` promise will only throw for errors in\n // promises passed to `handler.waitUntil()`.\n }\n try {\n await handler.runCallbacks('handlerDidRespond', {\n event,\n request,\n response,\n });\n await handler.doneWaiting();\n }\n catch (waitUntilError) {\n if (waitUntilError instanceof Error) {\n error = waitUntilError;\n }\n }\n await handler.runCallbacks('handlerDidComplete', {\n event,\n request,\n response,\n error: error,\n });\n handler.destroy();\n if (error) {\n throw error;\n }\n }\n}\nexport { Strategy };\n/**\n * Classes extending the `Strategy` based class should implement this method,\n * and leverage the {@link workbox-strategies.StrategyHandler}\n * arg to perform all fetching and cache logic, which will ensure all relevant\n * cache, cache options, fetch options and plugins are used (per the current\n * strategy instance).\n *\n * @name _handle\n * @instance\n * @abstract\n * @function\n * @param {Request} request\n * @param {workbox-strategies.StrategyHandler} handler\n * @return {Promise}\n *\n * @memberof workbox-strategies.Strategy\n */\n","/*\n Copyright 2020 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { copyResponse } from 'workbox-core/copyResponse.js';\nimport { cacheNames } from 'workbox-core/_private/cacheNames.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { Strategy } from 'workbox-strategies/Strategy.js';\nimport './_version.js';\n/**\n * A {@link workbox-strategies.Strategy} implementation\n * specifically designed to work with\n * {@link workbox-precaching.PrecacheController}\n * to both cache and fetch precached assets.\n *\n * Note: an instance of this class is created automatically when creating a\n * `PrecacheController`; it's generally not necessary to create this yourself.\n *\n * @extends workbox-strategies.Strategy\n * @memberof workbox-precaching\n */\nclass PrecacheStrategy extends Strategy {\n /**\n *\n * @param {Object} [options]\n * @param {string} [options.cacheName] Cache name to store and retrieve\n * requests. Defaults to the cache names provided by\n * {@link workbox-core.cacheNames}.\n * @param {Array} [options.plugins] {@link https://developers.google.com/web/tools/workbox/guides/using-plugins|Plugins}\n * to use in conjunction with this caching strategy.\n * @param {Object} [options.fetchOptions] Values passed along to the\n * {@link https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch#Parameters|init}\n * of all fetch() requests made by this strategy.\n * @param {Object} [options.matchOptions] The\n * {@link https://w3c.github.io/ServiceWorker/#dictdef-cachequeryoptions|CacheQueryOptions}\n * for any `cache.match()` or `cache.put()` calls made by this strategy.\n * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to\n * get the response from the network if there's a precache miss.\n */\n constructor(options = {}) {\n options.cacheName = cacheNames.getPrecacheName(options.cacheName);\n super(options);\n this._fallbackToNetwork =\n options.fallbackToNetwork === false ? false : true;\n // Redirected responses cannot be used to satisfy a navigation request, so\n // any redirected response must be \"copied\" rather than cloned, so the new\n // response doesn't contain the `redirected` flag. See:\n // https://bugs.chromium.org/p/chromium/issues/detail?id=669363&desc=2#c1\n this.plugins.push(PrecacheStrategy.copyRedirectedCacheableResponsesPlugin);\n }\n /**\n * @private\n * @param {Request|string} request A request to run this strategy for.\n * @param {workbox-strategies.StrategyHandler} handler The event that\n * triggered the request.\n * @return {Promise}\n */\n async _handle(request, handler) {\n const response = await handler.cacheMatch(request);\n if (response) {\n return response;\n }\n // If this is an `install` event for an entry that isn't already cached,\n // then populate the cache.\n if (handler.event && handler.event.type === 'install') {\n return await this._handleInstall(request, handler);\n }\n // Getting here means something went wrong. An entry that should have been\n // precached wasn't found in the cache.\n return await this._handleFetch(request, handler);\n }\n async _handleFetch(request, handler) {\n let response;\n const params = (handler.params || {});\n // Fall back to the network if we're configured to do so.\n if (this._fallbackToNetwork) {\n if (process.env.NODE_ENV !== 'production') {\n logger.warn(`The precached response for ` +\n `${getFriendlyURL(request.url)} in ${this.cacheName} was not ` +\n `found. Falling back to the network.`);\n }\n const integrityInManifest = params.integrity;\n const integrityInRequest = request.integrity;\n const noIntegrityConflict = !integrityInRequest || integrityInRequest === integrityInManifest;\n // Do not add integrity if the original request is no-cors\n // See https://github.com/GoogleChrome/workbox/issues/3096\n response = await handler.fetch(new Request(request, {\n integrity: request.mode !== 'no-cors'\n ? integrityInRequest || integrityInManifest\n : undefined,\n }));\n // It's only \"safe\" to repair the cache if we're using SRI to guarantee\n // that the response matches the precache manifest's expectations,\n // and there's either a) no integrity property in the incoming request\n // or b) there is an integrity, and it matches the precache manifest.\n // See https://github.com/GoogleChrome/workbox/issues/2858\n // Also if the original request users no-cors we don't use integrity.\n // See https://github.com/GoogleChrome/workbox/issues/3096\n if (integrityInManifest &&\n noIntegrityConflict &&\n request.mode !== 'no-cors') {\n this._useDefaultCacheabilityPluginIfNeeded();\n const wasCached = await handler.cachePut(request, response.clone());\n if (process.env.NODE_ENV !== 'production') {\n if (wasCached) {\n logger.log(`A response for ${getFriendlyURL(request.url)} ` +\n `was used to \"repair\" the precache.`);\n }\n }\n }\n }\n else {\n // This shouldn't normally happen, but there are edge cases:\n // https://github.com/GoogleChrome/workbox/issues/1441\n throw new WorkboxError('missing-precache-entry', {\n cacheName: this.cacheName,\n url: request.url,\n });\n }\n if (process.env.NODE_ENV !== 'production') {\n const cacheKey = params.cacheKey || (await handler.getCacheKey(request, 'read'));\n // Workbox is going to handle the route.\n // print the routing details to the console.\n logger.groupCollapsed(`Precaching is responding to: ` + getFriendlyURL(request.url));\n logger.log(`Serving the precached url: ${getFriendlyURL(cacheKey instanceof Request ? cacheKey.url : cacheKey)}`);\n logger.groupCollapsed(`View request details here.`);\n logger.log(request);\n logger.groupEnd();\n logger.groupCollapsed(`View response details here.`);\n logger.log(response);\n logger.groupEnd();\n logger.groupEnd();\n }\n return response;\n }\n async _handleInstall(request, handler) {\n this._useDefaultCacheabilityPluginIfNeeded();\n const response = await handler.fetch(request);\n // Make sure we defer cachePut() until after we know the response\n // should be cached; see https://github.com/GoogleChrome/workbox/issues/2737\n const wasCached = await handler.cachePut(request, response.clone());\n if (!wasCached) {\n // Throwing here will lead to the `install` handler failing, which\n // we want to do if *any* of the responses aren't safe to cache.\n throw new WorkboxError('bad-precaching-response', {\n url: request.url,\n status: response.status,\n });\n }\n return response;\n }\n /**\n * This method is complex, as there a number of things to account for:\n *\n * The `plugins` array can be set at construction, and/or it might be added to\n * to at any time before the strategy is used.\n *\n * At the time the strategy is used (i.e. during an `install` event), there\n * needs to be at least one plugin that implements `cacheWillUpdate` in the\n * array, other than `copyRedirectedCacheableResponsesPlugin`.\n *\n * - If this method is called and there are no suitable `cacheWillUpdate`\n * plugins, we need to add `defaultPrecacheCacheabilityPlugin`.\n *\n * - If this method is called and there is exactly one `cacheWillUpdate`, then\n * we don't have to do anything (this might be a previously added\n * `defaultPrecacheCacheabilityPlugin`, or it might be a custom plugin).\n *\n * - If this method is called and there is more than one `cacheWillUpdate`,\n * then we need to check if one is `defaultPrecacheCacheabilityPlugin`. If so,\n * we need to remove it. (This situation is unlikely, but it could happen if\n * the strategy is used multiple times, the first without a `cacheWillUpdate`,\n * and then later on after manually adding a custom `cacheWillUpdate`.)\n *\n * See https://github.com/GoogleChrome/workbox/issues/2737 for more context.\n *\n * @private\n */\n _useDefaultCacheabilityPluginIfNeeded() {\n let defaultPluginIndex = null;\n let cacheWillUpdatePluginCount = 0;\n for (const [index, plugin] of this.plugins.entries()) {\n // Ignore the copy redirected plugin when determining what to do.\n if (plugin === PrecacheStrategy.copyRedirectedCacheableResponsesPlugin) {\n continue;\n }\n // Save the default plugin's index, in case it needs to be removed.\n if (plugin === PrecacheStrategy.defaultPrecacheCacheabilityPlugin) {\n defaultPluginIndex = index;\n }\n if (plugin.cacheWillUpdate) {\n cacheWillUpdatePluginCount++;\n }\n }\n if (cacheWillUpdatePluginCount === 0) {\n this.plugins.push(PrecacheStrategy.defaultPrecacheCacheabilityPlugin);\n }\n else if (cacheWillUpdatePluginCount > 1 && defaultPluginIndex !== null) {\n // Only remove the default plugin; multiple custom plugins are allowed.\n this.plugins.splice(defaultPluginIndex, 1);\n }\n // Nothing needs to be done if cacheWillUpdatePluginCount is 1\n }\n}\nPrecacheStrategy.defaultPrecacheCacheabilityPlugin = {\n async cacheWillUpdate({ response }) {\n if (!response || response.status >= 400) {\n return null;\n }\n return response;\n },\n};\nPrecacheStrategy.copyRedirectedCacheableResponsesPlugin = {\n async cacheWillUpdate({ response }) {\n return response.redirected ? await copyResponse(response) : response;\n },\n};\nexport { PrecacheStrategy };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { assert } from 'workbox-core/_private/assert.js';\nimport { cacheNames } from 'workbox-core/_private/cacheNames.js';\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { waitUntil } from 'workbox-core/_private/waitUntil.js';\nimport { createCacheKey } from './utils/createCacheKey.js';\nimport { PrecacheInstallReportPlugin } from './utils/PrecacheInstallReportPlugin.js';\nimport { PrecacheCacheKeyPlugin } from './utils/PrecacheCacheKeyPlugin.js';\nimport { printCleanupDetails } from './utils/printCleanupDetails.js';\nimport { printInstallDetails } from './utils/printInstallDetails.js';\nimport { PrecacheStrategy } from './PrecacheStrategy.js';\nimport './_version.js';\n/**\n * Performs efficient precaching of assets.\n *\n * @memberof workbox-precaching\n */\nclass PrecacheController {\n /**\n * Create a new PrecacheController.\n *\n * @param {Object} [options]\n * @param {string} [options.cacheName] The cache to use for precaching.\n * @param {string} [options.plugins] Plugins to use when precaching as well\n * as responding to fetch events for precached assets.\n * @param {boolean} [options.fallbackToNetwork=true] Whether to attempt to\n * get the response from the network if there's a precache miss.\n */\n constructor({ cacheName, plugins = [], fallbackToNetwork = true, } = {}) {\n this._urlsToCacheKeys = new Map();\n this._urlsToCacheModes = new Map();\n this._cacheKeysToIntegrities = new Map();\n this._strategy = new PrecacheStrategy({\n cacheName: cacheNames.getPrecacheName(cacheName),\n plugins: [\n ...plugins,\n new PrecacheCacheKeyPlugin({ precacheController: this }),\n ],\n fallbackToNetwork,\n });\n // Bind the install and activate methods to the instance.\n this.install = this.install.bind(this);\n this.activate = this.activate.bind(this);\n }\n /**\n * @type {workbox-precaching.PrecacheStrategy} The strategy created by this controller and\n * used to cache assets and respond to fetch events.\n */\n get strategy() {\n return this._strategy;\n }\n /**\n * Adds items to the precache list, removing any duplicates and\n * stores the files in the\n * {@link workbox-core.cacheNames|\"precache cache\"} when the service\n * worker installs.\n *\n * This method can be called multiple times.\n *\n * @param {Array} [entries=[]] Array of entries to precache.\n */\n precache(entries) {\n this.addToCacheList(entries);\n if (!this._installAndActiveListenersAdded) {\n self.addEventListener('install', this.install);\n self.addEventListener('activate', this.activate);\n this._installAndActiveListenersAdded = true;\n }\n }\n /**\n * This method will add items to the precache list, removing duplicates\n * and ensuring the information is valid.\n *\n * @param {Array} entries\n * Array of entries to precache.\n */\n addToCacheList(entries) {\n if (process.env.NODE_ENV !== 'production') {\n assert.isArray(entries, {\n moduleName: 'workbox-precaching',\n className: 'PrecacheController',\n funcName: 'addToCacheList',\n paramName: 'entries',\n });\n }\n const urlsToWarnAbout = [];\n for (const entry of entries) {\n // See https://github.com/GoogleChrome/workbox/issues/2259\n if (typeof entry === 'string') {\n urlsToWarnAbout.push(entry);\n }\n else if (entry && entry.revision === undefined) {\n urlsToWarnAbout.push(entry.url);\n }\n const { cacheKey, url } = createCacheKey(entry);\n const cacheMode = typeof entry !== 'string' && entry.revision ? 'reload' : 'default';\n if (this._urlsToCacheKeys.has(url) &&\n this._urlsToCacheKeys.get(url) !== cacheKey) {\n throw new WorkboxError('add-to-cache-list-conflicting-entries', {\n firstEntry: this._urlsToCacheKeys.get(url),\n secondEntry: cacheKey,\n });\n }\n if (typeof entry !== 'string' && entry.integrity) {\n if (this._cacheKeysToIntegrities.has(cacheKey) &&\n this._cacheKeysToIntegrities.get(cacheKey) !== entry.integrity) {\n throw new WorkboxError('add-to-cache-list-conflicting-integrities', {\n url,\n });\n }\n this._cacheKeysToIntegrities.set(cacheKey, entry.integrity);\n }\n this._urlsToCacheKeys.set(url, cacheKey);\n this._urlsToCacheModes.set(url, cacheMode);\n if (urlsToWarnAbout.length > 0) {\n const warningMessage = `Workbox is precaching URLs without revision ` +\n `info: ${urlsToWarnAbout.join(', ')}\\nThis is generally NOT safe. ` +\n `Learn more at https://bit.ly/wb-precache`;\n if (process.env.NODE_ENV === 'production') {\n // Use console directly to display this warning without bloating\n // bundle sizes by pulling in all of the logger codebase in prod.\n console.warn(warningMessage);\n }\n else {\n logger.warn(warningMessage);\n }\n }\n }\n }\n /**\n * Precaches new and updated assets. Call this method from the service worker\n * install event.\n *\n * Note: this method calls `event.waitUntil()` for you, so you do not need\n * to call it yourself in your event handlers.\n *\n * @param {ExtendableEvent} event\n * @return {Promise}\n */\n install(event) {\n // waitUntil returns Promise\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return waitUntil(event, async () => {\n const installReportPlugin = new PrecacheInstallReportPlugin();\n this.strategy.plugins.push(installReportPlugin);\n // Cache entries one at a time.\n // See https://github.com/GoogleChrome/workbox/issues/2528\n for (const [url, cacheKey] of this._urlsToCacheKeys) {\n const integrity = this._cacheKeysToIntegrities.get(cacheKey);\n const cacheMode = this._urlsToCacheModes.get(url);\n const request = new Request(url, {\n integrity,\n cache: cacheMode,\n credentials: 'same-origin',\n });\n await Promise.all(this.strategy.handleAll({\n params: { cacheKey },\n request,\n event,\n }));\n }\n const { updatedURLs, notUpdatedURLs } = installReportPlugin;\n if (process.env.NODE_ENV !== 'production') {\n printInstallDetails(updatedURLs, notUpdatedURLs);\n }\n return { updatedURLs, notUpdatedURLs };\n });\n }\n /**\n * Deletes assets that are no longer present in the current precache manifest.\n * Call this method from the service worker activate event.\n *\n * Note: this method calls `event.waitUntil()` for you, so you do not need\n * to call it yourself in your event handlers.\n *\n * @param {ExtendableEvent} event\n * @return {Promise}\n */\n activate(event) {\n // waitUntil returns Promise\n // eslint-disable-next-line @typescript-eslint/no-unsafe-return\n return waitUntil(event, async () => {\n const cache = await self.caches.open(this.strategy.cacheName);\n const currentlyCachedRequests = await cache.keys();\n const expectedCacheKeys = new Set(this._urlsToCacheKeys.values());\n const deletedURLs = [];\n for (const request of currentlyCachedRequests) {\n if (!expectedCacheKeys.has(request.url)) {\n await cache.delete(request);\n deletedURLs.push(request.url);\n }\n }\n if (process.env.NODE_ENV !== 'production') {\n printCleanupDetails(deletedURLs);\n }\n return { deletedURLs };\n });\n }\n /**\n * Returns a mapping of a precached URL to the corresponding cache key, taking\n * into account the revision information for the URL.\n *\n * @return {Map} A URL to cache key mapping.\n */\n getURLsToCacheKeys() {\n return this._urlsToCacheKeys;\n }\n /**\n * Returns a list of all the URLs that have been precached by the current\n * service worker.\n *\n * @return {Array} The precached URLs.\n */\n getCachedURLs() {\n return [...this._urlsToCacheKeys.keys()];\n }\n /**\n * Returns the cache key used for storing a given URL. If that URL is\n * unversioned, like `/index.html', then the cache key will be the original\n * URL with a search parameter appended to it.\n *\n * @param {string} url A URL whose cache key you want to look up.\n * @return {string} The versioned URL that corresponds to a cache key\n * for the original URL, or undefined if that URL isn't precached.\n */\n getCacheKeyForURL(url) {\n const urlObject = new URL(url, location.href);\n return this._urlsToCacheKeys.get(urlObject.href);\n }\n /**\n * @param {string} url A cache key whose SRI you want to look up.\n * @return {string} The subresource integrity associated with the cache key,\n * or undefined if it's not set.\n */\n getIntegrityForCacheKey(cacheKey) {\n return this._cacheKeysToIntegrities.get(cacheKey);\n }\n /**\n * This acts as a drop-in replacement for\n * [`cache.match()`](https://developer.mozilla.org/en-US/docs/Web/API/Cache/match)\n * with the following differences:\n *\n * - It knows what the name of the precache is, and only checks in that cache.\n * - It allows you to pass in an \"original\" URL without versioning parameters,\n * and it will automatically look up the correct cache key for the currently\n * active revision of that URL.\n *\n * E.g., `matchPrecache('index.html')` will find the correct precached\n * response for the currently active service worker, even if the actual cache\n * key is `'/index.html?__WB_REVISION__=1234abcd'`.\n *\n * @param {string|Request} request The key (without revisioning parameters)\n * to look up in the precache.\n * @return {Promise}\n */\n async matchPrecache(request) {\n const url = request instanceof Request ? request.url : request;\n const cacheKey = this.getCacheKeyForURL(url);\n if (cacheKey) {\n const cache = await self.caches.open(this.strategy.cacheName);\n return cache.match(cacheKey);\n }\n return undefined;\n }\n /**\n * Returns a function that looks up `url` in the precache (taking into\n * account revision information), and returns the corresponding `Response`.\n *\n * @param {string} url The precached URL which will be used to lookup the\n * `Response`.\n * @return {workbox-routing~handlerCallback}\n */\n createHandlerBoundToURL(url) {\n const cacheKey = this.getCacheKeyForURL(url);\n if (!cacheKey) {\n throw new WorkboxError('non-precached-url', { url });\n }\n return (options) => {\n options.request = new Request(url);\n options.params = Object.assign({ cacheKey }, options.params);\n return this.strategy.handle(options);\n };\n }\n}\nexport { PrecacheController };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { PrecacheController } from '../PrecacheController.js';\nimport '../_version.js';\nlet precacheController;\n/**\n * @return {PrecacheController}\n * @private\n */\nexport const getOrCreatePrecacheController = () => {\n if (!precacheController) {\n precacheController = new PrecacheController();\n }\n return precacheController;\n};\n","/*\n Copyright 2020 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { getFriendlyURL } from 'workbox-core/_private/getFriendlyURL.js';\nimport { Route } from 'workbox-routing/Route.js';\nimport { generateURLVariations } from './utils/generateURLVariations.js';\nimport './_version.js';\n/**\n * A subclass of {@link workbox-routing.Route} that takes a\n * {@link workbox-precaching.PrecacheController}\n * instance and uses it to match incoming requests and handle fetching\n * responses from the precache.\n *\n * @memberof workbox-precaching\n * @extends workbox-routing.Route\n */\nclass PrecacheRoute extends Route {\n /**\n * @param {PrecacheController} precacheController A `PrecacheController`\n * instance used to both match requests and respond to fetch events.\n * @param {Object} [options] Options to control how requests are matched\n * against the list of precached URLs.\n * @param {string} [options.directoryIndex=index.html] The `directoryIndex` will\n * check cache entries for a URLs ending with '/' to see if there is a hit when\n * appending the `directoryIndex` value.\n * @param {Array} [options.ignoreURLParametersMatching=[/^utm_/, /^fbclid$/]] An\n * array of regex's to remove search params when looking for a cache match.\n * @param {boolean} [options.cleanURLs=true] The `cleanURLs` option will\n * check the cache for the URL with a `.html` added to the end of the end.\n * @param {workbox-precaching~urlManipulation} [options.urlManipulation]\n * This is a function that should take a URL and return an array of\n * alternative URLs that should be checked for precache matches.\n */\n constructor(precacheController, options) {\n const match = ({ request, }) => {\n const urlsToCacheKeys = precacheController.getURLsToCacheKeys();\n for (const possibleURL of generateURLVariations(request.url, options)) {\n const cacheKey = urlsToCacheKeys.get(possibleURL);\n if (cacheKey) {\n const integrity = precacheController.getIntegrityForCacheKey(cacheKey);\n return { cacheKey, integrity };\n }\n }\n if (process.env.NODE_ENV !== 'production') {\n logger.debug(`Precaching did not find a match for ` + getFriendlyURL(request.url));\n }\n return;\n };\n super(match, precacheController.strategy);\n }\n}\nexport { PrecacheRoute };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { removeIgnoredSearchParams } from './removeIgnoredSearchParams.js';\nimport '../_version.js';\n/**\n * Generator function that yields possible variations on the original URL to\n * check, one at a time.\n *\n * @param {string} url\n * @param {Object} options\n *\n * @private\n * @memberof workbox-precaching\n */\nexport function* generateURLVariations(url, { ignoreURLParametersMatching = [/^utm_/, /^fbclid$/], directoryIndex = 'index.html', cleanURLs = true, urlManipulation, } = {}) {\n const urlObject = new URL(url, location.href);\n urlObject.hash = '';\n yield urlObject.href;\n const urlWithoutIgnoredParams = removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching);\n yield urlWithoutIgnoredParams.href;\n if (directoryIndex && urlWithoutIgnoredParams.pathname.endsWith('/')) {\n const directoryURL = new URL(urlWithoutIgnoredParams.href);\n directoryURL.pathname += directoryIndex;\n yield directoryURL.href;\n }\n if (cleanURLs) {\n const cleanURL = new URL(urlWithoutIgnoredParams.href);\n cleanURL.pathname += '.html';\n yield cleanURL.href;\n }\n if (urlManipulation) {\n const additionalURLs = urlManipulation({ url: urlObject });\n for (const urlToAttempt of additionalURLs) {\n yield urlToAttempt.href;\n }\n }\n}\n","/*\n Copyright 2018 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport '../_version.js';\n/**\n * Removes any URL search parameters that should be ignored.\n *\n * @param {URL} urlObject The original URL.\n * @param {Array} ignoreURLParametersMatching RegExps to test against\n * each search parameter name. Matches mean that the search parameter should be\n * ignored.\n * @return {URL} The URL with any ignored search parameters removed.\n *\n * @private\n * @memberof workbox-precaching\n */\nexport function removeIgnoredSearchParams(urlObject, ignoreURLParametersMatching = []) {\n // Convert the iterable into an array at the start of the loop to make sure\n // deletion doesn't mess up iteration.\n for (const paramName of [...urlObject.searchParams.keys()]) {\n if (ignoreURLParametersMatching.some((regExp) => regExp.test(paramName))) {\n urlObject.searchParams.delete(paramName);\n }\n }\n return urlObject;\n}\n","/*\n Copyright 2019 Google LLC\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { registerRoute } from 'workbox-routing/registerRoute.js';\nimport { getOrCreatePrecacheController } from './utils/getOrCreatePrecacheController.js';\nimport { PrecacheRoute } from './PrecacheRoute.js';\nimport './_version.js';\n/**\n * Add a `fetch` listener to the service worker that will\n * respond to\n * [network requests]{@link https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers#Custom_responses_to_requests}\n * with precached assets.\n *\n * Requests for assets that aren't precached, the `FetchEvent` will not be\n * responded to, allowing the event to fall through to other `fetch` event\n * listeners.\n *\n * @param {Object} [options] See the {@link workbox-precaching.PrecacheRoute}\n * options.\n *\n * @memberof workbox-precaching\n */\nfunction addRoute(options) {\n const precacheController = getOrCreatePrecacheController();\n const precacheRoute = new PrecacheRoute(precacheController, options);\n registerRoute(precacheRoute);\n}\nexport { addRoute };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { logger } from 'workbox-core/_private/logger.js';\nimport { WorkboxError } from 'workbox-core/_private/WorkboxError.js';\nimport { Route } from './Route.js';\nimport { RegExpRoute } from './RegExpRoute.js';\nimport { getOrCreateDefaultRouter } from './utils/getOrCreateDefaultRouter.js';\nimport './_version.js';\n/**\n * Easily register a RegExp, string, or function with a caching\n * strategy to a singleton Router instance.\n *\n * This method will generate a Route for you if needed and\n * call {@link workbox-routing.Router#registerRoute}.\n *\n * @param {RegExp|string|workbox-routing.Route~matchCallback|workbox-routing.Route} capture\n * If the capture param is a `Route`, all other arguments will be ignored.\n * @param {workbox-routing~handlerCallback} [handler] A callback\n * function that returns a Promise resulting in a Response. This parameter\n * is required if `capture` is not a `Route` object.\n * @param {string} [method='GET'] The HTTP method to match the Route\n * against.\n * @return {workbox-routing.Route} The generated `Route`.\n *\n * @memberof workbox-routing\n */\nfunction registerRoute(capture, handler, method) {\n let route;\n if (typeof capture === 'string') {\n const captureUrl = new URL(capture, location.href);\n if (process.env.NODE_ENV !== 'production') {\n if (!(capture.startsWith('/') || capture.startsWith('http'))) {\n throw new WorkboxError('invalid-string', {\n moduleName: 'workbox-routing',\n funcName: 'registerRoute',\n paramName: 'capture',\n });\n }\n // We want to check if Express-style wildcards are in the pathname only.\n // TODO: Remove this log message in v4.\n const valueToCheck = capture.startsWith('http')\n ? captureUrl.pathname\n : capture;\n // See https://github.com/pillarjs/path-to-regexp#parameters\n const wildcards = '[*:?+]';\n if (new RegExp(`${wildcards}`).exec(valueToCheck)) {\n logger.debug(`The '$capture' parameter contains an Express-style wildcard ` +\n `character (${wildcards}). Strings are now always interpreted as ` +\n `exact matches; use a RegExp for partial or wildcard matches.`);\n }\n }\n const matchCallback = ({ url }) => {\n if (process.env.NODE_ENV !== 'production') {\n if (url.pathname === captureUrl.pathname &&\n url.origin !== captureUrl.origin) {\n logger.debug(`${capture} only partially matches the cross-origin URL ` +\n `${url.toString()}. This route will only handle cross-origin requests ` +\n `if they match the entire URL.`);\n }\n }\n return url.href === captureUrl.href;\n };\n // If `capture` is a string then `handler` and `method` must be present.\n route = new Route(matchCallback, handler, method);\n }\n else if (capture instanceof RegExp) {\n // If `capture` is a `RegExp` then `handler` and `method` must be present.\n route = new RegExpRoute(capture, handler, method);\n }\n else if (typeof capture === 'function') {\n // If `capture` is a function then `handler` and `method` must be present.\n route = new Route(capture, handler, method);\n }\n else if (capture instanceof Route) {\n route = capture;\n }\n else {\n throw new WorkboxError('unsupported-route-type', {\n moduleName: 'workbox-routing',\n funcName: 'registerRoute',\n paramName: 'capture',\n });\n }\n const defaultRouter = getOrCreateDefaultRouter();\n defaultRouter.registerRoute(route);\n return route;\n}\nexport { registerRoute };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport './_version.js';\n/**\n * Claim any currently available clients once the service worker\n * becomes active. This is normally used in conjunction with `skipWaiting()`.\n *\n * @memberof workbox-core\n */\nfunction clientsClaim() {\n self.addEventListener('activate', () => self.clients.claim());\n}\nexport { clientsClaim };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { addRoute } from './addRoute.js';\nimport { precache } from './precache.js';\nimport './_version.js';\n/**\n * This method will add entries to the precache list and add a route to\n * respond to fetch events.\n *\n * This is a convenience method that will call\n * {@link workbox-precaching.precache} and\n * {@link workbox-precaching.addRoute} in a single call.\n *\n * @param {Array} entries Array of entries to precache.\n * @param {Object} [options] See the\n * {@link workbox-precaching.PrecacheRoute} options.\n *\n * @memberof workbox-precaching\n */\nfunction precacheAndRoute(entries, options) {\n precache(entries);\n addRoute(options);\n}\nexport { precacheAndRoute };\n","/*\n Copyright 2019 Google LLC\n\n Use of this source code is governed by an MIT-style\n license that can be found in the LICENSE file or at\n https://opensource.org/licenses/MIT.\n*/\nimport { getOrCreatePrecacheController } from './utils/getOrCreatePrecacheController.js';\nimport './_version.js';\n/**\n * Adds items to the precache list, removing any duplicates and\n * stores the files in the\n * {@link workbox-core.cacheNames|\"precache cache\"} when the service\n * worker installs.\n *\n * This method can be called multiple times.\n *\n * Please note: This method **will not** serve any of the cached files for you.\n * It only precaches files. To respond to a network request you call\n * {@link workbox-precaching.addRoute}.\n *\n * If you have a single array of files to precache, you can just call\n * {@link workbox-precaching.precacheAndRoute}.\n *\n * @param {Array} [entries=[]] Array of entries to precache.\n *\n * @memberof workbox-precaching\n */\nfunction precache(entries) {\n const precacheController = getOrCreatePrecacheController();\n precacheController.precache(entries);\n}\nexport { precache };\n"],"names":["self","_","e","messageGenerator","fallback","code","args","msg","length","JSON","stringify","WorkboxError","Error","constructor","errorCode","details","super","this","name","normalizeHandler","handler","handle","Route","match","method","setCatchHandler","catchHandler","RegExpRoute","regExp","url","result","exec","href","origin","location","index","slice","Router","_routes","Map","_defaultHandlerMap","routes","addFetchListener","addEventListener","event","request","responsePromise","handleRequest","respondWith","addCacheListener","data","type","payload","requestPromises","Promise","all","urlsToCache","map","entry","Request","waitUntil","ports","then","postMessage","URL","protocol","startsWith","sameOrigin","params","route","findMatchingRoute","has","get","err","reject","_catchHandler","catch","async","catchErr","matchResult","Array","isArray","Object","keys","undefined","setDefaultHandler","set","registerRoute","push","unregisterRoute","routeIndex","indexOf","splice","defaultRouter","_cacheNameDetails","googleAnalytics","precache","prefix","runtime","suffix","registration","scope","_createCacheName","cacheName","filter","value","join","cacheNames","userCacheName","asyncFn","returnPromise","createCacheKey","urlObject","cacheKey","revision","cacheKeyURL","originalURL","searchParams","PrecacheInstallReportPlugin","updatedURLs","notUpdatedURLs","handlerWillStart","state","originalRequest","cachedResponseWillBeUsed","cachedResponse","PrecacheCacheKeyPlugin","precacheController","cacheKeyWillBeUsed","_precacheController","getCacheKeyForURL","headers","supportStatus","copyResponse","response","modifier","clonedResponse","clone","responseInit","Headers","status","statusText","modifiedResponseInit","body","testResponse","Response","error","canConstructResponseFromBodyStream","blob","stripParams","fullURL","ignoreParams","strippedURL","param","delete","Deferred","promise","resolve","quotaErrorCallbacks","Set","toRequest","input","StrategyHandler","strategy","options","_cacheKeys","assign","_strategy","_handlerDeferred","_extendLifetimePromises","_plugins","plugins","_pluginStateMap","plugin","fetch","mode","FetchEvent","preloadResponse","possiblePreloadResponse","hasCallback","cb","iterateCallbacks","thrownErrorMessage","message","pluginFilteredRequest","fetchResponse","fetchOptions","callback","runCallbacks","fetchAndCachePut","responseClone","cachePut","cacheMatch","key","matchOptions","effectiveRequest","getCacheKey","multiMatchOptions","caches","ms","setTimeout","String","replace","RegExp","responseToCache","_ensureResponseSafeToCache","cache","open","hasCacheUpdateCallback","oldResponse","strippedRequestURL","keysOptions","ignoreSearch","cacheKeys","cacheMatchIgnoreParams","put","executeQuotaErrorCallbacks","newResponse","statefulCallback","statefulParam","doneWaiting","shift","destroy","pluginsUsed","Strategy","responseDone","handleAll","_getResponse","_awaitComplete","_handle","waitUntilError","PrecacheStrategy","_fallbackToNetwork","fallbackToNetwork","copyRedirectedCacheableResponsesPlugin","_handleInstall","_handleFetch","integrityInManifest","integrity","integrityInRequest","noIntegrityConflict","_useDefaultCacheabilityPluginIfNeeded","defaultPluginIndex","cacheWillUpdatePluginCount","entries","defaultPrecacheCacheabilityPlugin","cacheWillUpdate","redirected","PrecacheController","_urlsToCacheKeys","_urlsToCacheModes","_cacheKeysToIntegrities","install","bind","activate","addToCacheList","_installAndActiveListenersAdded","urlsToWarnAbout","cacheMode","firstEntry","secondEntry","warningMessage","console","warn","installReportPlugin","credentials","currentlyCachedRequests","expectedCacheKeys","values","deletedURLs","getURLsToCacheKeys","getCachedURLs","getIntegrityForCacheKey","matchPrecache","createHandlerBoundToURL","getOrCreatePrecacheController","PrecacheRoute","urlsToCacheKeys","possibleURL","ignoreURLParametersMatching","directoryIndex","cleanURLs","urlManipulation","hash","urlWithoutIgnoredParams","paramName","some","test","removeIgnoredSearchParams","pathname","endsWith","directoryURL","cleanURL","additionalURLs","urlToAttempt","generateURLVariations","addRoute","capture","captureUrl","matchCallback","moduleName","funcName","clients","claim"],"mappings":"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"} \ No newline at end of file