blog-hexo/source/_posts/front-end/码场悟道.md
2023-12-26 12:54:52 +08:00

36 KiB
Raw Blame History

title categories status abbrlink
码场悟道
CS
doing 47478

模板引擎

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

兼容事件绑定

/*
兼容低版本IEele为需要绑定事件的元素
eventName为事件名保持addEventListener语法去掉onfun为事件响应函数
*/

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是否为对象有什么缺点?如何避免?

Atypeof 是否能准确判断一个对象变量,答案是否定的,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(鼠标移动) 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」,通过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;
}