设计模式的主题是将不变的部分与变化的部分找出来,然后将变化的地方通过模式封装起来。
基础
new、this、call、apply、bind
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
| function Person(name) { this.name = name; }
Person.prototype.getName = function () { return this.name; };
function New() { const obj = new Object(); const construct = [].shift.call(arguments); Object.setPrototypeOf(obj, construct.prototype); const ret = construct.apply(obj, arguments); return typeof ret === "object" ? ret : obj; }
const a = New(Person, "sven"); const b = new Person("sven");
console.log(a.getName()); console.log(b.getName());
console.log(Object.getPrototypeOf(a) === Person.prototype);
|
JS 给对象提供了一个名为__proto__
的隐藏属性,某个对象的__proto__
属性默认会指向它的构造器的原型对象,即{Constructor}.prototype
。
1 2
| let a = new Object(); console.log(Object.getPrototypeOf(a) === Object.prototype);
|
Object.prototype 对象的原型为 null,所以原型链是有起点的。
1
| console.log(Object.getPrototypeOf(Object.prototype) === null);
|
Function.prototype.call(this, ...)
是Function.prototype.apply(this, [])
上面的一颗语法糖,如果我们明确知道函数需要多少个参数,而且想一目了然地表达形式参数与实际参数的对应关系,那么可以用Function.prototype.call()
。
bind 模拟:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function bind() { const func = this; const newThis = [].shift.call(arguments); const args = [].slice.call(arguments);
return function () { return func.apply(newThis, [].concat.call(args, [].slice.call(arguments))); }; }
|
如何做到在类数组对象上使用数组对象方法?
以Array.prototype.push
为例,看看 V8 引擎内部的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function ArrayPush() { var n = TO_UINT32(this.length); var m = %_ArgumentsLength();
for (var i = 0; i < m; i++) { this[n + i] = %_Arguments(i); }
this.length = n + m; return this.length; }
|
从上面可知,要在某个对象上使用数组方法,对象必须满足:
- 对象本身可以增加属性
- 对象具有
length
属性并且可读写
闭包
JS 闭包的形成与变量的作用域和生命周期密切相关:
- 函数可以创建函数作用域,此时的函数像一层半透明的玻璃,在函数内部可以看见函数外部,而在函数外部却无法看清函数内部
- 对于生存在函数作用域内部的变量来说,函数一旦退出变量的生命周期随之结束
一个函数的返回值为另外一个函数 func,func 函数在原函数返回后可以继续访问原函数局部变量的能力(作用域引用)称之为闭包。
闭包应用
函数缓存机制:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| const mul = (function () { const cache = {};
const calculate = function () { let a = 1; for (let i = 0; i < arguments.length; i++) { a = a * arguments[i]; } return a; };
return function () { const key = Array.prototype.join.call(arguments); if (key in cache) { return cache[key]; }
return (cache[args] = calculate.apply(null, arguments)); }; })();
|
高阶函数
一个函数满足下列条件之一称之为高阶函数:
- 函数可以作为参数传递
- 函数可以作为返回值输出
高阶函数应用
单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| const getSingle = function (fn) { let ret = null; return function () { return ret || (ret = fn.apply(this, arguments)); }; };
const getScript = getSingle(function () { return { a: 1, b: 2 }; });
const script1 = getScript(); const script2 = getScript();
console.log(Object.is(script1, script2));
|
JS 中实现 AOP
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
| Function.prototype.before = function (fn) { const self = this; return function () { fn.apply(this, arguments); return self.apply(this, arguments); }; };
Function.prototype.after = function (fn) { const self = this; return function () { const ret = self.apply(this, arguments); fn.apply(this, arguments); return ret; }; };
function show(name) { console.log(name); }
show = show .before(function () { console.log("call before"); }) .after(function () { console.log("call after"); });
show("me");
|
函数 currying
一个函数首先会接受并保存一些参数但并不会立即求值,等再次调用满足求值条件时再一次性进行求值
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
| const currying = function (fn) { const args = [];
return function c() { if (arguments.length === 0) { return fn.apply(this, args); } else { [].push.apply(args, arguments); return c; } }; };
let cost = (function () { let money = 0;
return function () { for (var i = 0; i < arguments.length; i++) { money += arguments[i]; } return money; }; })();
cost = currying(cost);
console.log(cost(100)); console.log(cost(200)); console.log(cost(300)); console.log(cost());
|
函数 uncurrying
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Function.prototype.uncurrying = function () { const self = this; return function () { const context = [].shift.call(arguments); return self.apply(context, arguments); }; };
const push = Array.prototype.push.uncurrying();
const arr = [];
console.log(arr.length);
push(arr, 1);
console.log(arr.length);
|
throttle 节流函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| function throttle(fn, interval) { let firstTime = true; let timer;
return function () { if (firstTime) { fn.call(this, arguments); return (firstTime = false); } if (timer) { return; } timer = setTimeout(() => { fn.call(this, arguments); clearTimeout(timer); timer = null; }, interval || 500); }; }
window.onresize = throttle(function () { console.log("window resize"); }, 1000);
|
分时函数
场景:一瞬间大批量插入 DOM 节点会导致浏览器假死,采用分时函数,分批定时插入即可解决问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function timeChunk(arr, fn, count, interval) { const execute = function () { for (let i = 0; i < Math.min(arr.length, count || 1); i++) { const item = arr.shift(); fn(item); } };
const t = setInterval(() => { if (arr.length === 0) { return clearInterval(t); } execute(); }, interval || 500); }
timeChunk( Array.from({ length: 1000 }, (v, k) => (v = Math.random())), function (item) { console.log(item); }, 8 );
|
惰性加载函数
场景:嗅探浏览器支持的 API
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| let addEvent = function (ele, type, handler) { if (window.addEventListener) { addEvent = function (ele, type, handler) { ele.addEventListener(type, handler, false); }; } else if (windows.attachEvent) { addEvent = function (ele, type, handler) { ele.attachEvent("on" + type, handler); }; }
addEvent(ele, type, handler); };
|
设计模式