Hi Mozzie 显示 / 隐藏 左侧导航 [
⌘ K
偏好 ⌘ p
显示 / 隐藏 文章目录 ]

# 码场悟道

上次更新: 2024-03-15 23:14:48

# 模板引擎

严格的模板引擎的定义，输入模板字符串 + 数据，得到渲染过的字符串。实现上，从正则替换到拼 function 字符串到正经的 AST 解析各种各样，但从定义上来说都是差不多的。字符串渲染的性能其实也就在后端比较有意义，毕竟每一次渲染都是在消耗服务器资源，但在前端，用户只有一个，几十毫秒的渲染时间跟请求延迟比起来根本不算瓶颈。倒是前端的后续更新是字符串模板引擎的软肋，因为用渲染出来的字符串整个替换 innerHTML 是一个效率很低的更新方式。所以这样的模板引擎如今在纯前端情境下已经不再是好的选择，意义更多是在于方便前后端共用模板。

# 古老数据渲染 vm 的方式

这种写法，弊端太多了，玩具车

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Document</title>
      </head>
      <body>
        <ul id="app"></ul>
      </body>
      <script>
        var arr = [
          { name: "小明", age: 11, sex: "男" },
          { name: "小红", age: 22, sex: "女" },
        ];
        var listDOM = document.getElementById("app");
        arr.forEach(function (item) {
          let _li = document.createElement("li");
          _li.innerText = item.name;
          listDOM.appendChild(_li);
        });
      </script>
    </html>

# mustache 原理

- 1、先把模板字符串编译成 tokens(代号)
- 2、根据 tokens，结合数据渲染成 dom

> 本质上，tokens 是一个 js 嵌套数组没事模板字符串 js 的表示，他是`抽象语法树`，`虚拟节点`的开山鼻祖

假设有这么一个模板字符串

    <h1>我买了一个{{thing}},好{{mood}}啊</h1>

会编译成 tokens，如下：

    // 这里面每一个数组行都是一个 token，组起来就是 tokens
    // html 标签也会被看成纯文本
    [
      ["text", "<h1>我买了一个"],
      ["name", "thing"],
      ["text", "好"],
      ["name", "mood"],
      ["text", "啊</h1>"],
    ];

当模板存在循环式，带层级嵌套，如下：

    <div>
      <ul>
        {{#arr}}
        <li>{{.}}</li>
        {{/arr}}
      </ul>
    </div>

会被编译成

    [
      ["text", "<div><ul>"],
      [
        "#",
        "arr",
        [
          ["text", "li"],
          ["name", "."],
          ["text", "</li>"],
        ],
      ],
      ["text", "</ul></div>"],
    ];

如果是双重循环，带层级嵌套继续加一层，例如：

    <div>
      <ol>
        {{#students}}
        <li>
          学生{{item.name}}的爱好是
          <ol>
            {{#item.hobbies}}
            <li>{{.}}</li>
            {{/#item.hobbies}}
          </ol>
        </li>
        {{/#students}}
      </ol>
    </div>

会被编译成

    [
      ["text", "<div><ol>"],
      [
        "#",
        "students",
        null,
        null,
        [["text", "<li>学生"], ["name", "name"], ["text", "的爱好是<ol>"], ["#", "hobbies", null, null], [
            ['text','<li>'],
            ['name','.'],
            ['text','</li>']
        ]],
        ['text','<ol></li>'],
      ]],
      ['text','</ol></div>']
      ],
    ];

> 在`mustache.js`中完成上述这一过程的函数`parseTemplate`，可以去找源代码看

## tokens 生成算法

用简单的模板字符串举例：

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

有一个指针往右遍历，从`我`开始，遍历到`啊`结束，如下：

    /**
     *
     * 我买了一个{{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` 判断`

    // 标记为 text 放到 tokens 中
    token.push(["text", "我买了一个"]); // tokens: [['text', '我买了一个']]

结束，此时指针位置 = 4 - Step4: 指针右移 2 个长度，跳过`{{`，暂存此时`pos_last = 6` 此时，右边字符串(尾字符串)`thing}},好{{mood}}啊

右移 5 个长度，识别`模板内部数据对象`：

    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 个长度

    substring(post_last, 13 + 2); // ,好  2个长度
    // 标记为 text 放到 tokens 中
    token.push(["text", ",好"]); // tokens: [['text', '我买了一个']], ['name', 'thing'],['text', ',好' ]]

> 第二次，遇到 `{{` 剩下循环执行就行了，这个过程，我们可以称作`扫描 Scan` ## 扫描器 Scanner 新建一个 `Scanner.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`

>     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`

    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", "↵    "]

## 栈队列算法

上一步最后的输出，只有单层嵌套，如果是两层嵌套怎么办？

例如模板语法如下：

    var template = `
          哈哈哈
            {{#students}}
                {{#stu}}
                  {{stu.name}}买了一个  {{ thing  }},好{{mood}}啊{{a}}
                {{/stu}}
              {{item.name}}
            {{/students}}
        `;

经过`Parser`处理得到：

    /**
     *
     * ["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`都是`#`标记，我们需要利用算法处理他们的嵌套结构，处理成大约如下这样的结构：

    /**
     *
     *  ["text", "↵      哈哈哈↵        "]
     *  Array(3)
     *  "#"
     *  "students"
     *  Array(5)
     *      ["text", "↵            "]
     *      ["#", "stu", Array(9)]
     *      ["text", "↵          "]
     *      ["name", "item.name"]
     *      ["text", "↵        "]
     *  ["text", "↵    "]
     *
     * /

# 常用工具类

## 递归

    /**
     * {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短码

用于连接分享

    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

    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);
      }
    }

调用

    // 省略import
    const eventBus = new EventBus();
    eventBus.emit("login", [{ a: 1, d: 2 }]);
    eventBus.on(123, (d) => console.log(d)); // [{...}]

## 判断对象是否有某个 key

    let obj = { alias: "es6" };
    "alias" in obj; // true
    Reflect.has(obj, "alias"); // true

## 浏览器

## 版本信息

    window.navigator.userAgent;

## 兼容事件绑定

    /*
    兼容低版本IE，ele为需要绑定事件的元素，
    eventName为事件名（保持addEventListener语法，去掉on），fun为事件响应函数
    */

    function addEvent(ele, eventName, fun) {
      ele.addEventListener
        ? ele.addEventListener(eventName, fun, false)
        : ele.attachEvent("on" + eventNme, fun);
    }

## 数组对象

## reduce

    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

    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"]

## 随机字符串

    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` 对象

    Object.prototype.toString.call(obj) === "[object Object]";

## 倒计时

    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();

## 范围随机数

    // 能取到 min，取不到 max
    function getRandomRangeNum(min, max) {
      return min + Math.floor(Math.random() * (max - min));
    }

## 获取当前月的天数

    const getCurMonthDays = new Date(
      new Date().getFullYear(),
      new Date().getMonth() + 1,
      0
    ).getDate();

# 对象小操作

## 去虚假值

    let arr4 = ["小明", "小蓝", "", false, " ", undefined, null, 0, NaN, true];
    console.log(arr4.filter(Boolean)); // => ['小明', '小蓝', ' ', true]

## 头尾插入

效率比 `unshift()` 高

    let arr = [1, 2, 3];
    // 头插入
    ["haha"].concat(arr);
    // 尾插入
    arr.concat(["haha"]);

## 删除属性

    function deleteA(obj) {
      delete obj.A;
      return obj;
    }

    // 使用解构赋值
    const deleteA = ({ A, ...rest } = {}) => rest;

# 生产、加工、消费分离

- 从接口拿数据到视图 fetch api
- 加工 computed
- 消费 v-for

# 元数据

    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 的时候执行

    // 默认延时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

固定函数执行的速率，即所谓的“节流”。设置一个阀值，在阀值内，把触发的事件合并成一次执行；当到达阀值，必定执行一次事件。

    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），无论是否严格模式

    console.log(this === window); // true
    this.a = 37;
    console.log(window.a); // 37

## 函数上下文调用

- 非严格模式

没有被上一级的对象所调用, this 默认指向全局对象 window

    function f1() {
      return this;
    }
    f1() === window; // true

- 严格模式

this 指向 undefined

    function f2() {
      "use strict"; // 这里是严格模式
      return this;
    }
    f2() === undefined; // true

## 箭头函数

> 箭头函数中，call()、apply()、bind()方法无效

在全局代码中，箭头函数被设置为全局对象，总之箭头函数不改变 this 指向

    var globalObject = this;
    var foo = () => this;
    console.log(foo() === globalObject); // true

箭头函数作为对象的方法使用，指向全局 window 对象

    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 指向固化，这种特性很有利于封装回调函数

    // 总是指向 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`绑定到第一个参数对象上

    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`返回的是一个函数，需要手动执行

    var p1 = {
      name: "张三",
      age: 12,
      func: function () {
        console.log(`姓名:${this.name},年龄:${this.age}`);
      },
    };

    var p2 = {
      name: "李四",
      age: 15,
    };

    p1.func.bind(p2)(); //姓名:李四,年龄:15

# for 循环优化

    // 每次都要计算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);
    }

# 数组

## 扁平化去重升序排列

    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 体积小
- 天然跨域

    <button onClick="countClick()">haorooms</button>
    <script>
      function countClick() {
        new Image().src = `./haorooms.gif?${key}=${value}&${Math.random()} `;
      }
    </script>

# 事件委托

利用冒泡原理，委托父元素执行

    <ul>
      <li>苹果</li>
      <li>香蕉</li>
      <li>凤梨</li>
    </ul>

    document.querySelector("ul").onclick = (event) => {
      let target = event.target;
      if (target.nodeName === "LI") {
        console.log(target.innerHTML);
      }
    };

# 构造函数 + 原型模式

    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`数个数组，`...`解构符

    function fun1(param, ...args) {
      alert(args.length);
    }

# 跨页面通信

- cookie
- web worker
- localstorage

# iframe 跨域通信和不跨域通信

## 不跨域

    // fatherSay是父页面全局方法
    window.parent.fatherSay();
    // 父页面Dom
    window.parent.document.getElementById("元素id");
    // 副业页面获取frameID为`iframe_ID`的子页面的Dom
    window.frames["iframe_ID"].document.getElementById("元素id");

## 跨域 postMessage

子页面

    window.parent.postMessage("hello", "http://127.0.0.1:8089");

父页面接受

    window.addEventListener("message", function (event) {
      alert(123);
    });

# 对象类型判断

## 数组

    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()`：可以冻结对象

- 不能添加新属性
- 不能删除已有属性
- 不能修改已有属性的可枚举性、可配置性、可写性
- 不能修改已有属性的值
- 不能修改原型

浅冻结

    const obj1 = {
      internal: {},
    };

    Object.freeze(obj1);
    obj1.internal.a = "aValue";
    console.log(obj1.internal.a); // aValue

递归冻结

    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]`取值

    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`，运用比较频繁

    // 函数组件
    const Home = () => {
      return <div>home</div>;
    };
    {
      true && <Home />;
    }

# 使用有意义且易读的变量名

    👎 const yyyymmdstr = moment().format("YYYY/MM/DD");

    👍 const currentDate = moment().format("YYYY/MM/DD");

# 使用有意义的变量代替数组下标

    👎
    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);

# 变量名要简洁

    👎
    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;
    }

# 消除魔术字符串

    👎 setTimeout(blastOff, 86400000);

    👍 const MILLISECONDS_PER_DAY = 60 * 60 * 24 * 1000; //86400000;
    setTimeout(blastOff, MILLISECONDS_PER_DAY);

# 使用默认参数替代短路运算符

    👎
    function createMicrobrewery(name) {
      const breweryName = name || "Hipster Brew Co.";
      // ...
    }

    👍
    function createMicrobrewery(name = "Hipster Brew Co.") {
      // ...
    }

# 一个函数

    👎
    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 传递，并使用解构

    👎
    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
    });

# 函数名应该直接反映函数的作用

    👎
    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);

# 尽量使用纯函数

    👎
    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
    );

# 不要过度优化

    👎
    // 现代浏览器对于迭代器做了内部优化
    for (let i = 0, len = list.length; i < len; i++) {
      // ...
    }

    👍
    for (let i = 0; i < list.length; i++) {
      // ...
    }

# 关于模块化

## 无模块化

> 污染全局作用域、维护成本高、依赖关系不明显

script 标签引入 js 文件，相互罗列，但是被依赖的放在前面，否则使用就会报错。如下：

    <script src="jquery.js"></script>
    <script src="main.js"></script>
    <script src="other1.js"></script>
    <script src="other2.js"></script>
    <script src="other3.js"></script>

## CommonJS 规范(同步)

该规范最初是用在服务器端的 node 的，它有四个重要的环境变量为模块化的实现提供支持：module、exports、require、global。实际使用时，用 module.exports 定义当前模块对外输出的接口（不推荐直接用 exports），用 require 加载模块(同步)

> module.exports 本身就是一个对象

    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`

    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 模块栈中`

    module.define("calc", [], function () {
      return {
        first: function (arr) {
          return arr[0];
        },
      };
    });
    // {calc: { first: ƒ }}

## deps 依赖(导入)

对上面的 `define`函数稍加改造，这一步过程的本质，就是对 deps

    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`不同

    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`。当访问该属性时，会调用此函数.

    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 劫持！

    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()来说，访问到了其他函数内部的变量，所以形成了闭包

    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

# 递归侦测

    let obj = { a: { b: { c: {} } } };
    defineReactive(obj, "a", 4);

> 如何自动让`obj`对象的全部属性都`reactive`呢？

    let obj = {
      a: {
        b: {
          c: 5,
        },
      },
      d: 4,
    };

定义个方法`observe`，递归 `obj` 的每一层的每个 `prop`，检测是否有`__ob__`，如果没有，`defineReactive`，并且挂一个`Observer`实例在这个`props`上，例如：

`Observer对象`

    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`

    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`

    /**
     * 检测 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;
    }

重拾纯粹的写作

目录

隐藏目录 ]