模板引擎

严格的模板引擎的定义,输入模板字符串 + 数据,得到渲染过的字符串。实现上,从正则替换到拼 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", "↵    "]
 *
 * /

此时studentsstu都是#标记,我们需要利用算法处理他们的嵌套结构,处理成大约如下这样的结构:

/**
 *
 *  ["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 的结果也是 objectArray 的结果也是 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(鼠标移动) ,keydownkeyupkeypress(按下键盘)等等一系列事件的时候,我们并不希望频繁的触发这类监听,尤其当请求非常消耗资源时,这种操作会导致服务器性能急剧下降。

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」,通过paddingmarginheight等方式进行点击区域扩展

js 精度问题

常用类库:Math.jsBig.jsdecimal.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 导出

老生常谈的指针

说到底,stacka模块 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;
}

前端微应用 single-spa

随着Node革新的这一波工程化潮流,前端项目复杂度指数级增加,某个项目,可能由团队A负责,但当后续迭代,需要渐进增强,加入新的模块,团队A不足以支撑业务,需要引入新的开发团队B,为了维持生产环境业务的统一性和用户体验,新的模块也需要在统一的入口下,例如:

single-spa-1

传统的方案,肯定是iframe去做,但通常会遇到一些麻烦的事情,处理起来很不优雅,例如:

  • 显示区域受限制,比如子项目中显示弹窗蒙层时,蒙层只会覆盖 iframe 区域,无法覆盖整个页面,内容也无法真正居中。

  • 页面浏览记录无法自动被记录,刷新页面后 iframe 又自动回到首页。

  • 全局上下文完全隔离,变量不共享,页面间通信比较麻烦,比如子项目与主题框架、子项目之间通信等,只能采用 postMessage 方式。

  • 速度较慢,每次进入子应用时都要重建整个上下文。

单页面集成多个应用的架构诞生,例如阿里的qiankun.js

single-spa 够减少公司整体开发限制,团队 B 的技术选型完全可以不依赖团队 A

single-spa 实现微前端的整体流程

single-spa-2

  • 资源模块加载器:用来加载子项目初始化资源。我们将子项目的入口 js 构建成 umd 格式,然后使用模块加载器远程加载,通常会使用 SystemJs(不是必须)通用模块加载器来进行加载。

  • 子应用资源配置表:用来记录各个子应用的入口资源 url 信息,以便在切换不同子应用时使用模块加载器去远程加载。因为每次子应用更新后入口资源的 hash 通常会变化,所以需要服务端定时去更新该配置表,以便框架能及时加载子应用最新的资源。

注意:single-spa 本身是不支持子应用资源列表的,每个子应用只能将自己所有初始化资源打包到一个入口 js 中。如果子应用初始化资源有多个文件(可以通过 webpack-manifest-plugin 生成应用初始化资源清单),就需要按照上述方式来添加额外处理。

Catalog

  1. 1. 模板引擎
  2. 2. 古老数据渲染 vm 的方式
  3. 3. mustache 原理
    1. 3.1. tokens 生成算法
    2. 3.2. 栈队列算法
  4. 4. 常用工具类
    1. 4.1. 递归
    2. 4.2. 自增id短码
    3. 4.3. 手动实现 eventBus
    4. 4.4. 判断对象是否有某个 key
    5. 4.5. 浏览器
    6. 4.6. 版本信息
    7. 4.7. 兼容事件绑定
    8. 4.8. 数组对象
    9. 4.9. reduce
    10. 4.10. 对象内部根据 key 对 value 进行排序,取前 3
    11. 4.11. 随机字符串
    12. 4.12. 类型检测
    13. 4.13. 倒计时
    14. 4.14. 范围随机数
    15. 4.15. 获取当前月的天数
  5. 5. 对象小操作
    1. 5.1. 去虚假值
    2. 5.2. 头尾插入
    3. 5.3. 删除属性
  6. 6. 生产、加工、消费分离
  7. 7. 元数据
  8. 8. 防抖与节流
    1. 8.1. Debounce
    2. 8.2. Throttle
  9. 9. this 指向
    1. 9.1. 全局环境
    2. 9.2. 函数上下文调用
    3. 9.3. 箭头函数
  10. 10. call, apply, bind 与 es6
  11. 11. for 循环优化
  12. 12. 数组
    1. 12.1. 扁平化去重升序排列
  13. 13. 前端页面埋点 - 1x1.gif
  14. 14. 事件委托
  15. 15. 构造函数 + 原型模式
  16. 16. 剩余参数…args
  17. 17. 跨页面通信
  18. 18. iframe 跨域通信和不跨域通信
    1. 18.1. 不跨域
    2. 18.2. 跨域 postMessage
  19. 19. 对象类型判断
    1. 19.1. 数组
  20. 20. js 单线程,如何异步
  21. 21. 移动端最小触控区域
  22. 22. js 精度问题
  23. 23. 冻结 Object. freeze()
  24. 24. Reflect
    1. 24.1. Reflect.get(target, propertyKey, value[receiver])
    2. 24.2. Reflect.set(target, propertyKey, value[receiver])
    3. 24.3. Reflect.has(target, propertyKey)
  25. 25. if else 优化
    1. 25.1. 表驱动编程
    2. 25.2. 短路运算
  26. 26. 使用有意义且易读的变量名
  27. 27. 使用有意义的变量代替数组下标
  28. 28. 变量名要简洁
  29. 29. 消除魔术字符串
  30. 30. 使用默认参数替代短路运算符
  31. 31. 一个函数
  32. 32. 函数参数不多于 2 个,如果有很多参数就利用 object 传递,并使用解构
  33. 33. 函数名应该直接反映函数的作用
  34. 34. 尽量使用纯函数
  35. 35. 不要过度优化
  36. 36. 关于模块化
    1. 36.1. 无模块化
    2. 36.2. CommonJS 规范(同步)
    3. 36.3. AMD 规范(RequireJS)
    4. 36.4. CMD 规范(SeaJS)
    5. 36.5. ES6 import/export
  37. 37. AMD模块化实现
    1. 37.1. define 函数
    2. 37.2. deps 依赖(导入)
    3. 37.3. 老生常谈的指针
  38. 38. 缘起 Object.defineProperty()
  39. 39. 上帝的钥匙 get & set
  40. 40. 封装 defineReactive(obj, prop, val)
  41. 41. 递归侦测
  42. 42. 前端微应用 single-spa