# js 进阶

返回:大前端

避免重复造轮子,常用方法封装 实现前端开发的几个常用技巧
js-进阶 call 的使用及模拟实现 页面滚动相关技巧总结
JS 反爬:debugger 劝退爬虫 HTML 高级功能使用建议
前端 web 如何实现“签名手写”板 高阶组件

# 学习 Javascript 之节流和防抖

back

节流函数和防抖函数相信很多人都在日常业务开发中使用过,其实不管是节流函数还是防抖函数都是一种简单的高阶函数,他们都是通过将一个关键的外部变量保存在外层作用域,通过对这个变量的判断和操作来决定是否调用回调函数。

# 节流函数

函数节流让指函数有规律的进行调用,应用场景:

  • 游戏中发射子弹的频率(比如 1 秒发射一颗);
  • 滚动事件;

所谓节流即让回调函数在一定时间内只能调用一次,因此我们的节流函数需要的参数有两个:

  • 回调函数;
  • 延迟执行的时间;
/**
 * @param {function} fun 调用函数
 * @param {number} delay 延迟调用时间
 * @param {array} args 剩余参数
 */
const throttle = (fun, delay, ...args) => {
  let last = null;
  return (...rest) => {
    const now = +new Date();
    let _args = [...args, ...rest];
    if (now - last > delay) {
      fun.apply(null, _args);
      last = now;
    }
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

解释下上面的代码,我们通过在外层函数声明一个 last 变量,然后返回一个函数,在该函数中获取当前的时间,拼接外层函数的剩余参数和内层函数的参数,如果当前的时间戳和上一次调用的时间戳差大于延迟时间,那么就执行回调函数

var obj = { a: 1 };
var num = 0;
//实例
var throttleExample = throttle(
  function(...rest) {
    console.log(rest, this.a);
    num++;
  }.bind(obj),
  1000
);
//调用
throttleExample(num);
throttleExample(num);
throttleExample(num);
throttleExample(num);
throttleExample(num);
throttleExample(num);
throttleExample(num);

//VM3598:5 [0] 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

节流函数顾名思义就像是控制水流的函数,让水流匀速流动的函数。也就是说在一定时间内,函数不管调用多少次,实际只执行第一次调用。

/**
 * 节流,多次触发,间隔时间段执行,lodash中拷贝的源码
 * @param {Function} func
 * @param {Int} wait
 * @param {Object} options
 */
export function throttle(func, wait = 500, options) {
  //container.onmousemove = throttle(getUserAction, 1000);
  var timeout, context, args;
  var previous = 0;
  if (!options) options = { leading: false, trailing: true };

  var later = function() {
    previous = options.leading === false ? 0 : new Date().getTime();
    timeout = null;
    func.apply(context, args);
    if (!timeout) context = args = null;
  };

  var throttled = function() {
    var now = new Date().getTime();
    if (!previous && options.leading === false) previous = now;
    var remaining = wait - (now - previous);
    context = this;
    args = arguments;
    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func.apply(context, args);
      if (!timeout) context = args = null;
    } else if (!timeout && options.trailing !== false) {
      timeout = setTimeout(later, remaining);
    }
  };
  return throttled;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

# 防抖函数

函数防抖让函数在”调用’’之后的一段时间后生效,应用场景:

  • 输入框输入事件;
  • window.resize 事件,窗口大小调整事件;
  • 滚动事件;

和节流函数相似防抖函数同样需要两个参数:

回调函数;
延迟调用时间;

/**
 * @param {function} fun 调用函数
 * @param {number} delay 延迟调用时间
 * @param {array} args 剩余参数
 */
const debouce = (fun, delay, ...args) => {
  let timer = null;
  return (...rest) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fun.apply(null, [...args, ...rest]);
    }, delay);
  };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14

解释下上面代码,在外层函数声明一个 timer 变量,内层函数设置定时器,在 delay 时间后执行该函数,并将定时器返回的标志保存在 timer 里面,在此期间一旦有调用,就取消定时器,重新开始定时。

var obj = { a: 1 };
var num = 0;
//实例
var debouceExample = debouce(
  function(...rest) {
    console.log(rest, this.a);
    num++;
  }.bind(obj),
  1000
);
//调用
debouceExample(num);
debouceExample(num);
debouceExample(num);
1
2
3
4
5
6
7
8
9
10
11
12
13
14

如上代码调用三次 debouceExample 函数,1秒后打印一次[0] 1。防抖函数顾名思义防止函数抖动,在一定时间内不管调用多少次,执行的都会是最后一次的调用。而且最后一次的调用时间是在一定时间之后。

/**
 * lodash中源码
 * @param {*} func 要进行debouce的函数
 * @param {*} wait 等待时间,默认500ms
 * @param {*} immediate 是否立即执行
 */
export function debounce(func, wait = 500, immediate = false) {
  var timeout;
  return function() {
    var context = this;
    var args = arguments;

    if (timeout) clearTimeout(timeout);
    if (immediate) {
      // 如果已经执行过,不再执行
      var callNow = !timeout;
      timeout = setTimeout(function() {
        timeout = null;
      }, wait);
      if (callNow) func.apply(context, args);
    } else {
      timeout = setTimeout(function() {
        func.apply(context, args);
      }, wait);
    }
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

# 优雅的 js

返回顶部

# 2、多条件判断

  • 小白写法
    var Statistics = function(){
      console.log('执行')
      }
    switch (currentTab) {
      case 0:
        Statistics();           break;
      case 1:
        Statistics();           break;
      case 2:
        Statistics();         break;
      case 3:
        Statistics();         break;
1
2
3
4
5
6
7
8
9
10
11
12
  • 优雅写法
//将判断条件作为对象的属性名,将处理逻辑作为对象的属性值
var Statistics = function() {
  console.log("执行");
};
const comparativeTotles = new Map([
  [0, Statistics],
  [1, Statistics],
  [2, Statistics],
  [3, Statistics],
]);
let map = function(val) {
  return comparativeTotles.get(val);
};
let getMap = map(1); //如果查找不到返回undefined
if (!getMap) {
  console.log("查找不到");
} else {
  cnosole.log("执行操作");
  getMap();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • if else
/**
 * * 按钮点击事件
 * @param {number} status 活动状态:1开票中 2开票失败 3 开票成功 4 商品售罄 5 有库存未开团
 * @param {string} identity 身份标识:guest客态 master主态
 */
const onButtonClick = (status, identity) => {
  if (identity == "guest") {
    if (status == 1) {
      //函数处理
    } else if (status == 2) {
      //函数处理
    } else if (status == 3) {
      //函数处理
    } else if (status == 4) {
      //函数处理
    } else if (status == 5) {
      //函数处理
    } else {
      //函数处理
    }
  } else if (identity == "master") {
    if (status == 1) {
      //函数处理
    } else if (status == 2) {
      //函数处理
    } else if (status == 3) {
      //函数处理
    } else if (status == 4) {
      //函数处理
    } else if (status == 5) {
      //函数处理
    } else {
      //函数处理
    }
  }
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
  • 改完后
//利用数组循环的特性,符合条件的逻辑都会被执行,那就可以同时执行公共逻辑和单独逻辑。
const functionA = () => {
  /*do sth*/
};
// 单独业务逻辑
const functionB = () => {
  /*do sth*/
};
// 单独业务逻辑
const functionC = () => {
  /*send log*/
};
// 公共业务逻辑
const actions = new Map([
  [
    "guest_1",
    () => {
      functionA;
    },
  ],
  [
    "guest_2",
    () => {
      functionB;
    },
  ],
  [
    "guest_3",
    () => {
      functionC;
    },
  ],
  [
    "guest_4",
    () => {
      functionA;
    },
  ],
  [
    "default",
    () => {
      functionC;
    },
  ], //...
]);

/**
 * * 按钮点击事件
 * * @param {string} identity 身份标识:guest客态 master主态
 * * @param {number} status 活动状态:1开票中 2开票失败 3 开票成功 4 商品售罄 5 有库存未开团
 * */
const onButtonClick = (identity, status) => {
  let action = actions.get(`${identity}_${status}`) || actions.get("default");
  action.call(this);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55

# 一个合格的中级前端工程师需要掌握的 28 个 JavaScript 技巧

🔝🔝javascript

# 函数柯里化

🔝🔝28Skills

const display = (a, b, c, d, e, f, g, h) => [a, b, c, d, e, f, g, h];

/**
 * @description 函数柯里化(根据柯里化前的函数的参数数量决定柯里化后的函数需要执行多少次)
 * @param {function} fn -柯里化的函数
 */

function curry(fn) {
  if (fn.length <= 1) return fn;
  const generator = (...args) => {
    if (fn.length === args.length) {
      //执行fn并且返回执行结果
      return fn(...args);
    } else {
      return (...args2) => {
        //返回generator函数
        return generator(...args, ...args2);
      };
    }
  };
  return generator;
}
const curriedDisplay = curry(display);
console.log("curriedDisplay", curriedDisplay(1)(2)(3)(4)(5)(6)(7)(8));

// ES6简写
const curry2 = (fn) => {
  if (fn.length <= 1) return fn;
  const generator = (...args) =>
    args.length === fn.length
      ? fn(...args)
      : (...args2) => generator(...args, ...args2);
  return generator;
};
const curriedDisplay2 = curry2(display);
console.log("curriedDisplay2", curriedDisplay2(1)(2)(3)(4)(5)(6)(7)(8));

/**
 * @description 函数柯里化(支持占位符版本)
 * @param {function} fn -柯里化的函数
 * @param {String} [placeholder = "_"] -占位符
 */

const curry3 = (fn, placeholder = "_") => {
  curry3.placeholder = placeholder;
  if (fn.length <= 1) return fn;
  let argsList = [];
  const generator = (...args) => {
    let currentPlaceholderIndex = -1; // 记录了非当前轮最近的一个占位符下标,防止当前轮元素覆盖了当前轮的占位符
    args.forEach((arg) => {
      let placeholderIndex = argsList.findIndex(
        (item) => item === curry3.placeholder
      );
      if (placeholderIndex < 0) {
        // 如果数组中没有占位符直接往数组末尾放入一个元素
        currentPlaceholderIndex = argsList.push(arg) - 1;
        // 防止将元素填充到当前轮参数的占位符
        // (1,'_')('_',2) 数字2应该填充1后面的占位符,不能是2前面的占位符
      } else if (placeholderIndex !== currentPlaceholderIndex) {
        argsList[placeholderIndex] = arg;
      } else {
        // 当前元素是占位符的情况
        argsList.push(arg);
      }
    });
    let realArgsList = argsList.filter((arg) => arg !== curry3.placeholder); //过滤出不含占位符的数组
    if (realArgsList.length >= fn.length) {
      return fn(...argsList);
    } else {
      return generator;
    }
  };

  return generator;
};

const curriedDisplay3 = curry3(display);
console.log(
  "curriedDisplay3",
  curriedDisplay3("_", 2)(1, "_", 4)(3, "_")("_", 5)(6)(7, 8)
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81

柯里化是函数式编程的一个重要技巧,将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术

# 使用 reduce 实现数组的 flat 方法

🔝🔝28Skills

const selfFlat = function(depth = 1) {
  let arr = Array.prototype.slice.call(this);
  if (depth === 0) return arr;
  return arr.reduce((pre, cur) => {
    if (Array.isArray(cur)) {
      // 需要用 call 绑定 this 值,否则会指向 window
      return [...pre, ...selfFlat.call(cur, depth - 1)];
    } else {
      return [...pre, cur];
    }
  }, []);
};
1
2
3
4
5
6
7
8
9
10
11
12

# 循环实现数组的 reduce 方法

🔝🔝28Skills

Array.prototype.selfReduce = function(fn, initialValue) {
  let arr = Array.prototype.slice.call(this);
  let res;
  let startIndex;
  if (initialValue === undefined) {
    // 找到第一个非空单元(真实)的元素和下标
    for (let i = 0; i < arr.length; i++) {
      if (!arr.hasOwnProperty(i)) continue;
      startIndex = i;
      res = arr[i];
      break;
    }
  } else {
    res = initialValue;
  }
  // 遍历的起点为上一步中找到的真实元素后面一个真实元素
  // 每次遍历会跳过空单元的元素
  for (let i = ++startIndex || 0; i < arr.length; i++) {
    if (!arr.hasOwnProperty(i)) continue;
    res = fn.call(null, res, arr[i], i, this);
  }
  return res;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 循环实现数组的 some 方法

🔝🔝28Skills

// ES5循环实现 some 方法
const selfSome = function(fn, context) {
  let arr = Array.prototype.slice.call(this);
  // 空数组直接返回 false,数组的 every 方法则相反返回 true
  if (!arr.length) return false;
  for (let i = 0; i < arr.length; i++) {
    if (!arr.hasOwnProperty(i)) continue;
    let res = fn.call(context, arr[i], i, this);
    if (res) return true;
  }
  return false;
};
1
2
3
4
5
6
7
8
9
10
11
12

执行 some 方法的数组如果是一个空数组,最终始终会返回 false,而另一个数组的 every 方法中的数组如果是一个空数组,会始终返回 true

# 使用 reduce 实现数组 filter 方法

🔝🔝28Skills

// reduce实现filter
const selfFilter2 = function(fn, context) {
  return this.reduce((pre, cur, index) => {
    return fn.call(context, cur, index, this) ? [...pre, cur] : [...pre];
  }, []);
};
1
2
3
4
5
6

# 循环实现数组 filter 方法

🔝🔝28Skills

const selfFilter = function(fn, context) {
  let arr = Array.prototype.slice.call(this);
  let filteredArr = [];
  for (let i = 0; i < arr.length; i++) {
    if (!arr.hasOwnProperty(i)) continue;
    fn.call(context, arr[i], i, this) && filteredArr.push(arr[i]);
  }
  return filteredArr;
};
1
2
3
4
5
6
7
8
9

# 使用 reduce 实现数组 map 方法

🔝🔝28Skills

// reduce实现map
// 由于 reduce 会跳过空单元数组,所以这个 polyfill 无法处理空单元数组
const selfMap2 = function(fn, context) {
  let arr = Array.prototype.slice.call(this);
  return arr.reduce(
    (pre, cur, index) => [...pre, fn.call(context, cur, index, this)],
    []
  );
};
1
2
3
4
5
6
7
8
9
const numbers = [10, 20, 30, 40];
const doubledOver50 = numbers.reduce((finalList, num) => {
  num = num * 2;
  if (num > 50) {
    finalList.push(num);
  }
  return finalList;
}, []);
doubledOver50; // [60, 80]
1
2
3
4
5
6
7
8
9

# 循环实现数组 map 方法

🔝🔝28Skills

const htMap = function(fn, context) {
  let arr = Array.prototype.call(this);
  let mapArr = Array();
  for (let i = 0; i < arr.length; i++) {
    //稀疏数组的处理,通过 hasOwnProperty 来判断当前下标的元素是否存在与数组中
    if (!arr.hasOwnProperty(i)) {
      continue;
    }
    mapArr[i] = fn.call(context, arr[i], i, this);
  }
  return mappArr;
};
1
2
3
4
5
6
7
8
9
10
11
12

使用方法:将 selfMap 注入到 Array.prototype 上(下面数组的迭代方法同理)

Array.prototype.htMap = htMap[(1, 2, 3)].htMap((num) => num * 2); //[2,4,6]
1

# 重温一下 JS 进阶需要掌握的 13 个概念

back

# 变量赋值

back

JS 总是按照值来给变量赋值:
1、当指定的值是 JavaScript 的五种基本类型之一(即 Boolean,null,undefined,String 和 Number)时,分配是实际值。
2、当指定的值是 Array,Function 或 Object 时,将内存中对象的引用地址赋值给变量

看具体实例:

let var1 = "小智"; //是String类型
let var2 = var1;
var2 = "王大冶";
console.log(var1); // 小智
console.log(var2); // 王大冶

let var1 = { name: "小智" }; //是对象类型,其他Array、Function类似
let var2 = var1;
var2.name = "王大冶";
console.log(var1); // {name: "王大冶"}
console.log(var2); // {name: "王大冶"}
1
2
3
4
5
6
7
8
9
10
11

如果你期望它会像原始类型赋值那样,很可能会出问题!如果你创建了一个无意中会改变对象的函数,就会出现一些非预期的行为。

# 闭包

参考闭包

# 解构

back

JS 参数解构可以从对象中提取所需属性的常用方法。

const obj = {
  name: "小智",
  food: "鸡腿",
};
const { name, food } = obj;
console.log(name, food); // 小智 鸡腿
1
2
3
4
5
6

如果需要取别名,可以使用如下方式:

const obj = {
  name: "小智",
  food: "鸡腿",
};
const { name: myName, food: myFood } = obj;
console.log(myName, myFood); // 小智 鸡腿
1
2
3
4
5
6

解构经常也用于直接用于提取传给函数的参数。如果你熟悉 React,可能已经见过这个:

const person = {
  name: "小智",
  age: 24,
};
function introduce({ name, age }) {
  console.log(`我是 ${name} ,今天 ${age} 岁了!`);
}
console.log(introduce(person)); // 我是 小智 ,今天 24 岁了!
1
2
3
4
5
6
7
8

# 展开运算

back

ES6 的一个常用之一的特性就是展开(...)运算符了,在下面的例子中,Math.max 不能应用于 arr 数组,因为它不将数组作为参数,但它可以将各个元素作为参数传入。展开运算符...可用于提取数组的各个元素。

const arr = [4, 6, -1, 3, 10, 4];
const max = Math.max(...arr);
console.log(max); // 10
1
2
3

# 剩余参数

back

剩余参数语法和展开语法看起来的一样的,不同的是展开语法是为了解构数组和对象;而剩余参数和展开运算符是相反的,剩余参数收集多个参数合成一个数组。

function myFunc(...args) {
  console.log(args[0] + args[1]);
}
myFunc(1, 2, 3, 4); // 3
1
2
3
4
  • rest parameters 和 arguments 的区别
    arguments 是伪数组,包含所有的实参
    rest(剩余)参数 是标准的数组,可以使用数组的方法

# 数组方法

back、参考数组

# 严格相等

back

# Generators

back

# 对象比较

back

JS 新手经常所犯的错误是直接比较对象。对象变量指向内存中对象的引用,而不是对象本身!实际比较它们的一种方法是将对象转换为 JSON 字符串。这有一个缺点:对象属性顺序不能保证,比较对象的一种更安全的方法是引入专门进行深度对象比较的库(例如,lodash 中 isEqual)。

const joe1 = { name: "小智" };
const joe2 = { name: "小智" };
console.log(joe1 === joe2); // false

const joe1 = { name: "小智" };
const joe2 = joe1;
console.log(joe1 === joe2); // true
1
2
3
4
5
6
7

# 回调函数

back

举个例子,console.log 函数作为回调传递给 myFunc,它在 setTimeout 完成时执行。

function myFunc(text, callback) {
  setTimeout(function() {
    callback(text);
  }, 2000);
}
myFunc("Hello world!", console.log);
// 'Hello world!'
1
2
3
4
5
6
7

# Promises

back

可以使用 promise,将异步逻辑包装在 promise 中,成功时 resolve 或在失败时 reject 使用“then”来处理成功的情况,使用 catch 来处理异常。

const myPromise = new Promise(function(res, rej) {
  setTimeout(function() {
    if (Math.random() < 0.9) {
      return res("Hooray!");
    }
    return rej("Oh no!");
  }, 1000);
});
myPromise
  .then(function(data) {
    console.log("Success: " + data);
  })
  .catch(function(err) {
    console.log("Error: " + err);
  });
// 如果 Math.random() 返回的值小于 0.9,则打印以下内容
// "Success: Hooray!"
// 相反,则打印以下内容
// "Error: On no!"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# Async_Await

back

掌握了 promise 的用法后,你可能也会喜欢 async await,它只是一种基于 promise 的“语法糖”。在下面的示例中,咱们创建了一个 async 函数,并 await greeter。

const greeter = new Promise((res, rej) => {
  setTimeout(() => res("Hello world!"), 2000);
});
async function myFunc() {
  const greeting = await greeter;
  console.log(greeting);
}
myFunc();
// 'Hello world!'
1
2
3
4
5
6
7
8
9

# JS 开发技巧,避免重复造轮子,常用方法封装,提升辛福感的处理——第一弹

back

# 类型强制转换

back

# 遍历类数组对象

back

const elements = document.querySelectorAll(selector);
[].prototype.forEach.call(elements, (el, idx, list) => {
  console.log(el); // 元素节点
});
1
2
3
4

# 优化多层判断条件

back

const getScore = (score) => {
  const scoreData = new Array(101)
    .fill(0)
    .map((data, idx) => [idx, () => (idx < 60 ? "不及格" : "及格")]);
  const scoreMap = new Map(scoreData);
  return scoreMap.get(score) ? scoreMap.get(score)() : "未知分数";
};
getScore(30); // 不及格
1
2
3
4
5
6
7
8

# 函数

back

# 函数默认值

back

func = (l, m = 3, n = 4) => l * m * n;
func(2); //output: 24
1
2

注意,传入参数为undefined或者不传入的时候会使用默认参数,但是传入null还是会覆盖默认参数

# 强制参数

back

默认情况下,如果不向函数参数传值,那么 JS 会将函数参数设置为 undefined。其它一些语言则会发出警告或错误。要执行参数分配,可以使用 if 语句抛出未定义的错误,或者可以利用强制参数。

mandatory = () => {
  throw new Error("Missing parameter!");
};
foo = (bar = mandatory()) => {
  // 这里如果不传入参数,就会执行manadatory函数报出错误
  return bar;
};
1
2
3
4
5
6
7

# 隐式返回值

back

返回值是我们通常用来返回函数最终结果的关键字。只有一个语句的箭头函数,可以隐式返回结果(函数必须省略大括号{ },以便省略返回关键字)
返回多行语句(例如对象文本),需要使用( )而不是{ }来包裹函数体。这样可以确保代码以单个语句的形式进行求值。

function calcCircumference(diameter) {
  return Math.PI * diameter
}
// 简写为:
calcCircumference = diameter => (
  Math.PI * diameter;
)
1
2
3
4
5
6
7

# 惰性载入函数

back

在某个场景下我们的函数中有判断语句,这个判断依据在整个项目运行期间一般不会变化,所以判断分支在整个项目运行期间只会运行某个特定分支,那么就可以考虑惰性载入函数

function foo() {
  if (a !== b) {
    console.log("aaa");
  } else {
    console.log("bbb");
  }
}

// 优化后
function foo() {
  if (a != b) {
    foo = function() {
      console.log("aaa");
    };
  } else {
    foo = function() {
      console.log("bbb");
    };
  }
  return foo();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

# 一次性函数

back

跟上面的惰性载入函数同理,可以在函数体里覆写当前函数,那么可以创建一个一次性的函数,重新赋值之前的代码相当于只运行了一次,适用于运行一些只需要执行一次的初始化代码

var sca = function() {
  console.log("msg");
  sca = function() {
    console.log("foo");
  };
};
sca(); // msg
sca(); // foo
sca(); // foo
1
2
3
4
5
6
7
8
9

# 编写更鲁棒的 JavaScript 代码:7 个最佳实践

back

  • 1.编写构造函数时,在 .prototype 上添加方法
    • 这并不适用于类,因为类已经将方法附加到它们的 prototype 上。
function Frog(name, gender) {
  this.name = name;
  this.gender = gender;
}
Frog.prototype.leap = function(feet) {
  console.log(`Leaping ${feet}ft into the  air`);
};

//老式方法
function Frog(name, gender) {
  this.name = name;
  this.gender = gender;
  this.leap = function(feet) {
    console.log(`Leaping ${feet}ft into the  air`);
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

当把方法直接添加到 prototype 时,它们将在构造函数创建的所有实例之间共享。
换句话说,使用上个例子,如果创建三个独立的 Frog (从 this.leap = function() {...}),然后以创建三个独立的副本结束。这是一个问题,因为 leap 方法总是保持不变,不需要在实例上建立自己的副本。

  • 2.使用 TypeScript
  • 4.使用 JSON.parse 或 JSON.stringify 时,务必考虑使用 try/catch

来自JSON解析错误的危险是接受无效的JSON会导致应用程序崩溃。最近我们的一个 web 项目失败了,因为另一个内置程序包没有在 try/catch 里安装 JSON.parse 。最终导致了 web 页面失效,而且由于 JavaScript 运行时被破坏,除非内置程序包修复它,否则无法修正错误。

SyntaxError: Unexpected token }in JSON at position 107
1

不应总是期望有效的 JSON 输入,因为它会收到如“>”的奇怪字符,这在今天是很常见的。

  • 5.使用常规的.type属性进行区分
function createSpecies(type, name,  gender) {
  if (type ==='frog') {
    return createFrog(name, gender)
  } elseif (type ==='human') {
    return createHuman(name, gender)
  } elseif (type == undefined) {
    thrownewError('Cannot create  a species with an unknown type')
  }
}
const myNewFrog =createSpecies('frog', 'sally', 'female')
1
2
3
4
5
6
7
8
9
10
  • 6.使用工厂函数(factory function)

当函数被 new 关键字调用时,该函数就不再是工厂函数了。
使用工厂函数可以轻松的生成对象实例,且无需涉及类或 new 关键字。

function createFrog(name) {
  const children = [];
  return {
    addChild(frog) {
      children.push(frog);
    },
  };
}
const mikeTheFrog = createFrog("mike");
1
2
3
4
5
6
7
8
9
  • 7.使函数尽可能的简单

# 5 个提升 js 编码水平的实例

🔝🔝良好习惯

# 通用的数组/类数组对象封装

但是如果我们要循环一个类数组对象呢?
例如 NodeList。直接循环是会报错的:

document.querySelectorAll("div").map(e => e) // Uncaught TypeError: document.querySelectorAll(...).map is not a function

[...document.querySelectorAll("div")].map(e => e)


var listMap = function(array, type, fn) {
    return !fn ? array : Array.prototype[type]["call"](array, fn)
};
var divs = document.querySelectorAll("div");
listMap(divs, "forEach", function(e) {
    console.log(e)
});
1
2
3
4
5
6
7
8
9
10
11
12

# 批量生成对象元素

const createList = (item, idx) => {
  let obj = {};
  obj[`a{idx}`] = "data";
  return obj;
};
const listReducer = (acc, cur) => (!acc ? { ...cur } : { ...cur, ...acc });
const obj = Array.from(new Array(20), createList).reduce(listReducer);
1
2
3
4
5
6
7

# 变量

back

知名其意的方式为变量命名,通过这种方式,当再次看到变量名时,就能大概理解其中的用意
不要在变量名中添加额外的不需要的单词

let nameValue;
let theProduct;

let name;
let product;
1
2
3
4
5

函数名称应该是动词或短语,用以说明其背后的意图以及参数的意图。函数的名字应该说明他们做了什么。
避免使用大量参数,理想情况下,函数应该指定两个或更少的参数。参数越少,测试函数就越容易,参数多的情况可以使用对象

function getUsers(fields, fromDate, toDate) {}

function getUsers({ fields, fromDate, toDate }) {}
1
2
3

使用默认参数替代 || 操作
一个函数应该只做一件事,不要在一个函数中执行多个操作
使用Object.assign设置对象默认值(back)

const shapeConfig = {
    type: 'cube',
    width: 200,
    height: null,
};
function createShape = (config){
    config.type = config.type || 'cube';
    config.width = config.width || 200;
    config.height = config.height || 250;
}
createShape(shapeConfig);


const shapeConfig = {
    type: 'cube',
    width: 200,
};
function createShape = (config){
    config = Object.assign({
        type: 'cube',
        widht: 200,
        height: 250,
    }, config);
}
createShape(shapeConfig);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

不要使用标志作为参数,因为它们告诉函数做的比它应该做的多(back)

function createFile(name, isPublic) {
  if (isPublic) {
    fs.create(`./public/${name}`);
  } else {
    fs.create(name);
  }
}

function createFile(name) {
  fs.create(name);
}
function createPublicFile(name) {
  createFile(`./public/${name}`);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

不要污染全局变量,如果需要扩展现有对象,请使用 ES6 类和继承,而不是在原生对象的原型链上创建函数(back)

Array.prototype.myFunc = function myFunc() {};

class SuperArray extends Array {
  myFunc() {}
}
1
2
3
4
5

# 条件

back

避免使用反面条件
使用条件简写,仅对布尔值使用此方法,并且如果确信该值不会是undefined 或null的,则使用此方法

if (isValid === true) {
}

if (isValid) {
}
1
2
3
4
5

尽可能避免条件句,而是使用多态性继承

Class Car{
    getMax(){
        switch(this.type){
            case 'Ford':
                return this.someFactor+this.anotherFactor;
            case 'Mazda':
                return this.someFactor;
            case 'Mclaren':
                return this.someFactor-this.anotherFactor;
        }
    }
}


Class Car {
}
Class Ford extends Car {
    getMax(){
        return this.someFactor+this.anotherFactor;
    }
}
……
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#

back

使用链接,许多库(如 jQuery 和 Lodash)都使用这种模式。在类中,只需在每个函数的末尾返回this就可以将更多的该类方法链接到它上。
class 是 JS 中新的语法糖,工作方式就像以前的原型但比原型的方式更简单易懂

# JavaScript 设计模式

back

# 设计原则

back

Tips:在学习设计模式的时候,尽量分开去学,即先学习设计,然后再去学模式,这样的话,对于之后的理解更加容易一些,什么设计呢,其实就是指设计原则,而模式这里指的就是就是我们要讲的设计模式。现在共有 5 大设计原则,不管是哪种设计模式,都是遵循设计原则的。下面来分别介绍这 5 大设计原则。

  • 1.单一职责原则
    单一职责原则原则就是每个程序只负责做好一件事情,如果功能过于复杂就拆分开,每个部分保持独立。这个其实也符合我们当下流行框架 Vue 和 React 的组件化开发,把一个复杂的页面拆分成一些零散的组件,并且每个组件保持独立,同时也可在不同的页面当中实现复用。
  • 2.开放封闭原则
    开发封闭原则大白话的意思就是对扩展开放,对修改封闭。放到实际开发中如何去理解呢,我们日常的网站和 APP 开发每周都有发不同的版本来增加需求,那么增加需求的时候,尽量要做到扩展新代码,而非修改已有代码,如果我们修改已有代码无疑增加了风险,因为本来原来的代码是没有问题的,加了新的代码之后必然会增加不可预知的风险,当然有的个别需求必须修改已有代码,这个另说。同时这个原则也是我们软件设计的终极目标。
  • 3.李氏置换原则
    子类能够覆盖父类,父类能出现的地方,子类就可以出现,这个原则其实在 Java 等语言当中是较为常见的,多用于继承,而 JavaScript 作为弱类型语言,继承使用其实是很少的,这里简单提一下。
  • 4.接口独立原则
    接口独立原则的含义是保持接口的单一独立,避免出现胖接口,JavaScript 中是没有接口(typescript 例外),使用较少, 它是有点类似于单一职责原则,这里更关注接口。
  • 5.依赖倒置原则
    依赖倒置原则的含义是面向接口编程,依赖于抽象而不依赖于具体,使用方只关注接口而不关注具体类的实现,同样这里也是 JavaScript 中使用较少(没有接口&弱类型)

# 设计模式

back

# 工厂模式

back

# 工厂模式概念

工厂模式是由一个方法来确定是要创建哪个类的实例,在前端当中最为常见的工厂模式就是new操作的单独封装,当遇到 new 操作的时候,就要考虑是否该使用工厂模式。这里也可以结合生活中的例子去思考。当你去购买汉堡,直接点餐取餐,不会自己亲手做,商店要“封装”做汉堡的工作,做好直接给买者。也就是说通过提供原材料,最终得到是汉堡还是炸鸡,是由你自己决定的。

# 工厂模式前端中实例
  • 1.jQuery 当中的$('')
    jQuery 当中的$('div'),这里的$选择器就是已经封装好的API,这里我们直接使用即可。下面简单实现一个 JQuery 的$操作符,帮助大家加深理解。
class jQuery {
  constructor(selector) {
    let slice = Array.prototype.slice;
    let dom = slice.call(document.querySelectorAll(selector));
    let len = dom ? dom.length : 0;
    for (let i = 0; i < len; i++) {
      this[i] = dom[i];
    }
    this.length = len;
    this.selector = selector || "";
  }
  append(node) {}
  html(data) {}
  //等等API
}
window.$ = function(selector) {
  return new jQuery(selector);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  • 2.Vue 异步组件
    这个大家应该比较熟悉,而且官方文档讲的也非常详细,这里直接饮用官方文档的案例,在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 允许你以一个工厂函数的方式定义你的组件,这个工厂函数会异步解析你的组件定义。Vue 只有在这个组件需要被渲染的时候才会触发该工厂函数,且会把结果缓存起来供未来重渲染
Vue.component("async-example", function(resolve, reject) {
  setTimeout(function() {
    // 向 `resolve` 回调传递组件定义
    resolve({
      template: "<div>I am async!</div>",
    });
  }, 1000);
});
1
2
3
4
5
6
7
8

# 单例模式

back

单例模式符合单一职责原则,用大白话描述单例模式就是系统中被唯一使用,例如:电子商务网站常见的购物车和登录都是单例模式的运用。

# 单例模式前端中的实例
  • 1.jQuery 中的$('')
    仍旧是jQuery当中的$(' ')选择器,整个 jQuery 框架当中有一个这样的选择器。

  • 2.Redux 和 Vuex
    不管是 Redux 还是 Vuex,里面的状态 store 都是唯一的,Redux中的store只能通过Reducer去修改,而 Vuex 中的 store 只能通过 Mutation 修改,其余修改方式都是错误的。

# 适配器模式

back

适配器模式的含义是旧接口格式和使用者不兼容,中间加一个适配器接口。生活当中随处可见符合适配器模式的例子,如:插头转换器,电脑接口转换器。

# 适配器模式前端中的实例
ajax({
  url: "/getList",
  type: "Post",
  dataType: "json",
  data: {
    id: "123",
  },
}).done(function() {});
1
2
3
4
5
6
7
8

但是这个时候你接到的项目当中都是:$.ajax({...}),这个时候我们只需要加一层适配器即可,代码如下:

let $ = {
  ajax: function(options) {
    return ajax(options);
  },
};
1
2
3
4
5

# 装饰器模式

back

装饰器模式,装饰我们可以理解为就是给一个东西装饰另外一些东西使其更好看,对应到前端中就是为对象添加新功能,并且不改变其原有的结构和功能,这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。在我们日常生活中用到的手机壳就是经典的例子

# 代理模式

back

代理模式是使用者无权访问目标对象,中间加代理,通过代理做授权和控制,我们经常会用到这个模式,不管实际的开发也好,还是网络部署当中,都能够看到它的身影。如:科学上网 谷歌搜索

# 代理模式前端中的实例
  • 1.网页中的事件代理
    其实网页的事件代理也是非常常考的面试题之一,其实就是把元素绑定到父元素上面,而不是对其下面的每一个子元素都进行相应的绑定,下面举一个具体的实例:
<div id="item">
    <a href="#">a1</a>
    <a href="#">a2</a>
    <a href="#">a3</a>
    <a href="#">a4</a>
    <div>
    <button>点击增加一个a标签</button>
<script>
    let div1 = document.getElementById('div1')
    div1.addEventListener('click',function(e) {
        let target = e.target
        if(e.nodeName === 'A') {
            alert(target.innerHTML)
        }
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 2.jQuery 中的$proxy
    比如我们经常会遇到这样一种情况,如下代码所示
$("#div1").click(function() {
  $(this).addClass("red");
});
$("#div1").click(function() {
  setTimeout(function() {
    // this 不符合期望
    $(this).addClass("red");
  }, 1000);
});
1
2
3
4
5
6
7
8
9

解决的方法可能有的同学已经想到是先将 this 赋值给一个变量。

$("#div1").click(function() {
  setTimeout(function() {
    let _this = this;
    // this 不符合期望
    $(_this).addClass("red");
  }, 1000);
});
1
2
3
4
5
6
7

是的这种方法是对的,但是这样就会增加一个变量,所以这里用$proxy解决更好,代码如下:

$("#div1").click(function() {
  setTimeout(
    $proxy(function() {
      $(this).addClass("red");
    }),
    1000
  );
});
1
2
3
4
5
6
7
8

# 观察者模式

back

观察者模式就是只要你作为订阅者订阅了某个事件,当事件触发的时候,发布者就会通知你。这里可以类比我们去咖啡厅点咖啡,当我们点了咖啡之后,就可以去做别的事情,当咖啡完成时,服务员就会叫你来取,你到时候取走咖啡即可。

# 观察者模式前端中的实例
  • 1.网页中的事件绑定
<button id="btn1">btn</button>
<scirpt>
    $('#btn1').click(function() {
        console.log(1)
    })
</script>
1
2
3
4
5
6
  • 2.Node.js 的自定义事件 EventEmitter
const EventEmitter = require("events").EventEmitter;
const emitter1 = new EventEmitter();
emitter1.on("some", () => {
  //监听some事件
  console.log("some event is occured 1");
});
emitter1.on("some", () => {
  //监听some事件
  console.log("some event is occured 2");
});
emitter.emit("some");
1
2
3
4
5
6
7
8
9
10
11

# 状态模式

back

  • 状态总数(state)是有限的。
  • 任一时刻,只处在一种状态之中。
  • 某种条件下,会从一种状态转变(transition)到另一种状态。