├── bottom-learning ├── ts.md ├── ES5.md ├── event-loop.md ├── css那些事儿.md ├── 浏览器相关知识.md ├── es6中元编程.md ├── 基础知识.md ├── flutter异步编程.md ├── ts高级特性.md ├── js手撕系列.md └── js基础题.md ├── ideological-learning ├── 基础算法.md └── 前端中的设计模式.md ├── work-notes ├── 使用Dijkstra算法实现最短路线规划.md ├── vue路由守卫.md ├── 微信相关开发流程汇总.md ├── vue项目迁移中遇到的坑.md ├── 百度地图换高德中遇到的坑.md └── 正则.md ├── todo-list └── todo.md ├── static └── img │ └── 图片1.png ├── demo.ts ├── code.html ├── README.md ├── to-try-to-change-to-succeed └── 理财.md ├── flutter ├── Android相关.md ├── ios数字键盘解决方案.md ├── 路由详解.md ├── 安装包瘦身、加快启动速度.md └── 环信im.md ├── code.js └── advanced ├── 前端性能初探-h5.md └── 函数式编程.md /bottom-learning/ts.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /ideological-learning/基础算法.md: -------------------------------------------------------------------------------- 1 | # 算法的复杂度 -------------------------------------------------------------------------------- /work-notes/使用Dijkstra算法实现最短路线规划.md: -------------------------------------------------------------------------------- 1 | # 使用Dijkstra算法实现最短路线规划 -------------------------------------------------------------------------------- /todo-list/todo.md: -------------------------------------------------------------------------------- 1 | # todo-list 2 | - 写一个自己的前端脚手架,手动配置webpack(todo) 3 | - 4 | - 5 | -------------------------------------------------------------------------------- /static/img/图片1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gushisan/markdown/HEAD/static/img/图片1.png -------------------------------------------------------------------------------- /demo.ts: -------------------------------------------------------------------------------- 1 | type IFoo = ( 2 | uname: string, 3 | uage: number 4 | ) => { 5 | name: string; 6 | age: number; 7 | }; 8 | //参数类型 9 | type Ibar = Parameters; 10 | // type Ibar = [uname: string, uage: number] 11 | type T0 = ReturnType; 12 | // type T0 = { 13 | // name: string; 14 | // age: number; 15 | // } -------------------------------------------------------------------------------- /code.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 18 | 19 | 29 | 30 | -------------------------------------------------------------------------------- /work-notes/vue路由守卫.md: -------------------------------------------------------------------------------- 1 | # vue路由守卫 2 | > 通俗的说,路由守卫就是路由跳转过程中的一些钩子函数。从一个路由跳转到另一个路由中间有很多时间点都存在钩子函数,这些钩子函数能让你操作一些事,这就是路由守卫。 3 | 4 | ## 开始之前先举个例子 5 | 我们在某些网站进行在线的办公软件操作,当你点击关闭页面的时候会跳出弹窗,问你是否确定关闭 6 | ![](https://user-gold-cdn.xitu.io/2020/7/18/1735ff1f9cdeca5c?w=462&h=146&f=png&s=3462) 7 | 8 | 其实这个实现起来并不难,使用组件内的路由守卫就能实现 9 | ```typescript 10 | import { Component, Vue } from 'vue-property-decorator' 11 | Component.registerHooks([ 12 | 'beforeRouteLeave' 13 | ]) 14 | // 注意若是ts,需要加上这一句, 否则不生效 15 | 16 | 17 | 18 | beforeRouteLeave (to, from, next) { 19 | const answer = window.confirm('确认关闭?') 20 | if (answer) { 21 | next() 22 | } else { 23 | next(false) 24 | } 25 | } 26 | ``` -------------------------------------------------------------------------------- /bottom-learning/ES5.md: -------------------------------------------------------------------------------- 1 | # ES5基础巩固 2 | ## 预编译 3 | > 预编译发生在函数执行的前一刻 4 | ```js 5 | function fn(a) { 6 | console.log(a) 7 | var a = 123 8 | console.log(a) 9 | function a() {} 10 | console.log(a) 11 | var b = function() {} 12 | console.log(b) 13 | function d() {} 14 | } 15 | 16 | fn(1) 17 | // 运行结果: 18 | // [Function: a] 19 | // 123 20 | // 123 21 | // [Function: b] 22 | ``` 23 | ### 预编译四部曲 24 | 1. 创建AO对象 25 | 2. 找形参和变量声明,将变量和形参名作为AO属性名,值为undefined 26 | 3. 将实参值和形参统一 27 | 4. 在函数体里面找函数声明,值赋予函数体 28 | ```js 29 | // step1 创建AO对象 30 | AO {} 31 | 32 | // step2 变量和形参名作为AO属性名,值为undefined 33 | AO { 34 | a: undefined, 35 | b: undefined, 36 | d: undefined 37 | } 38 | 39 | // step3 实参形参统一 40 | AO { 41 | a: 1, 42 | b: undefined, 43 | d: undefined 44 | } 45 | 46 | // step4 函数声明提升 47 | AO { 48 | a: function a() {}, 49 | b: undefined, 50 | d: function d() {} 51 | } 52 | 53 | // step 5 从第一行 逐行执行 54 | ``` -------------------------------------------------------------------------------- /work-notes/微信相关开发流程汇总.md: -------------------------------------------------------------------------------- 1 | # 微信授权汇总 2 | > 在进行汇总之前我们要知道,H5是无法通过授权的方式拿到手机号的,H5想拿到手机号只能用户主动去填写。小程序是可以拿到用户微信所绑定的手机号的 3 | 4 | > 注意:开发前需要去微信的后台去获取相关资格 5 | ## 公众号开发-网页授权 6 | [官方文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html) 7 | #### (1)静默授权 8 | 静默授权的大致流程是: 9 | - 用户点击进入页面,跳转到微信授权链接(设置授权方式,回调链接) 10 | - 跳到设置好的回调链接,并在链接上拼上openid 11 | 12 | 静默授权仅能拿到用户的openid 13 | #### (2)非静默授权 14 | 非静默授权的大致流程是: 15 | - 用户点击进入页面,跳转到微信授权链接(设置授权方式,回调链接) 16 | - 弹出确认授权弹窗 17 | - 用户点击同意授权 18 | - 跳转到回调链接,同时url上会带有code 19 | - (后端处理)通过code换取网页授权access_token和openid 20 | - (后端处理)拉取用户信息(通过access_token和openid去换) 21 | - 将用户基本信息返回给前端 22 | 23 | 非静默授权可以获取到用户的基本信息,以及openID和unionID, 其中后面需要后端去处理,因为涉及到传递appsecret等敏感信息,前端不安全 24 | 25 | > 注意:用户的openid并不唯一,不同产品的openid不一样(h5和小程序用户的openid不同),用户的 UnionID 是唯一的。 26 | 27 | # 小程序-获取用户手机号码 28 | 小程序的获取流程相对更简单一些 29 | #### 获取手机号 30 | [官方文档](https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html) 31 | - 使用官方提供的按钮以及对应回调 32 | - 用户点击按钮 33 | - (微信操作)微信会去获取用户手机的验证码,来验证 34 | - 验证完成后,可以拿到包括敏感数据在内的完整用户信息的加密数据、加密算法的初始向量 35 | - 将获取到的加密数据和初始向量传给后端 36 | - 后端解密后,将用户手机号等信息返回给前端 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 学习笔记(持续更新) 2 | 3 | > 验证你是否真正掌握一个知识的最好方法,就是看你能否用直白浅显的语言把复杂深奥的问题和知识讲清楚

-- 费曼技巧「以教为学」

4 | 5 | ## 大纲 6 | ### 前端杂记 7 | - [`❤️前端中的设计模式`](./ideological-learning/前端中的设计模式.md) 8 | - 策略模式 9 | - 发布订阅者模式 10 | - 责任链模式(todo) 11 | - [`❤️前端基础知识积累`](./bottom-learning/基础知识.md) 12 | - 0.1+0.2 为什么不等于 0.3 13 | - == 和 === 的区别 14 | - ... 15 | - [`❤️浏览器相关知识`](./bottom-learning/浏览器相关知识.md) 16 | - 浏览器 HTTP 的缓存机制 17 | - [`❤️Event Loop`](./bottom-learning/event-loop.md) 18 | - Event Loop 19 | - [`js基础`](./bottom-learning/ES5.md) 20 | - 预编译 21 | - [`js基础题`](./bottom-learning/js基础题.md) 22 | - [`js手撕系列`](./bottom-learning/js手撕系列.md) 23 | - [`css那些事儿`](./bottom-learning/css那些事儿.md) 24 | - [`es6中元编程`](./bottom-learning/es6中元编程.md) 25 | - [`ts高级特性`](./bottom-learning/ts高级特性.md) 26 | 27 | ### 进阶 28 | - [函数式编程](advanced/函数式编程.md) 29 | - [前端领域驱动设计(DDD)demo](https://github.com/gushisan/xf-ts-ddd) 30 | - [前端性能初探-h5](advanced/前端性能初探-h5.md) 31 | ### 工作笔记 32 | - [百度地图换高德地图中遇到的坑](work-notes/百度地图换高德中遇到的坑.md) 33 | - [复习正则](work-notes/正则.md) 34 | - [vue路由守卫](work-notes/vue路由守卫.md) 35 | - [使用Dijkstra算法实现最短路线规划](work-notes/使用Dijkstra算法实现最短路线规划.md) 36 | ### Flutter 37 | - [flutter路由详解](flutter/路由详解.md) 38 | - [flutter安装包瘦身、加快启动速度](flutter/安装包瘦身、加快启动速度.md) 39 | - [flutter 异步编程](./bottom-learning/flutter异步编程.md) 40 | - 事件循环、Isolate 41 | - Stream(流) 42 | - [ios数字键盘解决方案](./flutter/ios数字键盘解决方案.md) 43 | ### 人生不止有技术 44 | - [理财](to-try-to-change-to-succeed/理财.md) -------------------------------------------------------------------------------- /work-notes/vue项目迁移中遇到的坑.md: -------------------------------------------------------------------------------- 1 | # vue项目迁移中遇到的坑 2 | > 背景: 因业务需要,将原来的老后台管理系统迁移到新的框架下,由于之前的后台比较混乱,将权限,菜单,代码等重构。迁移前`vue2.5.x + jquery + js + elment-ui`,迁移后 `vue2.6.x + ts + element-ui` 3 | 4 | # 问题代码示例 5 | ```html 6 | 12 | ``` 13 | ```typescript 14 | import { Component, Vue } from 'vue-property-decorator' 15 | 16 | @Component 17 | export default class MerchantDetail extends Vue { 18 | private userName: string = '张三' 19 | private roleForm: any = { 20 | merchantId: '', 21 | roleName: null, 22 | initPermId: [], 23 | operator: Cookies.get('totalRole') 24 | } 25 | 26 | mounted() { 27 | console.log(this.userName) // undefined 28 | console.log(this.roleForm) // undefined 29 | } 30 | } 31 | ``` 32 | 上面的代码中可以看出,data中已经定义了数据,但是节点上获取不到,生命周期中也获取不到。发现问题出现在哪了吗? 33 | 34 | 35 | 36 | # 改正后代码 37 | ```html 38 | 43 | ``` 44 | ```typescript 45 | import { Component, Vue } from 'vue-property-decorator' 46 | 47 | @Component 48 | export default class MerchantDetail extends Vue { 49 | private userName: string = '张三' 50 | private roleForm: any = { 51 | merchantId: '', 52 | roleName: null, 53 | initPermId: [], 54 | operator: '' 55 | } 56 | 57 | mounted() { 58 | this.roleForm.operator = Cookies.get('totalRole') 59 | console.log(this.userName) 60 | console.log(this.roleForm) 61 | } 62 | } 63 | ``` -------------------------------------------------------------------------------- /to-try-to-change-to-succeed/理财.md: -------------------------------------------------------------------------------- 1 | # 理财 2 | 3 | ## 变穷的4种方法 4 | > 要想变的有钱,先在弄清除是什么让自己没钱的 5 | - 意外(比如车祸、受伤等) 6 | - 疾病 7 | - 无规划的支出 8 | - 脆弱的投资系统 9 | 10 | ## 通货膨胀 11 | > 让自己的钱战胜通货膨胀,保证资产不贬值 12 | 13 | 流通中的钱增加了,但实际商品,服务的数量并没有相应的增加,买同样的东西就需要花更多的钱。 14 | 导致通货膨胀的原因: 15 | - 货币发行量增大 16 | - 收入的增加 17 | - 企业利润的上涨 18 | - 进口商品涨价 19 | 20 | > 举个例子:去年6月在银行存了10000元,活期利率0.35%。到今年6月cpi同比增长了1.9%。 故你存的10000元就贬值了1.55%,名义上你的账户上有:10035元,但是实际购买力相当于去年的:9845元。 存了一年就这样亏了155元 21 | 22 | ## 复利 23 | > 复利(Compound interest),是一种计算利息的方法。按照这种方法,利息除了会根据本金计算外,新得到的利息同样可以生息 24 | 25 | ## 吸引力法则 26 | > 关注什么就吸引什么 27 | 我们越是积极的关注和追求财富,越能增加我们变得富有的概率 28 | ## 宏观经济指标 29 | ### 判断经济的景气度 30 | | 名称 | 用途 | 说明 | 31 | | :-----:| :----: | :----: | 32 | | GDP (国内生产总值) | 衡量一个国家一定时期内全部生产活动的总产出 | 一般通过同比增长%(比去年同时期)来判断经济总体增长的情况 | 33 | | PMI (采购经理人指数) | 经济景气度的领先指标 | 当PMI大于50%时,说明经济在发展,当PMI小于50%时,说明经济在衰退 | 34 | 35 | ### 判断经济引擎的三驾马车 36 | | 名称 | 用途 | 说明 | 37 | | :-----:| :----: | :----: | 38 | | 固定资产投资增速(投资) | 衡量固定资产总投资量的变化 | 数字越大,说明固定资产投资增长越多 | 39 | | 社会消费品零售总额同比增速(消费) | 衡量消费品(含商品和服务)销售总额的增长 | 数字越大,说明社会总消费对应的总额增长越快 | 40 | | 出口增速(出口) | 出口量的月度变化 | 数字越大,说明出口量增长越多 | 41 | 42 | ### 判断货币供应的松紧度 43 | | 名称 | 用途 | 说明 | 44 | | :-----:| :----: | :----: | 45 | | M2 | 总货币供应量的统计口径(基础货币+衍生货币) | 反映了整个经济中货币和衍生货币(信贷)的总量 | 46 | | 存贷款基准利率 | 银行存贷款利率的制定基准 | 数字越小,代表较为宽松的货币政策,抑制存款,鼓励贷款 | 47 | | 存款准备金率 | 规定各大银行在央行存放的准备金比例,直接制约银行的放贷能力 | 准备金率越低,越鼓励宽松信贷 | 48 | 49 | ### 判断是否有通货膨胀 50 | | 名称 | 用途 | 说明 | 51 | | :-----:| :----: | :----: | 52 | | CPI(消费者物价指数) | 衡量日常消费的一篮子商品和服务的价格变化 | 数字越大,表示通货膨胀压力越大,以不超过3%为宜(居民消费) | 53 | | PPI(工业生产者出厂价格指数) | 衡量工业产品出厂价格总水平的变动程度 | 数字越大,工业产出产品价格增长越大(工业/消费品产出) | 54 | 55 | > 数据查询途径:[国家统计局网站](http://www.stats.gov.cn)、[中国人民银行网站](http://www.pbc.gov.cn) -------------------------------------------------------------------------------- /flutter/Android相关.md: -------------------------------------------------------------------------------- 1 | # Android获取证书指纹 2 | 应用市场、高德地图、厂商推送等会用到 3 | `keytool -v -list -keystore /Users/fengxiao/documents/code/flutter_base/android/app/flutter_base.jks` 4 | 5 | 6 | keytool -v -list -keystore [jks文件路径] 7 | 8 | # flutter Android打包 9 | 本次所记录的打包流程全部都是在Android Studio上完成的。 10 | 11 | ## 一、生成签名文件 12 | 13 | Android在打包之前需要一个签名文件。 14 | 15 | > eclipse的签名文件是以.ketstore为后缀的文件;Android Studio是以.jks为后缀的文件。 16 | 17 | 这里我们用命令行生成一个.jks的文件。我们使用的是Android Studio自带的debug.keystore密钥库。 18 | 19 | ```java 20 | // 生成 flutter_base.jks 21 | keytool -genkey -v -keystore ~/flutter_base1.jks -alias flutter_base1 -deststoretype pkcs12 -keyalg RSA -keysize 2048 -validity 10000 22 | -----------------------------生成文件路径-------------别名-- 23 | // 查看证书SHA1 24 | 2.查看证书信息 25 | keytool -list -v -keystore ~/flutter_base.jks 26 | // 回车.输入刚才的口令密码,即可查看证书的所有信息(例如申请一些三方需要的SHA1). 27 | 28 | ``` 29 | 30 | ## 二、Android Studio文件配置 31 | 32 | ### 1、导入sign.jks 33 | 34 | 在Flutter工程中/android/app/sign.jks,把sign.jks拖进来。 35 | 36 | ### 2、配置/android/app/build.gradle文件 37 | 38 | 打包新加的配置如下,可以参照对比: 39 | 40 | ``` 41 | android { 42 | signingConfigs { 43 | debug { 44 | storeFile file("flutter_base.jks") 45 | storePassword "xcb123123" 46 | keyAlias "flutter_base" 47 | keyPassword "xcb123123" 48 | } 49 | 50 | release { 51 | storeFile file("flutter_base.jks") 52 | storePassword "xcb123123" 53 | keyAlias "flutter_base" 54 | keyPassword "xcb123123" 55 | } 56 | } 57 | buildTypes { 58 | release { 59 | signingConfig signingConfigs.release 60 | minifyEnabled true 61 | useProguard true 62 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 63 | } 64 | debug { 65 | } 66 | } 67 | } 68 | ``` 69 | 70 | 71 | ## 三、Flutter打包apk 72 | 73 | 从终端进入flutter工程目录,运行以下命令自动生成apk,生成apk路径为:xxx/build/app/outputs/apk/xx.apk 74 | 75 | > $ flutter build apk -------------------------------------------------------------------------------- /flutter/ios数字键盘解决方案.md: -------------------------------------------------------------------------------- 1 | # ios数字键盘解决方案 2 | 场景: 在使用flutter开发的过程中我们发现当你使用数字输入框调起数字键盘时ios默认数字键盘无法关闭,默认没有关闭的方式。 3 | 4 | ## 原始解决方案 5 | ```dart 6 | FocusScope.of(context).requestFocus(FocusNode()); 7 | ``` 8 | 给需要关闭键盘的页面最外层加上点击事件,点击空白地方关闭键盘。 9 | 后来发现这种方式并不行,因为用户可能根本不知道去点击空白的地方关闭键盘。 10 | 11 | ## 更加深入的解决方案 12 | 去看了闲鱼ios端APP,会在键盘上加一个完成按钮。 13 | 14 | ### keyboard_actions 15 | [`keyboard_actions`](https://pub.flutter-io.cn/packages/keyboard_actions)是一个flutter 插件 ,刚好可以满足我们的需求,完成自定义的键盘功能 16 | 17 | ```dart 18 | keyboard_actions: "^3.3.1+1" 19 | ``` 20 | 21 | ```dart 22 | static KeyboardActionsConfig keyboardBuildConfig( 23 | BuildContext context, List focusNodeList) { 24 | return KeyboardActionsConfig( 25 | keyboardSeparatorColor: Colors.white, 26 | actions: focusNodeList 27 | .map((e) => KeyboardActionsItem(focusNode: e, toolbarButtons: [ 28 | //button 1 29 | (node) { 30 | return GestureDetector( 31 | behavior: HitTestBehavior.opaque, 32 | onTap: () => node.unfocus(), 33 | child: Container( 34 | padding: EdgeInsets.all(8.0), 35 | margin: EdgeInsets.only(right: getWidthPx(12)), 36 | child: Text( 37 | "完成", 38 | style: headline6(context).copyWith( 39 | color: blueColor, fontWeight: FontWeight.w600), 40 | ), 41 | ), 42 | ); 43 | }, 44 | ])) 45 | .toList()); 46 | } 47 | ``` 48 | 自定义配置 49 | 50 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/IMG_0944.PNG) 51 | 调整完毕后的键盘 52 | 53 | ### FocusNode 54 | 在使用的过程中发现,keyboard_actions是使用FocusNode来实现的, 在Flutter中使用FocusNode用来捕捉监听TextField的焦点获取与失去,同时也可通过FocusNode来使用绑定对应的TextField获取焦点与失去焦点,看一下FocusNode上面的常用方法 55 | - `canRequestFocus`: 是否能请求焦点 56 | - `context`: 焦点"附着"的 widget 的 BuildContext 57 | - `hasFocus`: 是否有焦点 58 | - `unfocus`: 放弃焦点, 如果当前 node 有焦点,并调用这个, 就放弃了焦点, 如果同时有软键盘弹起, 则软键盘收起 59 | - `requestFocus`: 请求焦点, 这个方法调用后, 会把焦点移到当前 60 | -------------------------------------------------------------------------------- /bottom-learning/event-loop.md: -------------------------------------------------------------------------------- 1 | # Event loop 2 | > event loop是计算机系统的一种运行机制,JavaScript就采用这种机制,来解决单线程运行带来的一些问题。弄懂这个就能清楚的知道js到底是怎样去执行的。 3 | 4 | 我们需要知道,js是单线程的,遇见异步操作会先进异步队列,而不是立即执行,我们需要知道以下两个概念: 5 | 6 | ## 宏队列,macrotask,这些异步任务包括: 7 | - setTimeout 8 | - setInterval 9 | - setImmediate (Node) 10 | - requestAnimationFrame (浏览器) 11 | - I/O 12 | - UI rendering (浏览器) 13 | 14 | ## 微队列,microtask,这些异步任务包括: 15 | - Promise 16 | - Object.observe 17 | - MutationObserver 18 | - process.nextTick (Node) 19 | 20 | ![](https://user-gold-cdn.xitu.io/2020/7/18/1735ff727a18d3a9?w=730&h=773&f=png&s=61128) 21 | 22 | ## 代码执行步骤 23 | 24 | - 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等) 25 | - 全局Script代码执行完毕后,调用栈Stack会清空 26 | - 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1 27 | - 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行 28 | - microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空 29 | - 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行 30 | - 执行完毕后,调用栈Stack为空 31 | - 重复第3-7个步骤 32 | 33 | 简单点概括就是同步代码顺序执行,碰到微任务进微队列,碰到宏任务进宏队列。同步代码执行完先将微队列中的所有任务执行完,微队列执行过程中碰到的微任务放到队列尾部这次一并执行。微队列执行完毕后执行宏队列头部的任务,执行完成之后再进微队列,执行完所有的微任务,依次循环,直到所有任务执行完毕 34 | 35 | ```js 36 | console.log(1) 37 | 38 | setTimeout(() => { 39 | console.log(2) 40 | Promise.resolve().then(() => { 41 | console.log(3) 42 | }) 43 | }, 0) 44 | 45 | new Promise((resolve, reject) => { 46 | console.log(4) 47 | resolve(5) 48 | }).then((res) => { 49 | console.log(res) 50 | }) 51 | 52 | setTimeout(() => { 53 | console.log(6) 54 | }, 0) 55 | 56 | console.log(7) 57 | // 输出结果1 4 7 5 2 3 6 58 | ``` 59 | 60 | 以上代码的输出结果你猜对了吗?我们来逐行分析 61 | - 第一行,console.log直接输出1 62 | - 然后碰到第一个setTimeout,setTimeout属于宏任务,进入宏队列,继续往下执行 63 | - 碰到一个promise(注意:promise只有resolve/reject才算异步),所以这里4直接输出,resolve进入微队列 64 | - 碰到第二个setTimeout,进宏队列 65 | - 7直接输出 66 | - 然后看微队列,之前进队列的依次出, 队列里面只有一个promise输出5,然后队列为空 67 | - 然后执行宏队列第一个,输出2,promise进微队列 68 | - 执行微队列,输出3,微队列为空 69 | - 执行宏队列第一个,输出6,到这里所有队列为空执行完毕 70 | 71 | ```js 72 | Promise.resolve().then(() => { 73 | console.log('mm') 74 | Promise.resolve().then(() => { 75 | console.log('xx') 76 | }).then(() => { 77 | console.log('yy') 78 | }) 79 | }).then(() => { 80 | console.log('nn') 81 | }) 82 | // 输出结果mm xx nn yy 83 | ``` 84 | 这段代码的输出结果你猜对了吗?我们再来分析: 85 | - 首先看代码中只有一个大promise,先执行,mm直接输出,promise进微队列,内部执行完毕 86 | - 然后外层.then进微队列 87 | - 执行微队列,输出xx,碰到内部.then加入队列尾部 88 | - 输出nn 89 | - 输出yy 90 | 91 | 参考:[带你彻底弄懂Event Loop](https://segmentfault.com/a/1190000022805585) -------------------------------------------------------------------------------- /bottom-learning/css那些事儿.md: -------------------------------------------------------------------------------- 1 | # css那些事儿 2 | 关于css的那些事儿 3 | ## 1.no-image 4 | 能用iconfont绝对不用图片,能用代码实现不用iconfont。还是那句话越接近底层性能越好 5 | 6 | css icon > iconfont > image 7 | 8 | [CSS ICON在线地址](https://cssicon.space/#/) 9 | 10 | ## 2.CSS HINT 11 | - 1.不要使用多个class选择元素,如a.foo.boo,这在ie6及以下不能正确解析 12 | - 2.移除空的css规则,如a{} 13 | - 3.正确的使用显示属性,如display:inline不要和width,height,float,margin,padding同时使用,display:inline-block不要和float 14 | 同时使用等 15 | - 4.避免过多的浮动,当浮动次数超过十次时,会显示警告 16 | - 5.避免使用过多的字号,当字号声明超过十种时,显示警告 17 | - 6.避免使用过多web字体,当使用超过五次时,显示警告 18 | - 7.避免使用id作为样式选择器 19 | - 8.标题元素只定义一次 20 | - 9.使用width:100%时要小心 21 | - 10.属性值为0时不要写单位 22 | - 11.各浏览器专属的css属性要有规范,例如.foo{-moz-border-radius:5px;border-radius:5px} 23 | - 12.避免使用看起来像正则表达式的css3选择器 24 | - 13.遵守盒模型规则 25 | 26 | ## 3.BFC IFC GFC FFC 27 | Box 是 CSS 布局的对象和基本单位, 直观点来说,就是一个页面是由很多个 28 | Box 组成的。元素的类型和 display 属性,决定了这个 Box 的类型。 不同类 29 | 型的 Box, 会参与不同的 Formatting Context(一个决定如何渲染文档的容 30 | 器),因此Box内的元素会以不同的方式渲染。 31 | - BFC(Block Formatting Contexts)直译为"块级格式化上下文",block-level box:display 属性为 block, list-item, table 的元素,会生成 block-level 32 | box。并且参与 block fomatting context; 33 | - IFC(Inline Formatting Contexts)直译为"内联格式化上下文",IFC的line 34 | box(线框)高度由其包含行内元素中最高的实际高度计算而来(不 35 | 受到竖直方向的padding/margin影响) 36 | - FFC(Flex Formatting Contexts)直译为"自适应格式化上下文",display值 37 | 为flex或者inline-flex的元素将会生成自适应容器(flex container) 38 | - GFC(GridLayout Formatting Contexts)直译为"网格布局格式化上下文", 39 | 当为一个元素设置display值为grid的时候,此元素将会获得一个独立 40 | 的渲染区域,我们可以通过在网格容器(grid container)上定义网格 41 | 定义行(grid definition rows)和网格定义列(grid definition columns) 42 | 属性各在网格项目(grid item)上定义网格行(grid row)和网格列 43 | (grid columns)为每一个网格项目(grid item)定义位置和空间。 44 | 45 | 46 | 其实我们最常见的就是BFC,详细说一说。 47 | 48 | ### 哪些元素会生成BFC? 49 | - 根元素 50 | - float属性不为none 51 | - position为absolute或fixed 52 | - display为inline-block, table-cell, table-caption, flex, 53 | inline-flex 54 | - overflow不为visible 55 | 56 | ### BFC能解决什么问题 57 | BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。反之也如此。触发BFC的区域相互独立不重叠,举个例子:BFC的区域不会与float box重叠,因为float不为none时也会触发BFC,bfc之间相互独立。 58 | 59 | BFC能解决的最为经典的一个问题就是,margin重叠。两个div,1个设置margin-bottom: 20px, 一个设置margin-top:10px;这时你会发现两个div之间其实只相距20px而不是30px,出现了重叠现象,这时通过触发bfc就能解决 60 | 61 | ## 矩阵(Matrix) 62 | 可能我们在日常开发中很少用到矩阵,用的更多的是transform,其实transform的skew(斜拉)、scale(缩放)、rotate(旋转)、translate(位移)等这些api只是封装好的便于使用的api,其实现原理还是Matrix(矩阵) 63 | 64 | ### `transform: matrix(a,b,c,d,e,f);` 65 | 矩阵看上去有6个参数,晦涩难懂,但其实只需要通过线性方程式的推导,我们会发现transform的api都能使用矩阵来实现 66 | 67 | 例如:matrix(sx, 0, 0, sy, 0, 0); 68 | 等同于scale(sx, sy); 69 | 70 | translate 等同于 transform: matrix(x,x,x,x, 水平偏移距离, 垂直偏移距离); 71 | 72 | matrix的确可以提高性能,但是处理起来比较麻烦,我们可以利用生成工具来实现 73 | ### 快速提高生成能力 74 | - [matrix在线生成](https://meyerweb.com/eric/tools/matrix/) 将你的transform代码转化为矩阵代码 75 | - [matrix 3D在线生成](http://ds-overdesign.com/) 76 | -------------------------------------------------------------------------------- /flutter/路由详解.md: -------------------------------------------------------------------------------- 1 | # flutter 路由从使用到原理 2 | 主要从使用和原理两个方面来理解flutter中的路由 3 | # 基础使用 4 | ## 1.组件路由 5 | 当我们第一次打开应用程序,出现在眼前的便是路由栈中的第一个也是最底部实例: 6 | 7 | ```dart 8 | void main() { 9 | runApp(MaterialApp(home: Screen1())); 10 | } 11 | ``` 12 | 要在堆栈上推送新的实例,我们可以调用导航器 Navigator.push ,传入当前 context 并且使用构建器函数创建 MaterialPageRoute 实例,该函数可以创建您想要在屏幕上显示的内容。 例如: 13 | ```dart 14 | RaisedButton( 15 | onPressed:(){ 16 | Navigator.push(context, MaterialPageRoute( 17 | builder: (BuildContext context) { 18 | return Scaffold( 19 | appBar: AppBar(title: Text('My Page')), 20 | body: Center( 21 | child: FlatButton( 22 | child: Text('POP'), 23 | onPressed: () { 24 | Navigator.pop(context); 25 | }, 26 | ), 27 | ), 28 | ); 29 | }, 30 | )); 31 | }, 32 | child: Text("Push to Screen 2"), 33 | ) 34 | ``` 35 | 点击执行上方操作,我们将成功打开第二个页面。push操作也就是向路由栈中添加路由,pop出,以实现页面的跳转 36 | 37 | ## 2.命名路由 38 | 将应用中需要访问的每个页面命名为不重复的字符串,我们便可以通过这个字符串来将该页面实例推进路由。 39 | 40 | 例如,'/ home' 表示 HomeScreen, '/ login' 表示 LoginScreen。 41 | 42 | ```dart 43 | MaterialApp( 44 | home: Screen1(), 45 | routes: { 46 | '/screen1': (BuildContext context) => Screen1(), 47 | '/screen2' : (BuildContext context) => Screen2(), 48 | '/screen3' : (BuildContext context) => Screen3(), 49 | '/screen4' : (BuildContext context) => Screen4() 50 | }, 51 | ) 52 | ``` 53 | 以上是创建路由的方法 54 | 55 | 下面来介绍跳转路由的方法 56 | ## 路由间跳转 57 | ### push 58 | ```dart 59 | Navigator.pushNamed(context, "/pageB") 60 | ``` 61 | 直接向路由栈中添加一个路由信息 62 | 63 | 例:a 跳转 b ,就可直接在a页面push b 64 | 65 | | before | after | 66 | | :-----:| :-----:| 67 | | - | - | 68 | | - | b | 69 | | a | a | 70 | ### pop 71 | ```dart 72 | Navigator.of(context).pop(); 73 | ``` 74 | 从 Navigator 的堆栈中弹出 75 | 76 | 例:接着上面的已经到b页面,现在想回到a,b页面直接pop即可 77 | | before | after | 78 | | :-----:| :-----:| 79 | | - | - | 80 | | b | - | 81 | | a | a | 82 | 83 | > push 只用于向栈中添加实例,pop 弹出实例 84 | 85 | ### pushReplacementNamed 与 popAndPushNamed 86 | ```dart 87 | Navigator.pushReplacementNamed(context, "/screen4"); 88 | 89 | Navigator.popAndPushNamed(context, "/screen4"); 90 | ``` 91 | 假设我们在 pageB 页面使用 pushReplacementNamed 与 popAndPushNamed 方法 跳转到 pageC。 92 | 93 | 此时路由栈情况如下: 94 | 95 | | before | after | 96 | | :-----:| :-----:| 97 | | - | - | 98 | | pageB | pageC | 99 | | pageA | pageA | 100 | 101 | pageC替代了pageB,也就是说在push的同时将当前页面pop掉了,这种场景非常常见,当你提交表单时,提交成功跳转后,并不希望还会回退到表单页 102 | 103 | pushReplacementNamed 和 popAndPushNamed实现的功能是一样的,不同的只是交互体验上的效果,popAndPushNamed 能够执行 pageB 弹出的动画与 pageC 推进的动画而 pushReplacementNamed 仅显示 pageC 推进的动画。 104 | 105 | ### pushNamedAndRemoveUntil 106 | -------------------------------------------------------------------------------- /flutter/安装包瘦身、加快启动速度.md: -------------------------------------------------------------------------------- 1 | # 1.apk瘦身 2 | 应用程序界的真理 - 越小越好 3 | 4 | 开始瘦身前先记录下未瘦身时的包大小 5 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201203-134529%402x.png) 6 | ## APK Analyser 7 | 使用 APK Analyser 分解你的 APK 8 | 9 | - 打开Android studio,直接将打好包的apk拖入 10 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201203-135157%402x.png) 11 | 12 | 可以看到lib目录占了70.4%的大小,原因是我们支持了三种ABI,这样虽然提升了兼容性,但是增大了包体积,这样显然是不好的。 13 | 14 | 如何解决? 15 | 16 | abi split, 为每个CPU架构单独打一个APK, 同时,Google Play 支持上传多个APK,就能根据不同的CPU架构,下发不同的apk, 但是,很遗憾,国内的应用商店目前还不支持 17 | 18 | ## 我们再来看看主流的APP是怎么处理的 19 | - 微信,只支持`arm64-v8a` 20 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201203-151235%402x.png) 21 | 试了下32位真的是不支持 22 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/Screenshot_20201203-151533.png) 23 | 但是在微信的官网有32位版本的下载通道,只是默认下载64位 24 | 25 | 支付宝和手Q适配的是`armeabi`,淘宝适配的是`armeabi-v7a` 26 | 27 | 可以看到各大主流APP为了包大小考虑都只选了一个ABI兼容 28 | 29 | ## 那么我们如何选择? 30 | 31 | 首先我们要知道 32 | - 只适配 `armeabi` 的APP可以跑在 `armeabi` , `x86` ,`x86_64` , `armeabi-v7a` , `arm64-v8` 上 33 | - 只适配 `armeabi-v7a` 可以运行在 `armeabi-v7a` 和 `arm64-v8a` 34 | - 只适配 `arm64-v8a` 可以运行在 `arm64-v8a` 上 35 | 36 | ### 三个方案: 37 | #### 1 只适配 `armeabi` 38 | - 优点:基本上适配了全部CPU架构(除了淘汰的mips和mips_64) 39 | - 缺点:性能低,相当于在绝大多数手机上都是需要辅助ABI或动态转码来兼容 40 | #### 2 只适配 `armeabi-v7a` 41 | 同理方案一,只是又筛掉了一部分老旧设备,在性能和兼容二者中比较平衡 42 | 43 | #### 3 只适配 arm64-v8 44 | - 优点: 性能最佳 45 | - 缺点: 只能运行在arm64-v8上,要放弃部分老旧设备用户 46 | 47 | 这三种方案都是可以的,现在的大厂APP适配中,这三种都有,大部分是前2种方案。具体选哪一种就看自己的考量了,以性能换兼容就arm64-v8,以兼容换性能armeabi,二者稍微平衡一点的就armeabi-v7a。 48 | 49 | 参考:[为何大厂APP如微信、支付宝、淘宝、手Q等只适配了armeabi-v7a/armeabi?](https://juejin.cn/post/6844904148589084680) 50 | 51 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201203-160024%402x.png) 52 | 改为只适配 `arm64-v8a` 后包大小减少约`41%` 53 | 54 | 55 | # 2.ipa瘦身 56 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201208-110130%402x.png) 57 | 瘦身前安装包大小 58 | 59 | ## 分析包内容 60 | 首先将打好的`.ipa`包的后缀名改为`.zip`,然后解压,解压后会得到一个Payload文件夹,点进去是该项目的包,右击显示包内容即可查看当前包里面所包含的东西。 61 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201208-113111%402x.png) 62 | 里面包含了所有东西,通过观察发现,`.ipa`的大小是51.5MB,解压后文件总大小为132.9MB。其中Frameworks占了101MB,还有很多png有压缩的空间 63 | 64 | 经过反复观察发现Frameworks中的内容无法压缩的更小,它的大小与项目依赖的框架个数有关,与支持的指令集也有关。 65 | 66 | Frameworks中的内容肯定是无法通过删除来减小的,而默认支持是指令集是armv7、arm64,通过APP store下发到对应机型上更是只保留了对应的指令集,故这方面无法优化。 67 | ## ImageOptim图片无损压缩 68 | 将项目中所有的png进行无损压缩 69 | ![](https://xcb-assets-dev.oss-cn-shanghai.aliyuncs.com/images/md/WX20201208-111546%402x.png) 70 | 71 | ## 去除不必要的调试符号 72 | 在buildSetting中, 73 | Strip Linked Product、 74 | Deployment Postprocessing、 75 | Symbols Hidden by Default在release版本应该设为yes,可以去除不必要的调试符号。需要注意的是Deployment Postprocessing如果在Debug情况下设置为YES会导致Debug调试时无法停在断点处。 76 | 77 | 78 | 优化完成ipa大小为30MB左右,实际安装大小为80MB,通过观察发现实际安装大小与ipa解压后大小一致。 -------------------------------------------------------------------------------- /work-notes/百度地图换高德中遇到的坑.md: -------------------------------------------------------------------------------- 1 | # 百度坐标和高德坐标的区别 2 | > 原有的老数据的经纬度是采用百度地图进行采集的,现在直接换为高德地图来展示会出现位置偏移,同样是经纬度为什么回有偏移呢?当然是采用了不同的标准所导致的。 3 | 4 | | 坐标系标准 | 简介 | 采用该标准的应用 | 5 | | :-----:| :----: | :----: | 6 | | 地球坐标系(WGS84) | 为一种大地坐标系,也是目前广泛使用的GPS全球卫星定位系统使用的坐标系。数据从专业GPS设备中取出,国际地图提供商使用的坐标系 | 谷歌国外地图、osm地图等国外的地图 | 7 | | 火星坐标 (GCJ-02)也叫国测局坐标系 | 中国标准,从国行移动设备中定位获取的坐标数据使用这个坐标系国家规定: 国内出版的各种地图系统(包括电子形式),必须至少采用GCJ-02对地理位置进行首次加密。 | iOS 地图(其实是高德)、Gogole地图、高德地图 | 8 | | 百度坐标 (BD-09) | 在GCJ02坐标系基础上再次加密,再加上百度自身的加偏算法,也就是在标准经纬度的基础之上进行了两次加偏,该坐标系的坐标值为经纬度格式,单位为度 | 百度地图 | 9 | 10 | # 高德地图接入流程 11 | #### (1)进入[高德开放平台](https://lbs.amap.com/)注册高德开发者个人/企业,注册完成后进入控制台,添加新应用以取得key 12 | ![](https://user-gold-cdn.xitu.io/2020/7/18/1735ff47cb993c2c?w=711&h=530&f=png&s=14731) 13 | 14 | #### (2) 引入js Api初始化地图 15 | 使用script标签方式引入 16 | ```html 17 |
18 | 19 | 20 | 26 | ``` 27 | 使用npm方式引入 28 | `npm i @amap/amap-jsapi-loader --save-dev` 29 | ```js 30 | import AMapLoader from '@amap/amap-jsapi-loader'; 31 | 32 | AMapLoader.load({ 33 | "key": "您申请的key值", // 申请好的Web端开发者Key,首次调用 load 时必填 34 | "version": "2.0", // 指定要加载的 JSAPI 的版本,缺省时默认为 1.4.15 35 | "plugins": [] //插件列表 36 | }).then((AMap)=>{ 37 | map = new AMap.Map('container'); 38 | }).catch(e => { 39 | console.log(e); 40 | }) 41 | ``` 42 | 43 | # 根据需要添加对应的地图功能 44 | ### 浏览器精准定位(获取当前经纬度) 45 | ```js 46 | AMap.plugin('AMap.Geolocation', function() { 47 | var geolocation = new AMap.Geolocation({ 48 | enableHighAccuracy: true,//是否使用高精度定位,默认:true 49 | timeout: 10000, //超过10秒后停止定位,默认:5s 50 | buttonPosition:'RB', //定位按钮的停靠位置 51 | buttonOffset: new AMap.Pixel(10, 20),//定位按钮与设置的停靠位置的偏移量,默认:Pixel(10, 20) 52 | zoomToAccuracy: true, //定位成功后是否自动调整地图视野到定位点 53 | }); 54 | map.addControl(geolocation); 55 | geolocation.getCurrentPosition(function(status,result){ 56 | if(status=='complete'){ 57 | // 返回complete即可取得经纬度 58 | console.log(result.position) 59 | }else{ 60 | console.log('error') 61 | } 62 | }); 63 | }) 64 | ``` 65 | > 注意:想要获取精准的经纬度需要使用https,并且电脑无法精准获取经纬度,手机必须打开GPS。 66 | 67 | ### h5路线规划 68 | ```js 69 | AMap.plugin('AMap.Driving', function() { 70 | var driving = new AMap.Driving({ 71 | map: map, 72 | // 驾车路线规划策略,AMap.DrivingPolicy.LEAST_TIME是最快捷模式 73 | policy: AMap.DrivingPolicy.LEAST_TIME 74 | }) 75 | // 起点坐标 76 | var startLngLat = [self.destinationP.lng, self.destinationP.lat] 77 | // 终点坐标 78 | var endLngLat = [self.markP.lng, self.markP.lat] 79 | 80 | driving.search(startLngLat, endLngLat, function (status, result) { 81 | // 未出错时,result即是对应的路线规划方案 82 | if (status === 'complete') { 83 | console.log('绘制步行路线完成') 84 | } else { 85 | console.log('步行路线数据查询失败') 86 | console.log(result) 87 | } 88 | }) 89 | }) 90 | ``` 91 | 92 | ### 跳转到高德地图并默认调起app 93 | ```js 94 | window.open(`https://uri.amap.com/navigation?from=${this.destinationP.lng},${this.destinationP.lat},当前位置&to=${this.markP.lng},${this.markP.lat},${this.name}&callnative=1`) 95 | ``` -------------------------------------------------------------------------------- /bottom-learning/浏览器相关知识.md: -------------------------------------------------------------------------------- 1 | # 浏览器HTTP缓存机制 2 | > http缓存机制是一种web性能优化的手段,对于从事web行业的我们很有必要去弄懂,起初我仅仅只是知道浏览器会对请求的静态文件进行缓存,至于如何缓存,为什么缓存并不知其所以然。在这里结合自己所学及理解,用简短的文字来说明。 3 | 4 | 我们在访问百度首页的时候,会发现不管怎么刷新页面,静态资源基本都是返回 200(from cache)?200?缓存状态不应该是304? 5 | 6 | ![](https://user-gold-cdn.xitu.io/2020/7/18/1735ff687c717f9a?w=886&h=288&f=png&s=22579) 7 | 让我们带着疑问继续看下去 8 | # 浏览器加载一个页面的流程: 9 | - 浏览器先根据这个资源的http头信息来判断是否命中强缓存。如果命中则直接加在缓存中的资源,并不会将请求发送到服务器 10 | - 如果未命中强缓存,则浏览器会将资源加载请求发送到服务器。服务器来判断浏览器本地缓存是否失效。若可以使用,则服务器并不会返回资源信息,浏览器继续从缓存加载资源 11 | - 如果未命中协商缓存,则服务器会将完整的资源返回给浏览器,浏览器加载新资源,并更新缓存 12 | 13 | # 强缓存 14 | > 命中强缓存时,浏览器并不会将请求发送给服务器。在Chrome的开发者工具中看到http的返回码是200,但是在Size列会显示为(from cache)。 15 | ## Expires 16 | - Expires是http1.0时代处理缓存的方式,用来启用缓存和定义缓存时间。Expires的值对应一个GMT(格林尼治时间),比如“Wed, 24 Jun 2020 03:33:06 GMT”来告诉浏览器资源缓存过期时间,如果还没过该时间点则不发请求。 17 | - 与之对应的有Pragma字段,当该字段值为“no-cache”的时候禁止缓存,即每次都发送新的请求。 18 | - 需要注意的是Pragma字段的优先级会更高,也就是说当Pragma设置为禁用,又给Expires定义一个还未到期的时间,这是还是会发起新的请求 19 | - 缺点:响应报文中Expires所定义的缓存时间是相对服务器上的时间而言的,如果客户端上的时间跟服务器上的时间不一致,那缓存时间可能就没意义了 20 | 21 | ## Cache-Control 22 | > 上述的Expires时间是相对服务器而言,无法保证和客户端时间统一,http1.1新增了 Cache-Control 来定义缓存过期时间,若报文中同时出现了 Pragma、Expires 和 Cache-Control,会以 Cache-Control 为准 23 | 24 | ### 作为Request Headres时候可取值 25 | - no-cache > 告知服务器不直接使用缓存,要求原服务器发送请求 26 | - no-store > 所有内容都不会被保存到缓存或Internet临时文件中 27 | - max-age=delta-seconds > 告知服务器客户端希望接收一个存在时间(Age)不大于delta-seconds秒的资源 28 | - max-stale [= delta-seconds] > 告知服务器客户端愿意接收一个超过缓存时间的资源,若有定义delta-seconds则为delta-seconds秒,若没有定义则为任意超出时间 29 | - min-fresh=delta-seconds > 告知服务器客户端希望接收一个在小于delta-seconds秒内被更新过的资源 30 | ### 作为Response Headres时候可取值 31 | - public > 表明任何情况下都得缓存该资源(即使是需要http认证的资源) 32 | - private[="field-name"] > 表明返回报文中全部或部分(若指定了field-name则为field-name的字段数据)仅开放给部分用户(服务器指定的share-user)做缓存使用,其他用户则不能缓存这些数据 33 | - no-cache > 不直接使用缓存,要求向服务器发起(新鲜度校验)请求 34 | - no-store > 所有内容都不会被保存到缓存或Internet临时文件中 35 | - no-transform > 告知客户端缓存文件时不得对实体数据做任何更改 36 | - max-age=delta-seconds > 告知客户端该资源在delta-seconds秒内是新鲜的,无需向服务器发送请求 37 | 38 | # 协商缓存 39 | > 若未命中强缓存,则浏览器会将请求发送至服务器。服务器根据http头信息中的Last-Modify/If-Modify-Since或Etag/If-None-Match来判断是否命中协商缓存。如果命中,则http返回码为304,浏览器从缓存中加载资源。 40 | 41 | ## Last-Modified 42 | - 表示资源的最后更改时间,服务器将资源传递给客户端时,会将资源的最后更改时间以:`Last-Modified: GMT` 的形式加在实体首部上一起返回给客户端 43 | - 客户端会为资源标记上该信息,下次再次请求时,会把该信息附带在请求报文中一并带给服务器去做检查,若传递的时间值与服务器上该资源最终修改时间是一致的,则说明该资源没有被修改过,直接返回304状态码 44 | 45 | ### If-Modified-Since 46 | > 示例:If-Modified-Since: Wed, 24 Jun 2020 03:33:06 GMT 47 | 48 | 该请求首部告诉服务器如果客户端传来的最后修改时间与服务器上的一致,则直接回送304 和响应报头即可。 49 | 50 | ### If-Unmodified-Since 51 | > 告诉服务器,若Last-Modified没有匹配上(资源在服务端的最后更新时间改变了),则应当返回412(Precondition Failed) 状态码给客户端。 52 | 53 | 54 | 当遇到下面情况时,If-Unmodified-Since 字段会被忽略 55 | - Last-Modified值对上了(资源在服务端没有新的修改) 56 | - 服务端需返回2XX和412之外的状态码 57 | - 传来的指定日期不合法 58 | 59 | ## ETag 60 | - 服务器会通过某种算法,给资源计算出一个唯一标志(比如md5标志),在把资源响应给客户端的时候,会在实体首部加上“ETag: 唯一标识符”一起返回给客户端 61 | - 客户端会保留该 ETag 字段,并在下一次请求时将其一并带过去给服务器。服务器只需要比较客户端传来的ETag跟自己服务器上该资源的ETag是否一致,就能很好地判断资源相对客户端而言是否被修改过了 62 | - 如果服务器发现ETag匹配不上,那么直接以常规GET 200回包形式将新的资源(当然也包括了新的ETag)发给客户端;如果ETag是一致的,则直接返回304知会客户端直接使用本地缓存即可。 63 | ### If-None-Match 64 | > 示例为 If-None-Match: "56fcccc8-1699" 65 | 66 | 告诉服务端如果 ETag 没匹配上需要重发资源数据,否则直接回送304 和响应报头 67 | 68 | ### If-Match 69 | 告诉服务器如果没有匹配到ETag,或者收到了“*”值而当前并没有该资源实体,则应当返回412(Precondition Failed) 状态码给客户端。否则服务器直接忽略该字段 70 | > 如果 Last-Modified 和 ETag 同时被使用,则要求它们的验证都必须通过才会返回304,若其中某个验证没通过,则服务器会按常规返回资源实体及200状态码 71 | ## 注意 72 | > Last-Modified 说好却也不是特别好,因为如果在服务器上,一个资源被修改了,但其实际内容根本没发生改变,会因为Last-Modified时间匹配不上而返回了整个实体给客户端(即使客户端缓存里有个一模一样的资源).为了解决Last-Modified可能存在的不准确的问题,Http1.1推出了 ETag 实体首部字段。 73 | 74 | 参考: [浅谈浏览器http的缓存机制](https://www.cnblogs.com/vajoy/p/5341664.html) -------------------------------------------------------------------------------- /bottom-learning/es6中元编程.md: -------------------------------------------------------------------------------- 1 | # es6中元编程 2 | 3 | 何为元编程? 4 | 5 | 「编写能改变语言语法特性或者运行时特性的程序」。换言之,一种语言本来做不到的事情,通过你编程来修改它,使得它可以做到了,这就是元编程。 6 | 7 | meta-programming元编程中的 元 的概念可以理解为 程序 本身。”元编程能让你拥有可以扩展程序自身能力 8 | 9 | 举个例子: 10 | ```js 11 | if (a == 1 && a == 2 && a == 3) { 12 | console.log("done"); 13 | } 14 | ``` 15 | 怎样才能让这个条件满足,输出done。按照正常的逻辑是无法完成的,毕竟一个值不可能同时满足等于1、2、3 16 | 17 | 这是就可以用到元编程来改变这个不可能 18 | 19 | ```js 20 | let a = { 21 | [Symbol.toPrimitive]: ((i) => () => ++i)(0) 22 | } 23 | 24 | if (a == 1 && a == 2 && a == 3) { 25 | console.log("done"); 26 | } 27 | // done 28 | ``` 29 | `Symbol.toPrimitive`在对象转换为原始值的时候会被调用,初始值为1,调用一次+1,就可以满足`a == 1 && a == 2 && a == 3`,同时`Symbol.toPrimitive`也可以接受一个参数hint,hint的取值为number、string、default 30 | ```js 31 | let obj = { 32 | [Symbol.toPrimitive](hint) { 33 | switch (hint) { 34 | case "number": 35 | return 123; 36 | case "string": 37 | return "str"; 38 | case "default": 39 | return "default"; 40 | } 41 | } 42 | } 43 | console.log(1-obj); // -122 44 | console.log(1+obj); // 1default 45 | console.log(`${obj}`); // str 46 | ``` 47 | 48 | ## 还有哪些元编程? 49 | ### proxy 50 | es5的Object.defineProperty()方法的es6升级版,用于自定义的对象的行为 51 | ```js 52 | let leon = { 53 | age: 30 54 | } 55 | const validator = { 56 | get: function(target, key){ 57 | // 若没这个属性返回37 58 | return key in target ? target[key] : 37; 59 | }, 60 | set(target,key,value){ 61 | if(typeof value!="number" || Number.isNaN(value)){ 62 | throw new Error("年龄得是数字"); 63 | } 64 | } 65 | } 66 | const proxy = new Proxy(leon,validator); 67 | console.log(proxy.name); 68 | // 37 69 | proxy.age = "hi"; 70 | // Error: 年龄得是数字 71 | ``` 72 | ### reflect-metadata 73 | 你可以通过装饰器来给类添加一些自定义的信息。然后通过反射将这些信息提取出来。当然你也可以通过反射来添加这些信息 74 | ```js 75 | require("reflect-metadata") 76 | class C { 77 | // @Reflect.metadata(metadataKey, metadataValue) 78 | method() { 79 | } 80 | } 81 | Reflect.defineMetadata("name", "jix", C.prototype, "method"); 82 | 83 | let metadataValue = Reflect.getMetadata("name", C.prototype, "method"); 84 | console.log(metadataValue); 85 | // jix 86 | ``` 87 | 88 | ## 应用 89 | ### 拓展数组索引访问 90 | 负索引访问,使`array[-N]` 与 `array[array.length - N]` 相同 91 | ```js 92 | let array = [1, 2, 3]; 93 | 94 | array = new Proxy(array, { 95 | get(target, prop, receiver) { 96 | if (prop < 0) { 97 | console.log(prop, 'prop') 98 | prop = +prop + target.length; 99 | } 100 | return Reflect.get(target, prop, receiver); 101 | } 102 | }); 103 | 104 | 105 | console.log(array[-1]); // 3 106 | console.log(array[-2]); // 2 107 | ``` 108 | ### 数据劫持 109 | ```js 110 | let handlers = Symbol('handlers'); 111 | 112 | function makeObservable(target) { 113 | // 初始化存储 handler 的数组 114 | target[handlers] = []; 115 | 116 | // 存储 handler 函数到数组中以便于未来调用 117 | target.observe = function(handler) { 118 | this[handlers].push(handler); 119 | }; 120 | 121 | // 创建代理以处理更改 122 | return new Proxy(target, { 123 | set(target, property, value, receiver) { 124 | // 转发写入操作到目标对象 125 | let success = Reflect.set(...arguments); 126 | // 如果设置属性的时候没有报错 127 | if (success) { 128 | // 调用所有 handler 129 | target[handlers].forEach(handler => handler(property, value)); 130 | } 131 | return success; 132 | } 133 | }); 134 | } 135 | 136 | let user = {}; 137 | 138 | user = makeObservable(user); 139 | 140 | user.observe((key, value) => { 141 | console.log(`SET ${key}=${value}`); 142 | }); 143 | 144 | user.name = "John"; 145 | // SET name=John 146 | ``` -------------------------------------------------------------------------------- /ideological-learning/前端中的设计模式.md: -------------------------------------------------------------------------------- 1 | # 基础设计模式学习 2 | > 设计模式(Design pattern)代表了最佳的实践, 是软件开发人员在软件开发过程中面临的一般问题的解决方案。这些解决方案是众多软件开发人员经过相当长的一段时间的试验和错误总结出来的,这里选取了部分基础前端常用的做介绍 3 | 4 | - 策略模式 5 | - 发布订阅者模式 6 | - 责任链模式 7 | ## 策略模式 8 | > 场景:当你需要大量校验的时候 9 | 10 | #### 第一步:定义策略对象 11 | ```js 12 | // 策略对象 13 | const strategies = { 14 | checkName: (name) => { // 姓名校验 15 | if (name === 'jix') { 16 | return true 17 | } 18 | return false 19 | }, 20 | checkPhone: (phone) => { // 电话校验 21 | if (typeof phone === 'number') { 22 | return true 23 | } 24 | return false 25 | } 26 | } 27 | ``` 28 | 将所有的校验方法放到一个校验对象中 29 | #### 第二步:创建校验器 30 | ```js 31 | let Validator = function() { 32 | this.checkQueue = [] // 校验队列 33 | 34 | // 添加策略 35 | this.add = (val, method) => { // val需要校验的值 method校验方法名 36 | this.checkQueue.push(()=> strategies[method](val)) 37 | } 38 | 39 | // 校验 40 | this.check = () => { 41 | for(let i = 0; i < this.checkQueue.length; i++) { 42 | let data = this.checkQueue[i]() 43 | if(!data) { 44 | return false 45 | } 46 | } 47 | return true 48 | } 49 | } 50 | ``` 51 | 到此前期的工作就已经准备好了 52 | #### 第三步:使用校验器来校验 53 | 54 | ```js 55 | let user1 = () => { 56 | const validator = new Validator() 57 | const data = { 58 | name: 'jix', 59 | phone: 666 60 | } 61 | 62 | validator.add(data.name, 'checkName') 63 | validator.add(data.phone, 'checkPhone') 64 | const result = validator.check() 65 | return result 66 | } 67 | console.log(user1()) // true 68 | ``` 69 | 当有数据需要校验的时候,直接通过实例化校验器来实现,这样条理更加清晰,添加校验项管理起来也更加方便。 70 | 71 | ## 发布订阅者模式 72 | > 发布订阅者模式再前端里面算是比较常见的,只要是用来定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知 73 | ```js 74 | class PubSub { 75 |   constructor() { 76 |     this.handleBars = {}; // 保存监听事件 77 |   } 78 |   // 订阅 79 |   subscribe(eventName, handle) { 80 |     try { 81 |       if (!this.handleBars.hasOwnProperty(eventName)) { 82 |         this.handleBars[eventName] = []; 83 |       } 84 |       if (typeof handle == 'function') { 85 |         this.handleBars[eventName].push(handle); 86 |       } else { 87 |         throw new Error(`你需要给${eventName}事件添加回调方法`); 88 |       } 89 |     } catch (error) { 90 |       console.warn(error); 91 |     } 92 |   } 93 |   // 发布 94 |   publish(eventName, ...arg) { 95 |     try { 96 |       if (this.handleBars.hasOwnProperty(eventName)) { 97 |         this.handleBars[eventName].map(item => { 98 |           item.apply(null, arg) 99 |         }) 100 |       } else { 101 |         throw new Error(`${eventName}事件未被注册`); 102 |       } 103 |     } catch (error) { 104 |       console.warn(error); 105 |     } 106 |   } 107 |   // 移除订阅 108 |   unSubscribe(eventName, handle) { 109 |     try { 110 |       if (this.handleBars.hasOwnProperty(eventName)) { 111 |         this.handleBars[eventName].map((item, index) => { 112 |           if (item === handle) { 113 |             console.log(item); 114 |             this.handleBars[eventName].splice(index, 1) 115 |           } 116 |         }) 117 |       } 118 |     } catch (error) { 119 |       console.warn(error); 120 |     } 121 |   } 122 | } 123 | ``` 124 | 125 | ```js 126 | // 实例化 127 | const sub = new PubSub(); 128 | 129 | // 订阅的回调方法 130 | function func1(type) { 131 |   console.log('尼古拉斯赵四:', type); 132 | } 133 | function func2(type) { 134 |   console.log('王凯旋:', type); 135 | } 136 | function func3(type) { 137 |   console.log('职业法师:', type); 138 | } 139 | // 订阅事件 140 | sub.subscribe('微信', func1) 141 | sub.subscribe('微信', func2) 142 | sub.subscribe('QQ', func3) 143 | 144 | setTimeout(() => { 145 |   // 触发订阅事件 146 |   sub.publish('微信', '公众号推文') 147 |   sub.publish('QQ', '动态更新', '666') 148 |   // 移除订阅的ready事件func1回调 149 |   sub.unSubscribe('微信', func1); 150 |   console.log(sub.handleBars); 151 | }, 1000) 152 | // 输出结果: 153 | // 尼古拉斯赵四: 公众号推文 154 | // 王凯旋: 公众号推文 155 | // 职业法师: 动态更新 156 | // { '微信': [ [Function: func2] ], QQ: [ [Function: func3] ] } 157 | ``` -------------------------------------------------------------------------------- /bottom-learning/基础知识.md: -------------------------------------------------------------------------------- 1 | # 关于跳出循环 2 | - for循环只能使用continue结束本次循环,break结束当前循环。不能使用return,会报错 3 | - forEach循环不能使用continue和break会报错,return无法结束循环(只能达到continue的效果),想结束foreach循环只能抛出错误 4 | 5 | ```js 6 | for (let i = 1; i < 3; i++) { 7 | if (i === 2) { 8 | return // Uncaught SyntaxError: Illegal return statement 9 | } 10 | console.log(i) 11 | } 12 | console.log('end') 13 | ``` 14 | return的作用是指定函数的返回值,在这里for循环的外部没有函数包裹,所以会报错,开发过程中有很多在循环中return的情况,这里我们要弄清楚 15 | 16 | ```js 17 | let arr = ['a', 'b', 'c', 'd'] 18 | 19 | arr.forEach((ele, index) => { 20 | if (index === 2) { 21 | return // 这里如果改成break/continue都会报错 22 | } 23 | console.log(ele) 24 | }) 25 | 26 | console.log('end') 27 | // 输出结果: a, b, d, end 28 | ``` 29 | 从上面的代码中可以看出,return的效果只是结束了当次循环,并没有跳出循环 30 | > forEach()无法在所有元素都传递给调用的函数之前终止遍历 -----摘抄《JavaScript权威指南》 31 | 32 | 解决方法:使用every或者some来代替forEach,或者使用try catch将循环包裹在循环内部抛出错误来结束 33 | 34 | # 0.1+0.2为什么不等于0.3 35 | ```js 36 | console.log(0.1+0.2) 37 | // 输出结果: 0.30000000000000004 38 | ``` 39 | 通过查阅资料得知JavaScript内部采用的IEEE 754标准,number类型默认为双精度浮点型(64位),然后我发现了以下的问题: 40 | - 首先我们都知道计算机内部存储是采用的二进制 41 | - 将0.1转为二进制是无限循环小数,0.2也是无线循环小数,在存储的过程中有没有可能会出现精度丢失呢 42 | 43 | ```js 44 | let a = 0.1 45 | console.log(a.toString(2)) 46 | // 输出结果: 0.0001100110011001100110011001100110011001100110011001101 47 | 48 | let b = 0.2 49 | console.log(b.toString(2)) 50 | // 输出结果: 0.001100110011001100110011001100110011001100110011001101 51 | 52 | let c = 0.1+0.2 53 | console.log(c.toString(2)) 54 | // 输出结果: 0.0100110011001100110011001100110011001100110011001101 55 | 56 | let d = 0.3 57 | console.log(d.toString(2)) 58 | // 输出结果: 0.010011001100110011001100110011001100110011001100110011 59 | 60 | console.log(Math.pow(2, 50) + 0.1) 61 | // 输出结果: 1125899906842624 62 | console.log(Math.pow(2, 49) + 0.1) 63 | // 输出结果: 562949953421312.1 64 | ``` 65 | 带着疑问经过一系列尝试,得出结论在涉及到长度溢出的时候会出现精度丢失问题,当长度溢出会默认做截取,从上面的输出中可以看出0.1+0.2转成二进制后大于0.3直接转二进制.当整数部分溢出也会出现截取现象 66 | 67 | ## ==和===的区别 68 | > 为什么ESLint推荐使用===代替==呢,肯定是有其中的原因的 69 | - == 代表相等,如果==两边是不同类型的,会隐式的转化为相同类型的进行比较 70 | - === 代表严格相等, 如果===两边是不同类型直接为false 71 | ```js 72 | console.log(0 == false) // true 73 | console.log(0 === false) // false 74 | console.log(Number(false)) // 0 75 | ``` 76 | 从上面的输出可以看出, `0==false`中间经历了一次隐式的`Number(false)`将==两边都转化为数字再进行比较的,而===两边类型不同直接为false,那么隐式的类型转换有什么弊端呢 77 | 78 | ```js 79 | let x = 1 80 | let obj = { 81 | valueOf: () => { 82 | x = 2 83 | return 0 84 | } 85 | } 86 | console.log(obj == 0, x) // true, 2 87 | // 可能导致意料之外的改变 88 | ``` 89 | 以上代码中我并不想去改变x的值,但是obj转number的时候,obj会调用自身的valueOf方法,导致了x值的改变,这次改变是意料之外的 90 | 91 | ```js 92 | let x = 1 93 | let obj = { 94 | valueOf: () => { 95 | return {} 96 | }, 97 | toString: () => { 98 | return {} 99 | } 100 | } 101 | console.log(obj == 0, x) 102 | // Uncaught TypeError: Cannot convert object to primitive value 103 | // 可能会出现意料之外的报错 104 | ``` 105 | 当valueOf和toString都没有返回时,会抛出异常 106 | > 对象转化成数字的规则: 107 | > - 如果对象有valueOf方法,则调用该方法,并返回相应的结果 108 | > - 当调用valueOf返回的依然不是数字,则会调用对象的toString方法,并返回相应的结果 109 | > - 否则抛出异常 110 | 111 | # 基础题 112 | ```js 113 | // 1 114 | console.log(1<2<3) // true 115 | console.log(3>2>1) // false 116 | // 1, 1<2 -> true true<3 -> 1<3 -> true 117 | // 3>2 -> true true>1 -> false 118 | 119 | // 2 120 | typeof null // object 121 | null instanceof Object // false 122 | 123 | // 3 124 | var end = Math.pow(2,53) 125 | var start = end - 100 126 | var count = 0 127 | for (var i = start; i <= end; i++ ) { 128 | count++ 129 | } 130 | console.log(count) 131 | // 死循环陷阱 2^53是能表示的最大值 132 | 133 | // 4 134 | var a = Date(0) // 当前时间字符串 135 | var b = new Date(0) // 1970年时间对象 136 | var c = new Date() // 当前时间对象 137 | 138 | [a === b, b === c, a === c] 139 | // false false false 140 | ``` 141 | 142 | # typescript 类写constructor和不写constructor的区别 143 | 当创建类的实例时,默认会调用类对应的constructor构造函数 144 | ```typescript 145 | class Person { 146 | name: string; 147 | age: number; 148 | 149 | constructor(name: string, age: number) { 150 | this.name = name; 151 | this.age = age; 152 | } 153 | } 154 | 155 | // 创建实例时,constructor构造函数被调用,初始化了 name 和 age 属性 156 | var p: Person = new Person('Felipe', 36); 157 | ``` 158 | 当不写constructor构造函数时,编译器默认给该类生成一个空的constructor构造函数 159 | 160 | # Object.freeze和Object.seal的区别? 161 | - `Object.preventExtension`: 禁止对象添加属性 162 | - `Object.seal`: 在对象上调用Object.preventExtension(...)并且把所有属性标记为configurable: false, 即不能给对象添加新属性, 也不能重新配置对象的所有属性 163 | - `Object.freeze`: 在对象上调用Object.seal(...)并把所有属性标记为writable: false, 即"不能给对象添加属性, 也不能修改对象的属性值, 并且还不能重新配置该对象的所有属性, 基本上不能对该对象做任何事情". 164 | -------------------------------------------------------------------------------- /code.js: -------------------------------------------------------------------------------- 1 | // 模拟实现Promise 2 | // Promise利用三大手段解决回调地狱: 3 | // 1. 回调函数延迟绑定 4 | // 2. 返回值穿透 5 | // 3. 错误冒泡 6 | 7 | // 定义三种状态 8 | const PENDING = 'PENDING'; // 进行中 9 | const FULFILLED = 'FULFILLED'; // 已成功 10 | const REJECTED = 'REJECTED'; // 已失败 11 | 12 | class Promise { 13 | constructor(exector) { 14 | // 初始化状态 15 | this.status = PENDING; 16 | // 将成功、失败结果放在this上,便于then、catch访问 17 | this.value = undefined; 18 | this.reason = undefined; 19 | // 成功态回调函数队列 20 | this.onFulfilledCallbacks = []; 21 | // 失败态回调函数队列 22 | this.onRejectedCallbacks = []; 23 | 24 | const resolve = value => { 25 | // 只有进行中状态才能更改状态 26 | if (this.status === PENDING) { 27 | this.status = FULFILLED; 28 | this.value = value; 29 | // 成功态函数依次执行 30 | this.onFulfilledCallbacks.forEach(fn => fn(this.value)); 31 | } 32 | } 33 | const reject = reason => { 34 | // 只有进行中状态才能更改状态 35 | if (this.status === PENDING) { 36 | this.status = REJECTED; 37 | this.reason = reason; 38 | // 失败态函数依次执行 39 | this.onRejectedCallbacks.forEach(fn => fn(this.reason)) 40 | } 41 | } 42 | try { 43 | // 立即执行executor 44 | // 把内部的resolve和reject传入executor,用户可调用resolve和reject 45 | exector(resolve, reject); 46 | } catch(e) { 47 | // executor执行出错,将错误内容reject抛出去 48 | reject(e); 49 | } 50 | } 51 | then(onFulfilled, onRejected) { 52 | onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; 53 | onRejected = typeof onRejected === 'function'? onRejected : 54 | reason => { throw new Error(reason instanceof Error ? reason.message : reason) } 55 | // 保存this 56 | const self = this; 57 | return new Promise((resolve, reject) => { 58 | if (self.status === PENDING) { 59 | self.onFulfilledCallbacks.push(() => { 60 | // try捕获错误 61 | try { 62 | // 模拟微任务 63 | setTimeout(() => { 64 | const result = onFulfilled(self.value); 65 | // 分两种情况: 66 | // 1. 回调函数返回值是Promise,执行then操作 67 | // 2. 如果不是Promise,调用新Promise的resolve函数 68 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 69 | }) 70 | } catch(e) { 71 | reject(e); 72 | } 73 | }); 74 | self.onRejectedCallbacks.push(() => { 75 | // 以下同理 76 | try { 77 | setTimeout(() => { 78 | const result = onRejected(self.reason); 79 | // 不同点:此时是reject 80 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 81 | }) 82 | } catch(e) { 83 | reject(e); 84 | } 85 | }) 86 | } else if (self.status === FULFILLED) { 87 | try { 88 | setTimeout(() => { 89 | const result = onFulfilled(self.value); 90 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 91 | }); 92 | } catch(e) { 93 | reject(e); 94 | } 95 | } else if (self.status === REJECTED) { 96 | try { 97 | setTimeout(() => { 98 | const result = onRejected(self.reason); 99 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 100 | }) 101 | } catch(e) { 102 | reject(e); 103 | } 104 | } 105 | }); 106 | } 107 | catch(onRejected) { 108 | return this.then(null, onRejected); 109 | } 110 | static resolve(value) { 111 | if (value instanceof Promise) { 112 | // 如果是Promise实例,直接返回 113 | return value; 114 | } else { 115 | // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED 116 | return new Promise((resolve, reject) => resolve(value)); 117 | } 118 | } 119 | static reject(reason) { 120 | return new Promise((resolve, reject) => { 121 | reject(reason); 122 | }) 123 | } 124 | static all(promiseArr) { 125 | const len = promiseArr.length; 126 | const values = new Array(len); 127 | // 记录已经成功执行的promise个数 128 | let count = 0; 129 | return new Promise((resolve, reject) => { 130 | for (let i = 0; i < len; i++) { 131 | // Promise.resolve()处理,确保每一个都是promise实例 132 | Promise.resolve(promiseArr[i]).then( 133 | val => { 134 | values[i] = val; 135 | count++; 136 | // 如果全部执行完,返回promise的状态就可以改变了 137 | if (count === len) resolve(values); 138 | }, 139 | err => reject(err), 140 | ); 141 | } 142 | }) 143 | } 144 | static race(promiseArr) { 145 | return new Promise((resolve, reject) => { 146 | promiseArr.forEach(p => { 147 | Promise.resolve(p).then( 148 | val => resolve(val), 149 | err => reject(err), 150 | ) 151 | }) 152 | }) 153 | } 154 | } -------------------------------------------------------------------------------- /bottom-learning/flutter异步编程.md: -------------------------------------------------------------------------------- 1 | # 事件循环、Isolate 2 | 开始前我们需要明白 `Dart 是单线程的并且 Flutter 依赖于 Dart` 3 | 4 | 如果你知道[js 中的event loop](https://github.com/gushisan/markdown/blob/master/bottom-learning/event-loop.md) 将很好理解dart的整个异步过程 5 | 6 | 先看一段代码 7 | ```dart 8 | import 'dart:async'; 9 | 10 | Future eventLoop() async{ 11 | print('A'); 12 | 13 | Future((){ 14 | print('F'); 15 | scheduleMicrotask((){print('H');}); 16 | Future((){ 17 | print('M'); 18 | }).then((_){ 19 | print('N'); 20 | }); 21 | }).then((_){ 22 | print('G'); 23 | }); 24 | 25 | Future((){ 26 | print('I'); 27 | }).then((_){ 28 | print('J'); 29 | }); 30 | 31 | scheduleMicrotask(text1); 32 | scheduleMicrotask((){print('D');}); 33 | 34 | print('B'); 35 | } 36 | 37 | void text1() { 38 | print('C'); 39 | scheduleMicrotask((){print('E');}); 40 | Future((){ 41 | print('K'); 42 | }).then((_){ 43 | print('L'); 44 | }); 45 | } 46 | ``` 47 | 你只到输出结果吗 48 | 49 | > 正确的输出顺序是: A B C D E F G H I J K L M N 50 | 51 | 52 | ## eventLoop 53 | ### 1、MicroTask 队列 54 | 微任务队列,一般使用`scheduleMicroTask`方法向队列中添加 55 | 56 | 这是大多数时候你不必使用的东西。比如,在整个 Flutter 源代码中 scheduleMicroTask() 方法仅被引用了 7 次, 所以最好优先考虑使用 Event 队列 57 | ### 2、Event 队列 58 | `I/O、手势、绘图、计时器、流、futures等等`异步操作都将进入event队列 59 | 60 | 尽量使用事件队列可以使微任务队列更短,降低事件队列卡死的可能性 61 | 62 | ## 代码执行顺序 63 | 首先我们知道dart是单线程的,所以dart的代码执行顺序是: 64 | 1. 同步代码依次执行 65 | 2. 碰到异步代码先进对应的队列中,然后继续执行下面的代码 66 | 3. 当同步代码执行完毕,先去看MicroTask 队列中的任务,将MicroTask队列中的任务依次执行完毕 67 | 4. MicroTask中的任务执行完毕后,再去看Event 队列中的任务,event队列出一个任务 然后执行 , 然后回到第三步 循环 直到所有队列都清空 68 | 69 | ## Isolate 70 | Isolate 是 Dart 中的 线程, Flutter的代码都是默认跑在root isolate上的 71 | > 「Isolate」在 Flutter 中并不共享内存。不同「Isolate」之间通过「消息」进行通信。 72 | 73 | ```dart 74 | import 'dart:async'; 75 | import 'dart:io'; 76 | import 'dart:isolate'; 77 | 78 | import 'package:flutter/foundation.dart'; 79 | import 'package:flutter/material.dart'; 80 | 81 | //一个普普通通的Flutter应用的入口 82 | //main函数这里有async关键字,是因为创建的isolate是异步的 83 | void main() async{ 84 | runApp(MyApp()); 85 | 86 | //asyncFibonacci函数里会创建一个isolate,并返回运行结果 87 | print(await asyncFibonacci(20)); 88 | } 89 | 90 | //这里以计算斐波那契数列为例,返回的值是Future,因为是异步的 91 | Future asyncFibonacci(int n) async{ 92 | //首先创建一个ReceivePort,为什么要创建这个? 93 | //因为创建isolate所需的参数,必须要有SendPort,SendPort需要ReceivePort来创建 94 | final response = new ReceivePort(); 95 | //开始创建isolate,Isolate.spawn函数是isolate.dart里的代码,_isolate是我们自己实现的函数 96 | //_isolate是创建isolate必须要的参数。 97 | await Isolate.spawn(_isolate,response.sendPort); 98 | //获取sendPort来发送数据 99 | final sendPort = await response.first as SendPort; 100 | //接收消息的ReceivePort 101 | final answer = new ReceivePort(); 102 | //发送数据 103 | sendPort.send([n,answer.sendPort]); 104 | //获得数据并返回 105 | return answer.first; 106 | } 107 | 108 | //创建isolate必须要的参数 109 | void _isolate(SendPort initialReplyTo){ 110 | final port = new ReceivePort(); 111 | //绑定 112 | initialReplyTo.send(port.sendPort); 113 | //监听 114 | port.listen((message){ 115 | //获取数据并解析 116 | final data = message[0] as int; 117 | final send = message[1] as SendPort; 118 | //返回结果 119 | send.send(syncFibonacci(data)); 120 | }); 121 | } 122 | 123 | int syncFibonacci(int n){ 124 | return n < 2 ? n : syncFibonacci(n-2) + syncFibonacci(n-1); 125 | } 126 | ``` 127 | 因为Root isolate会负责渲染,还有UI交互,如果我们有一个很耗时的操作呢?前面知道isolate里是一个event loop(事件循环),如果一个很耗时的task一直在运行,那么后面的UI操作都被阻塞了,所以如果我们有耗时的操作,就应该放在isolate里! 128 | # Stream(流) 129 | 什么是流? 130 | ![](https://user-gold-cdn.xitu.io/2018/10/1/1662d3f569b48736?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 131 | - 这个大机器就是StreamController,它是创建流的方式之一。 132 | - StreamController有一个入口,叫做sink 133 | - sink可以使用add方法放东西进来,放进去以后就不再关心了。 134 | - 当有东西从sink进来以后,我们的机器就开始工作 135 | - StreamController有一个出口,叫做stream 136 | - 机器处理完毕后就会把产品从出口丢出来,但是我们并不知道什么时候会出来,所以我们需要使用listen方法一直监听这个出口 137 | - 而且当多个物品被放进来了之后,它不会打乱顺序,而是先入先出 138 | 139 | ## 使用Stream 140 | ```dart 141 | StreamController controller = StreamController(); 142 | 143 | //监听这个流的出口,当有data流出时,打印这个data 144 | StreamSubscription subscription = 145 | controller.stream.listen((data)=>print("$data")); 146 | 147 | controller.sink.add(123); 148 | 149 | // 输出: 123 150 | ``` 151 | 你需要将一个方法交给stream的listen函数,这个方法入参(data)是我们的StreamController处理完毕后产生的结果,我们监听出口,并获得了这个结果(data)。这里可以使用lambda表达式,也可以是其他任何函数 152 | 153 | ## transform 154 | 如果你需要更多的控制转换,那么请使用transform()方法。他需要配合StreamTransformer进行使用。 155 | ```dart 156 | StreamController controller = StreamController(); 157 | 158 | final transformer = StreamTransformer.fromHandlers( 159 | handleData:(value, sink){ 160 | if(value==100){ 161 | sink.add("你猜对了"); 162 | } 163 | else{ sink.addError('还没猜中,再试一次吧'); 164 | } 165 | }); 166 | 167 | controller.stream 168 | .transform(transformer) 169 | .listen( 170 | (data) => print(data), 171 | onError:(err) => print(err)); 172 | 173 | controller.sink.add(23); 174 | //controller.sink.add(100); 175 | 176 | // 输出: 还没猜中,再试一次吧 177 | ``` 178 | StreamTransformer是我们stream的检查员,他负责接收stream通过的信息,然后进行处理返回一条新的流。 179 | - S代表之前的流的输入类型,我们这里是输入一个数字,所以是int。 180 | - T代表转化后流的输入类型,我们这里add进去的是一串字符串,所以是String。 181 | - handleData接收一个value并创建一条新的流并暴露sink,我们可以在这里对流进行转化 182 | 183 | ## Stream的种类 184 | - "Single-subscription" streams 单订阅流 185 | - "broadcast" streams 多订阅流 186 | 187 | ## 单订阅流 188 | ```dart 189 | StreamController controller = StreamController(); 190 | 191 | controller.stream.listen((data)=> print(data)); 192 | controller.stream.listen((data)=> print(data)); 193 | 194 | controller.sink.add(123); 195 | 196 | // 输出: Bad state: Stream has already been listened to. 单订阅流不能有多个收听者。 197 | ``` 198 | 单个订阅流在流的整个生命周期内仅允许有一个listener 199 | 200 | ## 多订阅流 201 | ```dart 202 | StreamController controller = StreamController(); 203 | //将单订阅流转化为广播流 204 | Stream stream = controller.stream.asBroadcastStream(); 205 | 206 | stream.listen((data)=> print(data)); 207 | stream.listen((data)=> print(data)); 208 | 209 | controller.sink.add(123); 210 | 211 | // 输出: 123 123 212 | ``` 213 | 广播流允许任意数量的收听者,且无论是否有收听者,他都能产生事件。所以中途进来的收听者将不会收到之前的消息。 214 | 215 | > 参考[深入了解isolate](https://juejin.im/post/6844903757759643662) -------------------------------------------------------------------------------- /work-notes/正则.md: -------------------------------------------------------------------------------- 1 | 2 | # 正则 3 | > 在平时的工作中常常会碰到正则,但是我发现,每次都忘记该怎么去写,所以在这里稍微复习总结一下 4 | # 先看题 5 | ```js 6 | /* 题目一 */ 7 | var str1 = '123456765464153513566' 8 | // 分割数字每三个以一个逗号划分(从后往前) 9 | // 如1234 -> 1,234 10 | 11 | 12 | /* 题目二 */ 13 | var str2 = "get-element-by-id" 14 | // 将-的命名方式改为小驼峰 15 | // 期望结果getElementById 16 | 17 | 18 | /* 题目三 */ 19 | var str3 = 'getElementById' 20 | console.log(str3.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()) 21 | // 写出运行结果 22 | ``` 23 | 看完这三个题目,你是否有想法,下面公布答案 24 | ```js 25 | // 题目一 26 | console.log(str1.replace(/(\d)(?=(\d{3})+$)/g,'$1,')) 27 | // 123,456,765,464,153,513,566 28 | 29 | // 题目二 30 | console.log(str2.replace(/-\w/g, ($0) => { 31 | return $0.slice(1).toUpperCase() 32 | })) 33 | // getElementById 34 | 35 | // 题目三 36 | // get-element-by-id 37 | ``` 38 | 39 | 如果你的答案全部正确,那么请忽略下面的内容 40 | 41 | # 复习 42 | 43 | ## 简单的匹配规则 44 | - 如想在`apple`这个单词里找到`a`这个字符,就直接用`/a/`这个正则就可以了 45 | - 如果想匹配`*`,需要使用`\`来转义去掉其本来的含义,正则就可以写成`/\*/` 46 | - 有一些字符本不是特殊字符,使用转义符号就会让它拥有特殊的含义 47 | 48 | | 特殊字符 | 正则表达式 | 记忆方式 | 49 | | :-----:| :----: | :----: | 50 | | 换行符 | \n | new line | 51 | | 换页符 | \f | form feed | 52 | | 回车符 | \r | return | 53 | | 空白符 | \s | space | 54 | | 制表符 | \t | tab | 55 | | 垂直制表符 | \v | vertical tab | 56 | | 回退符 | [\b] | backspace,之所以使用[]符号是避免和\b重复 | 57 | 58 | ## 匹配多字符 59 | - 集合的定义方式是使用中括号`[`和`]`,如`/[123]/`这个正则就能同时匹配1,2,3三个字符。用`/[0-9]/`就能匹配所有的数字, `/[a-z]/`则可以匹配所有的英文小写字母 60 | - 同时匹配多个字符的简便正则表达式: 61 | 62 | | 匹配区间 | 正则表达式 | 记忆方式 | 63 | | :-----:| :----: | :----: | 64 | | 除了换行符之外的任何字符 | . | 句号,除了句子结束符 | 65 | | 单个数字, [0-9] | \d | digit | 66 | | 除了[0-9] | \D | not digit | 67 | | 包括下划线在内的单个字符,[A-Za-z0-9_] | \w | word | 68 | | 非单字字符 | \W | not word | 69 | | 匹配空白字符,包括空格、制表符、换页符和换行符 | \s | space | 70 | | 匹配非空白字符 | \S | not space | 71 | 72 | ## 重复匹配 73 | - 元字符 `?` 代表了匹配一个字符或0个字符, 例匹配 `color` 和 `colour` 这两个单词, 就可以写为 `/colou?r/` 74 | - 元字符 `*` 用来表示匹配0个字符或无数个字符, 例匹配 `color` 和`colouuuuuur` 就可以写为 `/colou*r/` 75 | - 元字符+适用于要匹配同个字符出现1次或多次的情况。 `color`和`colour` 这两个单词, 若使用 `/colou+r/` 来匹配,就只能匹配到`colour` 76 | - 匹配特定的次数, 可以使用元字符 `{` 和 `}` 用来给重复匹配设置精确的区间范围。如 `a` 我想匹配3次,那么我就使用 `/a{3}/` 这个正则,或者说 `a` 我想匹配至少两次就是用 `/a{2,}/` 这个正则。 77 | - {x}: x次 78 | - {min, max}: 介于min次到max次之间 79 | - {min, }: 至少min次 80 | - {0, max}: 至多max次 81 | > 总结 82 | > | 匹配规则 | 元字符 | 83 | > | :-----:| :----: | 84 | > | 0次或1次 | ? | 85 | > | 0次或无数次 | * | 86 | > | 1次或无数次 | + | 87 | > | 特定次数 | {x}, {min, max} | 88 | 89 | ## 位置边界 90 | - 单词边界 `\b` , 例 `The cat scattered his food all over the room.` 匹配出所有的单词`cat` , 就可以写成 `/\bcat\b/g` 91 | - 字符串边界,元字符 `^` 用来匹配字符串的开头。而元字符`$` 用来匹配字符串的末尾。 92 | 93 | 94 | 边界总结: 95 | 96 | | 边界和标志 | 正则表达式 | 记忆方式 | 97 | | :-----:| :----: | :----: | 98 | | 单词边界 | \b | boundary | 99 | | 非单词边界 | \B | not boundary | 100 | | 字符串开头 | ^ | - | 101 | | 字符串结尾 | $ | - | 102 | | 多行模式 | m标志 | multiple of lines | 103 | | 忽略大小写 | i标志 | ignore case, case-insensitive | 104 | | 全局模式 | g标志 | global | 105 | 106 | ## 子表达式 107 | ### **分组** 108 | 所有以 `(` 和 `)` 元字符所包含的正则表达式被分为一组,每一个分组都是一个`子表达式`,它也是构成高级正则表达式的基础。 109 | ### **回溯引用** 110 | 回溯引用(backreference)指的是后面部分引用前面已经匹配到的子字符串。你可以把它想象成是变量,回溯引用的语法像`\1` , `\2`,....,其中 `\1` 表示引用的第一个子表达式,`\2` 表示引用的第二个子表达式,以此类推。而 `\0` 则表示整个表达式。 111 | ``` js 112 | // 例如 113 | // 匹配下面字符串中两个连续的单词 114 | // Hello what what is the first thing, and I am am scq000. 115 | var str4 = 'Hello what what is the first thing, and I am am scq000.' 116 | 117 | console.log(str4.match(/\b(\w+)\s\1/g)) 118 | ``` 119 | 120 | 用 `$1` , `$2` ...来引用要被替换的字符串 121 | ```js 122 | var str = 'abc abc 123'; 123 | str.replace(/(ab)c/g,'$1g'); 124 | // 得到结果 'abg abg 123' 125 | ``` 126 | 127 | 如果我们不想子表达式被引用,可以使用非捕获正则(?:regex)这样就可以避免浪费内存。 128 | ```js 129 | var str = 'scq000'. 130 | str.replace(/(scq00)(?:0)/, '$1,$2') 131 | // 返回scq00,$2 132 | // 由于使用了非捕获正则,所以第二个引用没有值,这里直接替换为$2 133 | 134 | var str4 = 'scq000 scq001' 135 | console.log(str4.replace(/(scq00)(?:0)/, '$1,$2')) 136 | // 返回 scq00,$2 scq001 137 | ``` 138 | #### **前向查找** 139 | 前向查找(lookahead)是用来限制后缀的。凡是以 `(?=regex)` 包含的子表达式在匹配过程中都会用来限制前面的表达式的匹配。例如`happy` `happily`这两个单词,我想获得以 `happ` 开头的副词,那么就可以使用`/happ(?=ily)/`来匹配, 就可以匹配到单词`happily`的`happ`前缀。如果我想过滤所有以 `happ` 开头的副词,那么也可以采用**负前向查找**的正则`/happ(?!ily)/`,就会匹配到`happy`单词的`happ`前缀。 140 | 141 | #### **后向查找** 142 | 后向查找(lookbehind)是通过指定一个子表达式,然后从符合这个子表达式的位置出发开始查找符合规则的字串。举个简单的例子: `apple` 和 `people` 都包含 `ple` 这个后缀,那么如果我只想找到 `apple` 的 `ple` ,该怎么做呢?我们可以通过限制app这个前缀,就能唯一确定 `ple` 这个单词了。 143 | ```js 144 | var str4 = 'apple people'; 145 | console.log(str4.replace(/(?<=ap)ple/,'-')) 146 | // 得到结果 'ap- people' 147 | // 说明匹配到的是单词apple的ple 148 | ``` 149 | `(?<=regex)` 的语法就是**后向查找**,`regex` 指代的子表达式会作为限制项进行匹配,匹配到这个子表达式后,就会继续向后查找。另外一种限制匹配是利用`(? 总结 152 | > | 回溯查找 | 正则 | 153 | > | :-----:| :----: | 154 | > | 引用 | \0,\1,\2 和 $0, $1, $2 | 155 | > | 非捕获组 | (?:) | 156 | > | 前向查找 | (?=) | 157 | > | 前向负查找 | (?!) | 158 | > | 后向查找 | (?<=) | 159 | > | 后向负查找 | (? 到这里正则差不多已经复习了一遍,我们现在再去看前面的三道题 170 | ## 题目一 171 | ```js 172 | /* 题目一 */ 173 | var str1 = '123456765464153513566' 174 | // 分割数字每三个以一个逗号划分(从后往前) 175 | // 如1234 -> 1,234 176 | console.log(str1.replace(/(\d)(?=(\d{3})+$)/g,'$1,')) 177 | // 123,456,765,464,153,513,566 178 | ``` 179 | 解析:`\d`表示单个数字,`(?=(\d{3})+$)`是一个前向查找,`\d{3})+$`表示匹配3位数字一次或者多次并且以三位数字结尾。连在一起看就是,匹配一个数字,数字后面的数字位数是3的倍数,所以匹配到的数字是`3, 6, 5, 4, 3, 3`,然后替换为`$1,`,故`3`替换为`3,` 、 `6`替换为`6,` .... 180 | 181 | ## 题目二 182 | ```js 183 | /* 题目二 */ 184 | var str2 = "get-element-by-id" 185 | // 将-的命名方式改为小驼峰 186 | // 期望结果getElementById 187 | console.log(str2.replace(/-\w/g, ($0) => { 188 | return $0.slice(1).toUpperCase() 189 | })) 190 | ``` 191 | 解析:首先`/-\w/g` 的意思是匹配所有前面是`-`的`单个字符`,匹配的结果是`-e, -b, -i`, 然后取其第二位(也就是将`-`截取掉),再转换为大写 192 | 193 | ## 题目三 194 | ```js 195 | /* 题目三 */ 196 | var str3 = 'getElementById' 197 | console.log(str3.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()) 198 | // 写出运行结果 199 | // 答案:get-element-by-id 200 | ``` 201 | 解析: `([a-z])([A-Z])`的意思就是匹配两个字母,并且第一个是小写,第二个是大写,所以匹配到的结果是`tE, tB, yI`,由于`()`代表分组,故`$1`代表的是匹配到的小写字母,`$2`代表的是匹配到的大写字母 202 | 203 | 204 | 205 | 206 | > 参考:[正则表达式不要背](https://juejin.im/post/5cdcd42551882568651554e6#heading-3) -------------------------------------------------------------------------------- /flutter/环信im.md: -------------------------------------------------------------------------------- 1 | # flutter环信sdk接入流程 2 | > 基于flutter 接入环信IM sdk,主要实现功能1v1聊天,语音消息、视频、语音电话、视频会议等,[环信sdk官网](https://www.easemob.com//?utm_source=baidu-ppwx) 3 | 4 | 5 | ## sdk的接入 6 | 7 | ### clone sdk到本地 8 | 地址[环信sdk](https://github.com/easemob/im_flutter_sdk) 9 | 10 | ### sdk引入 11 | 用本地的方式将sdk引入你的项目,在flutter项目中的pubspec.yaml 里面的 dependencies: 下添加: 12 | ```yaml 13 | im_flutter_sdk: 14 | path : /本地路径/im_flutter_sdk 15 | ``` 16 | 例如: 17 | ```yaml 18 | dependencies: 19 | flutter: 20 | sdk: flutter 21 | im_flutter_sdk: 22 | path: /Users/fengxiao/documents/demo/im_flutter_sdk 23 | ``` 24 | 然后 Package get 一下 25 | 26 | ### 初始化功能(ios) 27 | 1、在clone下来的sdk文件中拿到 /example/ios/Runner/ ,拿到Calls文件(环信原生iOS音视频相关UI文件),加到自己iOS项目中,这样可以自己在修改Calls中的原生iOS音视频相关UI文件,不会因为更新im_flutter_sdk,导致自己修改的UI部分也被更新。注意:不能直接复制需要使用xcode,勾选1、2、4项 28 | 29 | 30 | 2、在ios/Runner目录下的`Runner-Bridging-Header.h`文件中引入以下文件: 31 | ```h 32 | #import "EMCallPlugin.h" 33 | ``` 34 | 35 | 3、在ios/Runner目录下的`AppDelegate.swift`文件中注册plugin 36 | ```swift 37 | EMCallPlugin.register(with: self.registrar(forPlugin: "EMCallPlugin") as! FlutterPluginRegistrar) 38 | ``` 39 | 40 | 4、在ios/Runner目录下的Info.plist 添加权限 41 | ```plist 42 | NSCameraUsageDescription 43 | 需要使用您的相机 44 | NSLocationWhenInUseUsageDescription 45 | 需要使用您的位置信息 46 | NSMicrophoneUsageDescription 47 | 需要使用您的麦克风 48 | NSPhotoLibraryUsageDescription 49 | 需要使用您的相册 50 | NSContactsUsageDescription 51 | 需要访问您的通讯录 52 | ``` 53 | 54 | 5、在你的项目中的`main.dart`文件初始化的时候,加上sdk的初始化 55 | ```dart 56 | // 引入sdk 57 | import 'package:im_flutter_sdk/im_flutter_sdk.dart'; 58 | 59 | // 初始化 60 | EMOptions options = new EMOptions(appKey: "你申请的key"); 61 | EMClient.getInstance().init(options); 62 | 63 | // 开启debug模式 64 | EMClient.getInstance().setDebugMode(true); 65 | 66 | // 添加链接状态监听的接口 67 | EMClient.getInstance().addConnectionListener(this); 68 | 69 | // 初始化1v1音视频通话 70 | EMClient.getInstance().callManager().registerCallSharedManager(); 71 | 72 | // 初始化多人音视频通话 73 | EMClient.getInstance().conferenceManager().registerConferenceSharedManager(); 74 | ``` 75 | 76 | ## 登录注册功能接入 77 | > 注意:任何使用到sdk的地方都要引入:`import 'package:im_flutter_sdk/im_flutter_sdk.dart';` 78 | ### 登录功能 79 | ```dart 80 | // 查询当前是否登录 81 | bool isLoggedInBefore = await EMClient.getInstance().isLoggedInBefore(); 82 | 83 | // 登录 84 | EMClient.getInstance().login( 85 | username, 86 | password, 87 | onSuccess: (username) { 88 | // 登录成功 89 | }, 90 | onError: (code, desc) { 91 | // 登录失败 92 | switch(code) { 93 | case 2: { 94 | // 网络未连接! 95 | } 96 | break; 97 | 98 | case 202: { 99 | // 密码错误 100 | } 101 | break; 102 | 103 | case 204: { 104 | // 用户ID不存在 105 | } 106 | break; 107 | 108 | case 300: { 109 | // 无法连接服务器 110 | } 111 | break; 112 | 113 | default: { 114 | // desc 其他错误 115 | } 116 | break; 117 | } 118 | }); 119 | ``` 120 | ### 注册 121 | ```dart 122 | EMClient.getInstance().createAccount(username, password, 123 | onSuccess: (){ 124 | // 注册成功 125 | }, 126 | onError: (code, desc){ 127 | // 注册失败 128 | switch(code) { 129 | case 101: { 130 | // 用户ID不合法 131 | } 132 | break; 133 | 134 | case 102: { 135 | // 用户密码不合法 136 | } 137 | break; 138 | 139 | case 203: { 140 | // 用户ID已存在 141 | } 142 | break; 143 | 144 | case 300: { 145 | // 无法连接服务器 146 | } 147 | break; 148 | 149 | default: { 150 | // desc 其他错误 151 | } 152 | break; 153 | } 154 | } 155 | ); 156 | ``` 157 | ### 退出登录 158 | ```dart 159 | EMClient.getInstance().logout( 160 | false, // true 解除推送绑定 : false 不解除绑定 161 | onSuccess: (){ 162 | // 成功 163 | }, 164 | onError: (code, desc) { 165 | // 失败 166 | }, 167 | ); 168 | ``` 169 | 170 | ### 获取当前登录用户的信息 171 | ```dart 172 | userName = await EMClient.getInstance().getCurrentUser(); 173 | ``` 174 | 175 | ## 多人会议功能接入 176 | ```dart 177 | // 创建并加入会议 178 | EMClient.getInstance().conferenceManager().createAndJoinConference( 179 | EMConferenceType.EMConferenceTypeCommunication, // 会议类型 180 | '123', // 会议密码 181 | false, // 是否开启服务端录制 182 | false, // 录制时是否合并数据流 183 | onSuccess:(EMConference conf) { 184 | // 创建会议成功 185 | }, 186 | onError: (code, desc){ 187 | // 创建会议失败 188 | }); 189 | ``` 190 | 191 | ## 好友功能接入 192 | ```dart 193 | // 从服务器获取所有的好友 194 | EMClient.getInstance().contactManager().getAllContactsFromServer( 195 | onSuccess: (contacts){ 196 | // 成功 197 | }, 198 | onError: (code, desc){ 199 | // 失败 200 | } 201 | ); 202 | 203 | // 添加好友 204 | EMClient.getInstance().contactManager().addContact(username, null , 205 | onSuccess: () { 206 | // 成功 207 | }, 208 | onError: (code, desc){ 209 | // 失败 210 | }); 211 | 212 | // 同意好友请求 213 | EMClient.getInstance().contactManager().acceptInvitation(userName, 214 | onSuccess: (){ 215 | // 成功 216 | }, 217 | onError: (code, desc){ 218 | // 失败 219 | }); 220 | 221 | // 拒绝加好友请求 222 | EMClient.getInstance().contactManager().declineInvitation(userName, 223 | onSuccess: (){ 224 | // 成功 225 | }, 226 | onError: (code, desc){ 227 | // 失败 228 | }); 229 | 230 | // 删除联系人 231 | EMClient.getInstance().contactManager().deleteContact( 232 | userName, 233 | false, // true 保留会话和消息 false 不保留 234 | onSuccess: (){ 235 | // 成功 236 | }, 237 | onError: (code, desc){ 238 | // 失败 239 | }); 240 | ``` 241 | 242 | ## 消息相关功能接入 243 | ### 相关接口 244 | ```dart 245 | // 页面初始化加入 246 | // 添加消息监听 247 | EMClient.getInstance().chatManager().addMessageListener(this); 248 | // 添加链接状态监听的接口 249 | EMClient.getInstance().addConnectionListener(this); 250 | 251 | // 页面销毁时调用 252 | // 移除消息监听 253 | EMClient.getInstance().chatManager().removeMessageListener(this); 254 | // 移除链接状态监听的接口 255 | EMClient.getInstance().removeConnectionListener(this); 256 | 257 | // 获取所有会话 258 | Map map = await EMClient.getInstance().chatManager().getAllConversations(); 259 | 260 | // 删除与[userName] 的对话 261 | bool result = await EMClient.getInstance().chatManager().deleteConversation( 262 | username, 263 | true // 设置为true,则还会删除消息 264 | ); 265 | ``` 266 | ### 消息监听(实时更新消息) 267 | 1. 继承`EMConnectionListener`类 268 | 2. 重写`onMessageReceived`方法 269 | 3. 当有人发消息给当前用户就会触发`onMessageReceived`事件 270 | ```dart 271 | /// 消息监听 272 | void onMessageReceived(List messages){ 273 | 274 | } 275 | ``` 276 | 277 | ## 1v1聊天相关功能接入 278 | ### 语音、视频通话 279 | ```dart 280 | // 发起实时会话(语音、视频) 281 | EMClient.getInstance().callManager().startCall( 282 | EMCallType.Video, // 通话类型 video,voice 283 | toChatUsername, // 被呼叫的用户(不能与自己通话) 284 | true, // 是否开启服务端录制 285 | true, // 录制时是否合并数据流 286 | "1323", // 通话扩展信息,会传给被呼叫方 287 | onSuccess:() { 288 | // 成功 289 | } , 290 | onError:(code, desc){ 291 | // 失败 292 | }); 293 | ``` 294 | 295 | ### 发送图片、文本、语音消息 296 | ```dart 297 | // 1.图片消息 298 | // 创建一个图片消息 299 | EMMessage imageMessage = EMMessage.createImageSendMessage( 300 | imgPath, // 图片路径 301 | true, // 是否发送原图 302 | toChatUsername // 接收方id 303 | ); 304 | 305 | // 发送消息 306 | EMClient.getInstance().chatManager().sendMessage( 307 | imageMessage, // 前面创建的消息对象 308 | onSuccess:(){ 309 | // 发送成功 310 | } 311 | ); 312 | 313 | // 2.文本消息 314 | // 创建文本消息 315 | EMMessage message = EMMessage.createTxtSendMessage( 316 | text, // 发送内容 317 | toChatUsername // 接收方id 318 | ); 319 | // 消息体 320 | EMTextMessageBody body = EMTextMessageBody(text); 321 | message.body = body; 322 | // 发送消息统一调用上面的sendMessage 323 | 324 | // 3.语音消息 325 | EMMessage message = EMMessage.createVoiceSendMessage( 326 | path, // 本地语音路径 327 | length, // 语音长度(秒) 328 | toChatUsername // 接受人id 329 | ); 330 | // 发送消息统一调用上面的sendMessage 331 | 332 | ``` 333 | ### 获取消息 334 | > 消息监听同上,继承`EMConnectionListener`类,重写`onMessageReceived`方法 335 | ```dart 336 | // 获取与[id]聊天的消息 337 | conversation = await EMClient.getInstance().chatManager().getConversation( 338 | id, // 目标id 339 | 0, // 聊天类型 0单聊 340 | true // createIfNotExists 如果不存在则创建? 341 | ); 342 | 343 | // 所有消息设置为已读 344 | conversation.markAllMessagesAsRead(); 345 | 346 | // 根据传入的参数从db加载startMsgId之前(存储顺序)指定数量的message 347 | msgListFromDB = await conversation.loadMoreMsgFromDB( 348 | '', // startMsgId 349 | 20 // pageSize 350 | ); 351 | 352 | // 得到的msgListFromDB为当前最新的消息列表 353 | ``` -------------------------------------------------------------------------------- /advanced/前端性能初探-h5.md: -------------------------------------------------------------------------------- 1 | # 为什么要进⾏性能优化? 2 | > 虽然有一个所谓的8秒规则打开一个网页,用户有不同程度的耐心。作为 3 | 调查显示,如果一个网页加载时间超过4秒,流失率可能达到25%。一秒钟 4 | 延迟或3秒等待会使客户满意度降低16%。尤其是互联网用户越来越多 5 | 知情人士表示,他们对用户体验和网站加载速度会有更高的要求 6 | 成为网站收入的关键因素。 7 | ----[性能优化白皮书](https://www.cdnetworks.com/wp-content/uploads/2019/02/CDNetworks-Content-Acceleration-White-Paper.pdf) 8 | - 57%的⽤户更在乎⽹⻚在3秒内是否完成加载 9 | - 52%的在线⽤户认为⽹⻚打开速度影响到他们对⽹站的忠实度 10 | - 每慢1秒造成⻚⾯ PV(页面浏览量或点击量) 降低11%,⽤户满意度也随之降低降低16% 11 | - 近半数移动⽤户因为在10秒内仍未打开⻚⾯从⽽放弃 12 | 13 | # http缓存机制 14 | 试想,如果每个用户每次拿数据都直接向服务器请求,不论该数据有没有改变,这样显然是不合理的。所以浏览器中有缓存这个概念,合理的运用浏览器的缓存可以达到访问性能优化的效果 15 | 16 | 首先了解一下缓存的优先级,以下由优先级低到高依次介绍 17 | ## `last-modified / if-modified-since` 18 | 19 | 这是⼀组请求/相应头 20 | 21 | 响应头: 22 | last-modified: Wed, 16 May 2020 02:57:16 GMT 23 | 24 | 请求头: 25 | if-modified-since: Wed, 16 May 2020 05:55:38 GMT 26 | 27 | 服务器端返回资源时,如果头部带上了 last-modified,那么 28 | 资源下次请求时就会把值加⼊到请求头 if-modified-since 29 | 中,服务器可以对⽐这个值,确定资源是否发⽣变化,如果 30 | 没有发⽣变化,则返回 304。 31 | 32 | ## `etag / if-none-match` 33 | 这也是⼀组请求/相应头 响应头: 34 | 35 | etag: "D5FC8B85A045FF720547BC36FC872550" 36 | 37 | 请求头: 38 | if-none-match: "D5FC8B85A045FF720547BC36FC872550" 39 | 40 | 原理类似,服务器端返回资源时,如果头部带上了 etag,那么资源下 41 | 次请求时就会把值加⼊到请求头 if-none-match 中,服务器可以对⽐ 42 | 这个值,确定资源是否发⽣变化,如果没有发⽣变化,则返回 304。 43 | 44 | ## `expires` 45 | expires: Thu, 16 May 2019 03:05:59 GMT 46 | 47 | 在 http 头中设置⼀个过期时间,在这个过期时间之前,浏览器的请求都不会发出,⽽是⾃动从缓存中读取⽂件,除⾮缓存被清空,或者强制刷新。缺陷在于,服务器时间和⽤户端时间可能存在不⼀致,所以 HTTP/1.1 加⼊了 cache-control 头来改进这个问题。 48 | 49 | ## `cache-control` 50 | 设置过期的时间⻓度(秒),在这个时间范围内,浏览器请求都会直 51 | 接读缓存。当 expires 和 cache-control 都存在时,cache-control 的优先级更⾼。 52 | 53 | ## 总结 54 | 了解以上缓存的优先级后,我们大概可以复现浏览器在请求数据时所经过的步骤 55 | 56 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/3298907ef58a4b1b87c44fc494591ba2~tplv-k3u1fbpfcp-watermark.image) 57 | 58 | 当然,有时候缓存也是一个令人头痛的东西,在日常开发中,常常遇到以下这种场景。 59 | > 公司的h5项目新一期需求开发完毕,提交代码,Jenkins构建一气呵成,然后叫qa来测试。这时qa那边进网页看到的并不是最新构建的页面。好家伙,提着刀就来找你麻烦。最后发现是缓存的问题。emmm 60 | 61 | 以上问题纯属虚构,现实中用户并不会主动去清理缓存。出现上述问题完全是缓存造成的原因,你虽然构建完毕了,但是存在强缓,并不会去服务器拉取最新构建的代码,其实解决方法也很简单,简单粗暴,直接在index.html中禁用强缓 62 | ```html 63 | 64 | 65 | 66 | ``` 67 | 这时你会发现,构建完毕,就直接是最新的代码 68 | ## HTTP2多路复⽤ 69 | 我们都知道http是架构在TCP上的协议,为了保证传输的可靠性,在建立连接的时候需要经历三次握手。如果一次请求完成就会关闭本次的 Tcp 连接,下个请求又要从新建立 Tcp 连接传输完成数据再关闭,造成很大的性能损耗。 70 | ### `Keep-Alive` 71 | Keep-Alive解决的核心问题是: 一定时间内,同一域名多次请求数据,只建立一次 HTTP 请求,其他请求可复用每一次建立的连接通道,以达到提高请求效率的问题。 Keep-Alive还是存在一些问题,如串行的文件传输、同域并行请求限制带来的阻塞(6~8)个 72 | ### `管线化` 73 | HTTP 管线化可以克服同域并行请求限制带来的阻塞,它是建立在持久连接之上,是把所有请求一并发给服务器,但是服务器需要按照顺序一个一个响应,而不是等到一个响应回来才能发下一个请求,这样就节省了很多请求到服务器的时间。不过,HTTP 管线化仍旧有阻塞的问题,若上一响应迟迟不回,后面的响应都会被阻塞到。 74 | 75 | ### `多路复用` 76 | 多路复用代替原来的序列和阻塞机制。所有就是请求的都是通过一个 TCP 连接并发完成。因为在多路复用之前所有的传输是基于基础文本的,在多路复用中是基于二进制数据帧的传输、消息、流,所以可以做到乱序的传输。多路复用对同一域名下所有请求都是基于流,所以不存在同域并行的阻塞。 77 | 78 | > 多路复用代替了 HTTP1.x 的序列和阻塞机制,所有的相同域名请求都通过同一个 TCP 连接并发完成。同一 Tcp 中可以发送多个请求,对端可以通过帧中的标识知道属于哪个请求。通过这个技术,可以避免 HTTP 旧版本中的队头阻塞问题,极大的提高传输性能。 79 | 80 | 参考[Http2中的多路复用](https://juejin.cn/post/6844903935648497678#heading-6) 81 | # 一些性能指标及概念 82 | ## `FP` 、 `FCP` 83 | - `FP` First Paint, ⾸次绘制 84 | - `FCP` First Contentful Paint, ⾸次有内容的绘制 85 | 通过 [Paint Timing API](https://w3c.github.io/paint-timing/#first-contentful-paint) 来获取性能指标 86 | ```js 87 | const observer = new PerformanceObserver((list) => { 88 | for (const entry of list.getEntries()) { 89 | console.log(entry); 90 | } 91 | }); 92 | 93 | observer.observe({ entryTypes: ["paint"] }); 94 | 95 | // PerformancePaintTiming { 96 | // duration: 0 97 | // entryType: "paint" 98 | // name: "first-paint" 99 | // startTime: 62.43499999982305 100 | // } 101 | 102 | // PerformancePaintTiming { 103 | // duration: 0 104 | // entryType: "paint" 105 | // name: "first-contentful-paint" 106 | // startTime: 62.43499999982305 107 | // } 108 | ``` 109 | 110 | ## `LCP` 111 | - LCP指标代表的是视窗最大可见图片或者文本块的渲染时间。 112 | ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9wMS1qdWVqaW4uYnl0ZWltZy5jb20vdG9zLWNuLWktazN1MWZicGZjcC9jY2MwNzIyZDdiZTc0ZTc5YmFmNGQyZGNkNmVmOTZlM350cGx2LWszdTFmYnBmY3Atem9vbS0xLmltYWdl?x-oss-process=image/format,png) 113 | 114 | 根据google建议,为了给用户提供更好的产品体验,LCP应该低于2.5s。 115 | ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly9wMS1qdWVqaW4uYnl0ZWltZy5jb20vdG9zLWNuLWktazN1MWZicGZjcC9iYTQ3NzI4ZWFkOWE0OTc1YmE2NmI0MjE5NGRkYjMxMn50cGx2LWszdTFmYnBmY3Atem9vbS0xLmltYWdl?x-oss-process=image/format,png) 116 | 117 | 可以通过[ Largest Contentful Paint API](https://wicg.github.io/largest-contentful-paint/) 来获取 118 | ```js 119 | const observer = new PerformanceObserver((list) => { 120 | let perfEntries = list.getEntries(); 121 | let lastEntry = perfEntries[perfEntries.length - 1]; 122 | console.log(lastEntry) 123 | }); 124 | observer.observe({entryTypes: ['largest-contentful-paint']}); 125 | 126 | // LargestContentfulPaint { 127 | // duration: 0 128 | // element: img 129 | // entryType: "largest-contentful-paint" 130 | // id: "" 131 | // loadTime: 300.39 132 | // name: "" 133 | // renderTime: 0 134 | // size: 457824 135 | // startTime: 300.39 136 | // url: "https://imgconvert.csdnimg.cn/aHR0cHM6Ly9wMS1qdWVqaW4uYnl0ZWltZy5jb20vdG9zLW 137 | // } 138 | ``` 139 | 140 | ## `FID` 141 | First Input Delay 首次输入延迟,衡量的是从用户第一次与页面进行交互(即当他们单击链接,点击按钮或使用自定义的JavaScript驱动的控件)到浏览器实际上能够开始处理事件处理程序的时间。回应这种互动。 142 | 143 | 为了提供良好的用户体验,页面的FID应该小于100毫秒 144 | ![](https://user-gold-cdn.xitu.io/2020/6/27/172f38c4b38372ca?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) 145 | 146 | 可以使用[Event Timing API](https://wicg.github.io/event-timing/) 来获取 147 | ```js 148 | new PerformanceObserver((entryList) => { 149 | for (const entry of entryList.getEntries()) { 150 | const delay = entry.processingStart - entry.startTime; 151 | console.log('FID candidate:', delay, entry); 152 | } 153 | }).observe({type: 'first-input', buffered: true}); 154 | 155 | // FID candidate: 2.064999978756532 156 | // 157 | // PerformanceEventTiming { 158 | // cancelable: true 159 | // duration: 1512 160 | // entryType: "first-input" 161 | // name: "mousedown" 162 | // processingEnd: 1163.139999989653 163 | // processingStart: 1163.124999991851 164 | // startTime: 1161.0600000130944 165 | // target: img 166 | // } 167 | ``` 168 | ## `CLS` 169 | Cumulative Layout Shift 累积版式移位(CLS)是衡量用户视觉稳定性的一项重要的以用户为中心的度量标准,因为它有助于量化用户经历意外的版式移位的频率-较低的CLS有助于确保页面令人愉悦。 170 | CLS会测量在页面整个生命周期中发生的每个意外的版式移位的所有单独版式移位分数的总和。 171 | 172 | 173 | ![](https://webdev.imgix.net/vitals/cls_4x3.svg) 174 | 为了提供良好的用户体验,网站应努力使CLS得分不超过0.1 175 | 176 | 可以通过[Layout Instability API](https://github.com/WICG/layout-instability) 来获取 177 | ```js 178 | let cls = 0; 179 | 180 | new PerformanceObserver((entryList) => { 181 | for (const entry of entryList.getEntries()) { 182 | if (!entry.hadRecentInput) { 183 | cls += entry.value; 184 | console.log('Current CLS value:', cls, entry); 185 | } 186 | } 187 | }).observe({type: 'layout-shift', buffered: true}); 188 | ``` 189 | 190 | ## `Long tasks` 191 | `Long tasks` 超过了 50ms 的任务 192 | 193 | 可通过[Long Tasks API 194 | ](https://developer.mozilla.org/en-US/docs/Web/API/Long_Tasks_API) 来获取 195 | 196 | ```js 197 | const observer = new PerformanceObserver((list) => { 198 | for (const entry of list.getEntries()) { 199 | console.log(entry); 200 | } 201 | }); 202 | observer.observe({ entryTypes: ['longtask'] }); 203 | 204 | // 通过循环来触发 205 | for(var i=0; i < 10000; i++) { 206 | console.log(1) 207 | } 208 | 209 | // PerformanceLongTaskTiming { 210 | // attribution: [TaskAttributionTiming] 211 | // duration: 173 212 | // entryType: "longtask" 213 | // name: "self" 214 | // startTime: 93.09499998926185 215 | // } 216 | ``` 217 | 218 | # 网页渲染的整体过程 219 | ![](https://segmentfault.com/img/bVKg6n?w=1093&h=167) 220 | 1. 获取dom 分割层 221 | 2. 根据每层节点计算样式结果(Recalculate Style) 222 | 3. 为每个节点生成图形和位置 (Layout) 223 | 4. 将每个节点绘制填充到当前帧的图层位图中 (Paint) 224 | 5. 将图层上传到gpu 225 | 6. 将符合要求的多个图层合并成图像 (Composite Layers) 226 | 227 | ## 黑科技 228 | 我们写一个小球运动的动画 229 | ```css 230 | @keyframes run-around { 231 | 0%{ 232 | left: 0; 233 | top: 0; 234 | } 235 | 236 | 25%{ 237 | top: 0; 238 | left: 200px; 239 | } 240 | 241 | 50%{ 242 | left: 200px; 243 | top: 200px; 244 | } 245 | 246 | 75%{ 247 | left: 0; 248 | top: 200px; 249 | } 250 | } 251 | ``` 252 | 打开Chrome的`performance`面板刷新页面查看相关参数 253 | ![image.png](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b3221a8c136543cf8b81036999844833~tplv-k3u1fbpfcp-watermark.image) 254 | 255 | 换一种写法 256 | ```css 257 | @keyframes run-around { 258 | 0% { 259 | transform: translate(0,0); 260 | } 261 | 262 | 25% { 263 | transform: translate(200px,0); 264 | } 265 | 266 | 50% { 267 | transform: translate(200px,200px); 268 | } 269 | 270 | 75% { 271 | transform: translate(0,200px); 272 | } 273 | } 274 | ``` 275 | 再看一下performance 276 | ![image.png](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/7f74eb7b3eba432f917f80933a536607~tplv-k3u1fbpfcp-watermark.image) 277 | 278 | 你会发现,换了一种写法,总体加载速度变快了。这是为什么呢? 279 | 280 | 答案就是 transform,使用transform 后GPU直接参与渲染, 从而跳过`Layout`和 `Paint` 这两个步骤 281 | ![](https://segmentfault.com/img/bVKg6C?w=1093&h=167) 282 | 283 | 还有哪些会触发GPU直接渲染? 284 | 285 | css3d、video、webgl、transform、滤镜 这些也会触发硬件加速 286 | -------------------------------------------------------------------------------- /bottom-learning/ts高级特性.md: -------------------------------------------------------------------------------- 1 | # ts高级特性 2 | # Partial & Required 3 | - `Partial` 译为 部分的/局部的/不完全的, 作用是将一个接口的所有参数变为非必填 4 | - `Required` 译为必须的, 作用是将一个接口中所有非必填参数 变为必填,`Required` 的作用就是将某个类型里的属性全部变为必选项。 5 | ### ts中的声明 6 | ```ts 7 | /** 8 | * Make all properties in T optional 9 | */ 10 | type Partial = { 11 | [P in keyof T]?: T[P]; 12 | }; 13 | 14 | /** 15 | * Make all properties in T required 16 | */ 17 | type Required = { 18 | [P in keyof T]-?: T[P]; 19 | }; 20 | ``` 21 | 22 | ### Partial用法示例 23 | ```ts 24 | type Person = { 25 | name: string; 26 | age: number; 27 | } 28 | 29 | // 直接使用初始化所有参数都是必填 30 | let tom:Person = { 31 | name: 'tom', 32 | age: 20 33 | }; 34 | 35 | 36 | // 使用Partial将Person中所有的参数变为非必填 37 | type PartialPerson = Partial; 38 | 39 | let partialPerson: PartialPerson = { 40 | name: 'tom' 41 | }; 42 | ``` 43 | 44 | ### 特殊的情况 45 | ```ts 46 | type Person = { 47 | name: string; 48 | age: number; 49 | contact: { 50 | email: string; 51 | phone: number; 52 | wechat: string; 53 | } 54 | } 55 | 56 | type PartialPerson = Partial; 57 | 58 | // 可以看出来 第一层属性都变成了非必填 第二层都没变 59 | let partialPerson: PartialPerson = { 60 | name: 'tom', 61 | contact: { // error 62 | email: 'goodboyb@qq.com' 63 | } 64 | }; 65 | 66 | 67 | // 再来看看ts内部类型的定义 68 | /** 69 | * Make all properties in T optional 70 | */ 71 | type Partial = { 72 | [P in keyof T]?: T[P]; 73 | }; 74 | // 可以看出来并没有考虑内层 75 | 76 | 77 | // 稍微改造一下 78 | /** 79 | * Make all properties in T optional 80 | */ 81 | type DeepPartial = { 82 | [P in keyof T]?: T[P] extends Object ? DeepPartial : T[P]; 83 | } 84 | 85 | // 现在针对那种特殊情况就能处理了 86 | ``` 87 | ### Required 用法示例 88 | ```ts 89 | interface User { 90 | id: number; 91 | age: number; 92 | } 93 | type PartialUser = Partial; 94 | // type PartialUser = { 95 | // id?: number; 96 | // age?: number; 97 | // } 98 | type PickUser = Required; 99 | // type PickUser = { 100 | // id: number; 101 | // age: number; 102 | // } 103 | ``` 104 | 105 | # Record 106 | `Record` 译为 记录/记载, 作用是将一个类型的所有属性值都映射到另一个类型上并创造一个新的类型 107 | 108 | ### ts中的声明 109 | ```ts 110 | /** 111 | * Construct a type with a set of properties K of type T 112 | */ 113 | type Record = { 114 | [P in K]: T; 115 | }; 116 | ``` 117 | 看类型的定义就可以看出来,将K中的每个属性([P in K]),都转为T类型, K可以是联合类型、对象、枚举… 118 | ```ts 119 | type petsGroup = 'dog' | 'cat' | 'fish'; 120 | 121 | type numOrStr = number | string; 122 | 123 | type IPets = Record; 124 | 125 | // type IPets = { 126 | // dog: numOrStr; 127 | // cat: numOrStr; 128 | // fish: numOrStr; 129 | // } 130 | ``` 131 | # Pick 132 | `Pick`译为挑选/选择, 作用是从一个复合类型中,取出几个想要的类型的组合一个新的类型 133 | ### ts中的声明 134 | ```ts 135 | /** 136 | * From T, pick a set of properties whose keys are in the union K 137 | */ 138 | type Pick = { 139 | [P in K]: T[P]; 140 | }; 141 | ``` 142 | `K extends keyof T`的作用是约束K的key在T的key中,不能超出这个范围,否则会报错的 143 | ## `keyof` 144 | - keyof 用于获取某种类型的所有键,其返回类型是联合类型 145 | ```ts 146 | // keyof 用于获取某种类型的所有键,其返回类型是联合类型 147 | interface B { 148 | id: number; 149 | name: string; 150 | age: number; 151 | } 152 | 153 | type B1 = keyof B; 154 | // type B1 = "id" | "name" | "age" 155 | ``` 156 | ## `extends` 157 | 这里的extends并不是用来继承的, 而是用来限制类型 158 | ```ts 159 | // 对象extends 160 | type T = { 161 | id: number; 162 | name: string; 163 | } 164 | 165 | type K = { 166 | id: number; 167 | } 168 | type IType = K extends T ? K : T; 169 | // type IType = { 170 | // id: number; 171 | // name: string; 172 | // } 173 | // 此处 K extends T 限制K中必须有T的所有属性, 通俗点说就是T必须是K的子集 174 | 175 | 176 | // 联合类型extends 177 | type T = "id" | "name"; 178 | type K = "id"; 179 | type IType = K extends T ? K : T; 180 | // type IType = "id" 181 | // 此处限制为K必须包含于T,通俗点说就是K是T的子集 182 | ``` 183 | 使用`Pick`挑选属性组成新的类型 184 | ```ts 185 | interface B { 186 | id: number; 187 | name: string; 188 | age: number; 189 | } 190 | 191 | type PickB = Pick; 192 | 193 | // type PickB = { 194 | // id: number; 195 | // name: string; 196 | // } 197 | ``` 198 | 199 | # Exclude 200 | `Exclude` 译为排除/不包括, `Exclude` 表示从T中排除那些可分配给U的类型, 简单点说就是将 T 中某些属于 U 的类型移除掉。也可理解为取补集 201 | 202 | ts中的声明 203 | ```ts 204 | /** 205 | * Exclude from T those types that are assignable to U 206 | */ 207 | type Exclude = T extends U ? never : T; 208 | ``` 209 | 例 210 | ```ts 211 | // 例子1 212 | type T = { 213 | name: string 214 | age: number 215 | } 216 | 217 | type U = { 218 | name: string 219 | } 220 | 221 | type IType = Exclude 222 | // type IType = "age" 223 | 224 | type T0 = Exclude<"a" | "b" | "c", "a" | "b"> 225 | // type T0 = "c" 226 | type T1 = Exclude<"a" | "b" | "c", "a" | "b" | 's'> 227 | // type T1 = "c" 228 | ``` 229 | 230 | # Extract 231 | `Extract` 译为提取, `Extract`从T中提取那些可分配给U的类型, 简单点说就是提取T中,U也有的元素,也可理解为取交集 232 | 233 | ts中的定义 234 | ```ts 235 | /** 236 | * Extract from T those types that are assignable to U 237 | */ 238 | type Extract = T extends U ? T : never; 239 | ``` 240 | 例 241 | ```ts 242 | type T0 = Extract<"a" | "b" | "c", "a" | "f"> 243 | // type T0 = "a" 244 | 245 | type T = { 246 | name: string 247 | age: number 248 | } 249 | 250 | type U = { 251 | name: string 252 | } 253 | 254 | type IType = Extract 255 | // type IType = "name" 256 | ``` 257 | 258 | # ConstructorParameters 259 | `ConstructorParameters` 译为构造函数参数, 获取元组中构造函数类型的参数 260 | 261 | ts中的声明 262 | ```ts 263 | /** 264 | * Obtain the parameters of a constructor function type in a tuple 265 | */ 266 | type ConstructorParameters any> = T extends new (...args: infer P) => any ? P : never; 267 | 268 | ``` 269 | 270 | 可以用来获取类的参数类型组成的元组类型 271 | ```ts 272 | class People { 273 | name: string 274 | age: number 275 | 276 | constructor(name: string) { 277 | this.name = name; 278 | } 279 | } 280 | 281 | 282 | type IType = ConstructorParameters 283 | // type IType = [name: string] 284 | // 注意这里typeof操作是取类型的作用 285 | ``` 286 | 287 | ## `infer` 288 | 表示在 extends 条件语句中待推断的类型变量 289 | ```ts 290 | // 例子1 291 | // 若T是Array类型,则返回T的泛型,否则返回never类型 292 | type Union = T extends Array ? U: never 293 | 294 | type a = { 295 | name: string 296 | } 297 | 298 | type b = string[] 299 | 300 | 301 | type c = Union 302 | // type c = string 303 | type d = Union 304 | // type d = never 305 | 306 | 307 | // 例子2 308 | // 若T满足(param: infer P) => any,则返回函数入参的类型,否则直接返回T 309 | type ParamType = T extends (param: infer P) => any ? P: T; 310 | 311 | interface IDog { 312 | name: string; 313 | age:number; 314 | } 315 | 316 | type Func = (dog:IDog) => void; 317 | 318 | type Param = ParamType; // IDog 319 | type TypeString = ParamType //string 320 | ``` 321 | 322 | 理解了`infer` 我们在回来看ts中`ConstructorParameters` 的声明 323 | ```ts 324 | type ConstructorParameters any> = T extends new (...args: infer P) => any ? P : never; 325 | 326 | // T extends new (...args: any) => any 首先给T加了个约束 必须满足new (...args: any) => any 也就是说T必须是构造函数类型 327 | 328 | // T extends new (...args: infer P) => any ? P : never 329 | // T若满足new (...args: any) => any 则返回所有入参的类型, 否则返回never 330 | ``` 331 | 332 | # InstanceType 333 | `InstanceType` 译为实例类型, 用来获取构造函数的返回类型 334 | 335 | ts中的定义 336 | ```ts 337 | /** 338 | * Obtain the return type of a constructor function type 339 | */ 340 | type InstanceType any> = T extends new (...args: any) => infer R ? R : any; 341 | ``` 342 | 例 343 | ```ts 344 | class People { 345 | name: string 346 | age: number 347 | 348 | constructor(name: string) { 349 | this.name = name; 350 | } 351 | } 352 | 353 | type IType = InstanceType 354 | // type IType = People 355 | // 因为constructor默认返回this 356 | // constructor People(name: string): People 357 | ``` 358 | 359 | # NonNullable 360 | `NonNullable` 译为不可为空, `NonNullable`从T中排除null和undefined 361 | 362 | ts 中的定义 363 | ```ts 364 | /** 365 | * Exclude null and undefined from T 366 | */ 367 | type NonNullable = T extends null | undefined ? never : T; 368 | 369 | ``` 370 | 例 371 | ```ts 372 | type example = NonNullable 373 | // type example = string | number 374 | ``` 375 | 376 | # Parameters & ReturnType 377 | - `Parameters` 用来获取函数参数的类型 378 | - `ReturnType` 用来获取函数返回值类型 379 | 380 | 381 | ts中的定义 382 | ```ts 383 | /** 384 | * Obtain the parameters of a function type in a tuple 385 | */ 386 | type Parameters any> = T extends (...args: infer P) => any ? P : never; 387 | 388 | /** 389 | * Obtain the return type of a function type 390 | */ 391 | type ReturnType any> = T extends (...args: any) => infer R ? R : any; 392 | ``` 393 | 定义时写的很明确,不过多赘述 394 | 395 | 例 396 | ```ts 397 | type IFoo = ( 398 | uname: string, 399 | uage: number 400 | ) => { 401 | name: string; 402 | age: number; 403 | }; 404 | //参数类型 405 | type Ibar = Parameters; 406 | // type Ibar = [uname: string, uage: number] 407 | type T0 = ReturnType; 408 | // type T0 = { 409 | // name: string; 410 | // age: number; 411 | // } 412 | ``` 413 | 414 | # readonly & ReadonlyArray 415 | - `readonly` 只读 被`readonly` 标记的属性只能在声明时或类的构造函数中赋值,之后将不可改(即只读属性)。 416 | - `ReadonlyArray` 同理, 只读数组 417 | 418 | 419 | ```ts 420 | interface ReadonlyArray { 421 | /** Iterator of values in the array. */ 422 | [Symbol.iterator](): IterableIterator; 423 | 424 | /** 425 | * Returns an iterable of key, value pairs for every entry in the array 426 | */ 427 | entries(): IterableIterator<[number, T]>; 428 | 429 | /** 430 | * Returns an iterable of keys in the array 431 | */ 432 | keys(): IterableIterator; 433 | 434 | /** 435 | * Returns an iterable of values in the array 436 | */ 437 | values(): IterableIterator; 438 | } 439 | ``` 440 | 例 441 | ```ts 442 | interface Person { 443 | readonly id: number; 444 | } 445 | const data: Person = { 446 | id: 456, 447 | }; 448 | 449 | data.id = 789; 450 | // 无法分配到 "id" ,因为它是只读属性。ts(2540) 451 | 452 | const arr: number[] = [1, 2, 3, 4]; 453 | 454 | let ro: ReadonlyArray = arr; 455 | 456 | ro.push(444); 457 | // 类型“readonly number[]”上不存在属性“push”。ts(2339) 458 | ``` 459 | 460 | # 总结 461 | ts中有很多特性都可以在`lib.es5.ts`中去查看, 里面定义的接口和类型非常多,主要是理解ts定义的套路,就能举一反三 462 | 463 | 举个例子 464 | 就比如Parameters的定义是 465 | type Parameters any> = T extends (...args: infer P) => any ? P : never; 466 | 理解他定义的意思,接受一个泛型T并且使用extends约束他必须是一个函数类型,如果是 返回函数参数的类型, 不是返回never。 467 | 理解了这个,再稍微改造一下 468 | 接受一个泛型T并且使用extends约束他必须是一个函数类型,如果是 返回函数返回值的类型, 不是返回never。 469 | 这时就变成ReturnType的定义了 470 | 理解了这些ts内置的定义规则,完全可以根据自己的需求自定义 471 | ts内置的这些类型或接口大都使用extends、infer、keyof、in... 这些组装而来 -------------------------------------------------------------------------------- /advanced/函数式编程.md: -------------------------------------------------------------------------------- 1 | # 基础理论 2 | - 函数式编程(Functional Programming)其实相对于计算机的历史而言是一个非常古老的概念,甚至早于第一台计算机的诞生。函数式编程的基础模型来源于 λ (Lambda x=>x*2)演算,而 λ 演算并 非设计于在计算机上执行,它是在 20 世纪三十年代引入的一套用于研究函数定义、函数应用和递归的形式系统。 3 | - 函数式编程不是用函数来编程,也不是传统的面向过程编程。主旨在于将复杂的函数符合成简单的函数(计算理论,或者递归论, 或者拉姆达演算)。运算过程尽量写成一系列嵌套的函数调用 4 | - 真正的火热是随着React的高阶函数而逐步升温 5 | 6 | # 范畴论Category Theory 7 | - 函数式编程是范畴论的数学分支是一门很复杂的数学, 认为世界上所有概念体系都可以抽象出一个个范畴 8 | - 彼此之间存在某种关系概念、事物、对象等等,都构成范畴。任何事物只要找出他们之间的关系,就能定义 9 | - 箭头表示范畴成员之间的关系,正式的名称叫做"态射" (morphism)。范畴论认为,同一个范畴的所有成员, 就是不同状态的"变形"(transformation)。通过"态射", 一个成员可以变形成另一个成员。 10 | 11 | # 函数式编程常用核心概念 12 | - 纯函数 13 | - 偏应用函数、函数的柯里化 14 | - 函数组合 15 | - Point Free 16 | - 声明式与命令式代码 17 | - 惰性求值 18 | 19 | ## 纯函数 20 | > 定义:对于相同的输入,永远会得到相同的输出,而且没有任 何可观察的副作用,也不依赖外部环境的状态。 21 | 22 | ```js 23 | var xs = [1,2,3,4,5]; 24 | // Array.slice是纯函数,因为它没有副作用,对于固定的 输入,输出总是固定的 25 | 26 | xs.slice(0,3); // [1,2,3] 27 | xs.slice(0,3); // [1,2,3] 28 | ``` 29 | ### 优点: 30 | ```js 31 | import _ from 'lodash'; 32 | var sin = _.memorize(x => Math.sin(x)); 33 | 34 | // 第一次计算的时候会稍慢一点 35 | var a = sin(1); 36 | 37 | // 第二次有了缓存,速度极快 38 | var b = sin(1); 39 | 40 | // 纯函数不仅可以有效降低系统的复杂度,还有很多很棒的特性,比如可缓存性 41 | ``` 42 | ### 缺点: 43 | ```js 44 | //不纯的 45 | var min = 18; 46 | var checkAge = age => age > min; 47 | 48 | //纯的,这很函数式 49 | var checkAge = age => age > 18; 50 | 51 | // 在不纯的版本中,checkAge不仅取决于age还有外部依赖的变量min。 52 | // 纯的 checkAge 把关键数字18硬编码在函数内部,扩展性比较差,柯里化优雅的函数式解决。 53 | ``` 54 | 55 | ### 纯度和幂等性 56 | 幂等性是指执行无数次后还具有相同的效果,同一的参 数运行一次函数应该与连续两次结果一致。幂等性在函 数式编程中与纯度相关,但有不一致。 57 | ```js 58 | Math.abs(Math.abs(-42)) 59 | ``` 60 | 61 | ## 偏应用函数 62 | - 传递给函数一部分参数来调用它,让它返回一个函数去处理剩下的参数。 63 | - 偏函数之所以“偏”,在就在于其只能处理那些能与至少 一个case语句匹配的输入,而不能处理所有可能的输入 64 | 65 | ```js 66 | // 带一个函数参数 和 该函数的部分参数 67 | const partial = (f, ...args) => 68 | (...moreArgs) => f(...args, ...moreArgs) 69 | 70 | const add3 = (a, b, c) => a + b + c 71 | // 偏应用 `2` 和 `3` 到 `add3` 给你一个单参数的函数 72 | 73 | const fivePlus = partial(add3, 2, 3) 74 | console.log(fivePlus(4)) // 9 75 | 76 | 77 | //用bind来实现 78 | const add1More = add3.bind(null, 2, 3) 79 | console.log(add1More(4)) // 9 80 | ``` 81 | 82 | ## 函数的柯里化 83 | - 柯里化(Curried) 通过偏应用函数实现。 84 | - 传递给函数一部分参数来调用它,让它返回一个函数去 处理剩下的参数 85 | ```js 86 | // 柯里化之前 87 | function add(x, y) { 88 | return x + y; 89 | } 90 | add(1, 2) // 3 91 | 92 | // 柯里化之后 93 | function addX(y) { 94 | return function (x) { 95 | return x + y; 96 | }; 97 | } 98 | addX(2)(1) // 3 99 | 100 | // bind实现柯里化 101 | function foo(p1, p2) { 102 | this.val = p1 + p2; 103 | } 104 | var bar = foo.bind(null, "p1"); 105 | var baz = new bar("p2"); 106 | console.log(baz.val); // p1p2 107 | ``` 108 | 109 | 事实上柯里化是一种“预加载”函数的方法,通过传递较少的参数, 得到一个已经记住了这些参数的新函数,某种意义上讲,这是一种 对参数的“缓存”,是一种非常高效的编写函数的方法 110 | 111 | ## 函数组合 112 | 为了解决函数嵌套的问题,我们需要用到“函数组合”,让多个函数像拼积木一样 113 | ```js 114 | const compose = (f, g) => (x => f(g(x))); 115 | 116 | var first = arr => arr[0]; 117 | 118 | var reverse = arr => arr.reverse(); 119 | 120 | var last = compose(first, reverse); 121 | console.log(last([1,2,3,4,5])); // 5 122 | ``` 123 | 124 | ## Point Free 125 | > 把一些对象自带的方法转化成纯函数,不要命名转瞬即逝的中间变量。 126 | ```js 127 | const compose = (f, g) => (x => f(g(x))); 128 | 129 | var toUpperCase = word => word.toUpperCase(); 130 | 131 | var split = x => (str => str.split(x)); 132 | 133 | var f = compose(split(' '), toUpperCase); 134 | console.log(f("abcd aaa"));// [ 'ABCD', 'AAA' ] 135 | ``` 136 | 这种风格能够帮助我们减少不必要的命名,让代码保持简洁和通用。 137 | 138 | ## 声明式与命令式代码 139 | > 命令式代码的意思就是,我们通过编写一条又一条指令去让计算 机执行一些动作,这其中一般都会涉及到很多繁杂的细节。而声明式就要优雅很多了,我们通过写表达式的方式来声明我们想干什么,而不是通过一步一步的指示。 140 | 141 | ```js 142 | // 命令式 143 | let ceoList = []; 144 | for(var i = 0; i < companies.length; i++) 145 | ceoList.push(companies[i].CEO) 146 | } 147 | // 声明式 148 | let ceoList = companies.map(c => c.CEO); 149 | ``` 150 | ## 惰性求值、惰性函数 151 | 惰性函数很好理解,假如同一个函数被大量范围,并且这个函数内部又有许多判断来来检测函数,这样对于一个调用会浪费时间和浏览器资源,所有当第一次判断完成后,直接把这个函数改写,不在需要判断。 152 | ```js 153 | // 根据浏览器环境,执行不同的功能 154 | 155 | // 普通写法 156 | function normalCreate() { 157 | if (isChrome()) { 158 | console.log('normal create in chrome'); 159 | } else { 160 | console.log('normal create in other browser'); 161 | } 162 | } 163 | normalCreate(); 164 | normalCreate(); 165 | 166 | // 惰性函数写法 167 | function lazyLoadCreate () { 168 | console.log('first creating'); // pos-1 169 | if (isChrome()) { 170 | lazyLoadCreate = function () { 171 | console.log('create function in chrome'); 172 | } 173 | } else { 174 | lazyLoadCreate = function () { 175 | console.log('create function in other browser'); 176 | } 177 | } 178 | return lazyLoadCreate(); 179 | } 180 | 181 | lazyLoadCreate(); 182 | lazyLoadCreate(); 183 | ``` 184 | 185 | # 函数式编程-更加专业的术语 186 | - 高阶函数 187 | - 尾调用优化PTC 188 | - 闭包 189 | - 容器、Functor 190 | - 错误处理、Either、AP 191 | - IO 192 | - Monad 193 | 194 | ## 高阶函数 195 | > 函数当参数,把传入的函数做一个封装,然后返回这个封装 函数,达到更高程度的抽象。 196 | ```js 197 | var add = function(a, b){ 198 | return a + b; 199 | }; 200 | 201 | function math(func,array){ 202 | return func(array[0], array[1]); 203 | } 204 | math(add,[1,2]); // 3 205 | ``` 206 | 207 | ## 尾调用优化 208 | > 指函数内部的最后一个动作是函数调用。该调用的返回值,直接返回给函数。。 函数调用自身,称为递归。如果尾调用自身,就称为尾递归。递归需要保存大量的调用记录,很容易发生栈溢出错误,如果使用尾递归优化,将递归变为循环,那么只需要保存一个调用记录,这样就不会发生栈溢出错误了。 209 | 210 | ```js 211 | // 普通递归 212 | function factorial(n) { 213 | if (n === 1) return 1; 214 | return n * factorial(n - 1); 215 | } 216 | 217 | // 尾递归 218 | function factorial(n, total) { 219 | if (n === 1) return total; 220 | return factorial(n - 1, n * total); 221 | } 222 | ``` 223 | ### 传统递归 224 | 普通递归时,内存需要记录调用的堆栈所出 的深度和位置信息。在最底层计算返回值, 再根据记录的信息,跳回上一层级计算, 然后再跳回更高一层,依次运行,直到最外层的调用函数。在cpu计算和内存会消耗很多,而且当深度过大时,会出现堆栈溢出 225 | 226 | ```js 227 | function sum(n){ 228 | if (n === 1) return 1; 229 | return n + sum(n - 1); 230 | } 231 | 232 | // 执行过程 233 | // sum(5) 234 | // (5 + sum(4)) 235 | // (5 + (4 + sum(3))) 236 | // (5 + (4 + (3 + sum(2)))) 237 | // (5 + (4 + (3 + (2 + sum(1))))) 238 | // (5 + (4 + (3 + (2 + 1)))) 239 | // (5 + (4 + (3 + 3))) 240 | // (5 + (4 + 6)) 241 | // (5 + 10) 242 | // 15 243 | ``` 244 | 245 | ### 细数尾递归 246 | ```js 247 | function sum(x, total) { 248 | if (x === 1) { 249 | return x + total; 250 | } 251 | return sum(x - 1, x + total); 252 | } 253 | 254 | // 执行过程 255 | // sum(5, 0) 256 | // sum(4, 5) 257 | // sum(3, 9) 258 | // sum(2, 12) 259 | // sum(1, 14) 260 | // 15 261 | ``` 262 | 整个计算过程是线性的,调用一次sum(x, total)后,会进入下一个栈,相关的数据信息和 跟随进入,不再放在堆栈上保存。当计算完最后的值之后,直接返回到最上层的 sum(5,0)。这能有效的防止堆栈溢出。 263 | 264 | ### 注意 265 | - 尾递归的判断标准是函数运行【最后一步】是否调用自身, 而不是 是否在函数的【最后一行】调用自身, 最后一行调用其他函数 并返回叫尾调用。 266 | - 按道理尾递归调用调用栈永远都是更新当前的栈帧而已,这 样就完全避免了爆栈的危险。但是现如今的浏览器并未完全支持。原因有二 1在引擎层面消除递归是一个隐式的行为,开发者意识不到。2堆栈信息丢失了 开发者难已调试。 267 | 268 | ## 闭包 269 | 如下例子,虽然外层的 makePowerFn 函数执行完毕,栈上的调用 帧被释放,但是堆上的作用域并不被释放,因此 power 依旧可以 被 powerFn 函数访问,这样就形成了闭包 270 | ```js 271 | function makePowerFn(power) { 272 | function powerFn(base) { 273 | return Math.pow(base, power); 274 | } 275 | return powerFn; 276 | } 277 | var square = makePowerFn(2); 278 | square(3); // 9 279 | ``` 280 | 281 | ## 范畴与容器 282 | 1. 我们可以把”范畴”想象成是一个容器,里面包含两样东西。值 (value)、值的变形关系,也就是函数。 283 | 284 | 2. 范畴论使用函数,表达范畴之间的关系。 285 | 286 | 3. 伴随着范畴论的发展,就发展出一整套函数的运算方法。这套方法 起初只用于数学运算,后来有人将它在计算机上实现了,就变成了今 天的”函数式编程"。 287 | 288 | 4. 本质上,函数式编程只是范畴论的运算方法,跟数理逻辑、微积分、 行列式是同一类东西,都是数学方法,只是碰巧它能用来写程序。为什么函数式编程要求函数必须是纯的,不能有副作用?因为它是一种数学运算,原始目的就是求值,不做其他事情,否则就无法满足函数运算法则了。 289 | 290 | 5. 函数不仅可以用于同一个范畴之中值的转换,还可以用于将一个范畴转成另一个范畴。这就涉及到了`函子(Functor)`。 291 | 292 | 6. 函子是函数式编程里面最重要的数据类型,也是基本的运算 单位和功能单位。它首先是一种范畴,也就是说,是一个容器,包含了值和变形关系。比较特殊的是,它的变形关系可 以依次作用于每一个值,将当前容器变形成另一个容器。 293 | 294 | ## Functor(函子) 295 | 1. $(...) 返回的对象并不是一个原生的 DOM 对象,而是对于原生对象的一种封装,这在某种意义上就是一个“容器”(但它并不函数式) 296 | 297 | 2. Functor(函子)遵守一些特定规则的容器类型 298 | 299 | 3. Functor 是一个对于函数调用的抽象,我们赋予容器自己去调用函数的能力。把东西装进一个容器,只留出一个接口 map 给容器外的函数,map一个函数时,我们让容器自己来运行这个函数, 这样容器就可以自由地选择何时何地如何操作这个函数,以致于 拥有惰性求值、错误处理、异步调用等等非常牛掰的特性 300 | 301 | ```js 302 | var Container = function(x) { 303 | this.__value = x; 304 | } 305 | 306 | //函数式编程一般约定,函子有一个of方法 307 | Container.of = x => new Container(x); 308 | 309 | //一般约定,函子的标志就是容器具有map方法。该方法将容器 里面的每一个值,映射到另一个容器。 310 | Container.prototype.map = function(f) { 311 | return Container.of(f(this.__value)) 312 | } 313 | 314 | Container.of(3) 315 | .map(x => x + 1) // 结果 Container(4) 316 | .map(x => 'Result is ' + x); // Container('Result is 4') 317 | ``` 318 | ES6写法 319 | ```js 320 | class Functor { 321 | constructor(val) { 322 | this.val = val; 323 | } 324 | map(f) { 325 | return new Functor(f(this.val)); 326 | } 327 | } 328 | 329 | (new Functor(2)).map(function (two) { 330 | return two + 2; 331 | }); // Functor(4) 332 | ``` 333 | ### map 334 | - 上面代码中,Functor是一个函子,它的map方法接受函数f作为参数,然后返回一个新的函子,里面包含的值是被f处理过的 (f(this.val)) 335 | - 一般约定,函子的标志就是容器具有map方法。该方法将容器里面的每一个值,映射到另一个容器 336 | - 上面的例子说明,函数式编程里面的运算,都是通过函子完成, 即运算不直接针对值,而是针对这个值的容器----函子。函子本 身具有对外接口(map方法),各种函数就是运算符,通过接口 接入容器,引发容器里面的值的变形 337 | - 因此,学习函数式编程,实际上就是学习函子的各种运算。由 于可以把运算方法封装在函子里面,所以又衍生出各种不同类 型的函子,有多少种运算,就有多少种函子。函数式编程就变 成了运用不同的函子,解决实际问题 338 | 339 | ## Maybe 函子 340 | > 函子接受各种函数,处理容器内部的值。这里就有一个问题,容器内部的值可能是一个 空值(比如null),而外部函数未必有处理空值的机制,如果传入空值,很可能就会出错 341 | 342 | ```js 343 | var Maybe = function(x) { 344 | this.__value = x; 345 | } 346 | 347 | Maybe.of = function(x) { 348 | return new Maybe(x); 349 | } 350 | 351 | Maybe.prototype.map = function(f) { 352 | return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value)); 353 | } 354 | 355 | Maybe.prototype.isNothing = function() { 356 | return (this.__value === null || this.__value === undefined); 357 | } 358 | 359 | ``` 360 | ## Either 函子 361 | 条件运算if...else是最常见的运算之一,函数式编程里面,使用 Either 函子表达。Either 函子内部有两个值:左值(Left)和右值(Right)。右值是正常情况下使用的值,左值是右值不存在时使用的默认值 362 | ```js 363 | class Either extends Functor { 364 | constructor(left, right) { 365 | this.left = left; 366 | this.right = right; 367 | } 368 | 369 | map(f) { 370 | return this.right ? 371 | Either.of(this.left, f(this.right)) : 372 | Either.of(f(this.left), this.right); 373 | } 374 | } 375 | 376 | Either.of = function (left, right) { 377 | return new Either(left, right); 378 | 379 | 380 | // 使用 381 | 382 | var addOne = function (x) { 383 | return x + 1; 384 | }; 385 | 386 | Either.of(5, 6).map(addOne); 387 | // Either(5, 7); 388 | 389 | Either.of(1, null).map(addOne); 390 | // Either(2, null); 391 | ``` 392 | 393 | ```js 394 | var Left = function(x) { 395 | this.__value = x; 396 | } 397 | 398 | var Right = function(x) { 399 | this.__value = x; 400 | } 401 | 402 | Left.of = function(x) { 403 | return new Left(x); 404 | } 405 | 406 | Right.of = function(x) { 407 | return new Right(x); 408 | } 409 | 410 | // 这里不同!!! 411 | Left.prototype.map = function(f) { 412 | return this; 413 | } 414 | Right.prototype.map = function(f) { 415 | return Right.of(f(this.__value)); 416 | } 417 | ``` 418 | Left 和 Right 唯一的区别就在于 map 方法的实 现,Right.map 的行为和我们之前提到的 map 函数一 样。但是 Left.map 就很不同了:它不会对容器做任何事情,只是很简单地把这个容器拿进来又扔出去。 这个特性意味着,Left 可以用来传递一个错误消息。 419 | ```js 420 | var getAge = user => 421 | user.age ? Right.of(user.age) : Left.of("ERROR!"); 422 | 423 | getAge({name: 'stark', age: '21'}) 424 | .map(age => 'Age is ' + age); //=> Right('Age is 21') 425 | 426 | getAge({name: 'stark'}).map(age => 'Age is ' + age); //=> Left('ERROR!') 427 | ``` 428 | Left 可以让调用链中任意一环的错误立刻返回到调用链的尾部, 这给我们错误处理带来了很大的方便. 429 | 430 | ## AP函子 431 | 函子里面包含的值,完全可能是函数。我们可以想象 这样一种情况,一个函子的值是数值,另一个函子的值 是函数。 432 | ```js 433 | function addTwo(x) { 434 | return x + 2; 435 | } 436 | 437 | const A = Functor.of(2); 438 | const B = Functor.of(addTwo) 439 | ``` 440 | 上面代码中,函子`A`内部的值是`2`,函子`B`内部的值是函数`addTwo`. 441 | 有时,我们想让函子`B`内部的函数,可以使用函子`A`内部的值进行运算。这时就需要用到 `ap` 函子。 442 | 443 | ap 是 applicative(应用)的缩写。凡是部署了`ap`方法的函子,就是 ap 函子。 444 | ```js 445 | class Ap extends Functor { 446 | ap(F) { 447 | return Ap.of(this.val(F.val)); 448 | } 449 | } 450 | // ap方法的参数不是函数,而是另一个函子。 451 | 452 | // 上面例子可以写成下面的形式 453 | Ap.of(addTwo).ap(Functor.of(2)) 454 | // Ap(4) 455 | ``` 456 | ap 函子的意义在于,对于那些多参数的函数,就可以从多个容器之中取值,实现函子的链式操作 457 | ```js 458 | function add(x) { 459 | return function (y) { 460 | return x + y; 461 | }; 462 | } 463 | 464 | Ap.of(add).ap(Maybe.of(2)).ap(Maybe.of(3)); 465 | // Ap(5) 466 | ``` 467 | 468 | ## Monad 函子 469 | Monad就是一种设计模式,表示将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算 所需的函数,整个运算就会自动进行下去 470 | 471 | **Monad 函子的作用是,总是返回一个单层的函子**。它有一个`flatMap`方法,与`map`方法作用相同,唯一的区别是如果生成了一个嵌套函子,它会取出后者内部的值,保证返回的永远是一个单层的容器,不会出现嵌套的情况。 472 | ```js 473 | class Monad extends Functor { 474 | join() { 475 | return this.val; 476 | } 477 | flatMap(f) { 478 | return this.map(f).join(); 479 | } 480 | } 481 | ``` 482 | 上面代码中,如果函数`f`返回的是一个函子,那么`this.map(f)`就会生成一个嵌套的函子。所以,`join`方法保证了`flatMap`方法总是返回一个单层的函子。这意味着嵌套的函子会被铺平(flatten) 483 | 484 | ## IO 操作 485 | **真正的程序总要去接触肮脏的世界** 486 | 487 | Monad 函子的重要应用,就是实现 I/O (输入输出)操作。 488 | 489 | I/O 是不纯的操作,普通的函数式编程没法做,这时就需要把 IO 操作写成`Monad`函子,通过它来完成。 490 | 491 | ```js 492 | var fs = require('fs'); 493 | 494 | var readFile = function(filename) { 495 | return new IO(function() { 496 | return fs.readFileSync(filename, 'utf-8'); 497 | }); 498 | }; 499 | 500 | var print = function(x) { 501 | return new IO(function() { 502 | console.log(x); 503 | return x; 504 | }); 505 | } 506 | ``` 507 | 上面代码中,读取文件和打印本身都是不纯的操作,但是`readFile`和`print`却是纯函数,因为它们总是返回 IO 函子。 508 | 509 | 如果 IO 函子是一个`Monad`,具有`flatMap`方法,那么我们就可以像下面这样调用这两个函数。 510 | ```js 511 | readFile('./user.txt') 512 | .flatMap(print) 513 | ``` 514 | 这就是神奇的地方,上面的代码完成了不纯的操作,但是因为`flatMap`返回的还是一个 IO 函子,所以这个表达式是纯的。我们通过一个纯的表达式,完成带有副作用的操作,这就是 Monad 的作用。 515 | 516 | 由于返回还是 IO 函子,所以可以实现链式操作。因此,在大多数库里面,`flatMap`方法被改名成`chain` 517 | ```js 518 | var tail = function(x) { 519 | return new IO(function() { 520 | return x[x.length - 1]; 521 | }); 522 | } 523 | 524 | readFile('./user.txt') 525 | .flatMap(tail) 526 | .flatMap(print) 527 | 528 | // 等同于 529 | readFile('./user.txt') 530 | .chain(tail) 531 | .chain(print) 532 | ``` 533 | 上面代码读取了文件user.txt,然后选取最后一行输出 534 | 535 | # 总结 536 | 函数式编程不应被视为灵丹妙药。相反,它应 该被视为我们现有工具箱的一个很自然的补充 —— 它带来了更高的可组合性,灵活性以及容错 性。现代的JavaScript库已经开始尝试拥抱函数式编 程的概念以获取这些优势。Redux 作为一种 FLUX 的变种实现,核心理念也是状态机和函数式编程。 537 | 538 | 软件工程上讲『没有银弹』,函数式编程同样也不是万能的,它与烂大街的 OOP 一样,只是一种编程范式而已。很多实际应用中是很难用函数式去表达的,选择 OOP 亦或是其它编程范式或许会更简单。但我们要注意到函数式编程的核心理念, 如果说 OOP 降低复杂度是靠良好的封装、继承、多态以及接口定义的话,那么函 数式编程就是通过纯函数以及它们的组合、柯里化、Functor 等技术来降低系统复 杂度,而 React、Rxjs、Cycle.js 正是这种理念的代言。让我们一起拥抱函数式编程, 打开你程序的大门! 539 | 540 | > 参考[阮一峰-函数式编程](http://www.ruanyifeng.com/blog/2017/02/fp-tutorial.html) 541 | @yuanzhijia -------------------------------------------------------------------------------- /bottom-learning/js手撕系列.md: -------------------------------------------------------------------------------- 1 | # js手撕系列 2 | ## 1.数组扁平化 3 | 数组扁平化是指将一个多维数组变为一个一维数组 4 | ```js 5 | const arr = [1, [2, [3, [4, 5]]], 6]; 6 | // => [1, 2, 3, 4, 5, 6] 7 | ``` 8 | 9 | ### 方法1:使用flat() 10 | ```js 11 | const res1 = arr.flat(Infinity); 12 | // [1, 2, 3, 4, 5, 6] 13 | ``` 14 | flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。 15 | 16 | 使用 Infinity 作为深度,展开任意深度的嵌套数组 17 | 18 | 本质上就是是归纳(reduce) 与 合并(concat)的操作 19 | 20 | ### 方法2:使用正则 21 | ```js 22 | const res2 = JSON.stringify(arr).replace(/\[|\]/g, '').split(','); 23 | // ["1", "2", "3", "4", "5", "6"] 24 | // 这么写不够严谨, 可以看到原数组中是number,转完以后是string 25 | 26 | // 优化版本 27 | const res3 = JSON.parse('[' + JSON.stringify(arr).replace(/\[|\]/g, '') + ']'); 28 | // [1, 2, 3, 4, 5, 6] 29 | ``` 30 | 31 | ### 方法3:使用reduce 32 | ```js 33 | const flatten = arr => { 34 | return arr.reduce((pre, cur) => { 35 | return pre.concat(Array.isArray(cur) ? flatten(cur) : cur); 36 | }, []) 37 | } 38 | const res4 = flatten(arr); 39 | ``` 40 | 41 | ### 方法4:函数递归 42 | ```js 43 | const res5 = []; 44 | const fn = arr => { 45 | for (let i = 0; i < arr.length; i++) { 46 | if (Array.isArray(arr[i])) { 47 | fn(arr[i]); 48 | } else { 49 | res5.push(arr[i]); 50 | } 51 | } 52 | } 53 | fn(arr); 54 | ``` 55 | 56 | 方法3和方法4思路相似,遍历判断递归 57 | 58 | ## 2.数组去重 59 | ```js 60 | const arr = [1, 1, '1', 17, true, true, false, false, 'true', 'a', {}, {}]; 61 | // => [1, '1', 17, true, false, 'true', 'a', {}, {}] 62 | ``` 63 | 64 | ### 方法1:set 65 | 利用set特性 实现去重 66 | ```js 67 | const res1 = Array.from(new Set(arr)); 68 | ``` 69 | 70 | ### 方法2:循环判断 71 | ```js 72 | const unique2 = arr => { 73 | const res = []; 74 | for (let i = 0; i < arr.length; i++) { 75 | if (res.indexOf(arr[i]) === -1) res.push(arr[i]); 76 | } 77 | return res; 78 | } 79 | ``` 80 | 81 | ### 方法3:filter 82 | ```js 83 | const unique3 = arr => { 84 | return arr.filter((item, index) => { 85 | return arr.indexOf(item) === index; 86 | }); 87 | } 88 | ``` 89 | ### 方法4:Map 90 | ```js 91 | const unique4 = arr => { 92 | const map = new Map(); 93 | const res = []; 94 | for (let i = 0; i < arr.length; i++) { 95 | if (!map.has(arr[i])) { 96 | map.set(arr[i], true) 97 | res.push(arr[i]); 98 | } 99 | } 100 | return res; 101 | } 102 | ``` 103 | 104 | ## 3.类数组转化为数组 105 | 类数组是具有length属性,但不具有数组原型上的方法。常见的类数组有arguments、DOM操作方法返回的结果。 106 | 107 | ### 方法1:Array.from 108 | `Array.from()` 方法从一个类似数组或可迭代对象创建一个新的,浅拷贝的数组实例。 109 | ```js 110 | Array.from(document.querySelectorAll('div')) 111 | ``` 112 | 113 | ### 方法2:Array.prototype.slice.call() 114 | `Array.prototype.slice.call()`能将具有length属性的对象转成数组 115 | 116 | 类数组不是真的数组,没有数组上的方法,直接调用slice会报错 117 | 118 | 调用原型上的slice方法会将类数组转为数组,在进行截取操作 119 | ```js 120 | Array.prototype.slice.call(document.querySelectorAll('div')) 121 | ``` 122 | 123 | ### 方法3:...运算符 124 | es6 ...运算符可直接展开 125 | ```js 126 | [...document.querySelectorAll('div')] 127 | ``` 128 | 129 | ### 方法4:利用concat 130 | ```js 131 | Array.prototype.concat.apply([], document.querySelectorAll('div')); 132 | ``` 133 | 134 | ## 4.Array.prototype.filter() 135 | ![](https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/804ee51d522746c3b219548d038413c2~tplv-k3u1fbpfcp-zoom-1.image) 136 | ```js 137 | Array.prototype.filter = function(callback, thisArg) { 138 | if (this == undefined) { 139 | throw new TypeError('this is null or not undefined'); 140 | } 141 | if (typeof callback !== 'function') { 142 | throw new TypeError(callback + 'is not a function'); 143 | } 144 | const res = []; 145 | // 让O成为回调函数的对象传递(强制转换对象) 146 | const O = Object(this); 147 | // >>>0 保证len为number,且为正整数 148 | const len = O.length >>> 0; 149 | for (let i = 0; i < len; i++) { 150 | // 检查i是否在O的属性(会检查原型链) 151 | if (i in O) { 152 | // 回调函数调用传参 153 | if (callback.call(thisArg, O[i], i, O)) { 154 | res.push(O[i]); 155 | } 156 | } 157 | } 158 | return res; 159 | } 160 | ``` 161 | 162 | ## 5.Array.prototype.map() 163 | ![](https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/b099cf3e06bc4421abac4dc460a13c17~tplv-k3u1fbpfcp-zoom-1.image) 164 | ```js 165 | Array.prototype.map = function(callback, thisArg) { 166 | if (this == undefined) { 167 | throw new TypeError('this is null or not defined'); 168 | } 169 | if (typeof callback !== 'function') { 170 | throw new TypeError(callback + ' is not a function'); 171 | } 172 | const res = []; 173 | // 同理 174 | const O = Object(this); 175 | const len = O.length >>> 0; 176 | for (let i = 0; i < len; i++) { 177 | if (i in O) { 178 | // 调用回调函数并传入新数组 179 | res[i] = callback.call(thisArg, O[i], i, this); 180 | } 181 | } 182 | return res; 183 | } 184 | ``` 185 | 186 | ## 6.Array.prototype.forEach() 187 | ![](https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2c3819fb0c404ae5a8f4cddc4e80731e~tplv-k3u1fbpfcp-zoom-1.image) 188 | ```js 189 | Array.prototype.forEach = function(callback, thisArg) { 190 | if (this == null) { 191 | throw new TypeError('this is null or not defined'); 192 | } 193 | if (typeof callback !== "function") { 194 | throw new TypeError(callback + ' is not a function'); 195 | } 196 | const O = Object(this); 197 | const len = O.length >>> 0; 198 | let k = 0; 199 | while (k < len) { 200 | if (k in O) { 201 | callback.call(thisArg, O[k], k, O); 202 | } 203 | k++; 204 | } 205 | } 206 | ``` 207 | ## 7.Array.prototype.reduce() 208 | ![](https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e51625eb9e2d47799ff39c5956139af7~tplv-k3u1fbpfcp-zoom-1.image) 209 | ```js 210 | Array.prototype.reduce = function(callback, initialValue) { 211 | if (this == undefined) { 212 | throw new TypeError('this is null or not defined'); 213 | } 214 | if (typeof callback !== 'function') { 215 | throw new TypeError(callbackfn + ' is not a function'); 216 | } 217 | const O = Object(this); 218 | const len = this.length >>> 0; 219 | let accumulator = initialValue; 220 | let k = 0; 221 | // 如果第二个参数为undefined的情况下 222 | // 则数组的第一个有效值作为累加器的初始值 223 | if (accumulator === undefined) { 224 | while (k < len && !(k in O)) { 225 | k++; 226 | } 227 | // 如果超出数组界限还没有找到累加器的初始值,则TypeError 228 | if (k >= len) { 229 | throw new TypeError('Reduce of empty array with no initial value'); 230 | } 231 | accumulator = O[k++]; 232 | } 233 | while (k < len) { 234 | if (k in O) { 235 | accumulator = callback.call(undefined, accumulator, O[k], k, O); 236 | } 237 | k++; 238 | } 239 | return accumulator; 240 | } 241 | ``` 242 | ## 8.Function.prototype.apply() 243 | ```js 244 | /// 第一个参数是绑定的this,默认为window,第二个参数是数组或类数组 245 | Function.prototype.apply = function(context = window, args) { 246 | if (typeof this !== 'function') { 247 | throw new TypeError('Type Error'); 248 | } 249 | const fn = Symbol('fn'); 250 | context[fn] = this; 251 | 252 | const res = context[fn](...args); 253 | delete context[fn]; 254 | return res; 255 | } 256 | ``` 257 | 258 | ## 9.Function.prototype.call 259 | ```js 260 | // 和call唯一不同的是,call()方法接受的是一个参数列表 261 | Function.prototype.call = function(context = window, ...args) { 262 | if (typeof this !== 'function') { 263 | throw new TypeError('Type Error'); 264 | } 265 | const fn = Symbol('fn'); 266 | context[fn] = this; 267 | 268 | const res = context[fn](...args); 269 | delete context[fn]; 270 | return res; 271 | } 272 | ``` 273 | 274 | ## 10.Function.prototype.bind 275 | ```js 276 | Function.prototype.bind = function(context, ...args) { 277 | if (typeof this !== 'function') { 278 | throw new Error("Type Error"); 279 | } 280 | // 保存this的值 281 | var self = this; 282 | 283 | return function F() { 284 | // 考虑new的情况 285 | if(this instanceof F) { 286 | return new self(...args, ...arguments) 287 | } 288 | return self.apply(context, [...args, ...arguments]) 289 | } 290 | } 291 | ``` 292 | 293 | ## 11.debounce(防抖) 294 | 触发高频时间后n秒内函数只会执行一次,如果n秒内高频时间再次触发,则重新计算时间。 295 | ```js 296 | const debounce = (fn, time) => { 297 | let timeout = null; 298 | return function() { 299 | clearTimeout(timeout) 300 | timeout = setTimeout(() => { 301 | fn.apply(this, arguments); 302 | }, time); 303 | } 304 | }; 305 | ``` 306 | 防抖常应用于用户进行搜索输入节约请求资源,window触发resize事件时进行防抖只触发一次 307 | 308 | ## 12.throttle(节流) 309 | ```js 310 | const throttle = (fn, time) => { 311 | let flag = true; 312 | return function() { 313 | if (!flag) return; 314 | flag = false; 315 | setTimeout(() => { 316 | fn.apply(this, arguments); 317 | flag = true; 318 | }, time); 319 | } 320 | } 321 | ``` 322 | 高频时间触发,但n秒内只会执行一次,所以节流会稀释函数的执行频率。 323 | 324 | ## 13.函数柯里化 325 | ```js 326 | function add() { 327 | const _args = [...arguments]; 328 | function fn() { 329 | _args.push(...arguments); 330 | return fn; 331 | } 332 | fn.toString = function() { 333 | return _args.reduce((sum, cur) => sum + cur); 334 | } 335 | return fn; 336 | } 337 | ``` 338 | 经典面试题:实现add(1)(2)(3)(4)=10; 、 add(1)(1,2,3)(2)=9; 339 | 340 | ## 14.模拟new操作 341 | ```js 342 | function newOperator(ctor, ...args) { 343 | if (typeof ctor !== 'function') { 344 | throw new TypeError('Type Error'); 345 | } 346 | const obj = Object.create(ctor.prototype); 347 | const res = ctor.apply(obj, args); 348 | 349 | const isObject = typeof res === 'object' && res !== null; 350 | const isFunction = typeof res === 'function'; 351 | return isObject || isFunction ? res : obj; 352 | } 353 | ``` 354 | - 以ctor.prototype为原型创建一个对象 355 | - 执行构造函数并将this绑定到新创建的对象上 356 | - 判断构造函数执行返回的结果是否是引用数据类型,若是则返回构造函数执行的结果,否则返回创建的对象 357 | 358 | ## 15.instanceof 359 | instanceof运算符用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。 360 | 361 | 362 | ```js 363 | const myInstanceof = (left, right) => { 364 | // 基本数据类型都返回false 365 | if (typeof left !== 'object' || left === null) return false; 366 | let proto = Object.getPrototypeOf(left); 367 | while (true) { 368 | if (proto === null) return false; 369 | if (proto === right.prototype) return true; 370 | proto = Object.getPrototypeOf(proto); 371 | } 372 | } 373 | ``` 374 | 375 | ## 16.原型继承 376 | 这里只写寄生组合继承了,中间还有几个演变过来的继承但都有一些缺陷 377 | ```js 378 | function Parent() { 379 | this.name = 'parent'; 380 | } 381 | function Child() { 382 | Parent.call(this); 383 | this.type = 'children'; 384 | } 385 | Child.prototype = Object.create(Parent.prototype); 386 | Child.prototype.constructor = Child; 387 | ``` 388 | 389 | ## 17. Object.is 390 | Object.is解决的主要是这两个问题: 391 | ```js 392 | +0 === -0 // true 393 | NaN === NaN // false 394 | ``` 395 | 396 | ```js 397 | const is= (x, y) => { 398 | if (x === y) { 399 | // +0和-0应该不相等 400 | return x !== 0 || y !== 0 || 1/x === 1/y; 401 | } else { 402 | return x !== x && y !== y; 403 | } 404 | } 405 | ``` 406 | 407 | ## 18. Object.assign 408 | Object.assign()方法用于将所有可枚举属性的值从一个或多个源对象复制到目标对象。它将返回目标对象(请注意这个操作是浅拷贝) 409 | ```js 410 | Object.defineProperty(Object, 'assign', { 411 | value: function(target, ...args) { 412 | if (target == null) { 413 | return new TypeError('Cannot convert undefined or null to object'); 414 | } 415 | 416 | // 目标对象需要统一是引用数据类型,若不是会自动转换 417 | const to = Object(target); 418 | 419 | for (let i = 0; i < args.length; i++) { 420 | // 每一个源对象 421 | const nextSource = args[i]; 422 | if (nextSource !== null) { 423 | // 使用for...in和hasOwnProperty双重判断,确保只拿到本身的属性、方法(不包含继承的) 424 | for (const nextKey in nextSource) { 425 | if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { 426 | to[nextKey] = nextSource[nextKey]; 427 | } 428 | } 429 | } 430 | } 431 | return to; 432 | }, 433 | // 不可枚举 434 | enumerable: false, 435 | writable: true, 436 | configurable: true, 437 | }) 438 | ``` 439 | 440 | ## 19.深拷贝 441 | 递归的完整版本(考虑到了Symbol属性): 442 | 443 | ```js 444 | const cloneDeep1 = (target, hash = new WeakMap()) => { 445 | // 对于传入参数处理 446 | if (typeof target !== 'object' || target === null) { 447 | return target; 448 | } 449 | // 哈希表中存在直接返回 450 | if (hash.has(target)) return hash.get(target); 451 | 452 | const cloneTarget = Array.isArray(target) ? [] : {}; 453 | hash.set(target, cloneTarget); 454 | 455 | // 针对Symbol属性 456 | const symKeys = Object.getOwnPropertySymbols(target); 457 | if (symKeys.length) { 458 | symKeys.forEach(symKey => { 459 | if (typeof target[symKey] === 'object' && target[symKey] !== null) { 460 | cloneTarget[symKey] = cloneDeep1(target[symKey]); 461 | } else { 462 | cloneTarget[symKey] = target[symKey]; 463 | } 464 | }) 465 | } 466 | 467 | for (const i in target) { 468 | if (Object.prototype.hasOwnProperty.call(target, i)) { 469 | cloneTarget[i] = 470 | typeof target[i] === 'object' && target[i] !== null 471 | ? cloneDeep1(target[i], hash) 472 | : target[i]; 473 | } 474 | } 475 | return cloneTarget; 476 | } 477 | ``` 478 | 479 | ## 20. Promise 480 | 参考: [Promise](https://juejin.cn/post/6860037916622913550) 481 | ```js 482 | // 模拟实现Promise 483 | // Promise利用三大手段解决回调地狱: 484 | // 1. 回调函数延迟绑定 485 | // 2. 返回值穿透 486 | // 3. 错误冒泡 487 | 488 | // 定义三种状态 489 | const PENDING = 'PENDING'; // 进行中 490 | const FULFILLED = 'FULFILLED'; // 已成功 491 | const REJECTED = 'REJECTED'; // 已失败 492 | 493 | class Promise { 494 | constructor(exector) { 495 | // 初始化状态 496 | this.status = PENDING; 497 | // 将成功、失败结果放在this上,便于then、catch访问 498 | this.value = undefined; 499 | this.reason = undefined; 500 | // 成功态回调函数队列 501 | this.onFulfilledCallbacks = []; 502 | // 失败态回调函数队列 503 | this.onRejectedCallbacks = []; 504 | 505 | const resolve = value => { 506 | // 只有进行中状态才能更改状态 507 | if (this.status === PENDING) { 508 | this.status = FULFILLED; 509 | this.value = value; 510 | // 成功态函数依次执行 511 | this.onFulfilledCallbacks.forEach(fn => fn(this.value)); 512 | } 513 | } 514 | const reject = reason => { 515 | // 只有进行中状态才能更改状态 516 | if (this.status === PENDING) { 517 | this.status = REJECTED; 518 | this.reason = reason; 519 | // 失败态函数依次执行 520 | this.onRejectedCallbacks.forEach(fn => fn(this.reason)) 521 | } 522 | } 523 | try { 524 | // 立即执行executor 525 | // 把内部的resolve和reject传入executor,用户可调用resolve和reject 526 | exector(resolve, reject); 527 | } catch(e) { 528 | // executor执行出错,将错误内容reject抛出去 529 | reject(e); 530 | } 531 | } 532 | then(onFulfilled, onRejected) { 533 | onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value; 534 | onRejected = typeof onRejected === 'function'? onRejected : 535 | reason => { throw new Error(reason instanceof Error ? reason.message : reason) } 536 | // 保存this 537 | const self = this; 538 | return new Promise((resolve, reject) => { 539 | if (self.status === PENDING) { 540 | self.onFulfilledCallbacks.push(() => { 541 | // try捕获错误 542 | try { 543 | // 模拟微任务 544 | setTimeout(() => { 545 | const result = onFulfilled(self.value); 546 | // 分两种情况: 547 | // 1. 回调函数返回值是Promise,执行then操作 548 | // 2. 如果不是Promise,调用新Promise的resolve函数 549 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 550 | }) 551 | } catch(e) { 552 | reject(e); 553 | } 554 | }); 555 | self.onRejectedCallbacks.push(() => { 556 | // 以下同理 557 | try { 558 | setTimeout(() => { 559 | const result = onRejected(self.reason); 560 | // 不同点:此时是reject 561 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 562 | }) 563 | } catch(e) { 564 | reject(e); 565 | } 566 | }) 567 | } else if (self.status === FULFILLED) { 568 | try { 569 | setTimeout(() => { 570 | const result = onFulfilled(self.value); 571 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 572 | }); 573 | } catch(e) { 574 | reject(e); 575 | } 576 | } else if (self.status === REJECTED) { 577 | try { 578 | setTimeout(() => { 579 | const result = onRejected(self.reason); 580 | result instanceof Promise ? result.then(resolve, reject) : resolve(result); 581 | }) 582 | } catch(e) { 583 | reject(e); 584 | } 585 | } 586 | }); 587 | } 588 | catch(onRejected) { 589 | return this.then(null, onRejected); 590 | } 591 | static resolve(value) { 592 | if (value instanceof Promise) { 593 | // 如果是Promise实例,直接返回 594 | return value; 595 | } else { 596 | // 如果不是Promise实例,返回一个新的Promise对象,状态为FULFILLED 597 | return new Promise((resolve, reject) => resolve(value)); 598 | } 599 | } 600 | static reject(reason) { 601 | return new Promise((resolve, reject) => { 602 | reject(reason); 603 | }) 604 | } 605 | static all(promiseArr) { 606 | const len = promiseArr.length; 607 | const values = new Array(len); 608 | // 记录已经成功执行的promise个数 609 | let count = 0; 610 | return new Promise((resolve, reject) => { 611 | for (let i = 0; i < len; i++) { 612 | // Promise.resolve()处理,确保每一个都是promise实例 613 | Promise.resolve(promiseArr[i]).then( 614 | val => { 615 | values[i] = val; 616 | count++; 617 | // 如果全部执行完,返回promise的状态就可以改变了 618 | if (count === len) resolve(values); 619 | }, 620 | err => reject(err), 621 | ); 622 | } 623 | }) 624 | } 625 | static race(promiseArr) { 626 | return new Promise((resolve, reject) => { 627 | promiseArr.forEach(p => { 628 | Promise.resolve(p).then( 629 | val => resolve(val), 630 | err => reject(err), 631 | ) 632 | }) 633 | }) 634 | } 635 | } 636 | ``` 637 | ## 21. Promise.all 638 | Promise.all是支持链式调用的,本质上就是返回了一个Promise实例,通过resolve和reject来改变实例状态。 639 | ```js 640 | Promise.myAll = function(promiseArr) { 641 | return new Promise((resolve, reject) => { 642 | const ans = []; 643 | let index = 0; 644 | for (let i = 0; i < promiseArr.length; i++) { 645 | promiseArr[i] 646 | .then(res => { 647 | ans[i] = res; 648 | index++; 649 | if (index === promiseArr.length) { 650 | resolve(ans); 651 | } 652 | }) 653 | .catch(err => reject(err)); 654 | } 655 | }) 656 | } 657 | ``` 658 | ## 22.Promise.race 659 | ```js 660 | Promise.race = function(promiseArr) { 661 | return new Promise((resolve, reject) => { 662 | promiseArr.forEach(p => { 663 | // 如果不是Promise实例需要转化为Promise实例 664 | Promise.resolve(p).then( 665 | val => resolve(val), 666 | err => reject(err), 667 | ) 668 | }) 669 | }) 670 | } 671 | ``` 672 | ## 23. Promise并行限制 673 | 参考: [Promise并行限制](https://juejin.cn/post/6854573217013563405) 674 | 675 | 就是实现有并行限制的Promise调度器问题。 676 | 677 | ```js 678 | class Scheduler { 679 | constructor() { 680 | this.queue = []; 681 | this.maxCount = 2; 682 | this.runCounts = 0; 683 | } 684 | add(promiseCreator) { 685 | this.queue.push(promiseCreator); 686 | } 687 | taskStart() { 688 | for (let i = 0; i < this.maxCount; i++) { 689 | this.request(); 690 | } 691 | } 692 | request() { 693 | if (!this.queue || !this.queue.length || this.runCounts >= this.maxCount) { 694 | return; 695 | } 696 | this.runCounts++; 697 | 698 | this.queue.shift()().then(() => { 699 | this.runCounts--; 700 | this.request(); 701 | }); 702 | } 703 | } 704 | 705 | const timeout = time => new Promise(resolve => { 706 | setTimeout(resolve, time); 707 | }) 708 | 709 | const scheduler = new Scheduler(); 710 | 711 | const addTask = (time,order) => { 712 | scheduler.add(() => timeout(time).then(()=>console.log(order))) 713 | } 714 | 715 | 716 | addTask(1000, '1'); 717 | addTask(500, '2'); 718 | addTask(300, '3'); 719 | addTask(400, '4'); 720 | scheduler.taskStart() 721 | // 2 722 | // 3 723 | // 1 724 | // 4 725 | ``` 726 | 727 | ## 24. JSONP 728 | script标签不遵循同源协议,可以用来进行跨域请求,优点就是兼容性好但仅限于GET请求 729 | 730 | ```js 731 | const jsonp = ({ url, params, callbackName }) => { 732 | const generateUrl = () => { 733 | let dataSrc = ''; 734 | for (let key in params) { 735 | if (Object.prototype.hasOwnProperty.call(params, key)) { 736 | dataSrc += `${key}=${params[key]}&`; 737 | } 738 | } 739 | dataSrc += `callback=${callbackName}`; 740 | return `${url}?${dataSrc}`; 741 | } 742 | return new Promise((resolve, reject) => { 743 | const scriptEle = document.createElement('script'); 744 | scriptEle.src = generateUrl(); 745 | document.body.appendChild(scriptEle); 746 | window[callbackName] = data => { 747 | resolve(data); 748 | document.removeChild(scriptEle); 749 | } 750 | }) 751 | } 752 | ``` 753 | 754 | ## 25. AJAX 755 | ```js 756 | const getJSON = function(url) { 757 | return new Promise((resolve, reject) => { 758 | const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Mscrosoft.XMLHttp'); 759 | xhr.open('GET', url, false); 760 | xhr.setRequestHeader('Accept', 'application/json'); 761 | xhr.onreadystatechange = function() { 762 | if (xhr.readyState !== 4) return; 763 | if (xhr.status === 200 || xhr.status === 304) { 764 | resolve(xhr.responseText); 765 | } else { 766 | reject(new Error(xhr.responseText)); 767 | } 768 | } 769 | xhr.send(); 770 | }) 771 | } 772 | ``` 773 | 774 | ## 26. event模块 775 | 实现node中回调函数的机制,node中回调函数其实是内部使用了观察者模式。 776 | 777 | > 观察者模式:定义了对象间一种一对多的依赖关系,当目标对象Subject发生改变时,所有依赖它的对象Observer都会得到通知。 778 | 779 | ```js 780 | function EventEmitter() { 781 | this.events = new Map(); 782 | } 783 | 784 | // 需要实现的一些方法: 785 | // addListener、removeListener、once、removeAllListeners、emit 786 | 787 | // 模拟实现addlistener方法 788 | const wrapCallback = (fn, once = false) => ({ callback: fn, once }); 789 | EventEmitter.prototype.addListener = function(type, fn, once = false) { 790 | const hanlder = this.events.get(type); 791 | if (!hanlder) { 792 | // 没有type绑定事件 793 | this.events.set(type, wrapCallback(fn, once)); 794 | } else if (hanlder && typeof hanlder.callback === 'function') { 795 | // 目前type事件只有一个回调 796 | this.events.set(type, [hanlder, wrapCallback(fn, once)]); 797 | } else { 798 | // 目前type事件数>=2 799 | hanlder.push(wrapCallback(fn, once)); 800 | } 801 | } 802 | // 模拟实现removeListener 803 | EventEmitter.prototype.removeListener = function(type, listener) { 804 | const hanlder = this.events.get(type); 805 | if (!hanlder) return; 806 | if (!Array.isArray(this.events)) { 807 | if (hanlder.callback === listener.callback) this.events.delete(type); 808 | else return; 809 | } 810 | for (let i = 0; i < hanlder.length; i++) { 811 | const item = hanlder[i]; 812 | if (item.callback === listener.callback) { 813 | hanlder.splice(i, 1); 814 | i--; 815 | if (hanlder.length === 1) { 816 | this.events.set(type, hanlder[0]); 817 | } 818 | } 819 | } 820 | } 821 | // 模拟实现once方法 822 | EventEmitter.prototype.once = function(type, listener) { 823 | this.addListener(type, listener, true); 824 | } 825 | // 模拟实现emit方法 826 | EventEmitter.prototype.emit = function(type, ...args) { 827 | const hanlder = this.events.get(type); 828 | if (!hanlder) return; 829 | if (Array.isArray(hanlder)) { 830 | hanlder.forEach(item => { 831 | item.callback.apply(this, args); 832 | if (item.once) { 833 | this.removeListener(type, item); 834 | } 835 | }) 836 | } else { 837 | hanlder.callback.apply(this, args); 838 | if (hanlder.once) { 839 | this.events.delete(type); 840 | } 841 | } 842 | return true; 843 | } 844 | EventEmitter.prototype.removeAllListeners = function(type) { 845 | const hanlder = this.events.get(type); 846 | if (!hanlder) return; 847 | this.events.delete(type); 848 | } 849 | ``` 850 | 851 | ## 27. 图片懒加载 852 | 可以给img标签统一自定义属性data-src='default.png',当检测到图片出现在窗口之后再补充src属性,此时才会进行图片资源加载。 853 | 854 | ```js 855 | function lazyload() { 856 | const imgs = document.getElementsByTagName('img'); 857 | const len = imgs.length; 858 | // 视口的高度 859 | const viewHeight = document.documentElement.clientHeight; 860 | // 滚动条高度 861 | const scrollHeight = document.documentElement.scrollTop || document.body.scrollTop; 862 | for (let i = 0; i < len; i++) { 863 | const offsetHeight = imgs[i].offsetTop; 864 | if (offsetHeight < viewHeight + scrollHeight) { 865 | const src = imgs[i].dataset.src; 866 | imgs[i].src = src; 867 | } 868 | } 869 | } 870 | 871 | // 可以使用节流优化一下 872 | window.addEventListener('scroll', lazyload); 873 | ``` 874 | 875 | ## 28. 滚动加载 876 | 原理就是监听页面滚动事件,分析clientHeight、scrollTop、scrollHeight三者的属性关系。 877 | 878 | ```js 879 | window.addEventListener('scroll', function() { 880 | const clientHeight = document.documentElement.clientHeight; 881 | const scrollTop = document.documentElement.scrollTop; 882 | const scrollHeight = document.documentElement.scrollHeight; 883 | if (clientHeight + scrollTop >= scrollHeight) { 884 | // 检测到滚动至页面底部,进行后续操作 885 | // ... 886 | } 887 | }, false); 888 | ``` 889 | 890 | ## 29. 渲染几万条数据不卡住页面 891 | 渲染大数据时,合理使用createDocumentFragment和requestAnimationFrame,将操作切分为一小段一小段执行。 892 | ```js 893 | setTimeout(() => { 894 | // 插入十万条数据 895 | const total = 100000; 896 | // 一次插入的数据 897 | const once = 20; 898 | // 插入数据需要的次数 899 | const loopCount = Math.ceil(total / once); 900 | let countOfRender = 0; 901 | const ul = document.querySelector('ul'); 902 | // 添加数据的方法 903 | function add() { 904 | const fragment = document.createDocumentFragment(); 905 | for(let i = 0; i < once; i++) { 906 | const li = document.createElement('li'); 907 | li.innerText = Math.floor(Math.random() * total); 908 | fragment.appendChild(li); 909 | } 910 | ul.appendChild(fragment); 911 | countOfRender += 1; 912 | loop(); 913 | } 914 | function loop() { 915 | if(countOfRender < loopCount) { 916 | window.requestAnimationFrame(add); 917 | } 918 | } 919 | loop(); 920 | }, 0) 921 | ``` 922 | 923 | ## 30.打印出当前网页使用了多少种HTML元素 924 | ```js 925 | const fn = () => { 926 | return [...new Set([...document.querySelectorAll('*')].map(el => el.tagName))].length; 927 | } 928 | ``` 929 | DOM操作返回的是类数组,需要转换为数组之后才可以调用数组的方法 930 | 931 | ## 31.将VirtualDom转化为真实DOM结构 932 | 这是当前SPA应用的核心概念之一 933 | 934 | ```js 935 | // vnode结构: 936 | // { 937 | // tag, 938 | // attrs, 939 | // children, 940 | // } 941 | 942 | //Virtual DOM => DOM 943 | function render(vnode, container) { 944 | container.appendChild(_render(vnode)); 945 | } 946 | function _render(vnode) { 947 | // 如果是数字类型转化为字符串 948 | if (typeof vnode === 'number') { 949 | vnode = String(vnode); 950 | } 951 | // 字符串类型直接就是文本节点 952 | if (typeof vnode === 'string') { 953 | return document.createTextNode(vnode); 954 | } 955 | // 普通DOM 956 | const dom = document.createElement(vnode.tag); 957 | if (vnode.attrs) { 958 | // 遍历属性 959 | Object.keys(vnode.attrs).forEach(key => { 960 | const value = vnode.attrs[key]; 961 | dom.setAttribute(key, value); 962 | }) 963 | } 964 | // 子数组进行递归操作 965 | vnode.children.forEach(child => render(child, dom)); 966 | return dom; 967 | } 968 | ``` 969 | 970 | ## 32. 字符串解析问题 971 | ```js 972 | var a = { 973 | b: 123, 974 | c: '456', 975 | e: '789', 976 | } 977 | var str=`a{a.b}aa{a.c}aa {a.d}aaaa`; 978 | // => 'a123aa456aa {a.d}aaaa' 979 | ``` 980 | 实现函数使得将str字符串中的{}内的变量替换,如果属性不存在保持原样(比如{a.d}) 981 | 982 | 类似于模版字符串,但有一点出入,实际上原理大差不差 983 | 984 | ```js 985 | const fn1 = (str, obj) => { 986 | let res = ''; 987 | // 标志位,标志前面是否有{ 988 | let flag = false; 989 | let start; 990 | for (let i = 0; i < str.length; i++) { 991 | if (str[i] === '{') { 992 | flag = true; 993 | start = i + 1; 994 | continue; 995 | } 996 | if (!flag) res += str[i]; 997 | else { 998 | if (str[i] === '}') { 999 | flag = false; 1000 | res += match(str.slice(start, i), obj); 1001 | } 1002 | } 1003 | } 1004 | return res; 1005 | } 1006 | // 对象匹配操作 1007 | const match = (str, obj) => { 1008 | const keys = str.split('.').slice(1); 1009 | let index = 0; 1010 | let o = obj; 1011 | while (index < keys.length) { 1012 | const key = keys[index]; 1013 | if (!o[key]) { 1014 | return `{${str}}`; 1015 | } else { 1016 | o = o[key]; 1017 | } 1018 | index++; 1019 | } 1020 | return o; 1021 | } 1022 | ``` -------------------------------------------------------------------------------- /bottom-learning/js基础题.md: -------------------------------------------------------------------------------- 1 | # js基础题 2 | ## 1、请写出如下输出值,并写出把注释调的代码取消注释的值,并解释为什么? 3 | 未取消注释: 4 | ```js 5 | this.a = 20; 6 | 7 | var test = { 8 | a: 40, 9 | 10 | init: ()=> { 11 | console.log(this.a); 12 | function go() { 13 | // this.a = 60 14 | 15 | console.log(this.a); 16 | } 17 | 18 | go.prototype.a = 50; 19 | 20 | return go; 21 | } 22 | } 23 | 24 | // var p = test.init(); 25 | // p(); 26 | new(test.init())(); 27 | // 20 50 28 | ``` 29 | 解析: 30 | 31 | 1. 先执行init,this指向window,输出20 32 | 2. 执行完init返回函数go, 然后实例化再执行,输出this.a,先在自己身上找没有a,然后在上原型链上找 a = 50, 输出50 33 | 3. 注意没有new 不会上原型链上去找,但是值可以加上去 34 | 4. 找的顺序是 先在自己身上找 没有再到原型链上去找 35 | 36 | 取消注释: 37 | ```js 38 | this.a = 20; 39 | 40 | var test = { 41 | a: 40, 42 | 43 | init: ()=> { 44 | console.log(this.a); 45 | function go() { 46 | this.a = 60 47 | 48 | console.log(this.a); 49 | } 50 | 51 | go.prototype.a = 50; 52 | 53 | return go; 54 | } 55 | } 56 | 57 | var p = test.init(); 58 | p(); 59 | new(test.init())(); 60 | // 20 60 60 60 61 | ``` 62 | 解析: 63 | 64 | 1. 执行init,同上,输出20 65 | 2. p()执行, `this.a = 60` ,这里的this还是指向window,所以window.a = 60,这里输出60 66 | 3. 这里再去执行init,输出this.a ,去window上找,输出60 67 | 4. 此时实例化过后的go,本身的属性上a=60,`__proto__` 上a=50,优先找自身上的属性,故输出60 68 | 69 | ## 上面涉及到this指向的问题,这里总结一下 70 | ### 首先看普通函数的this 71 | ```js 72 | function f1() { 73 | console.log(this); 74 | } 75 | 76 | var obj={ 77 | x:10, 78 | fn: f1 79 | } 80 | 81 | f1(); // window 82 | obj.fn(); // {x: 10, fn: ƒ} 83 | new f1() // f1 {} 84 | f1.call(obj) // {x: 10, fn: ƒ} 85 | f1.apply(obj) // {x: 10, fn: ƒ} 86 | f1.bind(obj)() // {x: 10, fn: ƒ} 87 | ``` 88 | 从以上代码不难看出 89 | 1. 普通函数this指向调用此函数的对象 90 | 2. 如果函数用作构造函数,那么this指向构造函数创建的对象实例 91 | 3. 可以通过call、apply、bind来改变this指向 92 | 93 | 94 | ### 再来看看箭头函数 95 | ```js 96 | var f1 = () => { 97 | console.log(this); 98 | } 99 | 100 | var obj={ 101 | x:10, 102 | fn: f1 103 | } 104 | 105 | f1(); // window 106 | obj.fn(); // Window 107 | f1.call(obj) // Window 108 | f1.apply(obj) // Window 109 | f1.bind(obj)() // Window 110 | ``` 111 | 结论: 112 | 113 | 1. 箭头函数本身没有this,但是在它声明时可以捕获别人的this供自己使用 114 | 2. 箭头函数中的this是在它声明时捕获它所处作用域中的this 115 | 3. this一旦被捕获,以后将不再变化,即时是用call、apply、bind这样的方法也改变不了 116 | 4. 注意这里相比上面普通函数少了一个实例化,是因为箭头函数无法被实例化 117 | 118 | ## 2、请写出如下点击li的输出值,并用三种办法正确输出li里的数字 119 | ```html 120 |
    121 |
  • 1
  • 122 |
  • 2
  • 123 |
  • 3
  • 124 |
  • 4
  • 125 |
  • 5
  • 126 |
  • 6
  • 127 |
128 | 129 | 137 | 138 | ``` 139 | 140 | ### 解法1,使用this 141 | 因为function的this指向当前调用它的对象,所以this就是当前的dom 142 | ```js 143 | var list_li = document.getElementsByTagName("li"); 144 | for (var i = 0; i < list_li.length; i++) { 145 | list_li[i].onclick = function() { 146 | console.log(this.innerHTML); 147 | } 148 | } 149 | ``` 150 | ### 解法2,使用let 151 | 因为let存在块级作用域 152 | ```js 153 | var list_li = document.getElementsByTagName("li"); 154 | for (let i = 0; i < list_li.length; i++) { 155 | list_li[i].onclick = function() { 156 | console.log(i + 1); 157 | } 158 | } 159 | ``` 160 | 161 | ### 解法3,使用立即执行函数 162 | 立即执行函数有自己的作用域,并且执行后会销毁 163 | ```js 164 | var list_li = document.getElementsByTagName("li"); 165 | for (var i = 0; i < list_li.length; i++) { 166 | (function(i) { 167 | list_li[i].onclick = function() { 168 | console.log(i + 1); 169 | } 170 | })(i) 171 | } 172 | ``` 173 | 174 | ## 3、写出输出值,并解释为什么 175 | 代码段1 176 | ```js 177 | function test(m) { 178 | m = { 179 | v: 5 180 | } 181 | } 182 | var m = { 183 | k: 30 184 | }; 185 | 186 | test(m); 187 | alert(m.v); 188 | // undefined 189 | ``` 190 | 代码段2 191 | ```js 192 | function test(m) { 193 | m.xxx = { 194 | v: 5 195 | } 196 | } 197 | var m = { 198 | k: 30 199 | }; 200 | 201 | test(m); 202 | console.log(m) 203 | // {k: 30, xxx: {…}} 204 | ``` 205 | 解析: 206 | 207 | 先说代码段1,这里`test(m)`将对象m传入函数test本质上是引用传递,内部执行的`m = { v: 5 }`将内部的m进行了重写,此时内部m和外部m不再有关联。 208 | ```js 209 | // 例如 210 | var a = { 211 | qq: 25 212 | }; 213 | var b = a; 214 | 215 | b = { 216 | x: 11 217 | }; 218 | 219 | console.log(a); // {qq: 25} 220 | console.log(b); // {xx: 11} 221 | ``` 222 | 223 | 代码段2,`test(m)`是引用传递,故本质上内部m和外部m指向的是同一个地址,其中一个改变另一个也会跟着改变 224 | 225 | ## 4、请写出输出值,并解释为什么。 226 | ```js 227 | function yd() { 228 | console.log(1); 229 | } 230 | 231 | (function () { 232 | if (false) { 233 | function yd() { 234 | console.log(2); 235 | } 236 | } 237 | 238 | yd(); 239 | })(); 240 | 241 | // yd is not a function 242 | ``` 243 | 立即执行函数内部,yd函数声明被提升,但是函数体未被提升出来,这里立即执行函数内部yd为undefined 244 | 245 | 函数体在if中无法被提升,且if内的内容在执行时也未执行,故输出`yd is not a function` 246 | 247 | ## 5、 请用一句话算出0-100之间学生的学生等级,如90-100为一等生、80-90为二等生,以此类推。 248 | 249 | ```js 250 | 10 - parseInt(分数/10); 251 | ``` 252 | 253 | ## 6、请用一句话遍历变量a。禁止使用for, 已知var a = "abc"; 254 | ```js 255 | var a = "abc"; 256 | // 请用一句话遍历变量a,禁止使用for。 257 | 258 | // 解法一: 259 | console.log(...new Set(a)); 260 | 261 | // 解法二: 262 | console.log(Array.from(a)); 263 | 264 | // 解法三: 265 | console.log(Array.prototype.slice.call(a)); 266 | ``` 267 | 268 | ## 7、写出如下代码执行结果,并解释为什么 269 | 270 | ```js 271 | var length = 10; 272 | function fn() { 273 | console.log(this.length); 274 | } 275 | var yd = { 276 | length: 5, 277 | method: function(fn) { 278 | fn(); 279 | arguments[0](); 280 | } 281 | }; 282 | yd.method(fn, 1); 283 | // 10 2 284 | // this指向问题 285 | ``` 286 | 287 | ## 8.输出值是什么,为什么 288 | ```js 289 | console.log(a); 290 | console.log(typeof init(a)); 291 | 292 | var flag = true; 293 | 294 | if (!flag) { 295 | var a = 1; 296 | } 297 | 298 | if (flag) { 299 | function init(a) { 300 | init = a; 301 | console.log("init1"); 302 | } 303 | } else { 304 | function init(a) { 305 | init = a; 306 | console.log("init2"); 307 | } 308 | } 309 | // undefined 310 | // init is not a function 311 | // 预编译 if里面函数声明无法提升 312 | ``` 313 | 314 | ## 9.请写出如下输出值,并完成附加题的作答 315 | ```js 316 | function fn(){ 317 | console.log(this.length); 318 | } 319 | 320 | var init = { 321 | length:5, 322 | method:function() { 323 | "use strict"; 324 | fn(); 325 | arguments[0]() 326 | } 327 | } 328 | const result = init.method.bind(null); 329 | result(fn,1); 330 | // 0 2 331 | // this指向问题 this软绑硬绑 严格模式 332 | 333 | ``` 334 | ```js 335 | // 软绑硬绑解释 336 | 337 | // 软绑 338 | function fn(){ 339 | console.log(this.length); 340 | } 341 | 342 | const res = fn.bind(null) 343 | res(); 344 | // 0 this 指向window 345 | 346 | // 硬绑 347 | function fn(){ 348 | console.log(this.length); 349 | } 350 | 351 | const res = fn.bind({}) 352 | res(); 353 | // undefined this 指向{} 354 | ``` 355 | 也就是说this绑定null不生效 356 | 357 | 附加题 358 | ```js 359 | function init(a,b,c){ 360 | console.log(this.length); 361 | console.log(this.callee.length); 362 | } 363 | 364 | function fn(d){ 365 | arguments[0](10,20,30,40,50); 366 | } 367 | 368 | fn(init,10,20,30); 369 | // 4 1 370 | // this指向问题 this指向fn的arguments,arguments.callee指向当前arguments指向的函数,this.callee => fn , length就是形参的个数 371 | ``` 372 | 373 | ## 10.请问变量a会被GC回收么,为什么呢? 374 | ```js 375 | function test(){ 376 | var a = "init"; 377 | return function(){ 378 | eval(""); 379 | } 380 | } 381 | test()(); 382 | // 不会回收 383 | ``` 384 | 欺骗词法作用域 385 | 386 | 1. eval 不对词法作用域任何的变量进行解除绑定 387 | 2. with 浏览器一旦遇到with 放弃所有变量的回收 388 | 3. 如果调用了大函数 389 | ```js 390 | function init(){ 391 | var a = 30; 392 | var s = new Function("console.log(a)"); 393 | s(); 394 | } 395 | init(); 396 | // a is not defined 397 | 398 | function init(){ 399 | var a = 30; 400 | var s = new Function(console.log(a)); 401 | s(); 402 | } 403 | init(); 404 | // 30 405 | ``` 406 | 4.try--catch 欺骗了词法作用域 Catch---e 延长了作用域链 407 | 408 | ## 11,输出什么,为什么? 409 | ```js 410 | Object.prototype.a = 'a'; 411 | Function.prototype.a = 'a1'; 412 | function Person(){}; 413 | var init = new Person(); 414 | console.log('p.a: '+ init.a); 415 | console.log(init.__proto__.__proto__.constructor.constructor.constructor); 416 | // p.a: a 417 | // Function() { [native code] } 418 | ``` 419 | 如下图: 420 | 输出init.a时,回去init上去找有没有a这个属性,发现没有去__proto__也就是Person.prototype上去找,发现还是没有继续上__proto__上也就是Object.prototype上找,找到属性a的值为a 421 | 422 | 第二个问题一样,如下图,只需弄懂一个规则,自己身上找不到的就会上爹(`__proto__`)上去找,依次类推 423 | ![][base64prototype] 424 | 425 | Object.prototype 和 Function.prototype 打印的内容差距很大原因是什么呢? 426 | 427 | 如上图,一切皆对象,函数一等公民,最顶端的是function Function() 428 | 429 | [base64prototype]:data:image/jpeg;base64,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 --------------------------------------------------------------------------------