--- title: 码场悟道 categories: - Front-End status: doing --- # 模板引擎 严格的模板引擎的定义,输入模板字符串 + 数据,得到渲染过的字符串。实现上,从正则替换到拼 function 字符串到正经的 AST 解析各种各样,但从定义上来说都是差不多的。字符串渲染的性能其实也就在后端比较有意义,毕竟每一次渲染都是在消耗服务器资源,但在前端,用户只有一个,几十毫秒的渲染时间跟请求延迟比起来根本不算瓶颈。倒是前端的后续更新是字符串模板引擎的软肋,因为用渲染出来的字符串整个替换 innerHTML 是一个效率很低的更新方式。所以这样的模板引擎如今在纯前端情境下已经不再是好的选择,意义更多是在于方便前后端共用模板。 # 古老数据渲染 vm 的方式 这种写法,弊端太多了,玩具车 ```html Document ``` # mustache 原理 - 1、先把模板字符串编译成 tokens(代号) - 2、根据 tokens,结合数据渲染成 dom > 本质上,tokens 是一个 js 嵌套数组没事模板字符串 js 的表示,他是`抽象语法树`,`虚拟节点`的开山鼻祖 假设有这么一个模板字符串 ```html

我买了一个{{thing}},好{{mood}}啊

``` 会编译成 tokens,如下: ```js // 这里面每一个数组行都是一个 token,组起来就是 tokens // html 标签也会被看成纯文本 [ ["text", "

我买了一个"], ["name", "thing"], ["text", "好"], ["name", "mood"], ["text", "啊

"], ]; ``` 当模板存在循环式,带层级嵌套,如下: ```html
``` 会被编译成 ```js [ ["text", "
"], ]; ``` 如果是双重循环,带层级嵌套继续加一层,例如: ```html
    {{#students}}
  1. 学生{{item.name}}的爱好是
      {{#item.hobbies}}
    1. {{.}}
    2. {{/#item.hobbies}}
  2. {{/#students}}
``` 会被编译成 ```js [ ["text", "
    "], [ "#", "students", null, null, [["text", "
  1. 学生"], ["name", "name"], ["text", "的爱好是
      "], ["#", "hobbies", null, null], [ ['text','
    1. '], ['name','.'], ['text','
    2. '] ]], ['text','
        '], ]], ['text','
'] ], ]; ``` > 在`mustache.js`中完成上述这一过程的函数`parseTemplate`,可以去找源代码看 ## tokens 生成算法 用简单的模板字符串举例: ```html 我买了一个{{thing}},好{{mood}}啊 ``` 有一个指针往右遍历,从`我`开始,遍历到`啊`结束,如下: ```js /** * * 我买了一个{{thing}},好{{mood}}啊 * ↑ * * Step1:指针位置 = 1 * * 我买了一个{{thing}},好{{mood}}啊 * ↑ * * Step2:指针位置 = 2 * * 我买了一个{{thing}},好{{mood}}啊 * ↑ * * Step3:指针位置 = 4 * * 我买了一个{{thing}},好{{mood}}啊 * ↑ * * Step4:指针位置 = 11 * * 我买了一个{{thing}},好{{mood}}啊 * ↑ * * Step5:指针位置 = 15 * * 我买了一个{{thing}},好{{mood}}啊 * ↑ * * ... while(指针位置 >= 模板字符串.length) * * / ``` - Step1: 指针右移 1 个长度,以指针位置切割,字符串被分成`我`+`买了一个{{thing}},好{{mood}}啊` - Step2: 指针右移 1 个长度,以指针位置切割,字符串被分成`我买`+`了一个{{thing}},好{{mood}}啊` - Step3: 第一次遇到,通过 `indexOf("{{") == 0` 判断 ```js // 标记为 text 放到 tokens 中 token.push(["text", "我买了一个"]); // tokens: [['text', '我买了一个']] ``` 结束,此时指针位置 = 4 - Step4: 指针右移 2 个长度,跳过`{{`,暂存此时`pos_last = 6` 此时,右边字符串(尾字符串)`thing}},好{{mood}}啊` 右移 5 个长度,识别`模板内部数据对象`: ```js substring(post_last, 6 + 5); // thing 5个长度 // 标记为 name 放到 tokens 中 token.push(["name", "thing"]); // tokens: [['text', '我买了一个']], ['name', 'thing']] ``` 结束,此时指针位置 = 11 - Step5: > 遇到 `}}`,通过 `indexOf("}}") == 0`判断 指针右移 2 个长度,跳过`}}`,暂存此时`post_last = 13`,继续右移 2 个长度 ```js substring(post_last, 13 + 2); // ,好 2个长度 // 标记为 text 放到 tokens 中 token.push(["text", ",好"]); // tokens: [['text', '我买了一个']], ['name', 'thing'],['text', ',好' ]] ``` > 第二次,遇到 `{{` 剩下循环执行就行了,这个过程,我们可以称作`扫描 Scan` ## 扫描器 Scanner 新建一个 `Scanner.js`,用来扫描模板字符串,实现上面的原理 ```js /** * 模板字符串扫描器 */ class Scanner { constructor(templ) { this.templ = templ; // 模板字符串 this.tail = templ; // 尾字符串 this.pPos = 0; // 指针位置 } /** * 指针跳过模板标签 * @param {模板语法包围标签} tag */ jumpTag(tag) { if (this.tail.indexOf(tag) === 0) { this.pPos += tag.length; // 指针右移 tag.length 个长度 this.tail = this.templ.substring(this.pPos); // 尾字符串更新 } } /** * 指针遇见模板标签 {{ * @param {模板语法包围标签} tag */ missTag(tag) { let pPos_last = this.pPos; while (!this.eof() && this.tail.indexOf(tag) !== 0) { this.pPos++; this.tail = this.templ.substring(this.pPos); } return this.templ.substring(pPos_last, this.pPos); } eof() { return this.pPos >= this.templ.length; } } ``` ## 分析器 Parser 调用`Scanner.js` ```js let tmpl = `我买了一个{{thing}},好{{mood}}啊`; // 编译 模板字符串 => tokens const Parser = { createTokens: (tmpl) => { let scanner = new Scanner(tmpl); let tokens = []; // scanner 循环执行 while (!scanner.eof()) { ctx = scanner.missTag("{{"); // 返回 头字符串 if (ctx != "") { tokens.push(["text", ctx]); } scanner.jumpTag("{{"); // 跳过 模板字符 ctx = scanner.missTag("}}"); // 返回 {{ x }} if (ctx != "") { tokens.push(["name", ctx]); } scanner.jumpTag("}}"); } return tokens; }, }; console.log(Parser.createTokens(tmpl)); // 输出,非常的 奈一丝 // ["text", "我买了一个"] // ["name", "thing"] // ["text", ",好"] // ["name", "mood"] // ["text", "啊"] ``` ## 扫描器 Scanner 增强 上面的`Parser`只能识别`{{`和`}}`,如果模板语法复杂一点,比如加入 `{{#list}}...{{/list}}`,需要增强`Parser` ```js const template = ` 哈哈哈 {{#students}} 我买了一个 {{ thing }},好{{mood}}啊{{a}} {{item.name}} {{/students}} `; const Parser = { createTokens: (tmpl) => { let scanner = new Scanner(tmpl); let tokens = []; let ctx = ""; // scanner 循环执行 while (!scanner.eof()) { ctx = scanner.missTag("{{"); // 返回 头字符串 if (ctx != "") { tokens.push(["text", ctx]); } scanner.jumpTag("{{"); // 跳过 模板字符 ctx = scanner.missTag("}}"); // 返回 {{ x }} if (ctx != "") { switch (ctx[0]) { case "#": tokens.push(["#", ctx.substr(1)]); // {{# x }} break; case "/": tokens.push(["/", ctx.substr(1)]); break; default: tokens.push(["name", ctx]); break; } } scanner.jumpTag("}}"); } return tokens; }, }; console.log(Parser.createTokens(template)); // 输出 // ["text", "↵ 哈哈哈↵ "] // ["#", "students"] // ["text", "↵ 我买了一个 "] // ["name", " thing "] // ["text", ",好"] // ["name", "mood"] // ["text", "啊"] // ["name", "a"] // ["text", "↵ "] // ["name", "item.name"] // ["text", "↵ "] // ["/", "students"] // ["text", "↵ "] ``` ## 栈队列算法 上一步最后的输出,只有单层嵌套,如果是两层嵌套怎么办? 例如模板语法如下: ```js var template = ` 哈哈哈 {{#students}} {{#stu}} {{stu.name}}买了一个 {{ thing }},好{{mood}}啊{{a}} {{/stu}} {{item.name}} {{/students}} `; ``` 经过`Parser`处理得到: ```js /** * * ["text", "↵ 哈哈哈↵ "] * ["#", "students"] * ["text", "↵ "] * ["#", "stu"] * ["text", "↵ "] * ["name", "stu.name"] * ["text", "买了一个 "] * ["name", " thing "] * ["text", ",好"] * ["name", "mood"] * ["text", "啊"] * ["name", "a"] * ["text", "↵ "] * ["/", "stu"] * ["text", "↵ "] * ["name", "item.name"] * ["text", "↵ "] * ["/", "students"] * ["text", "↵ "] * * / ``` 此时`students`和`stu`都是`#`标记,我们需要利用算法处理他们的嵌套结构,处理成大约如下这样的结构: ```js /** * * ["text", "↵ 哈哈哈↵ "] * Array(3) * "#" * "students" * Array(5) * ["text", "↵ "] * ["#", "stu", Array(9)] * ["text", "↵ "] * ["name", "item.name"] * ["text", "↵ "] * ["text", "↵ "] * * / ``` # 常用工具类 ## 递归 ```js /** * {string} dir 递归根目录 * {object} list 暂存参数 */ const deep = async (dir, list = []) => { const dirs = await fs.promises.readdir(dir) for (let i = 0; i < dirs.length; i++) { const item = dirs[i] const itemPath = path.join(dir, item) const isDir = fs.statSync(itemPath).isDirectory() isDir ? await deep(itemPath, list) : list.push(itemPath) } return list } ``` ## 自增id短码 用于连接分享 ```typescript const createAscString = (id) => { const dictionary = [ "0123456789", "abcdefghigklmnopqrstuvwxyz", "ABCDEFGHIGKLMNOPQRSTUVWXYZ", ]; let chars = dictionary.join("").split(""), radix = chars.length, qutient = 1000 * 1000 * 9999 + +id, arr = []; while (qutient) { mod = qutient % radix; qutient = (qutient - mod) / radix; arr.unshift(chars[mod]); } return arr.join(""); }; console.log(createAscString(100000000)); ``` ## 手动实现 eventBus ```js export default class EventBus { constructor() { // key-value : eventName-date this.callbacks = {}; } /** * 监听事件 * @param {事件名} eventName * @param {回调函数} callback */ on(eventName, callback) { this.checkType(eventName).callbacks[eventName] ? callback(this.callbacks[eventName]) : this.error(`The event has not been declared`); } /** * 注册一个事件 * @param {事件名} eventName * @param {传递的对象} data */ emit(eventName, data) { this.checkType(eventName).callbacks[eventName] = data; } /** * 注销事件,不传参数默认注销全部事件 * @param {事件名} eventName */ off(eventName) { eventName ? this.checkType(eventName).removeEvent(eventName) : this.emptyEvent(); } /** * 移出事件 * @param {事件名} eventName */ removeEvent(eventName) { Reflect.deleteProperty(this.callbacks, eventName); } /** * 清空全部事件 */ emptyEvent() { this.callbacks = []; } /** * 参数类型校验 * @param {参数} param * @param {合法的类型} validType */ checkType(param, validType = "string") { if (typeof param !== validType) this.error(`(param, ${param}) should be of ${validType} type`); return this; // 缅怀jQ链式调用 } /** * 错误提示 * @param {提示文字} text */ error(text) { throw new Error(text); } } ``` 调用 ```js // 省略import const eventBus = new EventBus(); eventBus.emit("login", [{ a: 1, d: 2 }]); eventBus.on(123, (d) => console.log(d)); // [{...}] ``` ## 判断对象是否有某个 key ```javascript let obj = { alias: "es6" }; "alias" in obj; // true Reflect.has(obj, "alias"); // true ``` ## 浏览器 ## 版本信息 ```javascript window.navigator.userAgent; ``` ## 兼容事件绑定 ```javascript /* 兼容低版本IE,ele为需要绑定事件的元素, eventName为事件名(保持addEventListener语法,去掉on),fun为事件响应函数 */ function addEvent(ele, eventName, fun) { ele.addEventListener ? ele.addEventListener(eventName, fun, false) : ele.attachEvent("on" + eventNme, fun); } ``` ## 数组对象 ## reduce ```javascript var arr = [1, 2, 3, 4]; var sum = arr.reduce(function(prev, cur, index, arr) { console.log(prev, cur, index); return prev + cur; },0) //注意这里设置了初始值 console.log(arr, sum); // 求和 const sum = arr.reduce((p,c) => p+c) ``` ## 对象内部根据 key 对 value 进行排序,取前 3 ```javascript let datasource = [ { price: 1, alias: "watermelon" }, { price: 3, alias: "orange" }, { price: 2, alias: "banana" }, { price: 4, alias: "apple" }, ]; // 降序排列 let compare = (key) => (a, b) => b[key] - a[key]; let sorted = datasource .sort(compare("price")) .slice(0, 3) .map((i) => i["alias"]); // 返回 ["apple", "orange", "banana"] ``` ## 随机字符串 ```javascript const getRandomRangeNum = (len = 32) => { // 略去不宜辨识字符 let dictionary = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz"; let maxPos = dictionary.length; let res = ""; for (let i = 0; i < len; i++) { res += dictionary.charAt(Math.floor(Math.random() * maxPos)); } return res; }; ``` ## 类型检测 Q:使用`typeof foo === "object"`检测`foo`是否为对象有什么缺点?如何避免? A:用 `typeof` 是否能准确判断一个对象变量,答案是否定的,`null` 的结果也是 `object`,`Array` 的结果也是 `object`,有时候我们需要的是 "纯粹" 的 `object` 对象 ```js Object.prototype.toString.call(obj) === "[object Object]"; ``` ## 倒计时 ```javascript class Countdown { constructor(startNum, endNum, interval) { [this.startNum, this.endNum, this.interval] = [startNum, endNum, interval]; } execute() { var timer = setTimeout(() => { if (this.startNum >= this.endNum) { console.log(this.startNum); this.startNum -= 1; this.execute(); } else { clearTimeout(timer); } }, this.interval); } } // 实例化调用 var countdown = new Countdown(5, 0, 1000).execute(); ``` ## 范围随机数 ```javascript // 能取到 min,取不到 max function getRandomRangeNum(min, max) { return min + Math.floor(Math.random() * (max - min)); } ``` ## 获取当前月的天数 ```javascript const getCurMonthDays = new Date( new Date().getFullYear(), new Date().getMonth() + 1, 0 ).getDate(); ``` # 对象小操作 ## 去虚假值 ```js let arr4 = ["小明", "小蓝", "", false, " ", undefined, null, 0, NaN, true]; console.log(arr4.filter(Boolean)); // => ['小明', '小蓝', ' ', true] ``` ## 头尾插入 效率比 `unshift()` 高 ```js let arr = [1, 2, 3]; // 头插入 ["haha"].concat(arr); // 尾插入 arr.concat(["haha"]); ``` ## 删除属性 ```js function deleteA(obj) { delete obj.A; return obj; } // 使用解构赋值 const deleteA = ({ A, ...rest } = {}) => rest; ``` # 生产、加工、消费分离 - 从接口拿数据到视图 fetch api - 加工 computed - 消费 v-for # 元数据 ```js import "reflect-metadata"; // npm install reflect-metadata function Role(name: string): ClassDecorator { return (target) => { Reflect.defineMetadata("role", name, target); }; } @Role("admin") class Post {} const metadata = Reflect.getMetadata("role", Post); Reflect.set(Post, "role2", metadata); console.log(Reflect.get(Post, "role2")); // admin ``` # 防抖与节流 在页面上监听诸如`scroll`(页面滚动),`mousemove`(鼠标移动) ,`keydown`, `keyup`, `keypress`(按下键盘)等等一系列事件的时候,我们并不希望频繁的触发这类监听,尤其当请求非常消耗资源时,这种操作会导致服务器性能急剧下降。 ## Debounce 把触发非常频繁的事件合并成一次延迟执行,如果对监听函数使用 100ms 的容忍时间,那么时间在第 3.1s 的时候执行 ```javascript // 默认延时100ms function debounce(func, dealy = 100) { let timer; return function () { // 暂存this和参数 let _this = this; let args = arguments; // 清除定时器,确保不执行func clearTimeout(timer); timer = setTimeout(function () { func.apply(_this, args); }, dealy); }; } // 执行函数 function handler() { console.log(`delay 100ms ,then handle`); } // dom添加监听 document .querySelector("#someNode") .addEventListener("scroll", debounce(handler)); ``` ## Throttle 固定函数执行的速率,即所谓的“节流”。设置一个阀值,在阀值内,把触发的事件合并成一次执行;当到达阀值,必定执行一次事件。 ```javascript function throttle(func, delay) { let statTime = 0; return function () { let currentTime = +new Date(); if (currentTime - statTime > delay) { func.apply(this, arguments); statTime = currentTime; } }; } // 执行函数 function resizeHandler() { console.log(`resize`); } // window添加监听 window.onresize = throttle(resizeHandler, 300); ``` # this 指向 ## 全局环境 全局环境下,this 始终指向全局对象(window),无论是否严格模式 ```javascript console.log(this === window); // true this.a = 37; console.log(window.a); // 37 ``` ## 函数上下文调用 - 非严格模式 没有被上一级的对象所调用, this 默认指向全局对象 window ```javascript function f1() { return this; } f1() === window; // true ``` - 严格模式 this 指向 undefined ```javascript function f2() { "use strict"; // 这里是严格模式 return this; } f2() === undefined; // true ``` ## 箭头函数 > 箭头函数中,call()、apply()、bind()方法无效 在全局代码中,箭头函数被设置为全局对象,总之箭头函数不改变 this 指向 ```javascript var globalObject = this; var foo = () => this; console.log(foo() === globalObject); // true ``` 箭头函数作为对象的方法使用,指向全局 window 对象 ```javascript var obj = { i: 10, b: () => console.log(this.i, this), c: function () { console.log(this.i, this); }, }; obj.b(); // undefined window{...} obj.c(); // 10 Object {...} ``` 箭头函数可以让 this 指向固化,这种特性很有利于封装回调函数 ```javascript // 总是指向 handler 对象。如果不使用箭头函数则指向全局 document 对象 var handler = { id: "123456", init: function () { document.addEventListener( "click", (event) => this.doSomething(event.type), false ); }, doSomething: function (type) { console.log("Handling " + type + " for " + this.id); }, }; ``` # call, apply, bind 与 es6 js 的函数继承于`Function.prototype`对象,因此每个函数都会有 apply、call、bind 方法 > call 和 apply 的作用,完全一样,唯一的区别就是在参数上面。 `call, apply, bind`改变函数中 `this 指向` 的三兄弟,把`this`绑定到第一个参数对象上 ```javascript function displayHobbies(...hobbies) { console.log(`${this.name} likes ${hobbies.join(", ")}.`); } // 下面两个等价 displayHobbies.call({ name: "Bob" }, "swimming", "basketball", "anime"); // Bob likes swimming, basketball, anime. displayHobbies.apply({ name: "Bob" }, ["swimming", "basketball", "anime"]); // Bob likes swimming, basketball, anime. ``` `bind`返回的是一个函数,需要手动执行 ```js var p1 = { name: "张三", age: 12, func: function () { console.log(`姓名:${this.name},年龄:${this.age}`); }, }; var p2 = { name: "李四", age: 15, }; p1.func.bind(p2)(); //姓名:李四,年龄:15 ``` # for 循环优化 ```javascript // 每次都要计算array.length for (let i = 0; i < array.length; i++) { console.log(i); } // 使用leng缓存array长度 for (let i = 0, length = array.length; i < length; i++) { console.log(i); } ``` # 数组 ## 扁平化去重升序排列 ```javascript let arr = [[1, 2, 2], [3, 4, 5, 5], [6, 7, 8, 9, [11, 12, [12, 13, [14]]]], 10]; arr.flat(Infinity); // [1, 2, 2, 3, 4, 5, 5, 6, 7, 8, 9, 11, 12, 12, 13, 14, 10] let result = Array.from(new Set(arr.flat(Infinity)).sort((a, b) => a - b)); ``` # 前端页面埋点 - 1x1.gif 通常用在,统计页面点击,曝光,停留时间,签发……等场景 - 比 PNG/JPG 体积小 - 天然跨域 ```html ``` # 事件委托 利用冒泡原理,委托父元素执行 ```html ``` ```javascript document.querySelector("ul").onclick = (event) => { let target = event.target; if (target.nodeName === "LI") { console.log(target.innerHTML); } }; ``` # 构造函数 + 原型模式 ```javascript function Person(name, age, job) { this.name = name; this.age = age; this.job = job; } Person.prototype.say = function (text) { console.log(`${this.name}say:${text}`); }; ``` # 剩余参数...args 剩余参数`args`数个数组,`...`解构符 ```javascript function fun1(param, ...args) { alert(args.length); } ``` # 跨页面通信 - cookie - web worker - localstorage # iframe 跨域通信和不跨域通信 ## 不跨域 ```javascript // fatherSay是父页面全局方法 window.parent.fatherSay(); // 父页面Dom window.parent.document.getElementById("元素id"); // 副业页面获取frameID为`iframe_ID`的子页面的Dom window.frames["iframe_ID"].document.getElementById("元素id"); ``` ## 跨域 postMessage 子页面 ```javascript window.parent.postMessage("hello", "http://127.0.0.1:8089"); ``` 父页面接受 ```javascript window.addEventListener("message", function (event) { alert(123); }); ``` # 对象类型判断 ## 数组 ```javascript let arr = []; arr instanceof Array; // true Array.isArray(arr); // true Object.prototype.toString.call(arr); // "[object Array]" ``` # js 单线程,如何异步 - 主线程 执行 js 中所有的代码。 - 主线程 在执行过程中发现了需要异步的任务任务后扔给浏览器(浏览器创建多个线程执行,顺便创造一个`回调队列`。 - 主线程 已经执行完毕所有同步代码。监听`回调队列`一旦 浏览器 中某个线程任务完成将会改变回调函数的状态。主线程查看到某个函数的状态为已完成,就会执行该`回调队列`中对应的回调函数。 # 移动端最小触控区域 苹果推荐是 44pt x 44pt 「具体看 WWDC 14」,通过`padding`、`margin`、`height`等方式进行点击区域扩展 # js 精度问题 常用类库:`Math.js`、`Big.js`、`decimal.js` # 冻结 Object. freeze() `const`生命的简单变量不可修改,但是复杂对象可以被修改,`Object. freeze()`:可以冻结对象 - 不能添加新属性 - 不能删除已有属性 - 不能修改已有属性的可枚举性、可配置性、可写性 - 不能修改已有属性的值 - 不能修改原型 浅冻结 ```javascript const obj1 = { internal: {}, }; Object.freeze(obj1); obj1.internal.a = "aValue"; console.log(obj1.internal.a); // aValue ``` 递归冻结 ```javascript function deepFreeze(obj) { // 获取定义在obj上的属性名 var propNames = Object.getOwnPropertyNames(obj); // 在冻结自身之前冻结属性 propNames.forEach(function (name) { var prop = obj[name]; // 如果prop是个对象,冻结它 if (typeof prop == "object" && prop !== null) deepFreeze(prop); }); return Object.freeze(obj); } ``` # Reflect ## Reflect.get(target, propertyKey, value[receiver]) 获取对象身上某个属性的值,类似于 target[name]。 ## Reflect.set(target, propertyKey, value[receiver]) 将值分配给属性的函数。返回一个 Boolean,如果更新成功,则返回 true。 ## Reflect.has(target, propertyKey) 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。 # if else 优化 ## 表驱动编程 空间换时间,设置`obj={key:value}`,通过`obj[key]`取值 ```javascript calculateGrade(score){ const table = { 100: 'A', 90: 'A', 80: 'B', 70: 'C', 60: 'D', others: 'E' } return table[Math.floor(score/10)*10] || table['others'] } ``` ## 短路运算 react 没有`v-if`,运用比较频繁 ```js // 函数组件 const Home = () => { return
home
; }; { true && ; } ``` # 使用有意义且易读的变量名 ```js 👎 const yyyymmdstr = moment().format("YYYY/MM/DD"); 👍 const currentDate = moment().format("YYYY/MM/DD"); ``` # 使用有意义的变量代替数组下标 ```js 👎 const address = "One Infinite Loop, Cupertino 95014"; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; saveCityZipCode( address.match(cityZipCodeRegex)[1], address.match(cityZipCodeRegex)[2] ); 👍 const address = "One Infinite Loop, Cupertino 95014"; const cityZipCodeRegex = /^[^,\\]+[,\\\s]+(.+?)\s*(\d{5})?$/; const [_, city, zipCode] = address.match(cityZipCodeRegex) || []; saveCityZipCode(city, zipCode); ``` # 变量名要简洁 ```js 👎 const Car = { carMake: "Honda", carModel: "Accord", carColor: "Blue" }; function paintCar(car, color) { car.carColor = color; } 👍 const Car = { make: "Honda", model: "Accord", color: "Blue" }; function paintCar(car, color) { car.color = color; } ``` # 消除魔术字符串 ```js 👎 setTimeout(blastOff, 86400000); 👍 const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000; setTimeout(blastOff, MILLISECONDS_PER_DAY); ``` # 使用默认参数替代短路运算符 ```js 👎 function createMicrobrewery(name) { const breweryName = name || "Hipster Brew Co."; // ... } 👍 function createMicrobrewery(name = "Hipster Brew Co.") { // ... } ``` # 一个函数 ```js 👎 function emailClients(clients) { clients.forEach(client => { const clientRecord = database.lookup(client); if (clientRecord.isActive()) { email(client); } }); } 👍 function emailActiveClients(clients) { clients.filter(isActiveClient).forEach(email); } function isActiveClient(client) { const clientRecord = database.lookup(client); return clientRecord.isActive(); } ---------------------分割线----------------------- 👎 function createFile(name, temp) { if (temp) { fs.create(`./temp/${name}`); } else { fs.create(name); } } 👍 function createFile(name) { fs.create(name); } function createTempFile(name) { createFile(`./temp/${name}`); } ``` # 函数参数不多于 2 个,如果有很多参数就利用 object 传递,并使用解构 ```js 👎 function createMenu(title, body, buttonText, cancellable) { // ... } createMenu("Foo", "Bar", "Baz", true); 👍 function createMenu({ title, body, buttonText, cancellable }) { // ... } createMenu({ title: "Foo", body: "Bar", buttonText: "Baz", cancellable: true }); ``` # 函数名应该直接反映函数的作用 ```js 👎 function addToDate(date, month) { // ... } const date = new Date(); // It's hard to tell from the function name what is added addToDate(date, 1); 👍 function addMonthToDate(month, date) { // ... } const date = new Date(); addMonthToDate(1, date); ``` # 尽量使用纯函数 ```js 👎 const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; let totalOutput = 0; for (let i = 0; i < programmerOutput.length; i++) { totalOutput += programmerOutput[i].linesOfCode; } 👍 const programmerOutput = [ { name: "Uncle Bobby", linesOfCode: 500 }, { name: "Suzie Q", linesOfCode: 1500 }, { name: "Jimmy Gosling", linesOfCode: 150 }, { name: "Gracie Hopper", linesOfCode: 1000 } ]; const totalOutput = programmerOutput.reduce( (totalLines, output) => totalLines + output.linesOfCode, 0 ); ``` # 不要过度优化 ```js 👎 // 现代浏览器对于迭代器做了内部优化 for (let i = 0, len = list.length; i < len; i++) { // ... } 👍 for (let i = 0; i < list.length; i++) { // ... } ``` # 关于模块化 ## 无模块化 > 污染全局作用域、维护成本高、依赖关系不明显 script 标签引入 js 文件,相互罗列,但是被依赖的放在前面,否则使用就会报错。如下: ```js ``` ## CommonJS 规范(同步) 该规范最初是用在服务器端的 node 的,它有四个重要的环境变量为模块化的实现提供支持:module、exports、require、global。实际使用时,用 module.exports 定义当前模块对外输出的接口(不推荐直接用 exports),用 require 加载模块(同步) > module.exports 本身就是一个对象 ```js module.exports = { foo: "bar" }; //true module.exports.foo = "bar"; //true。 ``` CommonJS 用同步的方式加载模块。在服务端,模块文件都存在本地磁盘,读取非常快,所以这样做不会有问题。但是在浏览器端,限于网络原因,CommonJS 不适合浏览器端模块加载,更合理的方案是使用异步加载,比如下边 AMD 规范。 ## AMD 规范(RequireJS) 承接上文,AMD 规范则是非同步加载模块,允许指定回调函数,AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。 - `require([module], callback)`:加载模块 - `define(id, [depends], callback)`:定义模块 - `require.config()`:配置路径、依赖关系 ## CMD 规范(SeaJS) CMD 是 在推广过程中对模块定义的规范化产出 ## ES6 import/export 通过 babel 将不被支持的 import 编译为当前受到广泛支持的 AMD 规范 # AMD模块化实现 ## define 函数 用来声明模块名`module`,依赖数组`deps`,以及模块的作用`callback` ```js var module = (function () { var stack = {}; //模块存储栈,存的是模块执行后的结果 function define(module, deps, callback) { stack[module] = callback.apply(null, deps); // 压栈 console.log(stack); // 查看栈存储的模块 } return { define: define }; })(); ``` 调用`define`试试,发现 `calc`模块被压入了`stack 模块栈中` ```js module.define("calc", [], function () { return { first: function (arr) { return arr[0]; }, }; }); // {calc: { first: ƒ }} ``` ## deps 依赖(导入) 对上面的 `define`函数稍加改造,这一步过程的本质,就是对 deps ```js var module = (function (window) { var stack = {}; //模块存储栈, function define(module, deps, callback) { // 把 define 函数依赖数组中的模块从 stack 模块中拿出 deps.map(function (mod, index) { deps[index] = stack[mod]; // 赋值给当前模块的 deps }); stack[module] = callback.apply(null, deps); console.log(stack); // 查看栈存储的模块 } return { define: define }; })(window); module.define("calc", [], function () { return { first: function (arr) { return arr[0]; }, }; }); module.define("number", ["calc"], function (calc) { return { res: calc.first([4, 3, 2, 1]), //4 }; }); // 此时 stack 打印的结果为 //> calc: {first: ƒ} //> number: {res: 4} ``` > 从本质上看,define 函数的第二个参数 deps 数组,相当于 import 导入,并且如果第三个参数 callback 采用 `return { }`,也就相当于 export 导出 ## 老生常谈的指针 说到底,`stack`中 `a模块` export 是一个指针,`{a:value}`(内存地址),所以,`b 模块`会改变`a.a`的值,这点和 `cmd`不同 ```js module.define("a", [], function () { return { a: 1, }; }); module.define("b", ["a"], function (a) { a.a = 2; }); module.define("c", ["a"], function (a) { console.log(a.a); // 2 }); ``` # 缘起 Object.defineProperty() 给`目标对象`上定义一个新属性,或者修改`目标对象`属性,并且返回新对象 # 上帝的钥匙 get & set 属性的`getter`函数,如果没有 `getter`则尾 `undefined`。当访问该属性时,会调用此函数. ```js let obj = {}; Object.defineProperty(obj, "a", { get() { return 7; }, set(val) { console.log(`FAILED!改变 a 属性,新值为:${val},但是被重写 set 劫持了`); }, }); console.log(obj.a); obj.a = 4; ``` > 上帝的钥匙被找到了 劫持!劫持!还是 TMD 劫持! ```js let obj = {}; let tempValue = 0; Object.defineProperty(obj, "a", { get() { return tempValue; }, set(newValue) { tempValue = newValue; }, }); console.log(obj.a); // 0 obj.a = 4; console.log(obj.a); // 4 ``` # 封装 defineReactive(obj, prop, val) > 这里 defineReactive 第三个参数 val 替代了上一步中全局变量`tempValue`,对于 get()、set()来说,访问到了其他函数内部的变量,所以形成了闭包 ```js function defineReactive(obj, prop, val) { Object.defineProperty(obj, prop, { get() { console.log(`劫持,你访问了${prop}属性`); return val; }, set(newValue) { if (val === newValue) return; console.log(`劫持,你改变了${prop}属性`); val = newValue; }, }); } let obj = {}; defineReactive(obj, "a", 4); console.log(obj.a); // 劫持,你访问了a属性 4 obj.a = 7; // 劫持,你改变了a属性 console.log(obj.a); // 劫持,你访问了a属性 7 ``` # 递归侦测 ```js let obj = { a: { b: { c: {} } } }; defineReactive(obj, "a", 4); ``` > 如何自动让`obj`对象的全部属性都`reactive`呢? ```js let obj = { a: { b: { c: 5, }, }, d: 4, }; ``` 定义个方法`observe`,递归 `obj` 的每一层的每个 `prop`,检测是否有`__ob__`,如果没有,`defineReactive`,并且挂一个`Observer`实例在这个`props`上,例如: `Observer对象` ```js class Observer { constructor(value) { def(value, "__ob__", this, false); // 不可枚举,不能给__ob__添加__ob__ this.walk(value); } // 遍历每一个 prop的 value walk(value) { for (let prop in value) { defineReactive(value, prop); } } } ``` `defineReactive.js` ```js export default function defineReactive(obj, prop, val) { if (arguments.length === 2) val = obj[prop]; // 如果2个参数 let childNode = observe(val); Object.defineProperty(obj, prop, { enumerable: true, configurable: true, get() { console.log(`你访问了${prop}属性`); return val; }, set(newValue) { if (val === newValue) return; console.log(`你改变了${prop}属性`); val = newValue; childNode = observe(newValue); }, }); } ``` `observe.js` ```js /** * 检测 obj 身上有没有 __ob__(Observer 实例) * @param {*} value * @returns */ export default function observe(value) { if (typeof value != "object") return; let ob; //! 用__ob__是为了属性不重名,被覆盖 if (typeof value.__ob__ != "undefined") { ob = value.__ob__; } else { ob = new Observer(value); } return ob; } ```