夕君

夕君 查看完整档案

上海编辑  |  填写毕业院校YT  |  CTO 编辑 zanjs.com 编辑
编辑

华丽丽的跌倒,胜过无谓的徘徊

个人动态

夕君 收藏了文章 · 2019-11-21

彻底理解浏览器缓存机制

推荐理由:文章思路清晰,鞭辟入里。

概述

浏览器的缓存机制也就是我们说的 HTTP 缓存机制,其机制是根据 HTTP 报文的缓存标示进行的。

所以在分析浏览器缓存之前,我们先使用图文简单介绍一下 HTTP 报文,HTTP 报文分为两种:

  • HTTP 请求(Request)报文。报文格式为:

    1. 请求行
    2. HTTP(通用信息头,请求头,实体头)。
    3. 请求报文主体(只有 POST 才有报文主体)。

请求报文
请求报文主体

  • HTTP 响应(Response)报文,报文格式为:

    1. 状态行
    2. HTTP 头(通用信息头,响应头,实体头)。
    3. 响应报文主体

响应报文
响应报文主体

注:通用信息头指的是请求和响应报文都支持的头域,分别为:Cache-Control、Connection、Date、Pragma、Transfer-Encoding、Upgrade、Via。
实体头则是实体信息的实体头域,分别为:Allow、Content-Base、Content-Encoding、Content-Language、Content-Length、Content-Location、Content-MD5、Content-Range、Content-Type、Etag、Expires、Last-Modified、extension-header。
这里只是为了方便理解,将通用信息头,响应头/请求头,实体头都归为了HTTP头。

以上的概念在这里我们不做多讲解,只简单介绍,有兴趣的童鞋可以自行研究。

缓存过程分析

浏览器与服务器通信的方式为应答模式,即,浏览器发起 HTTP 请求 –> 服务器响应该请求。

那么浏览器第一次向服务器发起该请求后拿到请求结果,会根据响应报文中 HTTP 头的缓存标识,决定是否缓存结果,是则将请求结果和缓存标识存入浏览器缓存中,简单的过程如下图:

第一次发起HTTP请求

由上图我们可以知道:

  • 浏览器每次发起请求,都会先在浏览器缓存中查找该请求的结果以及缓存标识。
  • 浏览器每次拿到返回的请求结果都会将该结果和缓存标识存入浏览器缓存中。

以上两点结论就是浏览器缓存机制的关键,他确保了每个请求的缓存存入与读取,只要我们再理解浏览器缓存的使用规则,那么所有的问题就迎刃而解了,本文也将围绕着这点进行详细分析。

为了方便大家理解,这里我们根据是否需要向服务器重新发起 HTTP 请求将缓存过程分为两个部分,分别是强制缓存协商缓存

强制缓存

强制缓存就是向浏览器缓存查找该请求结果,并根据该结果的缓存规则来决定是否使用该缓存结果的过程,强制缓存的情况主要有三种:

  • 不存在该缓存结果和缓存标识,强制缓存失效,则直接向服务器发起请求(跟第一次发起请求一致),如下图:

强制缓存

  • 存在该缓存结果和缓存标识,但该结果已失效,强制缓存失效,则使用协商缓存(暂不分析),如下图:

强制缓存

  • 存在该缓存结果和缓存标识,且该结果尚未失效,强制缓存生效,直接返回该结果,如下图:

强制缓存

那么强制缓存的缓存规则是什么?

当浏览器向服务器发起请求时,服务器会将缓存规则放入 HTTP 响应报文的 HTTP 头中和请求结果一起返回给浏览器,控制强制缓存的字段分别是 ExpiresCache-Control,其中 Cache-Control 优先级比 Expires 高。

Expires

ExpiresHTTP/1.0 控制网页缓存的字段,其值为服务器返回该请求结果缓存的到期时间,即再次发起该请求时,如果客户端的时间小于Expires 的值时,直接使用缓存结果。

ExpiresHTTP/1.0 的字段,但是现在浏览器默认使用的是 HTTP/1.1,那么在 HTTP/1.1 中网页缓存还是否由 Expires 控制?

到了 HTTP/1.1Expire 已经被 Cache-Control 替代,原因在于 Expires 控制缓存的原理是使用客户端的时间与服务端返回的时间做对比,那么如果客户端与服务端的时间因为某些原因(例如时区不同;客户端和服务端有一方的时间不准确)发生误差,那么强制缓存则会直接失效,这样的话强制缓存的存在则毫无意义,那么 Cache-Control 又是如何控制的呢?

Cache-Control

HTTP/1.1 中,Cache-Control 是最重要的规则,主要用于控制网页缓存,主要取值为:

  • public:所有内容都将被缓存(客户端和代理服务器都可缓存)。
  • private:所有内容只有客户端可以缓存,Cache-Control 的默认取值。
  • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定。
  • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存。
  • max-age=xxx (xxx is numeric):缓存内容将在 xxx 秒后失效。

接下来,我们直接看一个例子,如下:

强制缓存

由上面的例子我们可以知道:

HTTP 响应报文中 expires 的时间值,是一个绝对值。

HTTP 响应报文中 Cache-Controlmax-age=600,是相对值。

由于 Cache-Control 的优先级比 expires,那么直接根据 Cache-Control 的值进行缓存,意思就是说在 600 秒内再次发起该请求,则会直接使用缓存结果,强制缓存生效。

注:在无法确定客户端的时间是否与服务端的时间同步的情况下,Cache-Control 相比于 expires 是更好的选择,所以同时存在时,只有Cache-Control 生效。

了解强制缓存的过程后,我们拓展性的思考一下:

浏览器的缓存存放在哪里,如何在浏览器中判断强制缓存是否生效?

强制缓存

这里我们以博客的请求为例,状态码为灰色的请求则代表使用了强制缓存,请求对应的 Size 值则代表该缓存存放的位置,分别为 from memory cachefrom disk cache

那么 from memory cachefrom disk cache 又分别代表的是什么呢?什么时候会使用 from disk cache,什么时候会使用from memory cache 呢?

from memory cache 代表使用内存中的缓存,from disk cache 则代表使用的是硬盘中的缓存,浏览器读取缓存的顺序为 memory –> disk

虽然我已经直接把结论说出来了,但是相信有不少人对此不能理解,那么接下来我们一起详细分析一下缓存读取问题,这里仍让以我的博客为例进行分析:

  1. 访问 https://heyingye.github.io/ –> 200
  2. 关闭博客的标签页 –> 重新打开 https://heyingye.github.io/ –> 200 (from disk cache)
  3. 刷新 –> 200(from memory cache)
看到这里可能有人小伙伴问了,最后一个步骤刷新的时候,不是同时存在着 from disk cache 和 from memory cache 吗?

对于这个问题,我们需要了解内存缓存(from memory cache)和硬盘缓存(from disk cache),如下:

  • 内存缓存(from memory cache):内存缓存具有两个特点,分别是快速读取时效性

    • 快速读取:内存缓存会将编译解析后的文件,直接存入该进程的内存中,占据该进程一定的内存资源,以方便下次运行使用时的快速读取。
    • 时效性:一旦该进程关闭,则该进程的内存则会清空。
  • 硬盘缓存(from disk cache):硬盘缓存则是直接将缓存写入硬盘文件中,读取缓存需要对该缓存存放的硬盘文件进行 I/O 操作,然后重新解析该缓存内容,读取复杂,速度比内存缓存慢。

在浏览器中,浏览器会在 js 和图片等文件解析执行后直接存入内存缓存中,那么当刷新页面时只需直接从内存缓存中读取;而 css 文件则会存入硬盘文件中,所以每次渲染页面都需要从硬盘读取缓存。

协商缓存

协商缓存就是强制缓存失效后,浏览器携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存的过程

主要有以下两种情况:

  • 协商缓存生效,返回 304,如下:

    协商缓存

  • 协商缓存失效,返回 200 和请求结果结果,如下:

    协商缓存

同样,协商缓存的标识也是在响应报文的 HTTP 头中和请求结果一起返回给浏览器的,控制协商缓存的字段分别有:

  • Last-Modified / If-Modified-Since.
  • Etag / If-None-Match.

其中 Etag / If-None-Match 的优先级比 Last-Modified / If-Modified-Since 高。

Last-Modified / If-Modified-Since

  • Last-Modified 是服务器响应请求时,返回该资源文件在服务器最后被修改的时间,如下:

    协商缓存

  • If-Modified-Since 则是客户端再次发起该请求时,携带上次请求返回的 Last-Modified 值,通过此字段值告诉服务器该资源上次请求返回的最后被修改时间。服务器收到该请求,发现请求头含有 If-Modified-Since 字段,则会根据 If-Modified-Since 的字段值与该资源在服务器的最后被修改时间做对比,若服务器的资源最后被修改时间大于 If-Modified-Since 的字段值,则重新返回资源,状态码为 200;否则则返回 304,代表资源无更新,可继续使用缓存文件,如下:

    协商缓存

Etag / If-None-Match

  • Etag 是服务器响应请求时,返回当前资源文件的一个唯一标识(由服务器生成),如下:

    协商缓存

  • If-None-Match 是客户端再次发起该请求时,携带上次请求返回的唯一标识 Etag 值,通过此字段值告诉服务器该资源上次请求返回的唯一标识值。服务器收到该请求后,发现该请求头中含有 If-None-Match,则会根据 If-None-Match 的字段值与该资源在服务器的 Etag值做对比,一致则返回 304,代表资源无更新,继续使用缓存文件;不一致则重新返回资源文件,状态码为 200,如下:

    协商缓存

注:Etag / If-None-Match 优先级高于 Last-Modified / If-Modified-Since,同时存在则只有 Etag / If-None-Match 生效。

总结

强制缓存优先于协商缓存进行,若强制缓存 ExpiresCache-Control 生效则直接使用缓存,若不生效则进行协商缓存(Last-Modified / If-Modified-SinceEtag / If-None-Match)。

协商缓存由服务器决定是否使用缓存,若协商缓存失效,那么代表该请求的缓存失效,重新获取请求结果,再存入浏览器缓存中;生效则返回 304,继续使用缓存,主要过程如下:

HTTP 缓存

以上便是浏览器缓存的过程,若有错误之处,敬请指正。

转载自 Heying Ye's Personal Website 个人博客。

追加总结

强制缓存 - 处理资源是否在设置的规则范围内

  • 通过时间戳控制 expires
  • 通过一些规则控制 cache-control

    • public:所有内容都将被缓存(客户端和代理服务器都可缓存)。
    • private:所有内容只有客户端可以缓存,Cache-Control 的默认取值。
    • no-cache:客户端缓存内容,但是是否使用缓存则需要经过协商缓存来验证决定。
    • no-store:所有内容都不会被缓存,即不使用强制缓存,也不使用协商缓存。
    • max-age=xxx (xxx is numeric):缓存内容将在 xxx 秒后失效。

协商缓存 - 处理资源是否有变化

  • 通过时间纬度控制 If-Modified / If-Modified-Since
  • 通过唯一标识控制 Etag / If-none-Match
查看原文

夕君 提出了问题 · 2019-09-29

Android Studio 自带的模拟器无法访问所在局域网服务

window 10 安装完 Android Studio 并且安装了 自带的模拟器后

打开浏览器,可以访问 baidu.com , 外网可以访问, 就是 所在局域网的 服务没法访问,

比如, 电脑的 ip 是 192.168.0.10, 电脑起了一个 API 服务是 : http:192.168.0.10:8088

模拟器的浏览器 打开 http:192.168.0.10:8088 这个地址是无法访问的

求大佬指导一二

关注 2 回答 1

夕君 收藏了文章 · 2019-09-07

Licia 支持小程序的 JS 工具库

导语

Licia 是一套在开发中实践积累起来的实用 JavaScript 工具库。该库目前拥有超过 300 个模块,同时支持浏览器、node 及小程序运行环境,提供了包括日期格式化、md5、颜色转换等实用模块,可以极大地提高开发效率。

前言

因为小程序运行的是 JavaScript 代码,传统前端所使用的 JS 库理应也能够被用在小程序中才对。然而,经过实际测试,你会发现有相当一部分 npm 包是无法直接在小程序中跑起来的。比如前端工程师十分常用的 lodash,在小程序中引入会报错。

为什么会这样?

主要原因就是绝大部分库的开发者在设计时只会考虑两种运行环境,浏览器和 node,而小程序并不会在其考虑范围内。因此,只要开发者的 JS 代码使用了只有浏览器与 node 中才有的接口,如 DOM 操作、文件读写等,该库就不能正常地运行在小程序环境中。除此之外,假如他们使用了小程序禁用的功能,例如全局变量与动态代码执行,这时候代码跑在小程序环境也会出错。

使用

使用 npm 安装

1、 安装 npm 包

npm i miniprogram-licia --save

2、点击开发者工具中的菜单栏:工具 --> 构建 npm

3、直接在代码中引入使用

const licia = require('miniprogram-licia');

licia.md5('licia'); // -> 'e59f337d85e9a467f1783fab282a41d0'
licia.safeGet({a: {b: 1}}, 'a.b'); // -> 1

生成定制化 util.js

使用 npm 包的方式会将所有功能引入到代码包中,大概会增加 100 kb 的大小。如果你只想引入所需脚本,可以使用在线工具生成定制化 util 库。

1、访问 https://licia.liriliri.io/builder.html

2、输入需要的模块名,点击生成下载 util.js。

3、将生成的工具库拷贝到小程序项目任意目录下然后直接引入使用。

const util = require('../lib/util');

util.wx.getStorage({
  key: 'test'
}).then(res => console.log(res.data));

优点

1、目前拥有 270 多个模块可在小程序中正常运行,而 underscore 只有 120 个函数左右。

2、与 lodash 相比增加了不少更加实用的函数,比如 md5、atob、btoa、Emitter、dateFormat 等。

3、可以直接在小程序中引入运行,不像 lodash 需要进行一定的修改才能正常跑在小程序中。

4、定制化生成可以使用更小体积的工具库,这在限制了代码包大小的小程序中十分有用。

附录

这里只简单列出函数及其功能介绍,详细的用法请访问官网查看。

注:模块名右边有小程序图标即表明可以在小程序中使用。

  1. Class: 创建 JavaScript 类。
  2. Color: 颜色转换。
  3. Dispatcher: Flux 调度器。
  4. Emitter: 提供观察者模式的 Event emitter 类。
  5. Enum: Enum 类实现。
  6. JsonTransformer: JSON 转换器。
  7. LinkedList: 双向链表实现。
  8. Logger: 带日志级别的简单日志库。
  9. Lru: 简单 LRU 缓存。
  10. Promise: 轻量 Promise 实现。
  11. PseudoMap: 类似 es6 的 Map,不支持遍历器。
  12. Queue: 队列数据结构。
  13. QuickLru: 不使用链表的 LRU 实现。
  14. ReduceStore: 简单类 redux 状态管理。
  15. Stack: 栈数据结构。
  16. State: 简单状态机。
  17. Store: 内存存储。
  18. Tween: JavaScript 补间动画库。
  19. Url: 简单 url 操作库。
  20. Validator: 对象属性值校验。
  21. abbrev: 计算字符串集的缩写集合。
  22. after: 创建一个函数,只有在调用 n 次后才会调用一次。
  23. allKeys: 获取对象的所有键名,包括自身的及继承的。
  24. arrToMap: 将字符串列表转换为映射。
  25. atob: window.atob,运行在 node 环境时使用 Buffer 进行模拟。
  26. average: 获取数字的平均值。
  27. base64: base64 编解码。
  28. before: 创建一个函数,只能调用少于 n 次。
  29. binarySearch: 二分查找实现。
  30. bind: 创建一个绑定到指定对象的函数。
  31. btoa: window.btoa,运行在 node 环境时使用 Buffer 进行模拟。
  32. bubbleSort: 冒泡排序实现。
  33. bytesToStr: 将字节数组转换为字符串。
  34. callbackify: 将返回 Promise 的函数转换为使用回调的函数。
  35. camelCase: 将字符串转换为驼峰式。
  36. capitalize: 将字符串的第一个字符转换为大写,其余字符转换为小写。
  37. castPath: 将值转换为属性路径数组。
  38. centerAlign: 字符串居中。
  39. char: 根据指定的整数返回 unicode 编码为该整数的字符。
  40. chunk: 将数组拆分为指定长度的子数组。
  41. clamp: 将数字限定于指定区间。
  42. className: 合并 class。
  43. clone: 对指定对象进行浅复制。
  44. cloneDeep: 深复制。
  45. cmpVersion: 比较版本号。
  46. combine: 创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值。
  47. compact: 返回数组的拷贝并移除其中的虚值。
  48. compose: 将多个函数组合成一个函数。
  49. concat: 将多个数组合并成一个数组。
  50. contain: 检查数组中是否有指定值。
  51. convertBase: 对数字进行进制转换。
  52. createAssigner: 用于创建 extend,extendOwn 和 defaults 等模块。
  53. curry: 函数柯里化。
  54. dateFormat: 简单日期格式化。
  55. debounce: 返回函数的防反跳版本。
  56. decodeUriComponent: 类似 decodeURIComponent 函数,只是输入不合法时不抛出错误并尽可能地对其进行解码。
  57. defaults: 填充对象的默认值。
  58. define: 定义一个模块,需要跟 use 模块配合使用。
  59. defineProp: Object.defineProperty(defineProperties) 的快捷方式。
  60. delay: 在指定时长后执行函数。
  61. detectBrowser: 使用 ua 检测浏览器信息。
  62. detectMocha: 检测是否有 mocha 测试框架在运行。
  63. detectOs: 使用 ua 检测操作系统。
  64. difference: 创建一个数组,该数组的元素不存在于给定的其它数组中。
  65. dotCase: 将字符串转换为点式。
  66. each: 遍历集合中的所有元素,用每个元素当做参数调用迭代器。
  67. easing: 缓动函数,参考 http://jqueryui.com/
  68. endWith: 检查字符串是否以指定字符串结尾。
  69. escape: 转义 HTML 字符串,替换 &,<,>,",`,和 ' 字符。
  70. escapeJsStr: 转义字符串为合法的 JavaScript 字符串字面量。
  71. escapeRegExp: 转义特殊字符用于 RegExp 构造函数。
  72. every: 检查是否集合中的所有元素都能通过真值检测。
  73. extend: 复制多个对象中的所有属性到目标对象上。
  74. extendDeep: 类似 extend,但会递归进行扩展。
  75. extendOwn: 类似 extend,但只复制自己的属性,不包括原型链上的属性。
  76. extractBlockCmts: 从源码中提取块注释。
  77. extractUrls: 从文本中提取 url。
  78. fibonacci: 计算斐波那契数列中某位数字。
  79. fileSize: 将字节数转换为易于阅读的形式。
  80. fill: 在数组指定位置填充指定值。
  81. filter: 遍历集合中的每个元素,返回所有通过真值检测的元素组成的数组。
  82. find: 找到集合中第一个通过真值检测的元素。
  83. findIdx: 返回第一个通过真值检测元素在数组中的位置。
  84. findKey: 返回对象中第一个通过真值检测的属性键名。
  85. findLastIdx: 同 findIdx,只是查找顺序改为从后往前。
  86. flatten: 递归拍平数组。
  87. fnParams: 获取函数的参数名列表。
  88. format: 使用类似于 printf 的方式来格式化字符串。
  89. fraction: 转换数字为分数形式。
  90. freeze: Object.freeze 的快捷方式。
  91. freezeDeep: 递归进行 Object.freeze。
  92. gcd: 使用欧几里德算法求最大公约数。
  93. getUrlParam: 获取 url 参数值。
  94. has: 检查属性是否是对象自身的属性(原型链上的不算)。
  95. hslToRgb: 将 hsl 格式的颜色值转换为 rgb 格式。
  96. identity: 返回传入的第一个参数。
  97. idxOf: 返回指定值第一次在数组中出现的位置。
  98. indent: 对文本的每一行进行缩进处理。
  99. inherits: 使构造函数继承另一个构造函数原型链上的方法。
  100. insertionSort: 插入排序实现。
  101. intersect: 计算所有数组的交集。
  102. intersectRange: 计算两个区间的交集。
  103. invert: 生成一个新对象,该对象的键名和键值进行调换。
  104. isAbsoluteUrl: 检查 url 是否是绝对地址。
  105. isArgs: 检查值是否是参数类型。
  106. isArr: 检查值是否是数组类型。
  107. isArrBuffer: 检查值是否是 ArrayBuffer 类型。
  108. isArrLike: 检查值是否是类数组对象。
  109. isBool: 检查值是否是布尔类型。
  110. isBrowser: 检测是否运行于浏览器环境。
  111. isClose: 检查两个数字是否近似相等。
  112. isDataUrl: 检查字符串是否是有效的 Data Url。
  113. isDate: 检查值是否是 Date 类型。
  114. isEmail: 简单检查值是否是合法的邮件地址。
  115. isEmpty: 检查值是否是空对象或空数组。
  116. isEqual: 对两个对象进行深度比较,如果相等,返回真。
  117. isErr: 检查值是否是 Error 类型。
  118. isEven: 检查数字是否是偶数。
  119. isFinite: 检查值是否是有限数字。
  120. isFn: 检查值是否是函数。
  121. isGeneratorFn: 检查值是否是 Generator 函数。
  122. isInt: 检查值是否是整数。
  123. isJson: 检查值是否是有效的 JSON。
  124. isLeapYear: 检查年份是否是闰年。
  125. isMap: 检查值是否是 Map 对象。
  126. isMatch: 检查对象所有键名和键值是否在指定的对象中。
  127. isMiniProgram: 检测是否运行于微信小程序环境中。
  128. isMobile: 使用 ua 检测是否运行于移动端浏览器。
  129. isNaN: 检测值是否是 NaN。
  130. isNative: 检查值是否是原生函数。
  131. isNil: 检查值是否是 null 或 undefined,等价于 value == null。
  132. isNode: 检测是否运行于 node 环境中。
  133. isNull: 检查值是否是 Null 类型。
  134. isNum: 检测值是否是数字类型。
  135. isNumeric: 检查值是否是数字,包括数字字符串。
  136. isObj: 检查值是否是对象。
  137. isOdd: 检查数字是否是奇数。
  138. isPlainObj: 检查值是否是用 Object 构造函数创建的对象。
  139. isPrime: 检查整数是否是质数。
  140. isPrimitive: 检测值是否是字符串,数字,布尔值或 null。
  141. isPromise: 检查值是否是类 promise 对象。
  142. isRegExp: 检查值是否是正则类型。
  143. isRelative: 检查路径是否是相对路径。
  144. isSet: 检查值是否是 Set 类型。
  145. isSorted: 检查数组是否有序。
  146. isStr: 检查值是否是字符串。
  147. isTypedArr: 检查值是否 TypedArray 类型。
  148. isUndef: 检查值是否是 undefined。
  149. isUrl: 简单检查值是否是有效的 url 地址。
  150. isWeakMap: 检查值是否是 WeakMap 类型。
  151. isWeakSet: 检查值是否是 WeakSet 类型。
  152. kebabCase: 将字符串转换为短横线式。
  153. keyCode: 键码键名转换。
  154. keys: 返回包含对象自身可遍历所有键名的数组。
  155. last: 获取数组的最后一个元素。
  156. linkify: 将文本中的 url 地址转换为超链接。
  157. longest: 获取数组中最长的一项。
  158. lowerCase: 转换字符串为小写。
  159. lpad: 对字符串进行左填充。
  160. ltrim: 删除字符串头部指定字符或空格。
  161. map: 对集合的每个元素调用转换函数生成与之对应的数组。
  162. mapObj: 类似 map,但针对对象,生成一个新对象。
  163. matcher: 传入对象返回函数,如果传入参数中包含该对象则返回真。
  164. max: 获取数字中的最大值。
  165. md5: MD5 算法实现。
  166. memStorage: Web Storage 接口的纯内存实现。
  167. memoize: 缓存函数计算结果。
  168. mergeSort: 归并排序实现。
  169. methods: 获取对象中所有方法名。
  170. min: 获取数字中的最小值。
  171. moment: 简单的类 moment.js 实现。
  172. ms: 时长字符串与毫秒转换库。
  173. negate: 创建一个将原函数结果取反的函数。
  174. nextTick: 能够同时运行在 node 和浏览器端的 next tick 实现。
  175. noop: 一个什么也不做的空函数。
  176. normalizeHeader: 标准化 HTTP 头部名。
  177. normalizePath: 标准化文件路径中的斜杠。
  178. now: 获取当前时间戳。
  179. objToStr: Object.prototype.toString 的别名。
  180. omit: 类似 pick,但结果相反。
  181. once: 创建只能调用一次的函数。
  182. optimizeCb: 用于高效的函数上下文绑定。
  183. pad: 对字符串进行左右填充。
  184. pairs: 将对象转换为包含【键名,键值】对的数组。
  185. parallel: 同时执行多个函数。
  186. parseArgs: 命令行参数简单解析。
  187. partial: 返回局部填充参数的函数,与 bind 模块相似。
  188. pascalCase: 将字符串转换为帕斯卡式。
  189. perfNow: 高精度时间戳。
  190. pick: 过滤对象。
  191. pluck: 提取数组对象中指定属性值,返回一个数组。
  192. precision: 获取数字的精度。
  193. promisify: 转换使用回调的异步函数,使其返回 Promise。
  194. property: 返回一个函数,该函数返回任何传入对象的指定属性。
  195. query: 解析序列化 url 的 query 部分。
  196. quickSort: 快排实现。
  197. raf: requestAnimationFrame 快捷方式。
  198. random: 在给定区间内生成随机数。
  199. randomItem: 随机获取数组中的某项。
  200. range: 创建整数数组。
  201. rc4: RC4 对称加密算法实现。
  202. reduce: 合并多个值成一个值。
  203. reduceRight: 类似于 reduce,只是从后往前合并。
  204. reject: 类似 filter,但结果相反。
  205. remove: 移除集合中所有通过真值检测的元素,返回包含所有删除元素的数组。
  206. repeat: 重复字符串指定次数。
  207. restArgs: 将给定序号后的参数合并成一个数组。
  208. rgbToHsl: 将 rgb 格式的颜色值转换为 hsl 格式。
  209. root: 根对象引用,对于 nodeJs,取 global 对象,对于浏览器,取 window 对象。
  210. rpad: 对字符串进行右填充。
  211. rtrim: 删除字符串尾部指定字符或空格。
  212. safeCb: 创建回调函数,内部模块使用。
  213. safeDel: 删除对象属性。
  214. safeGet: 获取对象属性值,路径不存在时不报错。
  215. safeSet: 设置对象属性值。
  216. sample: 从集合中随机抽取部分样本。
  217. selectionSort: 选择排序实现。
  218. shuffle: 将数组中元素的顺序打乱。
  219. size: 获取对象的大小或类数组元素的长度。
  220. sleep: 使用 Promise 模拟暂停方法。
  221. slice: 截取数组的一部分生成新数组。
  222. snakeCase: 转换字符串为下划线式。
  223. some: 检查集合中是否有元素通过真值检测。
  224. sortBy: 遍历集合中的元素,将其作为参数调用函数,并以得到的结果为依据对数组进行排序。
  225. spaceCase: 将字符串转换为空格式。
  226. splitCase: 将不同命名式的字符串拆分成数组。
  227. splitPath: 将路径拆分为文件夹路径,文件名和扩展名。
  228. startWith: 检查字符串是否以指定字符串开头。
  229. strHash: 使用 djb2 算法进行字符串哈希。
  230. strToBytes: 将字符串转换为字节数组。
  231. stringify: JSON 序列化,支持循环引用和函数。
  232. stripAnsi: 清除字符串中的 ansi 控制码。
  233. stripCmt: 清除源码中的注释。
  234. stripColor: 清除字符串中的 ansi 颜色控制码。
  235. stripHtmlTag: 清除字符串中的 html 标签。
  236. sum: 计算数字和。
  237. swap: 交换数组中的两项。
  238. template: 将模板字符串编译成函数用于渲染。
  239. throttle: 返回函数的节流阀版本。
  240. timeAgo: 将时间格式化成多久之前的形式。
  241. timeTaken: 获取函数的执行时间。
  242. times: 调用目标函数 n 次。
  243. toArr: 将任意值转换为数组。
  244. toBool: 将任意值转换为布尔值。
  245. toDate: 将任意值转换为日期类型。
  246. toInt: 将任意值转换为整数。
  247. toNum: 将任意值转换为数字。
  248. toSrc: 将函数转换为源码。
  249. toStr: 将任意值转换为字符串。
  250. topoSort: 拓扑排序实现。
  251. trim: 删除字符串两边指定字符或空格。
  252. tryIt: 在 try catch 块中运行函数。
  253. type: 获取 JavaScript 对象的内部类型。
  254. types: 仅用于生成 ts 定义文件。
  255. ucs2: UCS-2 编解码。
  256. unescape: 和 escape 相反,转义 HTML 实体回去。
  257. union: 返回传入所有数组的并集。
  258. uniqId: 生成全局唯一 id。
  259. unique: 返回数组去重后的副本。
  260. unzip: 与 zip 相反。
  261. upperCase: 转换字符串为大写。
  262. upperFirst: 将字符串的第一个字符转换为大写。
  263. use: 使用 define 创建的模块。
  264. utf8: UTF-8 编解码。
  265. values: 返回对象所有的属性值。
  266. vlq: vlq 编解码。
  267. waitUntil: 等待直到条件函数返回真值。
  268. waterfall: 按顺序执行函数序列。
  269. wrap: 将函数封装到包裹函数里面, 并把它作为第一个参数传给包裹函数。
  270. wx: 小程序 wx 对象的 promise 版本。
  271. zip: 将每个数组中相应位置的值合并在一起。
查看原文

夕君 收藏了文章 · 2019-09-07

烧脑!JS+Canvas带你体验「偶消奇不消」的智商挑战

启逻辑之高妙,因想象而自由

层叠拼图Plus是一款需要空间想象力和逻辑推理能力完美结合的微信小游戏,偶消奇不消,在简单的游戏规则下却有着无数种可能性,需要你充分发挥想象力去探索,看似简单却具有极大的挑战性和趣味性,这就是其魅力所在!温馨提示,体验后再阅读此文体验更佳哦!

预览:

Talk is cheap. Show me the code

层叠拼图Plus微信小游戏采用js+canvas实现,没有使用任何游戏引擎,对于初学者来说,也比较容易入门。接下来,我将通过以下几个点循序渐进的讲解层叠拼图Plus微信小游戏的实现。

  • 如何解决Canvas绘图模糊?
  • 如何绘制任意多边形图形?
  • 1 + 1 = 0,「偶消奇不消」的效果如何实现?
  • 如何判断一个点是否在任意多边形内部 ?
  • 如何判断游戏结果是否正确?
  • 排行榜的展示
  • 游戏性能优化

如何解决Canvas绘图模糊?

canvas 绘图时,会从两个物理像素的中间位置开始绘制并向两边扩散 0.5 个物理像素。当设备像素比为 1 时,一个 1px 的线条实际上占据了两个物理像素(每个像素实际上只占一半),由于不存在 0.5 个像素,所以这两个像素本来不应该被绘制的部分也被绘制了,于是 1 物理像素的线条变成了 2 物理像素,视觉上就造成了模糊

绘图模糊的原因知道了,在微信小游戏里面又该如何解决呢?

const ratio = wx.getSystemInfoSync().pixelRatio
let ctx = canvas.getContext('2d')
canvas.width = screenWidth * ratio
canvas.height = screenHeight * ratio

ctx.fillStyle = 'black'
ctx.font = `${18 * ratio}px Arial`
ctx.fillText('我是清晰的文字', x * ratio, y * ratio)

ctx.fillStyle = 'red'
ctx.fillRect(x * ratio, y * ratio, width * ratio, height * ratio)

可以看到,我们先通过 wx.getSystemInfoSync().pixelRatio 获取设备的像素比ratio,然后将在屏 Canvas 的宽度和高度按照所获取的像素比ratio进行放大,在绘制文字、图片的时候,坐标点 xy 和所要绘制图形的 widthheight均需要按照像素比 ratio 进行缩放,这样我们就可以清晰的在高清屏中绘制想要的文字、图片。

可参考微信官方 缩放策略调整

另外,需要注意的是,这里的 canvas 是由 weapp-adapter 预先调用 wx.createCanvas() 创建一个上屏 Canvas,并暴露为一个全局变量 canvas

如何绘制任意多边形图形?

任意一个多边形图形,是由多个平面坐标点所组成的图形区域。

在游戏画布内,我们以左上角为坐标原点 {x: 0, y: 0} ,一个多边形包含多个单位长度的平面坐标点,如:[{ x: 1, y: 3 }, { x: 5, y: 3 }, { x: 3, y: 5 }] 表示为一个三角形的区域,需要注意的是,xy 并不是真实的平面坐标值,而是通过屏幕宽度计算出来的单位长度,在画布内的真实坐标值则为 {x: x * itemWidth, y: y * itemWidth}

绘制多边形代码实现如下:

/**
 * 绘制多边形
 */
export default class Block {
    constructor() { }
    init(points, itemWidth, ctx) {
        this.points = []
        this.itemWidth = itemWidth // 单位长度
        this.ctx = ctx
        for (let i = 0; i < points.length; i++) {
            let point = points[i]
            this.points.push({
                x: point.x * this.itemWidth,
                y: point.y * this.itemWidth
            })
        }
    }

    draw() {
        this.ctx.globalCompositeOperation = 'xor'
        this.ctx.fillStyle = 'black'
        this.ctx.beginPath()
        this.ctx.moveTo(this.points[0].x, this.points[0].y)
        for (let i = 1; i < this.points.length; i++) {
            let point = this.points[i]
            this.ctx.lineTo(point.x, point.y)
        }
        this.ctx.closePath()
        this.ctx.fill()
    }
}

使用:

let points = [
    [{ x: 4, y: 5 }, { x: 8, y: 9 }, { x: 4, y: 9 }],
    [{ x: 10, y: 8 }, { x: 10, y: 12 }, { x: 6, y: 12 }],
    [{ x: 7, y: 4 }, { x: 11, y: 4 }, { x: 11, y: 8 }]
]
points.map((sub_points) => {
    let block = new Block()
    block.init(sub_points, this.itemWidth, this.ctx)
    block.draw()
})

效果如下图:

CanvasRenderingContext2D其他使用方法可参考:CanvasRenderingContext2D API 列表

1 + 1 = 0,「偶消奇不消」的效果如何实现?

1 + 1 = 0,是层叠拼图Plus小游戏玩法的精髓所在。

有经验的同学,也许一眼就发现了,1 + 1 = 0 刚好符合通过 异或运算 得出的结果。当然,细心的同学也可能已经发现,在 如何绘制任意多边形图形 这一章节内,有一句特殊的代码:this.ctx.globalCompositeOperation = 'xor',也正是通过设置 CanvasContextglobalCompositeOperation 属性值为 xor 便实现了「偶消奇不消」的神奇效果。

globalCompositeOperation

globalCompositeOperation 是指 在绘制新形状时应用的合成操作的类型,其他效果可参考:globalCompositeOperation 示例

如何判断一个点是否在任意多边形内部?

当回转数为 0 时,点在闭合曲线外部。

讲到这里,我们已经知道如何在Canvas画布内绘制出偶消奇不消效果的层叠图形了,接下来我们来看下玩家如何移动选中的图形。我们发现绘制出的图形对象并没有提供点击事件绑定之类的操作,那又如何判断玩家选中了哪个图形呢?这里我们就需要去实现如何判断玩家触摸事件的xy坐标在哪个多边形图形内部区域,从而判断出玩家选中的是哪一个多边形图形。

判断一个点是否在任意多边形内部有多种方法,比如:

  • 射线法
  • 面积判别法
  • 叉乘判别法
  • 回转数法
  • ...

层叠拼图Plus小游戏内,采用的是 回转数 法来判断玩家触摸点是否在多边形内部。回转数 是拓扑学中的一个基本概念,具有很重要的性质和用途。当然,展开讨论 回转数 的概念并不在该文的讨论范围内,我们仅需了解一个概念:当回转数为 0 时,点在闭合曲线外部。

上面面这张图动态演示了回转数的概念:图中红色曲线关于点(人所在位置)的回转数为 2

对于给定的点和多边形,回转数应该怎么计算呢?

  • 用线段分别连接点和多边形的全部顶点

  • 计算所有点与相邻顶点连线的夹角

  • 计算所有夹角和。注意每个夹角都是有方向的,所以有可能是负值

最后根据角度累加值计算回转数。360°(2π)相当于一次回转。

在使用 JavaScript 实现时,需要注意以下问题:

  • JavaScript 的数只有 64 位双精度浮点这一种。对于三角函数产生的无理数,浮点数计算不可避免会造成一些误差,因此在最后计算回转数需要做取整操作。
  • 通常情况下,平面直角坐标系内一个角的取值范围是 -π 到 π 这个区间,这也是 JavaScript 三角函数 Math.atan2() 返回值的范围。但 JavaScript 并不能直接计算任意两条线的夹角,我们只能先计算两条线与 x 正轴夹角,再取两者差值。这个差值的结果就有可能超出 π 这个区间,因此我们还需要处理差值超出取值区间的情况。

代码实现:

/**
 * 判断点是否在多边形内/边上
 */
isPointInPolygon(p, poly) {
    let px = p.x,
        py = p.y,
        sum = 0

    for (let i = 0, l = poly.length, j = l - 1; i < l; j = i, i++) {
        let sx = poly[i].x,
            sy = poly[i].y,
            tx = poly[j].x,
            ty = poly[j].y

        // 点与多边形顶点重合或在多边形的边上
        if ((sx - px) * (px - tx) >= 0 &&
            (sy - py) * (py - ty) >= 0 &&
            (px - sx) * (ty - sy) === (py - sy) * (tx - sx)) {
            return true
        }

        // 点与相邻顶点连线的夹角
        let angle = Math.atan2(sy - py, sx - px) - Math.atan2(ty - py, tx - px)

        // 确保夹角不超出取值范围(-π 到 π)
        if (angle >= Math.PI) {
            angle = angle - Math.PI * 2
        } else if (angle <= -Math.PI) {
            angle = angle + Math.PI * 2
        }
        sum += angle
    }

    // 计算回转数并判断点和多边形的几何关系
    return Math.round(sum / Math.PI) === 0 ? false : true
}

注:该章节内容图片均来自网络,如有侵权,请告知删除。另外有兴趣的同学可以使用其他方法来实现判断一个点是否在任意多边形内部。

如何判断游戏结果是否正确?

探索的过程固然精彩,而结果却更令我们期待

通过前面的介绍我们可以知道,判断游戏结果是否正确其实就是比对玩家组合图形的 xor 结果与目标图形的 xor 结果。那么如何求多个多边形 xor 的结果呢? polygon-clipping 正是为此而生的。它不仅支持 xor 操作,还有其他的比如:union, intersection, difference 等操作。
层叠拼图Plus游戏内通过 polygon-clipping 又是怎样实现游戏结果判断的呢?

  • 目标图形

多边形平面坐标点集合:

points = [
    [{ x: 6, y: 6 }, { x: 10, y: 6 }, { x: 10, y: 10 }, { x: 6, y: 10 }],
    [{ x: 8, y: 6 }, { x: 10, y: 8 }, { x: 8, y: 10 }, { x: 6, y: 8 }]
]
/**
 * 获取 多个多边形 xor 结果
 */
const polygonClipping = require('polygon-clipping')

polygonXor(points) {
    let poly = []
    points.forEach(function (sub_points) {
        let temp = []
        sub_points.forEach(function (point) {
            temp.push([point.x, point.y])
        })
        poly.push([temp])
    })

    let results = polygonClipping.xor(...poly)

    // 找出左上角的点
    let min_x = 100, min_y = 100
    results.forEach(function (sub_results) {
        sub_results.forEach(function (temps) {
            temps.forEach(function (point) {
                if (point[0] < min_x) min_x = point[0]
                if (point[1] < min_y) min_y = point[1]
            })
        })
    })

    // 以左上角为参考点 多边形平移至 原点 {x: 0, y: 0}
    results.forEach(function (sub_results) {
        sub_results.forEach(function (temps) {
            temps.forEach(function (point) {
                point[0] -= min_x
                point[1] -= min_y
            })
        })
    })
}
let result = this.polygonXor(points)

xor结果:

[
    [[[0, 0], [2, 0], [0, 2], [0, 0]]],
    [[[0, 2], [2, 4], [0, 4], [0, 2]]],
    [[[2, 0], [4, 0], [4, 2], [2, 0]]],
    [[[2, 4], [4, 2], [4, 4], [2, 4]]]
]

同理计算出玩家操作图形的xor结果进行比对即可得出答案正确与否。

需要注意的是,获取玩家的 xor 结果并不能直接拿来与目标图形xor 结果进行比较,我们需要将xor 的结果以左上角为参考点将图形平移至原点内,然后再进行比较,如果结果一致,则代表玩家答案正确。

排行榜的展示

有人的地方就有江湖,有江湖的地方就有排行

在看本章节内容之前,建议先浏览一遍排行榜相关的官方文档:好友排行榜关系链数据,以便对相关内容有个大概的了解。

  • 开放数据域

开放数据域是一个封闭、独立的 JavaScript 作用域。要让代码运行在开放数据域,需要在 game.json 中添加配置项 openDataContext 指定开放数据域的代码目录。添加该配置项表示小游戏启用了开放数据域,这将会导致一些限制。

// game.json
{
  "openDataContext": "src/myOpenDataContext"
}
  • 在游戏内使用 wx.setUserCloudStorage(obj) 对玩家游戏数据进行托管。
  • 在开放数据域内使用 wx.getFriendCloudStorage(obj)拉取当前用户所有同玩好友的托管数据
  • 展示关系链数据

如果想要展示通过关系链 API 获取到的用户数据,如绘制排行榜等业务场景,需要将排行榜绘制到 sharedCanvas 上,再在主域将 sharedCanvas 渲染上屏。

// src/myOpenDataContext/index.js
let sharedCanvas = wx.getSharedCanvas()

function drawRankList (data) {
  data.forEach((item, index) => {
    // ...
  })
}

wx.getFriendCloudStorage({
  success: res => {
    let data = res.data
    drawRankList(data)
  }
})

sharedCanvas 是主域和开放数据域都可以访问的一个离屏画布。在开放数据域调用 wx.getSharedCanvas() 将返回 sharedCanvas

// src/myOpenDataContext/index.js
let sharedCanvas = wx.getSharedCanvas()
let context = sharedCanvas.getContext('2d')
context.fillStyle = 'red'
context.fillRect(0, 0, 100, 100)

在主域中可以通过开放数据域实例访问 sharedCanvas,通过 drawImage() 方法可以将 sharedCanvas 绘制到上屏画布。

// game.js
let openDataContext = wx.getOpenDataContext()
let sharedCanvas = openDataContext.canvas

let canvas = wx.createCanvas()
let context = canvas.getContext('2d')
context.drawImage(sharedCanvas, 0, 0)

sharedCanvas 本质上也是一个离屏 Canvas,而重设 Canvas 的宽高会清空 Canvas 上的内容。所以要通知开放数据域去重绘 sharedCanvas

// game.js
openDataContext.postMessage({
  command: 'render'
})

// src/myOpenDataContext/index.js
openDataContext.onMessage(data => {
  if (data.command === 'render') {
    // 重绘 sharedCanvas
  }
})

需要注意的是:sharedCanvas 的宽高只能在主域设置,不能在开放数据域中设置。

游戏性能优化

性能优化,简而言之,就是在不影响系统运行正确性的前提下,使之运行地更快,完成特定功能所需的时间更短。

一款能让人心情愉悦的游戏,性能问题必然不能成为绊脚石。那么可以从哪些方面对游戏进行性能优化呢?

离屏 Canvas

层叠拼图Plus小游戏内,针对需要大量使用且绘图繁复的静态场景,都是使用离屏 Canvas进行绘制的,如首页网格背景、关卡列表、排名列表等。在微信内 wx.createCanvas() 首次调用创建的是显示在屏幕上的画布,之后调用创建的都是离屏画布。初始化时将静态场景绘制完备,需要时直接拷贝离屏Canvas的图像即可。Canvas 绘制本身就是不断的更新帧从而达到动画的效果,通过使用离屏 Canvas,就大大减少了一些静态内容在上屏Canvas的绘制,从而提升了绘制性能。

this.offScreenCanvas = wx.createCanvas()
this.offScreenCanvas.width = this.width * ratio
this.offScreenCanvas.height = this.height * ratio

this.ctx.drawImage(this.offScreenCanvas, x * ratio, y * ratio, this.offScreenCanvas.width, this.offScreenCanvas.height)

内存优化

玩家在游戏过程中拖动方块的移动其实就是不断更新多边形图形的坐标信息,然后不断的清空画布再重新绘制,可以想象,这个绘制是非常频繁的,按照普通的做法就需要不断去创建多个新的 Block 对象。针对游戏中需要频繁更新的对象,我们可以通过使用对象池的方法进行优化,对象池维护一个装着空闲对象的池子,如果需要对象的时候,不是直接new,而是从对象池中取出,如果对象池中没有空闲对象,则新建一个空闲对象,层叠拼图Plus小游戏内使用的是官方demo内已经实现的对象池类,实现如下:

const __ = {
  poolDic: Symbol('poolDic')
}

/**
 * 简易的对象池实现
 * 用于对象的存贮和重复使用
 * 可以有效减少对象创建开销和避免频繁的垃圾回收
 * 提高游戏性能
 */
export default class Pool {
  constructor() {
    this[__.poolDic] = {}
  }

  /**
   * 根据对象标识符
   * 获取对应的对象池
   */
  getPoolBySign(name) {
    return this[__.poolDic][name] || ( this[__.poolDic][name] = [] )
  }

  /**
   * 根据传入的对象标识符,查询对象池
   * 对象池为空创建新的类,否则从对象池中取
   */
  getItemByClass(name, className) {
    let pool = this.getPoolBySign(name)

    let result = (  pool.length
                  ? pool.shift()
                  : new className()  )

    return result
  }

  /**
   * 将对象回收到对象池
   * 方便后续继续使用
   */
  recover(name, instance) {
    this.getPoolBySign(name).push(instance)
  }
}

垃圾回收

小游戏中,JavaScript 中的每一个 CanvasImage 对象都会有一个客户端层的实际纹理储存,实际纹理储存中存放着 CanvasImage 的真实纹理,通常会占用相当一部分内存。

每个客户端实际纹理储存的回收时机依赖于 JavaScript 中的 CanvasImage 对象回收。在 JavaScriptCanvasImage 对象被回收之前,客户端对应的实际纹理储存不会被回收。通过调用 wx.triggerGC() 方法,可以加快触发 JavaScriptCore Garbage Collection(垃圾回收),从而触发 JavaScript 中没有引用的 CanvasImage 回收,释放对应的实际纹理储存。

GC 具体触发时机还要取决于 JavaScriptCore 自身机制,并不能保证调用 wx.triggerGC() 能马上触发回收,层叠拼图Plus小游戏在每局游戏开始或结束都会触发一下,及时回收内存垃圾,以保证最良好的游戏体验。

多线程 Worker

对于游戏来说,每帧 16ms 是极其宝贵的,如果有一些可以异步处理的任务,可以放置于 Worker 中运行,待运行结束后,再把结果返回到主线程。Worker 运行于一个单独的全局上下文与线程中,不能直接调用主线程的方法,Worker 也不具备渲染的能力。 Worker与主线程之间的数据传输,双方使用 Worker.postMessage() 来发送数据,Worker.onMessage() 来接收数据,传输的数据并不是直接共享,而是被复制的。

// game.json
{
  "workers": "workers"
}

// 创建worker线程
let worker = worker = wx.createWorker('workers/request/index.js') // 文件名指定 worker 的入口文件路径,绝对路径

// 主线程向 Worker 发送消息
worker.postMessage({
  msg: 'hello worker'
})

// 主线程监听 Worker 返回消息
worker.onMessage(function (res) {
  console.log(res)
})

需要注意的是:Worker 最大并发数量限制为 1 个,创建下一个前请用 Worker.terminate() 结束当前 Worker

其他 Worker 相关的内容请参考微信官方文档:多线程 Worker

结语

短短的一篇文章,定不能将层叠拼图Plus小游戏的前前后后讲明白讲透彻,加上文笔有限,有描述不当的地方还望多多海涵。其实最让人心累的还是软著的申请过程,由于各种原因前前后后花了将近三个月的时间,本来也想写一下软著申请相关的内容,最后发现篇幅有点长,无奈作罢,争取后面花点时间整理一下我这边的经验,希望可以帮助到需要的童鞋。

由于项目结构以及代码还比较混乱,个人觉得,目前暂时还不适合开源。好在,小游戏内的所有核心代码以及游戏实现思想均已呈上,有兴趣的同学如果有相关方面的疑问也可以与我多多交流,大家互相学习,共同进步。

江湖不远,我们游戏里见!

查看原文

夕君 提出了问题 · 2019-09-06

Jenkins node Error: EACCES: permission denied, mkdir

jenkins node 全局安装了 webpack

使用全局安装的 webpack 编译代码, 无法创建 dist 目录 , 提示没用权限

错误栈

···
at Array.reduce (<anonymous>)

at mkdir (E:\tools\Jenkins\tools\jenkins.plugins.nodejs.tools.NodeJSInstallation\node12\node_modules\@panli\panda\node_modules\write-to-file-webpack\index.js:13:32

···

关注 2 回答 1

夕君 提出了问题 · 2019-08-26

nodejs log4js 按日期目录写文件

log4js.configure

log4js.configure({
    replaceConsole: true,
    appenders: {
        cheese: {
            // 设置类型为 dateFile
            type: 'dateFile',
            // 配置文件名为 myLog.log
            filename: 'logs/myLog.log',
            // 指定编码格式为 utf-8
            encoding: 'utf-8',
            // 配置 layout,此处使用自定义模式 pattern
            layout: {
                type: "pattern",
                // 配置模式,下面会有介绍
                pattern: '{"date":"%d","level":"%p","category":"%c","host":"%h","pid":"%z","data":\'%m\'}'
            },
            // 日志文件按日期(天)切割
            pattern: "-yyyy-MM-dd",
            // 回滚旧的日志文件时,保证以 .log 结尾 (只有在 alwaysIncludePattern 为 false 生效)
            keepFileExt: true,
            // 输出的日志文件名是都始终包含 pattern 日期结尾
            alwaysIncludePattern: true,
        },
    },
    categories: {
        // 设置默认的 categories
        default: {appenders: ['cheese'], level: 'debug'},
    }
});

这时候已经定义好了 要写入的目录

问题出现的环境背景及自己尝试过哪些方法

后来想到修改 filename 目录 为

filename: logs/${new Date().getDate()}/info.log,

你期待的结果是什么?

每次写入日志的时候 获取 当前日期

logs/20190826/info.log

关注 2 回答 1

夕君 收藏了文章 · 2019-05-26

两个最多可以提高千倍效率的Go语言代码小技巧

http://dawngrp.com/gao-xiao-de-goyu-yan-bian-ma-ji-qiao/

1.不要使用+和fmt.Sprintf操作字符串

+操作字符串很方便,但是真的很慢,在Go语言里使用+会导致你的程序跑得可能比脚本语言还满,不相信的可以自己做个测试,用+操作,迭代十万次,Python、Javascript都比Go快很多(是很多噢,不是一点点)

func TestStr(t *testing.T) {
    str := ""
    for i := 0; i < 100000; i++ {
        str += "test"
    }
}

测试结果

PASS: TestStr (3.32s)
str=""
for i in range(100000):
    str+="test"

测试结果:

~/» time python test.py
0.03s user 0.03s system 81% cpu 0.078 total

作为静态语言的Go,居然在这么一个段简单的代码上执行效率比Python慢了100倍,不可思议吧?不是Go的问题,而是在Go中使用+处理字符串是很消耗性能的,而Python应该是对+操作字符串进行了重载优化。(Javascript +操作字符串也很快)

最有效的方式是采用buffer
strBuf := bytes.NewBufferString("")
for i := 0; i < 100000; i++ {
    strBuf.WriteString("test")
}

结果可以自己测试,会让你很惊讶

有一些需要简单组合两个字符串,用Buffer麻烦了点,比较容易让人想到的就是用fmt.Sprintf()来组合,很多包里的源码也是这么写的。其实fmt的Sprintf也非常慢,如果没有复杂的类型转换输出的情况下,使用strings.Join性能会高很多

func TestStr(t *testing.T) {
    a, b := "Hello", "world"
    for i := 0; i < 1000000; i++ {
        fmt.Sprintf("%s%s", a, b)
        //strings.Join([]string{a, b}, "")
    }
}
PASS: TestStr (0.29s)
func TestStr(t *testing.T) {
    a, b := "Hello", "world"
    for i := 0; i < 1000000; i++ {
        //fmt.Sprintf("%s%s", a, b)
        strings.Join([]string{a, b}, "")
    }
}
PASS: TestStr (0.09s)

从结果来看strings.Join 比用Sprint快4倍左右吧。

2.对于固定字段的键值对,用临时Struct,不要用map[string]interface{}

举个简单的例子

func TestData(t *testing.T) {

    for i := 0; i < 100000000; i++ {
        var a struct {
            Name string
            Age  int
        }
        a.Name = "Hello"
        a.Age = 10
    }
}
PASS: TestData (0.04s)
func TestData2(t *testing.T) {

    for i := 0; i < 100000000; i++ {
        var a = map[string]interface{}{}
        a["Name"] = "Hello"
        a["Age"] = 10
    }
}
PASS: TestData2 (38.30s)

相差上千倍的效率呢!
在能够知道字段的情况下,用临时Struct在运行期间不需要动态分配内容,并且不需要像map那样去检查索引,所以速度会快非常多。

查看原文

夕君 提出了问题 · 2019-03-29

vue 数组 list 绑定 动态 class


<li v-for="(item, index) in list" :key="index" class="li1" >
</li>

希望渲染出来的 class 是 li1li2li3

关注 2 回答 1

夕君 收藏了文章 · 2019-03-09

2019前端面试题汇总(主要为Vue)

毕业之后就在一直合肥小公司工作,没有老司机、没有技术氛围,在技术的道路上我只能独自摸索。老板也只会画饼充饥,前途一片迷茫看不到任何希望。于是乎,我果断辞职,在新年开工之际来到杭州,这里的互联网公司应该是合肥的几十倍吧。。。。
刚来3天,面试了几家公司,有些规模比较小,有些是创业公司,也有些已经发展的不错了;今天把最近的面试题目做个汇总,也给自己复个盘,由于我的技术栈主要为Vue,所以大部分题目都是Vue开发相关的。

1. 谈谈你对MVVM开发模式的理解

MVVM分为Model、View、ViewModel三者。
Model 代表数据模型,数据和业务逻辑都在Model层中定义;
View 代表UI视图,负责数据的展示;
ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;
ModelView 并无直接关联,而是通过 ViewModel 来进行联系的,ModelViewModel 之间有着双向数据绑定的联系。因此当 Model 中的数据改变时会触发 View 层的刷新,View 中由于用户交互操作而改变的数据也会在 Model 中同步。
这种模式实现了 ModelView 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作 dom

2. Vue 有哪些指令?

v-html、v-show、v-if、v-for等等

3. v-if 和 v-show 有什么区别?

v-show 仅仅控制元素的显示方式,将 display 属性在 block 和 none 来回切换;而v-if会控制这个 DOM 节点的存在与否。当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。

4. 简述Vue的响应式原理

当一个Vue实例创建时,vue会遍历data选项的属性,用 Object.defineProperty 将它们转为 getter/setter并且在内部追踪相关依赖,在属性被访问和修改时通知变化。
每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而致使它关联的组件得以更新。

5. Vue中如何在组件内部实现一个双向数据绑定?

假设有一个输入框组件,用户输入时,同步父组件页面中的数据
具体思路:父组件通过 props 传值给子组件,子组件通过 $emit 来通知父组件修改相应的props值,具体实现如下:

import Vue from 'vue'

const component = {
  props: ['value'],
  template: `
    <div>
      <input type="text" @input="handleInput" :value="value">
    </div>
  `,
  data () {
    return {
    }
  },
  methods: {
    handleInput (e) {
      this.$emit('input', e.target.value)
    }
  }
}

new Vue({
  components: {
    CompOne: component
  },
  el: '#root',
  template: `
    <div>
      <comp-one :value1="value" @input="value = arguments[0]"></comp-one>
    </div>
  `,
  data () {
    return {
      value: '123'
    }
  }
})

可以看到,当输入数据时,父子组件中的数据是同步改变的:
Image 1.png

Image 2.png

我们在父组件中做了两件事,一是给子组件传入props,二是监听input事件并同步自己的value属性。那么这两步操作能否再精简一下呢?答案是可以的,你只需要修改父组件:

template: `
    <div>
      <!--<comp-one :value1="value" @input="value = arguments[0]"></comp-one>-->
      <comp-one v-model="value"></comp-one>
    </div>
  `

v-model 实际上会帮我们完成上面的两步操作。

6. Vue中如何监控某个属性值的变化?

比如现在需要监控data中,obj.a 的变化。Vue中监控对象属性的变化你可以这样:

watch: {
      obj: {
      handler (newValue, oldValue) {
        console.log('obj changed')
      },
      deep: true
    }
  }

deep属性表示深层遍历,但是这么写会监控obj的所有属性变化,并不是我们想要的效果,所以做点修改:

watch: {
   'obj.a': {
      handler (newName, oldName) {
        console.log('obj.a changed')
      }
   }
  }

还有一种方法,可以通过computed 来实现,只需要:

computed: {
    a1 () {
      return this.obj.a
    }
}

利用计算属性的特性来实现,当依赖改变时,便会重新计算一个新值。

7. Vue中给data中的对象属性添加一个新的属性时会发生什么,如何解决?

示例:

<template>
  <div>
    <ul>
      <li v-for="value in obj" :key="value">
        {{value}}
      </li>
    </ul>
    <button @click="addObjB">添加obj.b</button>
  </div>
</template>
<script>
export default {
  data () {
    return {
      obj: {
        a: 'obj.a'
      }
    }
  },
  methods: {
    addObjB () {
      this.obj.b = 'obj.b'
      console.log(this.obj)
    }
  }
}
</script>
<style></style>

点击button会发现,obj.b 已经成功添加,但是视图并未刷新:
Image 3.png

Image 4.png

原因在于在Vue实例创建时,obj.b并未声明,因此就没有被Vue转换为响应式的属性,自然就不会触发视图的更新,这时就需要使用Vue的全局api $set():

addObjB () {
      // this.obj.b = 'obj.b'
      this.$set(this.obj, 'b', 'obj.b')
      console.log(this.obj)
    }

$set()方法相当于手动的去把obj.b处理成一个响应式的属性,此时视图也会跟着改变了:
Image 5.png

8. delete和Vue.delete删除数组的区别

delete只是被删除的元素变成了 empty/undefined 其他的元素的键值还是不变。
Vue.delete直接删除了数组 改变了数组的键值。

    var a=[1,2,3,4]
    var b=[1,2,3,4]
    delete a[1]
    console.log(a)
    this.$delete(b,1)
    console.log(b)

Image 6.png

Image 7.png

9.如何优化SPA应用的首屏加载速度慢的问题?

  • 将公用的JS库通过script标签外部引入,减小app.bundel的大小,让浏览器并行下载资源文件,提高下载速度;
  • 在配置 路由时,页面和组件使用懒加载的方式引入,进一步缩小 app.bundel 的体积,在调用某个组件时再加载对应的js文件;
  • 加一个首屏 loading 图,提升用户体验;

10. 前端如何优化网站性能?

  1. 减少 HTTP 请求数量

在浏览器与服务器进行通信时,主要是通过 HTTP 进行通信。浏览器与服务器需要经过三次握手,每次握手需要花费大量时间。而且不同浏览器对资源文件并发请求数量有限(不同浏览器允许并发数),一旦 HTTP 请求数量达到一定数量,资源请求就存在等待状态,这是很致命的,因此减少 HTTP 的请求数量可以很大程度上对网站性能进行优化。

    • CSS Sprites:国内俗称 CSS 精灵,这是将多张图片合并成一张图片达到减少 HTTP 请求的一种解决方案,可以通过 CSS background 属性来访问图片内容。这种方案同时还可以减少图片总字节数。
    • 合并 CSS 和 JS 文件:现在前端有很多工程化打包工具,如:grunt、gulp、webpack等。为了减少 HTTP 请求数量,可以通过这些工具再发布前将多个 CSS 或者 多个 JS 合并成一个文件。
    • 采用 lazyLoad:俗称懒加载,可以控制网页上的内容在一开始无需加载,不需要发请求,等到用户操作真正需要的时候立即加载出内容。这样就控制了网页资源一次性请求数量。
    1. 控制资源文件加载优先级

    浏览器在加载 HTML 内容时,是将 HTML 内容从上至下依次解析,解析到 link 或者 script 标签就会加载 href 或者 src 对应链接内容,为了第一时间展示页面给用户,就需要将 CSS 提前加载,不要受 JS 加载影响。
    一般情况下都是 CSS 在头部,JS 在底部。

    1. 利用浏览器缓存
      浏览器缓存是将网络资源存储在本地,等待下次请求该资源时,如果资源已经存在就不需要到服务器重新请求该资源,直接在本地读取该资源。
    2. 减少重排(Reflow)
      基本原理:重排是 DOM 的变化影响到了元素的几何属性(宽和高),浏览器会重新计算元素的几何属性,会使渲染树中受到影响的部分失效,浏览器会验证 DOM 树上的所有其它结点的 visibility 属性,这也是 Reflow 低效的原因。如果 Reflow 的过于频繁,CPU 使用率就会急剧上升。

    减少 Reflow,如果需要在 DOM 操作时添加样式,尽量使用 增加 class 属性,而不是通过 style 操作样式。

    1. 减少 DOM 操作
    2. 图标使用 IconFont 替换

    11. 网页从输入网址到渲染完成经历了哪些过程?

    大致可以分为如下7步:

    1. 输入网址;
    2. 发送到DNS服务器,并获取域名对应的web服务器对应的ip地址;
    3. 与web服务器建立TCP连接;
    4. 浏览器向web服务器发送http请求;
    5. web服务器响应请求,并返回指定url的数据(或错误信息,或重定向的新的url地址);
    6. 浏览器下载web服务器返回的数据及解析html源文件;
    7. 生成DOM树,解析css和js,渲染页面,直至显示完成;

    12. jQuery获取的dom对象和原生的dom对象有何区别?

    js原生获取的dom是一个对象,jQuery对象就是一个数组对象,其实就是选择出来的元素的数组集合,所以说他们两者是不同的对象类型不等价。

    • 原生DOM对象转jQuery对象:
    var box = document.getElementById('box');
    var $box = $(box);
    • jQuery对象转原生DOM对象:
    var $box = $('#box');
    var box = $box[0];

    13. jQuery如何扩展自定义方法

    (jQuery.fn.myMethod=function () {
           alert('myMethod');
    })
    // 或者:
    (function ($) {
            $.fn.extend({
                 myMethod : function () {
                      alert('myMethod');
                 }
            })
    })(jQuery)

    使用:

    $("#div").myMethod();

    目前来看公司面试的问题还是比较基础的,但是对于某些只追求会用并不研究其原理的同学来说可能就没那么容易了。所以大家不仅要追求学习的广度,更要追求深度。
    OK,希望自己能早日拿到心仪的offer.

    参考:
    浅谈网站性能之前端性能优化

    查看原文

    夕君 收藏了文章 · 2019-02-23

    面试官问:JS的继承

    前言

    这是面试官问系列的第五篇,旨在帮助读者提升JS基础知识,包含new、call、apply、this、继承相关知识。
    面试官问系列文章如下:感兴趣的读者可以点击阅读。
    1.面试官问:能否模拟实现JS的new操作符
    2.面试官问:能否模拟实现JS的bind方法
    3.面试官问:能否模拟实现JS的call和apply方法
    4.面试官问:JS的this指向
    5.面试官问:JS的继承

    用过React的读者知道,经常用extends继承React.Component

    // 部分源码
    function Component(props, context, updater) {
      // ...
    }
    Component.prototype.setState = function(partialState, callback){
        // ...
    }
    const React = {
        Component,
        // ...
    }
    // 使用
    class index extends React.Component{
        // ...
    }

    点击这里查看 React github源码

    面试官可以顺着这个问JS继承的相关问题,比如:ES6class继承用ES5如何实现。据说很多人答得不好。<br/>

    构造函数、原型对象和实例之间的关系

    要弄懂extends继承之前,先来复习一下构造函数、原型对象和实例之间的关系。
    代码表示:

    function F(){}
    var f = new F();
    // 构造器
    F.prototype.constructor === F; // true
    F.__proto__ === Function.prototype; // true
    Function.prototype.__proto__ === Object.prototype; // true
    Object.prototype.__proto__ === null; // true
    
    // 实例
    f.__proto__ === F.prototype; // true
    F.prototype.__proto__ === Object.prototype; // true
    Object.prototype.__proto__ === null; // true

    笔者画了一张图表示:
    构造函数-原型对象-实例关系图By@若川

    ES6 extends 继承做了什么操作

    我们先看看这段包含静态方法的ES6继承代码:

    // ES6
    class Parent{
        constructor(name){
            this.name = name;
        }
        static sayHello(){
            console.log('hello');
        }
        sayName(){
            console.log('my name is ' + this.name);
            return this.name;
        }
    }
    class Child extends Parent{
        constructor(name, age){
            super(name);
            this.age = age;
        }
        sayAge(){
            console.log('my age is ' + this.age);
            return this.age;
        }
    }
    let parent = new Parent('Parent');
    let child = new Child('Child', 18);
    console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
    Parent.sayHello(); // hello
    parent.sayName(); // my name is Parent
    console.log('child: ', child); // child:  Child {name: "Child", age: 18}
    Child.sayHello(); // hello
    child.sayName(); // my name is Child
    child.sayAge(); // my age is 18

    其中这段代码里有两条原型链,不信看具体代码。

    // 1、构造器原型链
    Child.__proto__ === Parent; // true
    Parent.__proto__ === Function.prototype; // true
    Function.prototype.__proto__ === Object.prototype; // true
    Object.prototype.__proto__ === null; // true
    // 2、实例原型链
    child.__proto__ === Child.prototype; // true
    Child.prototype.__proto__ === Parent.prototype; // true
    Parent.prototype.__proto__ === Object.prototype; // true
    Object.prototype.__proto__ === null; // true

    一图胜千言,笔者也画了一张图表示,如图所示:

    ES6继承(extends)关系图By@若川
    结合代码和图可以知道。
    ES6 extends 继承,主要就是:

    1. 把子类构造函数(Child)的原型(__proto__)指向了父类构造函数(Parent),
    2. 把子类实例child的原型对象(Child.prototype) 的原型(__proto__)指向了父类parent的原型对象(Parent.prototype)。

    这两点也就是图中用不同颜色标记的两条线。

    1. 子类构造函数Child继承了父类构造函数Preant的里的属性。使用super调用的(ES5则用call或者apply调用传参)。

    也就是图中用不同颜色标记的两条线。
    看过《JavaScript高级程序设计-第3版》 章节6.3继承的读者应该知道,这2和3小点,正是寄生组合式继承,书中例子没有第1小点
    1和2小点都是相对于设置了__proto__链接。那问题来了,什么可以设置了__proto__链接呢。

    newObject.createObject.setPrototypeOf可以设置__proto__

    说明一下,__proto__这种写法是浏览器厂商自己的实现。
    再结合一下图和代码看一下的newnew出来的实例的__proto__指向构造函数的prototype,这就是new做的事情。
    摘抄一下之前写过文章的一段。面试官问:能否模拟实现JS的new操作符,有兴趣的读者可以点击查看。

    new做了什么:

    1. 创建了一个全新的对象。
    2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
    3. 生成的新对象会绑定到函数调用的this
    4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
    5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

    Object.createES5提供的

    Object.create(proto, [propertiesObject])
    方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。
    它接收两个参数,不过第二个可选参数是属性描述符(不常用,默认是undefined)。对于不支持ES5的浏览器,MDN上提供了ployfill方案。
    MDN Object.create()

    // 简版:也正是应用了new会设置__proto__链接的原理。
    if(typeof Object.create !== 'function'){
        Object.create = function(proto){
            function F() {}
            F.prototype = proto;
            return new F();
        }
    }

    Object.setPrototypeOfES6提供的

    Object.setPrototypeOfMDN

    Object.setPrototypeOf() 方法设置一个指定的对象的原型 ( 即, 内部[[Prototype]]属性)到另一个对象或 null
    Object.setPrototypeOf(obj, prototype)

    `ployfill`
    // 仅适用于Chrome和FireFox,在IE中不工作:
    Object.setPrototypeOf = Object.setPrototypeOf || function (obj, proto) {
      obj.__proto__ = proto;
      return obj; 
    }

    nodejs源码就是利用这个实现继承的工具函数的。
    nodejs utils inherits

    function inherits(ctor, superCtor) {
      if (ctor === undefined || ctor === null)
        throw new ERR_INVALID_ARG_TYPE('ctor', 'Function', ctor);
    
      if (superCtor === undefined || superCtor === null)
        throw new ERR_INVALID_ARG_TYPE('superCtor', 'Function', superCtor);
    
      if (superCtor.prototype === undefined) {
        throw new ERR_INVALID_ARG_TYPE('superCtor.prototype',
                                       'Object', superCtor.prototype);
      }
      Object.defineProperty(ctor, 'super_', {
        value: superCtor,
        writable: true,
        configurable: true
      });
      Object.setPrototypeOf(ctor.prototype, superCtor.prototype);
    }

    ES6extendsES5版本实现

    知道了ES6 extends继承做了什么操作和设置__proto__的知识点后,把上面ES6例子的用ES5就比较容易实现了,也就是说实现寄生组合式继承,简版代码就是:

    // ES5 实现ES6 extends的例子
    function Parent(name){
        this.name = name;
    }
    Parent.sayHello = function(){
        console.log('hello');
    }
    Parent.prototype.sayName = function(){
        console.log('my name is ' + this.name);
        return this.name;
    }
    
    function Child(name, age){
        // 相当于super
        Parent.call(this, name);
        this.age = age;
    }
    // new
    function object(){
        function F() {}
        F.prototype = proto;
        return new F();
    }
    function _inherits(Child, Parent){
        // Object.create
        Child.prototype = Object.create(Parent.prototype);
        // __proto__
        // Child.prototype.__proto__ = Parent.prototype;
        Child.prototype.constructor = Child;
        // ES6
        // Object.setPrototypeOf(Child, Parent);
        // __proto__
        Child.__proto__ = Parent;
    }
    _inherits(Child,  Parent);
    Child.prototype.sayAge = function(){
        console.log('my age is ' + this.age);
        return this.age;
    }
    var parent = new Parent('Parent');
    var child = new Child('Child', 18);
    console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
    Parent.sayHello(); // hello
    parent.sayName(); // my name is Parent
    console.log('child: ', child); // child:  Child {name: "Child", age: 18}
    Child.sayHello(); // hello
    child.sayName(); // my name is Child
    child.sayAge(); // my age is 18

    我们完全可以把上述ES6的例子通过babeljs转码成ES5来查看,更严谨的实现。

    // 对转换后的代码进行了简要的注释
    "use strict";
    // 主要是对当前环境支持Symbol和不支持Symbol的typeof处理
    function _typeof(obj) {
        if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
            _typeof = function _typeof(obj) {
                return typeof obj;
            };
        } else {
            _typeof = function _typeof(obj) {
                return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
            };
        }
        return _typeof(obj);
    }
    // _possibleConstructorReturn 判断Parent。call(this, name)函数返回值 是否为null或者函数或者对象。
    function _possibleConstructorReturn(self, call) {
        if (call && (_typeof(call) === "object" || typeof call === "function")) {
            return call;
        }
        return _assertThisInitialized(self);
    }
    // 如何 self 是void 0 (undefined) 则报错
    function _assertThisInitialized(self) {
        if (self === void 0) {
            throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
        }
        return self;
    }
    // 获取__proto__
    function _getPrototypeOf(o) {
        _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
            return o.__proto__ || Object.getPrototypeOf(o);
        };
        return _getPrototypeOf(o);
    }
    // 寄生组合式继承的核心
    function _inherits(subClass, superClass) {
        if (typeof superClass !== "function" && superClass !== null) {
            throw new TypeError("Super expression must either be null or a function");
        }
        // Object.create()方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__。 
        // 也就是说执行后 subClass.prototype.__proto__ === superClass.prototype; 这条语句为true
        subClass.prototype = Object.create(superClass && superClass.prototype, {
            constructor: {
                value: subClass,
                writable: true,
                configurable: true
            }
        });
        if (superClass) _setPrototypeOf(subClass, superClass);
    }
    // 设置__proto__
    function _setPrototypeOf(o, p) {
        _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
            o.__proto__ = p;
            return o;
        };
        return _setPrototypeOf(o, p);
    }
    // instanceof操作符包含对Symbol的处理
    function _instanceof(left, right) {
        if (right != null && typeof Symbol !== "undefined" && right[Symbol.hasInstance]) {
            return right[Symbol.hasInstance](left);
        } else {
            return left instanceof right;
        }
    }
    
    function _classCallCheck(instance, Constructor) {
        if (!_instanceof(instance, Constructor)) {
            throw new TypeError("Cannot call a class as a function");
        }
    }
    // 按照它们的属性描述符 把方法和静态属性赋值到构造函数的prototype和构造器函数上
    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);
        }
    }
    // 把方法和静态属性赋值到构造函数的prototype和构造器函数上
    function _createClass(Constructor, protoProps, staticProps) {
        if (protoProps) _defineProperties(Constructor.prototype, protoProps);
        if (staticProps) _defineProperties(Constructor, staticProps);
        return Constructor;
    }
    
    // ES6
    var Parent = function () {
        function Parent(name) {
            _classCallCheck(this, Parent);
            this.name = name;
        }
        _createClass(Parent, [{
            key: "sayName",
            value: function sayName() {
                console.log('my name is ' + this.name);
                return this.name;
            }
        }], [{
            key: "sayHello",
            value: function sayHello() {
                console.log('hello');
            }
        }]);
        return Parent;
    }();
    
    var Child = function (_Parent) {
        _inherits(Child, _Parent);
        function Child(name, age) {
            var _this;
            _classCallCheck(this, Child);
            // Child.__proto__ => Parent
            // 所以也就是相当于Parent.call(this, name); 是super(name)的一种转换
            // _possibleConstructorReturn 判断Parent.call(this, name)函数返回值 是否为null或者函数或者对象。
            _this = _possibleConstructorReturn(this, _getPrototypeOf(Child).call(this, name));
            _this.age = age;
            return _this;
        }
        _createClass(Child, [{
            key: "sayAge",
            value: function sayAge() {
                console.log('my age is ' + this.age);
                return this.age;
            }
        }]);
        return Child;
    }(Parent);
    
    var parent = new Parent('Parent');
    var child = new Child('Child', 18);
    console.log('parent: ', parent); // parent:  Parent {name: "Parent"}
    Parent.sayHello(); // hello
    parent.sayName(); // my name is Parent
    console.log('child: ', child); // child:  Child {name: "Child", age: 18}
    Child.sayHello(); // hello
    child.sayName(); // my name is Child
    child.sayAge(); // my age is 18

    如果对JS继承相关还是不太明白的读者,推荐阅读以下书籍的相关章节,可以自行找到相应的pdf版本。

    推荐阅读JS继承相关的书籍章节

    《JavaScript高级程序设计第3版》-第6章 面向对象的程序设计,6种继承的方案,分别是原型链继承、借用构造函数继承、组合继承、原型式继承、寄生式继承、寄生组合式继承。图灵社区本书地址,后文放出github链接,里面包含这几种继承的代码demo

    《JavaScript面向对象编程第2版》-第6章 继承,12种继承的方案。1.原型链法(仿传统)、2.仅从原型继承法、3.临时构造器法、4.原型属性拷贝法、5.全属性拷贝法(即浅拷贝法)、6.深拷贝法、7.原型继承法、8.扩展与增强模式、9.多重继承法、10.寄生继承法、11.构造器借用法、12.构造器借用与属性拷贝法。

    ES6标准入门-第21章class的继承

    《深入理解ES6》-第9章JavaScript中的类

    《你不知道的JavaScript-上卷》第6章 行为委托和附录A ES6中的class

    总结

    继承对于JS来说就是父类拥有的方法和属性、静态方法等,子类也要拥有。子类中可以利用原型链查找,也可以在子类调用父类,或者从父类拷贝一份到子类等方案。
    继承方法可以有很多,重点在于必须理解并熟
    悉这些对象、原型以及构造器的工作方式,剩下的就简单了。寄生组合式继承是开发者使用比较多的。
    回顾寄生组合式继承。主要就是三点:

    1. 子类构造函数的__proto__指向父类构造器,继承父类的静态方法
    2. 子类构造函数的prototype__proto__指向父类构造器的prototype,继承父类的方法。
    3. 子类构造器里调用父类构造器,继承父类的属性。

    行文到此,文章就基本写完了。文章代码和图片等资源放在这里github inhertdemo展示es6-extends,结合console、source面板查看更佳。

    读者发现有不妥或可改善之处,欢迎评论指出。另外觉得写得不错,可以点赞、评论、转发,也是对笔者的一种支持。

    笔者学习源码整体架构系列

    1.学习 jQuery 源码整体架构,打造属于自己的 js 类库
    2.学习 underscore 源码整体架构,打造属于自己的函数式编程类库
    3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库
    4.学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK
    5.学习 vuex 源码整体架构,打造属于自己的状态管理库
    6.学习 axios 源码整体架构,打造属于自己的请求库
    7.学习 koa 源码的整体架构,浅析koa洋葱模型原理和co原理

    关于

    作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
    若川的博客,使用vuepress重构了,阅读体验可能更好些
    掘金专栏,欢迎关注~
    segmentfault前端视野专栏,欢迎关注~
    知乎前端视野专栏,欢迎关注~
    github blog,相关源码和资源都放在这里,求个star^_^~

    欢迎加微信交流和关注公众号

    可能比较有趣的微信公众号,长按扫码关注。也可以加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。

    若川视野

    查看原文

    认证与成就

    • 获得 61 次点赞
    • 获得 408 枚徽章 获得 29 枚金徽章, 获得 165 枚银徽章, 获得 214 枚铜徽章

    擅长技能
    编辑

    开源项目 & 著作
    编辑

    (゚∀゚ )
    暂时没有

    注册于 2015-03-09
    个人主页被 1.4k 人浏览