如下面试题来自腾讯、阿里、网易、饿了么、美团、拼多多、百度等等大厂综合起来常考的题目。javascript
简历不是一份记流水帐的东西,而是让用人方了解你的亮点的。css
平时有在作一些修改简历的收费服务,也算看过蛮多简历了。不少简历都有以下特征html
以上相似简历能够说用人方也看了无数份,彻底抓不到你的亮点。除非你呆过大厂或者教育背景不错或者技术栈符合人家要求了,不然基本就是看运气约面试了。前端
如下是我常常给别人修改简历的意见:vue
简历页数控制在 2 页如下java
作到以上内容,而后在投递简历的过程当中加上一份求职信,对你的求职之路相信能帮上不少忙。node
当执行 JS 代码时,会生成执行环境,只要代码不是写在函数中的,就是在全局执行环境中,函数中的代码会产生函数执行环境,只此两种执行环境。react
接下来让咱们看一个老生常谈的例子,var
c++
b() // call b
console.log(a) // undefined
var a = 'Hello world'
function b() {
console.log('call b')
}
复制代码
想必以上的输出你们确定都已经明白了,这是由于函数和变量提高的缘由。一般提高的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于你们理解。可是更准确的解释应该是:在生成执行环境时,会有两个阶段。第一个阶段是建立的阶段,JS 解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为 undefined,因此在第二个阶段,也就是代码执行阶段,咱们能够直接提早使用。git
在提高的过程当中,相同的函数会覆盖上一个函数,而且函数优先于变量提高
b() // call b second
function b() {
console.log('call b fist')
}
function b() {
console.log('call b second')
}
var b = 'Hello world'
复制代码
var
会产生不少错误,因此在 ES6中引入了 let
。let
不能在声明前使用,可是这并非常说的 let
不会提高,let
提高了,在第一阶段内存也已经为他开辟好了空间,可是由于这个声明的特性致使了并不能在声明前使用。
首先说下前二者的区别。
call
和 apply
都是为了解决改变 this
的指向。做用都是相同的,只是传参的方式不一样。
除了第一个参数外,call
能够接收一个参数列表,apply
只接受一个参数数组。
let a = {
value: 1
}
function getValue(name, age) {
console.log(name)
console.log(age)
console.log(this.value)
}
getValue.call(a, 'yck', '24')
getValue.apply(a, ['yck', '24'])
复制代码
bind
和其余两个方法做用也是一致的,只是该方法会返回一个函数。而且咱们能够经过 bind
实现柯里化。
对于实现如下几个函数,能够从几个方面思考
window
Function.prototype.myBind = function (context) {
if (typeof this !== 'function') {
throw new TypeError('Error')
}
var _this = this
var args = [...arguments].slice(1)
// 返回一个函数
return function F() {
// 由于返回了一个函数,咱们能够 new F(),因此须要判断
if (this instanceof F) {
return new _this(...args, ...arguments)
}
return _this.apply(context, args.concat(...arguments))
}
}
复制代码
Function.prototype.myCall = function (context) {
var context = context || window
// 给 context 添加一个属性
// getValue.call(a, 'yck', '24') => a.fn = getValue
context.fn = this
// 将 context 后面的参数取出来
var args = [...arguments].slice(1)
// getValue.call(a, 'yck', '24') => a.fn('yck', '24')
var result = context.fn(...args)
// 删除 fn
delete context.fn
return result
}
复制代码
Function.prototype.myApply = function (context) {
var context = context || window
context.fn = this
var result
// 须要判断是否存储第二个参数
// 若是存在,就将第二个参数展开
if (arguments[1]) {
result = context.fn(...arguments[1])
} else {
result = context.fn()
}
delete context.fn
return result
}
复制代码
每一个函数都有 prototype
属性,除了 Function.prototype.bind()
,该属性指向原型。
每一个对象都有 __proto__
属性,指向了建立该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,可是 [[prototype]]
是内部属性,咱们并不能访问到,因此使用 _proto_
来访问。
对象能够经过 __proto__
来寻找不属于该对象的属性,__proto__
将对象链接起来组成了原型链。
若是你想更进一步的了解原型,能够仔细阅读 深度解析原型中的各个难点。
Object.prototype.toString.call(xx)
。这样咱们就能够得到相似 [object Type]
的字符串。instanceof
能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype
。function a() {
return () => {
return () => {
console.log(this)
}
}
}
console.log(a()()())
复制代码
箭头函数实际上是没有 this
的,这个函数中的 this
只取决于他外面的第一个不是箭头函数的函数的 this
。在这个例子中,由于调用 a
符合前面代码中的第一个状况,因此 this
是 window
。而且 this
一旦绑定了上下文,就不会被任何代码改变。
this
是不少人会混淆的概念,可是其实他一点都不难,你只须要记住几个规则就能够了。
function foo() {
console.log(this.a)
}
var a = 1
foo()
var obj = {
a: 2,
foo: foo
}
obj.foo()
// 以上二者状况 `this` 只依赖于调用函数前的对象,优先级是第二个状况大于第一个状况
// 如下状况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向
var c = new foo()
c.a = 3
console.log(c.a)
// 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
复制代码
async 和 await
相比直接使用 Promise
来讲,优点在于处理 then
的调用链,可以更清晰准确的写出代码。缺点在于滥用 await
可能会致使性能问题,由于 await
会阻塞代码,也许以后的异步代码并不依赖于前者,但仍然须要等待前者完成,致使代码失去了并发性。
下面来看一个使用 await
的代码。
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
复制代码
对于以上代码你可能会有疑惑,这里说明下原理
b
先执行,在执行到 await 10
以前变量 a
仍是 0,由于在 await
内部实现了 generators
,generators
会保留堆栈中东西,因此这时候 a = 0
被保存了下来await
是异步操做,遇到await
就会当即返回一个pending
状态的Promise
对象,暂时返回执行代码的控制权,使得函数外的代码得以继续执行,因此会先执行 console.log('1', a)
a = 10
Generator 是 ES6 中新增的语法,和 Promise 同样,均可以用来异步编程
// 使用 * 表示这是一个 Generator 函数
// 内部能够经过 yield 暂停代码
// 经过调用 next 恢复执行
function* test() {
let a = 1 + 2;
yield 2;
yield 3;
}
let b = test();
console.log(b.next()); // > { value: 2, done: false }
console.log(b.next()); // > { value: 3, done: false }
console.log(b.next()); // > { value: undefined, done: true }
复制代码
从以上代码能够发现,加上 *
的函数执行后拥有了 next
函数,也就是说函数执行后返回了一个对象。每次调用 next
函数能够继续执行被暂停的代码。如下是 Generator 函数的简单实现
// cb 也就是编译过的 test 函数
function generator(cb) {
return (function() {
var object = {
next: 0,
stop: function() {}
};
return {
next: function() {
var ret = cb(object);
if (ret === undefined) return { value: undefined, done: true };
return {
value: ret,
done: false
};
}
};
})();
}
// 若是你使用 babel 编译后能够发现 test 函数变成了这样
function test() {
var a;
return generator(function(_context) {
while (1) {
switch ((_context.prev = _context.next)) {
// 能够发现经过 yield 将代码分割成几块
// 每次执行 next 函数就执行一块代码
// 而且代表下次须要执行哪块代码
case 0:
a = 1 + 2;
_context.next = 4;
return 2;
case 4:
_context.next = 6;
return 3;
// 执行完毕
case 6:
case "end":
return _context.stop();
}
}
});
}
复制代码
Promise 是 ES6 新增的语法,解决了回调地狱的问题。
能够把 Promise 当作一个状态机。初始是 pending
状态,能够经过函数 resolve
和 reject
,将状态转变为 resolved
或者 rejected
状态,状态一旦改变就不能再次变化。
then
函数会返回一个 Promise 实例,而且该返回值是一个新的实例而不是以前的实例。由于 Promise 规范规定除了 pending
状态,其余状态是不能够改变的,若是返回的是一个相同实例的话,多个 then
调用就失去意义了。
对于 then
来讲,本质上能够把它当作是 flatMap
// 三种状态
const PENDING = "pending";
const RESOLVED = "resolved";
const REJECTED = "rejected";
// promise 接收一个函数参数,该函数会当即执行
function MyPromise(fn) {
let _this = this;
_this.currentState = PENDING;
_this.value = undefined;
// 用于保存 then 中的回调,只有当 promise
// 状态为 pending 时才会缓存,而且每一个实例至多缓存一个
_this.resolvedCallbacks = [];
_this.rejectedCallbacks = [];
_this.resolve = function (value) {
if (value instanceof MyPromise) {
// 若是 value 是个 Promise,递归执行
return value.then(_this.resolve, _this.reject)
}
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = RESOLVED;
_this.value = value;
_this.resolvedCallbacks.forEach(cb => cb());
}
})
};
_this.reject = function (reason) {
setTimeout(() => { // 异步执行,保证执行顺序
if (_this.currentState === PENDING) {
_this.currentState = REJECTED;
_this.value = reason;
_this.rejectedCallbacks.forEach(cb => cb());
}
})
}
// 用于解决如下问题
// new Promise(() => throw Error('error))
try {
fn(_this.resolve, _this.reject);
} catch (e) {
_this.reject(e);
}
}
MyPromise.prototype.then = function (onResolved, onRejected) {
var self = this;
// 规范 2.2.7,then 必须返回一个新的 promise
var promise2;
// 规范 2.2.onResolved 和 onRejected 都为可选参数
// 若是类型不是函数须要忽略,同时也实现了透传
// Promise.resolve(4).then().then((value) => console.log(value))
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => throw r;
if (self.currentState === RESOLVED) {
return (promise2 = new MyPromise(function (resolve, reject) {
// 规范 2.2.4,保证 onFulfilled,onRjected 异步执行
// 因此用了 setTimeout 包裹下
setTimeout(function () {
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === REJECTED) {
return (promise2 = new MyPromise(function (resolve, reject) {
setTimeout(function () {
// 异步执行onRejected
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (reason) {
reject(reason);
}
});
}));
}
if (self.currentState === PENDING) {
return (promise2 = new MyPromise(function (resolve, reject) {
self.resolvedCallbacks.push(function () {
// 考虑到可能会有报错,因此使用 try/catch 包裹
try {
var x = onResolved(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
self.rejectedCallbacks.push(function () {
try {
var x = onRejected(self.value);
resolutionProcedure(promise2, x, resolve, reject);
} catch (r) {
reject(r);
}
});
}));
}
};
// 规范 2.3
function resolutionProcedure(promise2, x, resolve, reject) {
// 规范 2.3.1,x 不能和 promise2 相同,避免循环引用
if (promise2 === x) {
return reject(new TypeError("Error"));
}
// 规范 2.3.2
// 若是 x 为 Promise,状态为 pending 须要继续等待不然执行
if (x instanceof MyPromise) {
if (x.currentState === PENDING) {
x.then(function (value) {
// 再次调用该函数是为了确认 x resolve 的
// 参数是什么类型,若是是基本类型就再次 resolve
// 把值传给下个 then
resolutionProcedure(promise2, value, resolve, reject);
}, reject);
} else {
x.then(resolve, reject);
}
return;
}
// 规范 2.3.3.3.3
// reject 或者 resolve 其中一个执行过得话,忽略其余的
let called = false;
// 规范 2.3.3,判断 x 是否为对象或者函数
if (x !== null && (typeof x === "object" || typeof x === "function")) {
// 规范 2.3.3.2,若是不能取出 then,就 reject
try {
// 规范 2.3.3.1
let then = x.then;
// 若是 then 是函数,调用 x.then
if (typeof then === "function") {
// 规范 2.3.3.3
then.call(
x,
y => {
if (called) return;
called = true;
// 规范 2.3.3.3.1
resolutionProcedure(promise2, y, resolve, reject);
},
e => {
if (called) return;
called = true;
reject(e);
}
);
} else {
// 规范 2.3.3.4
resolve(x);
}
} catch (e) {
if (called) return;
called = true;
reject(e);
}
} else {
// 规范 2.3.4,x 为基本类型
resolve(x);
}
}
复制代码
上图中的 toPrimitive
就是对象转基本类型。
这里来解析一道题目 [] == ![] // -> true
,下面是这个表达式为什么为 true
的步骤
// [] 转成 true,而后取反变成 false
[] == false
// 根据第 8 条得出
[] == ToNumber(false)
[] == 0
// 根据第 10 条得出
ToPrimitive([]) == 0
// [].toString() -> ''
'' == 0
// 根据第 6 条得出
0 == 0 // -> true
复制代码
===
用于判断二者类型和值是否相同。 在开发中,对于后端返回的 code
,能够经过 ==
去判断。
V8 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。所以,V8 将内存(堆)分为新生代和老生代两部分。
新生代中的对象通常存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,一定有一个空间是使用的,另外一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,若是有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
老生代中的对象通常存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
在讲算法前,先来讲下什么状况下对象会出如今老生代空间中:
老生代中的空间很复杂,有以下几个空间
enum AllocationSpace {
// TODO(v8:7464): Actually map this space's memory as read-only.
RO_SPACE, // 不变的对象空间
NEW_SPACE, // 新生代用于 GC 复制算法的空间
OLD_SPACE, // 老生代常驻对象空间
CODE_SPACE, // 老生代代码对象空间
MAP_SPACE, // 老生代 map 对象
LO_SPACE, // 老生代大空间对象
NEW_LO_SPACE, // 新生代大空间对象
FIRST_SPACE = RO_SPACE,
LAST_SPACE = NEW_LO_SPACE,
FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};
复制代码
在老生代中,如下状况会先启动标记清除算法:
在这个阶段中,会遍历堆中全部的对象,而后标记活的对象,在标记完成后,销毁全部没有被标记的对象。在标记大型对内存时,可能须要几百毫秒才能完成一次标记。这就会致使一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标志。在增量标记期间,GC 将标记工做分解为更小的模块,可让 JS 应用逻辑在模块间隙执行一会,从而不至于让应用出现停顿状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术可让 GC 扫描和标记对象时,同时容许 JS 运行,你能够点击 该博客 详细阅读。
清除对象后会形成堆内存出现碎片的状况,当碎片超过必定限制后会启动压缩算法。在压缩过程当中,将活的对象像一端移动,直到全部对象都移动完成而后清理掉不须要的内存。
闭包的定义很简单:函数 A 返回了一个函数 B,而且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
function A() {
let a = 1
function B() {
console.log(a)
}
return B
}
复制代码
你是否会疑惑,为何函数 A 已经弹出调用栈了,为何函数 B 还能引用到函数 A 中的变量。由于函数 A 中的变量这时候是存储在堆上的。如今的 JS 引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。
经典面试题,循环中使用闭包解决 var
定义函数的问题
for ( var i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
首先由于 setTimeout
是个异步函数,全部会先把循环所有执行完毕,这时候 i
就是 6 了,因此会输出一堆 6。
解决办法两种,第一种使用闭包
for (var i = 1; i <= 5; i++) {
(function(j) {
setTimeout(function timer() {
console.log(j);
}, j * 1000);
})(i);
}
复制代码
第二种就是使用 setTimeout
的第三个参数
for ( var i=1; i<=5; i++) {
setTimeout( function timer(j) {
console.log( j );
}, i*1000, i);
}
复制代码
第三种就是使用 let
定义 i
了
for ( let i=1; i<=5; i++) {
setTimeout( function timer() {
console.log( i );
}, i*1000 );
}
复制代码
由于对于 let
来讲,他会建立一个块级做用域,至关于
{ // 造成块级做用域
let i = 0
{
let ii = i
setTimeout( function timer() {
console.log( ii );
}, i*1000 );
}
i++
{
let ii = i
}
i++
{
let ii = i
}
...
}
复制代码
前者存储在栈上,后者存储在堆上
众所周知 JS 是门非阻塞单线程语言,由于在最初 JS 就是为了和浏览器交互而诞生的。若是 JS 是门多线程的语言话,咱们在多个线程中处理 DOM 就可能会发生问题(一个线程中新加节点,另外一个线程中删除节点),固然能够引入读写锁解决这个问题。
JS 在执行的过程当中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。若是遇到异步的代码,会被挂起并加入到 Task(有多种 task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,因此本质上来讲 JS 中的异步仍是同步行为。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
console.log('script end');
复制代码
以上代码虽然 setTimeout
延时为 0,其实仍是异步。这是由于 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,不足会自动增长。因此 setTimeout
仍是会在 script end
以后打印。
不一样的任务源会被分配到不一样的 Task 队列中,任务源能够分为 微任务(microtask) 和 宏任务(macrotask)。在 ES6 规范中,microtask 称为 jobs
,macrotask 称为 task
。
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
new Promise((resolve) => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout
复制代码
以上代码虽然 setTimeout
写在 Promise
以前,可是由于 Promise
属于微任务而 setTimeout
属于宏任务,因此会有以上的打印。
微任务包括 process.nextTick
,promise
,Object.observe
,MutationObserver
宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
不少人有个误区,认为微任务快于宏任务,实际上是错误的。由于宏任务中包括了 script
,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
因此正确的一次 Event loop 顺序是这样的
经过上述的 Event loop 顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做 DOM 的话,为了更快的 界面响应,咱们能够把操做 DOM 放入微任务中。
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
复制代码
timer
timers 阶段会执行 setTimeout
和 setInterval
一个 timer
指定的时间并非准确时间,而是在达到这个时间后尽快执行回调,可能会由于系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647]
,若是设定的时间不在这个范围,将被设置为1。
**I/O **
I/O 阶段会执行除了 close 事件,定时器和 setImmediate
的回调
idle, prepare
idle, prepare 阶段内部实现
poll
poll 阶段很重要,这一阶段中,系统会作两件事情
而且当 poll 中没有定时器的状况下,会发现如下两件事情
setImmediate
须要执行,poll 阶段会中止而且进入到 check 阶段执行 setImmediate
setImmediate
须要执行,会等待回调被加入到队列中并当即执行回调若是有别的定时器须要被执行,会回到 timer 阶段执行回调。
check
check 阶段执行 setImmediate
close callbacks
close callbacks 阶段执行 close 事件
而且在 Node 中,有些状况下的定时器执行顺序是随机的
setTimeout(() => {
console.log('setTimeout');
}, 0);
setImmediate(() => {
console.log('setImmediate');
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 由于可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 不然会执行 setTimeout
复制代码
固然在这种状况下,执行顺序是相同的
var fs = require('fs')
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
// 由于 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,因此会当即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 因此以上输出必定是 setImmediate,setTimeout
复制代码
上面介绍的都是 macrotask 的执行状况,microtask 会在以上每一个阶段完成后当即执行。
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
// 以上代码在浏览器和 node 中打印状况是不一样的
// 浏览器中必定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2
复制代码
Node 中的 process.nextTick
会先于其余 microtask 执行。
setTimeout(() => {
console.log("timer1");
Promise.resolve().then(function() {
console.log("promise1");
});
}, 0);
process.nextTick(() => {
console.log("nextTick");
});
// nextTick, timer1, promise1
复制代码
JS 是单线程的,因此 setTimeout
的偏差实际上是没法被彻底解决的,缘由有不少,多是回调中的,有多是浏览器中的各类事件致使。这也是为何页面开久了,定时器会不许的缘由,固然咱们能够经过必定的办法去减小这个偏差。
如下是一个相对准备的倒计时实现
var period = 60 * 1000 * 60 * 2
var startTime = new Date().getTime();
var count = 0
var end = new Date().getTime() + period
var interval = 1000
var currentInterval = interval
function loop() {
count++
var offset = new Date().getTime() - (startTime + count * interval); // 代码执行所消耗的时间
var diff = end - new Date().getTime()
var h = Math.floor(diff / (60 * 1000 * 60))
var hdiff = diff % (60 * 1000 * 60)
var m = Math.floor(hdiff / (60 * 1000))
var mdiff = hdiff % (60 * 1000)
var s = mdiff / (1000)
var sCeil = Math.ceil(s)
var sFloor = Math.floor(s)
currentInterval = interval - offset // 获得下一次循环所消耗的时间
console.log('时:'+h, '分:'+m, '毫秒:'+s, '秒向上取整:'+sCeil, '代码执行时间:'+offset, '下次循环间隔'+currentInterval) // 打印 时 分 秒 代码执行时间 下次循环间隔
setTimeout(loop, currentInterval)
}
setTimeout(loop, currentInterval)
复制代码
你是否在平常开发中遇到一个问题,在滚动事件中须要作个复杂计算或者实现一个按钮的防二次点击操做。
这些需求均可以经过函数防抖动来实现。尤为是第一个需求,若是在频繁的事件回调中作复杂计算,颇有可能致使页面卡顿,不如将屡次计算合并为一次计算,只在一个精确点作操做。
PS:防抖和节流的做用都是防止函数屡次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的间隔小于wait,防抖的状况下只会调用一次,而节流的 状况会每隔必定时间(参数wait)调用函数。
咱们先来看一个袖珍版的防抖理解一下防抖的实现:
// func是用户传入须要防抖的函数
// wait是等待时间
const debounce = (func, wait = 50) => {
// 缓存一个定时器id
let timer = 0
// 这里返回的函数是每次用户实际调用的防抖函数
// 若是已经设定过定时器了就清空上一次的定时器
// 开始一个新的定时器,延迟执行用户传入的方法
return function(...args) {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, wait)
}
}
// 不难看出若是用户调用该函数的间隔小于wait的状况下,上一次的时间还未到就被清除了,并不会执行函数
复制代码
这是一个简单版的防抖,可是有缺陷,这个防抖只能在最后调用。通常的防抖会有immediate选项,表示是否当即调用。这二者的区别,举个栗子来讲:
延迟执行
的防抖函数,它老是在一连串(间隔小于wait的)函数触发以后调用。当即执行
的防抖函数,它老是在第一次调用,而且下一次调用必须与前一次调用的时间间隔大于wait才会触发。下面咱们来实现一个带有当即执行选项的防抖函数
// 这个是用来获取当前时间戳的
function now() {
return +new Date()
}
/** * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否当即调用函数 * @return {function} 返回客户调用函数 */
function debounce (func, wait = 50, immediate = true) {
let timer, context, args
// 延迟执行函数
const later = () => setTimeout(() => {
// 延迟函数执行完毕,清空缓存的定时器序号
timer = null
// 延迟执行的状况下,函数会在延迟函数中执行
// 使用到以前缓存的参数和上下文
if (!immediate) {
func.apply(context, args)
context = args = null
}
}, wait)
// 这里返回的函数是每次实际调用的函数
return function(...params) {
// 若是没有建立延迟执行函数(later),就建立一个
if (!timer) {
timer = later()
// 若是是当即执行,调用函数
// 不然缓存参数和调用上下文
if (immediate) {
func.apply(this, params)
} else {
context = this
args = params
}
// 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个
// 这样作延迟函数会从新计时
} else {
clearTimeout(timer)
timer = later()
}
}
}
复制代码
总体函数实现的不难,总结一下。
null
,就能够再次点击了。[1, [2], 3].flatMap((v) => v + 1)
// -> [2, 3, 4]
复制代码
若是想将一个多维数组完全的降维,能够这样实现
const flattenDeep = (arr) => Array.isArray(arr)
? arr.reduce( (a, b) => [...a, ...flattenDeep(b)] , [])
: [arr]
flattenDeep([1, [[2], [3, [4]], 5]])
复制代码
这个问题一般能够经过 JSON.parse(JSON.stringify(object))
来解决。
let a = {
age: 1,
jobs: {
first: 'FE'
}
}
let b = JSON.parse(JSON.stringify(a))
a.jobs.first = 'native'
console.log(b.jobs.first) // FE
复制代码
可是该方法也是有局限性的:
undefined
symbol
let obj = {
a: 1,
b: {
c: 2,
d: 3,
},
}
obj.c = obj.b
obj.e = obj.a
obj.b.c = obj.c
obj.b.d = obj.b
obj.b.e = obj.b.c
let newObj = JSON.parse(JSON.stringify(obj))
console.log(newObj)
复制代码
若是你有这么一个循环引用对象,你会发现你不能经过该方法深拷贝
在遇到函数、 undefined
或者 symbol
的时候,该对象也不能正常的序列化
let a = {
age: undefined,
sex: Symbol('male'),
jobs: function() {},
name: 'yck'
}
let b = JSON.parse(JSON.stringify(a))
console.log(b) // {name: "yck"}
复制代码
你会发如今上述状况中,该方法会忽略掉函数和 undefined
。
可是在一般状况下,复杂数据都是能够序列化的,因此这个函数能够解决大部分问题,而且该函数是内置函数中处理深拷贝性能最快的。固然若是你的数据中含有以上三种状况下,能够使用 lodash 的深拷贝函数。
若是你所需拷贝的对象含有内置类型而且不包含函数,能够使用 MessageChannel
function structuralClone(obj) {
return new Promise(resolve => {
const {port1, port2} = new MessageChannel();
port2.onmessage = ev => resolve(ev.data);
port1.postMessage(obj);
});
}
var obj = {a: 1, b: {
c: b
}}
// 注意该方法是异步的
// 能够处理 undefined 和循环引用对象
(async () => {
const clone = await structuralClone(obj)
})()
复制代码
typeof
对于基本类型,除了 null
均可以显示正确的类型
typeof 1 // 'number'
typeof '1' // 'string'
typeof undefined // 'undefined'
typeof true // 'boolean'
typeof Symbol() // 'symbol'
typeof b // b 没有声明,可是还会显示 undefined
复制代码
typeof
对于对象,除了函数都会显示 object
typeof [] // 'object'
typeof {} // 'object'
typeof console.log // 'function'
复制代码
对于 null
来讲,虽然它是基本类型,可是会显示 object
,这是一个存在好久了的 Bug
typeof null // 'object'
复制代码
PS:为何会出现这种状况呢?由于在 JS 的最第一版本中,使用的是 32 位系统,为了性能考虑使用低位存储了变量的类型信息,000
开头表明是对象,然而 null
表示为全零,因此将它错误的判断为 object
。虽然如今的内部类型判断代码已经改变了,可是对于这个 Bug 倒是一直流传下来。
instanceof
能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的 prototype
。
咱们也能够试着实现一下 instanceof
function instanceof(left, right) {
// 得到类型的原型
let prototype = right.prototype
// 得到对象的原型
left = left.__proto__
// 判断对象的类型是否等于类型的原型
while (true) {
if (left === null)
return false
if (prototype === left)
return true
left = left.__proto__
}
}
复制代码
特性 | cookie | localStorage | sessionStorage | indexDB |
---|---|---|---|---|
数据生命周期 | 通常由服务器生成,能够设置过时时间 | 除非被清理,不然一直存在 | 页面关闭就清理 | 除非被清理,不然一直存在 |
数据存储大小 | 4K | 5M | 5M | 无限 |
与服务端通讯 | 每次都会携带在 header 中,对于请求性能影响 | 不参与 | 不参与 | 不参与 |
从上表能够看到,cookie
已经不建议用于存储。若是没有大量数据存储需求的话,能够使用 localStorage
和 sessionStorage
。对于不怎么改变的数据尽可能使用 localStorage
存储,不然能够用 sessionStorage
存储。
对于 cookie
,咱们还须要注意安全性。
属性 | 做用 |
---|---|
value | 若是用于保存用户登陆态,应该将该值加密,不能使用明文的用户标识 |
http-only | 不能经过 JS 访问 Cookie,减小 XSS 攻击 |
secure | 只能在协议为 HTTPS 的请求中携带 |
same-site | 规定浏览器不能在跨域请求中携带 Cookie,减小 CSRF 攻击 |
Load 事件触发表明页面中的 DOM,CSS,JS,图片已经所有加载完毕。
DOMContentLoaded 事件触发表明初始的 HTML 被彻底加载和解析,不须要等待 CSS,JS,图片加载。
由于浏览器出于安全考虑,有同源策略。也就是说,若是协议、域名或者端口有一个不一样就是跨域,Ajax 请求会失败。
咱们能够经过如下几种经常使用方法解决跨域的问题
JSONP 的原理很简单,就是利用 <script>
标签没有跨域限制的漏洞。经过 <script>
标签指向一个须要访问的地址并提供一个回调函数来接收数据当须要通信时。
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
function jsonp(data) {
console.log(data)
}
</script>
复制代码
JSONP 使用简单且兼容性不错,可是只限于 get
请求。
在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就须要本身封装一个 JSONP,如下是简单实现
function jsonp(url, jsonpCallback, success) {
let script = document.createElement("script");
script.src = url;
script.async = true;
script.type = "text/javascript";
window[jsonpCallback] = function(data) {
success && success(data);
};
document.body.appendChild(script);
}
jsonp(
"http://xxx",
"callback",
function(value) {
console.log(value);
}
);
复制代码
CORS须要浏览器和后端同时支持。IE 8 和 9 须要经过 XDomainRequest
来实现。
浏览器会自动进行 CORS 通讯,实现CORS通讯的关键是后端。只要后端实现了 CORS,就实现了跨域。
服务端设置 Access-Control-Allow-Origin
就能够开启 CORS。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源。
该方式只能用于二级域名相同的状况下,好比 a.test.com
和 b.test.com
适用于该方式。
只须要给页面添加 document.domain = 'test.com'
表示二级域名都相同就能够实现跨域
这种方式一般用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另外一个页面判断来源并接收消息
// 发送消息端
window.parent.postMessage('message', 'http://test.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://test.com') {
console.log('验证经过')
}
});
复制代码
若是一个节点中的子节点是动态生成的,那么子节点须要注册事件的话应该注册在父节点上
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script> let ul = document.querySelector('#ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script>
复制代码
事件代理的方式相对于直接给目标注册事件来讲,有如下优势
Service workers 本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步API。
目前该技术一般用来作缓存文件,提升首屏速度,能够试着来实现这个功能。
// index.js
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("sw.js")
.then(function(registration) {
console.log("service worker 注册成功");
})
.catch(function(err) {
console.log("servcie worker 注册失败");
});
}
// sw.js
// 监听 `install` 事件,回调中缓存所需文件
self.addEventListener("install", e => {
e.waitUntil(
caches.open("my-cache").then(function(cache) {
return cache.addAll(["./index.html", "./index.js"]);
})
);
});
// 拦截全部请求事件
// 若是缓存中已经有请求的数据就直接用缓存,不然去请求数据
self.addEventListener("fetch", e => {
e.respondWith(
caches.match(e.request).then(function(response) {
if (response) {
return response;
}
console.log("fetch source");
})
);
});
复制代码
打开页面,能够在开发者工具中的 Application
看到 Service Worker 已经启动了
缓存对于前端性能优化来讲是个很重要的点,良好的缓存策略能够下降资源的重复加载提升网页的总体加载速度。
一般浏览器缓存策略分为两种:强缓存和协商缓存。
实现强缓存能够经过两种响应头实现:Expires
和 Cache-Control
。强缓存表示在缓存期间不须要请求,state code
为 200
Expires: Wed, 22 Oct 2018 08:41:00 GMT
复制代码
Expires
是 HTTP / 1.0 的产物,表示资源会在 Wed, 22 Oct 2018 08:41:00 GMT
后过时,须要再次请求。而且 Expires
受限于本地时间,若是修改了本地时间,可能会形成缓存失效。
Cache-control: max-age=30
复制代码
Cache-Control
出现于 HTTP / 1.1,优先级高于 Expires
。该属性表示资源会在 30 秒后过时,须要再次请求。
若是缓存过时了,咱们就能够使用协商缓存来解决问题。协商缓存须要请求,若是缓存有效会返回 304。
协商缓存须要客户端和服务端共同实现,和强缓存同样,也有两种实现方式。
Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将 Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来。
可是若是在本地打开缓存文件,就会形成 Last-Modified
被修改,因此在 HTTP / 1.1 出现了 ETag
。
ETag
相似于文件指纹,If-None-Match
会将当前 ETag
发送给服务器,询问该资源 ETag
是否变更,有变更的话就将新的资源发送回来。而且 ETag
优先级比 Last-Modified
高。
对于大部分的场景均可以使用强缓存配合协商缓存解决,可是在一些特殊的地方可能须要选择特殊的缓存策略
Cache-control: no-store
,表示该资源不须要缓存Cache-Control: no-cache
并配合 ETag
使用,表示该资源已被缓存,可是每次都会发送请求询问资源是否更新。Cache-Control: max-age=31536000
并配合策略缓存使用,而后对文件进行指纹处理,一旦文件名变更就会马上下载新的文件。重绘和回流是渲染步骤中的一小节,可是这两个步骤对于性能影响很大。
color
就叫称为重绘回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流。
因此如下几个动做可能会致使性能问题:
不少人不知道的是,重绘和回流其实和 Event loop 有关。
resize
或者 scroll
,有的话会去触发事件,因此 resize
和 scroll
事件也是至少 16ms 才会触发一次,而且自带节流功能。requestAnimationFrame
回调IntersectionObserver
回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差requestIdleCallback
回调。以上内容来自于 HTML 文档
使用 translate
替代 top
<div class="test"></div>
<style> .test { position: absolute; top: 10px; width: 100px; height: 100px; background: red; } </style>
<script> setTimeout(() => { // 引发回流 document.querySelector('.test').style.top = '100px' }, 1000) </script>
复制代码
使用 visibility
替换 display: none
,由于前者只会引发重绘,后者会引起回流(改变了布局)
把 DOM 离线后修改,好比:先把 DOM 给 display:none
(有一次 Reflow),而后你修改100次,而后再把它显示出来
不要把 DOM 结点的属性值放在一个循环里当成循环里的变量
for(let i = 0; i < 1000; i++) {
// 获取 offsetTop 会致使回流,由于须要去获取正确的值
console.log(document.querySelector('.test').style.offsetTop)
}
复制代码
不要使用 table 布局,可能很小的一个小改动会形成整个 table 的从新布局
动画实现的速度的选择,动画速度越快,回流次数越多,也能够选择使用 requestAnimationFrame
CSS 选择符从右往左匹配查找,避免 DOM 深度过深
将频繁运行的动画变为图层,图层可以阻止该节点回流影响别的元素。好比对于 video
标签,浏览器会自动将该节点变为图层。
对于一张 100 * 100 像素的图片来讲,图像上有 10000 个像素点,若是每一个像素的值是 RGBA 存储的话,那么也就是说每一个像素有 4 个通道,每一个通道 1 个字节(8 位 = 1个字节),因此该图片大小大概为 39KB(10000 * 1 * 4 / 1024)。
可是在实际项目中,一张图片可能并不须要使用那么多颜色去显示,咱们能够经过减小每一个像素的调色板来相应缩小图片的大小。
了解了如何计算图片大小的知识,那么对于如何优化图片,想必你们已经有 2 个思路了:
head
中script
标签放在 body
底部,由于 JS 文件执行会阻塞渲染。固然也能够把 script
标签放在任意位置而后加上 defer
,表示该文件会并行下载,可是会放到 HTML 解析完成后顺序执行。对于没有任何依赖的 JS 文件能够加上 async
,表示加载和渲染后续文档元素的过程将和 JS 文件的加载与执行并行无序进行。Webworker
。Webworker
可让咱们另开一个线程执行脚本而不影响渲染。静态资源尽可能使用 CDN 加载,因为浏览器对于单个域名有并发请求上限,能够考虑使用多个 CDN 域名。对于 CDN 加载静态资源须要注意 CDN 域名要与主站不一样,不然每次请求都会带上主站的 Cookie。
本质就是编译器,当代码转为字符串生成 AST,对 AST 进行转变最后再生成新的代码
好比你想实现一个编译结束退出命令的插件
class BuildEndPlugin {
apply (compiler) {
const afterEmit = (compilation, cb) => {
cb()
setTimeout(function () {
process.exit(0)
}, 1000)
}
compiler.plugin('after-emit', afterEmit)
}
}
module.exports = BuildEndPlugin
复制代码
在 V16 版本中引入了 Fiber 机制。这个机制必定程度上的影响了部分生命周期的调用,而且也引入了新的 2 个 API 来解决问题。
在以前的版本中,若是你拥有一个很复杂的复合组件,而后改动了最上层组件的 state
,那么调用栈可能会很长
调用栈过长,再加上中间进行了复杂的操做,就可能致使长时间阻塞主线程,带来很差的用户体验。Fiber 就是为了解决该问题而生。
Fiber 本质上是一个虚拟的堆栈帧,新的调度器会按照优先级自由调度这些帧,从而将以前的同步渲染改为了异步渲染,在不影响体验的状况下去分段计算更新。
对于如何区别优先级,React 有本身的一套逻辑。对于动画这种实时性很高的东西,也就是 16 ms 必须渲染一次保证不卡顿的状况下,React 会每 16 ms(之内) 暂停一下更新,返回来继续渲染动画。
对于异步渲染,如今渲染有两个阶段:reconciliation
和 commit
。前者过程是能够打断的,后者不能暂停,会一直更新界面直到完成。
Reconciliation 阶段
componentWillMount
componentWillReceiveProps
shouldComponentUpdate
componentWillUpdate
Commit 阶段
componentDidMount
componentDidUpdate
componentWillUnmount
由于 reconciliation
阶段是能够被打断的,因此 reconciliation
阶段会执行的生命周期函数就可能会出现调用屡次的状况,从而引发 Bug。因此对于 reconciliation
阶段调用的几个函数,除了 shouldComponentUpdate
之外,其余都应该避免去使用,而且 V16 中也引入了新的 API 来解决这个问题。
getDerivedStateFromProps
用于替换 componentWillReceiveProps
,该函数会在初始化和 update
时被调用
class ExampleComponent extends React.Component {
// Initialize state in constructor,
// Or with a property initializer.
state = {};
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.someMirroredValue !== nextProps.someValue) {
return {
derivedData: computeDerivedState(nextProps),
someMirroredValue: nextProps.someValue
};
}
// Return null to indicate no change to state.
return null;
}
}
复制代码
getSnapshotBeforeUpdate
用于替换 componentWillUpdate
,该函数会在 update
后 DOM 更新前被调用,用于读取最新的 DOM 数据。
class ExampleComponent extends React.Component {
// 用于初始化 state
constructor() {}
// 用于替换 `componentWillReceiveProps` ,该函数会在初始化和 `update` 时被调用
// 由于该函数是静态函数,因此取不到 `this`
// 若是须要对比 `prevProps` 须要单独在 `state` 中维护
static getDerivedStateFromProps(nextProps, prevState) {}
// 判断是否须要更新组件,多用于组件性能优化
shouldComponentUpdate(nextProps, nextState) {}
// 组件挂载后调用
// 能够在该函数中进行请求或者订阅
componentDidMount() {}
// 用于得到最新的 DOM 数据
getSnapshotBeforeUpdate() {}
// 组件即将销毁
// 能够在此处移除订阅,定时器等等
componentWillUnmount() {}
// 组件销毁后调用
componentDidUnMount() {}
// 组件更新后调用
componentDidUpdate() {}
// 渲染组件函数
render() {}
// 如下函数不建议使用
UNSAFE_componentWillMount() {}
UNSAFE_componentWillUpdate(nextProps, nextState) {}
UNSAFE_componentWillReceiveProps(nextProps) {}
}
复制代码
setState
在 React 中是常用的一个 API,可是它存在一些问题,可能会致使犯错,核心缘由就是由于这个 API 是异步的。
首先 setState
的调用并不会立刻引发 state
的改变,而且若是你一次调用了多个 setState
,那么结果可能并不如你期待的同样。
handle() {
// 初始化 `count` 为 0
console.log(this.state.count) // -> 0
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
this.setState({ count: this.state.count + 1 })
console.log(this.state.count) // -> 0
}
复制代码
第一,两次的打印都为 0,由于 setState
是个异步 API,只有同步代码运行完毕才会执行。setState
异步的缘由我认为在于,setState
可能会致使 DOM 的重绘,若是调用一次就立刻去进行重绘,那么调用屡次就会形成没必要要的性能损失。设计成异步的话,就能够将屡次调用放入一个队列中,在恰当的时候统一进行更新过程。
第二,虽然调用了三次 setState
,可是 count
的值仍是为 1。由于屡次调用会合并为一次,只有当更新结束后 state
才会改变,三次调用等同于以下代码
Object.assign(
{},
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
{ count: this.state.count + 1 },
)
复制代码
固然你也能够经过如下方式来实现调用三次 setState
使得 count
为 3
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
this.setState((prevState) => ({ count: prevState.count + 1 }))
}
复制代码
若是你想在每次调用 setState
后得到正确的 state
,能够经过以下代码实现
handle() {
this.setState((prevState) => ({ count: prevState.count + 1 }), () => {
console.log(this.state)
})
}
复制代码
nextTick
可让咱们在下次 DOM 更新循环结束以后执行延迟回调,用于得到更新后的 DOM。
在 Vue 2.4 以前都是使用的 microtasks,可是 microtasks 的优先级太高,在某些状况下可能会出现比事件冒泡更快的状况,但若是都使用 macrotasks 又可能会出现渲染的性能问题。因此在新版本中,会默认使用 microtasks,但在特殊状况下会使用 macrotasks,好比 v-on。
对于实现 macrotasks ,会先判断是否能使用 setImmediate
,不能的话降级为 MessageChannel
,以上都不行的话就使用 setTimeout
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
macroTimerFunc = () => {
setImmediate(flushCallbacks)
}
} else if (
typeof MessageChannel !== 'undefined' &&
(isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]')
) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = flushCallbacks
macroTimerFunc = () => {
port.postMessage(1)
}
} else {
/* istanbul ignore next */
macroTimerFunc = () => {
setTimeout(flushCallbacks, 0)
}
}
复制代码
nextTick
同时也支持 Promise 的使用,会判断是否实现了 Promise
export function nextTick(cb?: Function, ctx?: Object) {
let _resolve
// 将回调函数整合进一个数组中
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
if (useMacroTask) {
macroTimerFunc()
} else {
microTimerFunc()
}
}
// 判断是否能够使用 Promise
// 能够的话给 _resolve 赋值
// 这样回调函数就能以 promise 的方式调用
if (!cb && typeof Promise !== 'undefined') {
return new Promise(resolve => {
_resolve = resolve
})
}
}
复制代码
生命周期函数就是组件在初始化或者数据更新时会触发的钩子函数。
在初始化时,会调用如下代码,生命周期就是经过 callHook
调用的
Vue.prototype._init = function(options) {
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate') // 拿不到 props data
initInjections(vm)
initState(vm)
initProvide(vm)
callHook(vm, 'created')
}
复制代码
能够发如今以上代码中,beforeCreate
调用的时候,是获取不到 props 或者 data 中的数据的,由于这些数据的初始化都在 initState
中。
接下来会执行挂载函数
export function mountComponent {
callHook(vm, 'beforeMount')
// ...
if (vm.$vnode == null) {
vm._isMounted = true
callHook(vm, 'mounted')
}
}
复制代码
beforeMount
就是在挂载前执行的,而后开始建立 VDOM 并替换成真实 DOM,最后执行 mounted
钩子。这里会有个判断逻辑,若是是外部 new Vue({})
的话,不会存在 $vnode
,因此直接执行 mounted
钩子了。若是有子组件的话,会递归挂载子组件,只有当全部子组件所有挂载完毕,才会执行根组件的挂载钩子。
接下来是数据更新时会调用的钩子函数
function flushSchedulerQueue() {
// ...
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
if (watcher.before) {
watcher.before() // 调用 beforeUpdate
}
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' +
(watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`),
watcher.vm
)
break
}
}
}
callUpdatedHooks(updatedQueue)
}
function callUpdatedHooks(queue) {
let i = queue.length
while (i--) {
const watcher = queue[i]
const vm = watcher.vm
if (vm._watcher === watcher && vm._isMounted) {
callHook(vm, 'updated')
}
}
}
复制代码
上图还有两个生命周期没有说,分别为 activated
和 deactivated
,这两个钩子函数是 keep-alive
组件独有的。用 keep-alive
包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated
钩子函数,命中缓存渲染后会执行 actived
钩子函数。
最后就是销毁组件的钩子函数了
Vue.prototype.$destroy = function() {
// ...
callHook(vm, 'beforeDestroy')
vm._isBeingDestroyed = true
// remove self from parent
const parent = vm.$parent
if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
remove(parent.$children, vm)
}
// teardown watchers
if (vm._watcher) {
vm._watcher.teardown()
}
let i = vm._watchers.length
while (i--) {
vm._watchers[i].teardown()
}
// remove reference from data ob
// frozen object may not have observer.
if (vm._data.__ob__) {
vm._data.__ob__.vmCount--
}
// call the last hook...
vm._isDestroyed = true
// invoke destroy hooks on current rendered tree
vm.__patch__(vm._vnode, null)
// fire destroyed hook
callHook(vm, 'destroyed')
// turn off all instance listeners.
vm.$off()
// remove __vue__ reference
if (vm.$el) {
vm.$el.__vue__ = null
}
// release circular reference (#6759)
if (vm.$vnode) {
vm.$vnode.parent = null
}
}
复制代码
在执行销毁操做前会调用 beforeDestroy
钩子函数,而后进行一系列的销毁操做,若是有子组件的话,也会递归销毁子组件,全部子组件都销毁完毕后才会执行根组件的 destroyed
钩子函数。
v-model
实现父传子,子传父。由于 v-model 默认解析成 :value 和 :inputprops
$children
访问子组件数组,注意该数组乱序v-bind={$attrs}
,经过对象的方式筛选出父组件中传入但子组件不须要的 props.native
修饰器的) v-on
事件监听器。$emit
触发props
$parent
访问父组件前端路由实现起来其实很简单,本质就是监听 URL 的变化,而后匹配路由规则,显示相应的页面,而且无须刷新。目前单页面使用的路由就只有两种实现方式
www.test.com/#/
就是 Hash URL,当 #
后面的哈希值发生变化时,不会向服务器请求数据,能够经过 hashchange
事件来监听到 URL 的变化,从而进行跳转页面。
History 模式是 HTML5 新推出的功能,比之 Hash URL 更加美观
MVVM 由如下三个内容组成
在 JQuery 时期,若是须要刷新 UI 时,须要先取到对应的 DOM 再更新 UI,这样数据和业务的逻辑就和页面有强耦合。
在 MVVM 中,UI 是经过数据驱动的,数据一旦改变就会相应的刷新对应的 UI,UI 若是改变,也会改变对应的数据。这种方式就能够在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel 只关心数据和业务的处理,不关心 View 如何处理数据,在这种状况下,View 和 Model 均可以独立出来,任何一方改变了也不必定须要改变另外一方,而且能够将一些可复用的逻辑放在一个 ViewModel 中,让多个 View 复用这个 ViewModel。
在 MVVM 中,最核心的也就是数据双向绑定,例如 Angluar 的脏数据检测,Vue 中的数据劫持。
当触发了指定事件后会进入脏数据检测,这时会调用 $digest
循环遍历全部的数据观察者,判断当前值是否和先前的值有区别,若是检测到变化的话,会调用 $watch
函数,而后再次调用 $digest
循环直到发现没有变化。循环至少为二次 ,至多为十次。
脏数据检测虽然存在低效的问题,可是不关心数据是经过什么方式改变的,均可以完成任务,可是这在 Vue 中的双向绑定是存在问题的。而且脏数据检测能够实现批量检测出更新的值,再去统一更新 UI,大大减小了操做 DOM 的次数。因此低效也是相对的,这就仁者见仁智者见智了。
Vue 内部使用了 Object.defineProperty()
来实现双向绑定,经过这个函数能够监听到 set
和 get
的事件。
var data = { name: 'yck' }
observe(data)
let name = data.name // -> get value
data.name = 'yyy' // -> change value
function observe(obj) {
// 判断类型
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
}
})
}
复制代码
以上代码简单的实现了如何监听数据的 set
和 get
的事件,可是仅仅如此是不够的,还须要在适当的时候给属性添加发布订阅
<div>
{{name}}
</div>
复制代码
在解析如上模板代码时,遇到 {{name}}
就会给属性 name
添加发布订阅。
// 经过 Dep 解耦
class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
// sub 是 Watcher 实例
this.subs.push(sub)
}
notify() {
this.subs.forEach(sub => {
sub.update()
})
}
}
// 全局属性,经过该属性配置 Watcher
Dep.target = null
function update(value) {
document.querySelector('div').innerText = value
}
class Watcher {
constructor(obj, key, cb) {
// 将 Dep.target 指向本身
// 而后触发属性的 getter 添加监听
// 最后将 Dep.target 置空
Dep.target = this
this.cb = cb
this.obj = obj
this.key = key
this.value = obj[key]
Dep.target = null
}
update() {
// 得到新值
this.value = this.obj[this.key]
// 调用 update 方法更新 Dom
this.cb(this.value)
}
}
var data = { name: 'yck' }
observe(data)
// 模拟解析到 `{{name}}` 触发的操做
new Watcher(data, 'name', update)
// update Dom innerText
data.name = 'yyy'
复制代码
接下来,对 defineReactive
函数进行改造
function defineReactive(obj, key, val) {
// 递归子属性
observe(val)
let dp = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
console.log('get value')
// 将 Watcher 添加到订阅
if (Dep.target) {
dp.addSub(Dep.target)
}
return val
},
set: function reactiveSetter(newVal) {
console.log('change value')
val = newVal
// 执行 watcher 的 update 方法
dp.notify()
}
})
}
复制代码
以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的 getter 来实现发布订阅的添加。
Object.defineProperty
虽然已经可以实现双向绑定了,可是他仍是有缺陷的。
虽然 Vue 中确实能检测到数组数据的变化,可是实际上是使用了 hack 的办法,而且也是有缺陷的。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
// hack 如下几个函数
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
// 得到原生函数
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
// 调用原生函数
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// 触发更新
ob.dep.notify()
return result
})
})
复制代码
反观 Proxy 就没以上的问题,原生支持监听数组变化,而且能够直接对整个对象进行拦截,因此 Vue 也将在下个大版本中使用 Proxy 替换 Object.defineProperty
let onWatch = (obj, setBind, getLogger) => {
let handler = {
get(target, property, receiver) {
getLogger(target, property)
return Reflect.get(target, property, receiver);
},
set(target, property, value, receiver) {
setBind(value);
return Reflect.set(target, property, value);
}
};
return new Proxy(obj, handler);
};
let obj = { a: 1 }
let value
let p = onWatch(obj, (v) => {
value = v
}, (target, property) => {
console.log(`Get '${property}' = ${target[property]}`);
})
p.a = 2 // bind `value` to `2`
p.a // -> Get 'a' = 2
复制代码
虚拟 DOM 涉及的内容不少,具体能够参考我以前 写的文章
在 TCP 协议中,主动发起请求的一端为客户端,被动链接的一端称为服务端。无论是客户端仍是服务端,TCP 链接创建完后都能发送和接收数据,因此 TCP 也是一个全双工的协议。
起初,两端都为 CLOSED 状态。在通讯开始前,双方都会建立 TCB。 服务器建立完 TCB 后遍进入 LISTEN 状态,此时开始等待客户端发送数据。
第一次握手
客户端向服务端发送链接请求报文段。该报文段中包含自身的数据通信初始序号。请求发送后,客户端便进入 SYN-SENT 状态,x
表示客户端的数据通讯初始序号。
第二次握手
服务端收到链接请求报文段后,若是赞成链接,则会发送一个应答,该应答中也会包含自身的数据通信初始序号,发送完成后便进入 SYN-RECEIVED 状态。
第三次握手
当客户端收到链接赞成的应答后,还要向服务端发送一个确认报文。客户端发完这个报文段后便进入ESTABLISHED 状态,服务端收到这个应答后也进入 ESTABLISHED 状态,此时链接创建成功。
PS:第三次握手能够包含数据,经过 TCP 快速打开(TFO)技术。其实只要涉及到握手的协议,均可以使用相似 TFO 的方式,客户端和服务端存储相同 cookie,下次握手时发出 cookie 达到减小 RTT 的目的。
你是否有疑惑明明两次握手就能够创建起链接,为何还须要第三次应答?
由于这是为了防止失效的链接请求报文段被服务端接收,从而产生错误。
能够想象以下场景。客户端发送了一个链接请求 A,可是由于网络缘由形成了超时,这时 TCP 会启动超时重传的机制再次发送一个链接请求 B。此时请求顺利到达服务端,服务端应答完就创建了请求。若是链接请求 A 在两端关闭后终于抵达了服务端,那么这时服务端会认为客户端又须要创建 TCP 链接,从而应答了该请求并进入 ESTABLISHED 状态。此时客户端实际上是 CLOSED 状态,那么就会致使服务端一直等待,形成资源的浪费。
PS:在创建链接中,任意一端掉线,TCP 都会重发 SYN 包,通常会重试五次,在创建链接中可能会遇到 SYN FLOOD 攻击。遇到这种状况你能够选择调低重试次数或者干脆在不能处理的状况下拒绝请求。
拥塞处理和流量控制不一样,后者是做用于接收方,保证接收方来得及接受数据。而前者是做用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的状况。
拥塞处理包括了四个算法,分别为:慢开始,拥塞避免,快速重传,快速恢复。
慢开始算法,顾名思义,就是在传输开始时将发送窗口慢慢指数级扩大,从而避免一开始就传输大量数据致使网络拥塞。
慢开始算法步骤具体以下
拥塞避免算法相比简单点,每过一个 RTT 窗口大小只加一,这样可以避免指数级增加致使网络拥塞,慢慢将大小调整到最佳值。
在传输过程当中可能定时器超时的状况,这时候 TCP 会认为网络拥塞了,会立刻进行如下步骤:
快速重传通常和快恢复一块儿出现。一旦接收端收到的报文出现失序的状况,接收端只会回复最后一个顺序正确的报文序号(没有 Sack 的状况下)。若是收到三个重复的 ACK,无需等待定时器超时再重发而是启动快速重传。具体算法分为两种:
TCP Taho 实现以下
TCP Reno 实现以下
TCP New Reno 算法改进了以前 TCP Reno 算法的缺陷。在以前,快恢复中只要收到一个新的 ACK 包,就会退出快恢复。
在 TCP New Reno 中,TCP 发送方先记下三个重复 ACK 的分段的最大序号。
假如我有一个分段数据是 1 ~ 10 这十个序号的报文,其中丢失了序号为 3 和 7 的报文,那么该分段的最大序号就是 10。发送端只会收到 ACK 序号为 3 的应答。这时候重发序号为 3 的报文,接收方顺利接收并会发送 ACK 序号为 7 的应答。这时候 TCP 知道对端是有多个包未收到,会继续发送序号为 7 的报文,接收方顺利接收并会发送 ACK 序号为 11 的应答,这时发送端认为这个分段接收端已经顺利接收,接下来会退出快恢复阶段。
HTTPS 仍是经过了 HTTP 来传输信息,可是信息经过 TLS 协议进行了加密。
TLS 协议位于传输层之上,应用层之下。首次进行 TLS 协议传输须要两个 RTT ,接下来能够经过 Session Resumption 减小到一个 RTT。
在 TLS 中使用了两种加密技术,分别为:对称加密和非对称加密。
对称加密:
对称加密就是两边拥有相同的秘钥,两边都知道如何将密文加密解密。
非对称加密:
有公钥私钥之分,公钥全部人均可以知道,能够将数据用公钥加密,可是将数据解密必须使用私钥解密,私钥只有分发公钥的一方才知道。
TLS 握手过程以下图:
经过以上步骤可知,在 TLS 握手阶段,两端使用非对称加密的方式来通讯,可是由于非对称加密损耗的性能比对称加密大,因此在正式传输数据时,两端使用对称加密的方式通讯。
PS:以上说明的都是 TLS 1.2 协议的握手状况,在 1.3 协议中,首次创建链接只须要一个 RTT,后面恢复链接不须要 RTT 了。
script
标签的话,会判断是否存在 async
或者 defer
,前者会并行进行下载并执行 JS,后者会先下载文件,而后等待 HTML 解析完成后顺序执行,若是以上都没有,就会阻塞住渲染流程直到 JS 执行完毕。遇到文件下载的会去下载文件,这里若是使用 HTTP 2.0 协议的话会极大的提升多图的下载效率。DOMContentLoaded
事件2XX 成功
3XX 重定向
4XX 客户端错误
5XX 服务器错误
如下两个函数是排序中会用到的通用函数,就不一一写了
function checkArray(array) {
if (!array || array.length <= 2) return
}
function swap(array, left, right) {
let rightValue = array[right]
array[right] = array[left]
array[left] = rightValue
}
复制代码
冒泡排序的原理以下,从第一个元素开始,把当前元素和下一个索引元素进行比较。若是当前元素大,那么就交换位置,重复操做直到比较到最后一个元素,那么此时最后一个元素就是该数组中最大的数。下一轮重复以上操做,可是此时最后一个元素已是最大数了,因此不须要再比较最后一个元素,只须要比较到 length - 1
的位置。
如下是实现该算法的代码
function bubble(array) {
checkArray(array);
for (let i = array.length - 1; i > 0; i--) {
// 从 0 到 `length - 1` 遍历
for (let j = 0; j < i; j++) {
if (array[j] > array[j + 1]) swap(array, j, j + 1)
}
}
return array;
}
复制代码
该算法的操做次数是一个等差数列 n + (n - 1) + (n - 2) + 1
,去掉常数项之后得出时间复杂度是 O(n * n)
插入排序的原理以下。第一个元素默认是已排序元素,取出下一个元素和当前元素比较,若是当前元素大就交换位置。那么此时第一个元素就是当前的最小数,因此下次取出操做从第三个元素开始,向前对比,重复以前的操做。
如下是实现该算法的代码
function insertion(array) {
checkArray(array);
for (let i = 1; i < array.length; i++) {
for (let j = i - 1; j >= 0 && array[j] > array[j + 1]; j--)
swap(array, j, j + 1);
}
return array;
}
复制代码
该算法的操做次数是一个等差数列 n + (n - 1) + (n - 2) + 1
,去掉常数项之后得出时间复杂度是 O(n * n)
选择排序的原理以下。遍历数组,设置最小值的索引为 0,若是取出的值比当前最小值小,就替换最小值索引,遍历完成后,将第一个元素和最小值索引上的值交换。如上操做后,第一个元素就是数组中的最小值,下次遍历就能够从索引 1 开始重复上述操做。
如下是实现该算法的代码
function selection(array) {
checkArray(array);
for (let i = 0; i < array.length - 1; i++) {
let minIndex = i;
for (let j = i + 1; j < array.length; j++) {
minIndex = array[j] < array[minIndex] ? j : minIndex;
}
swap(array, i, minIndex);
}
return array;
}
复制代码
该算法的操做次数是一个等差数列 n + (n - 1) + (n - 2) + 1
,去掉常数项之后得出时间复杂度是 O(n * n)
归并排序的原理以下。递归的将数组两两分开直到最多包含两个元素,而后将数组排序合并,最终合并为排序好的数组。假设我有一组数组 [3, 1, 2, 8, 9, 7, 6]
,中间数索引是 3,先排序数组 [3, 1, 2, 8]
。在这个左边数组上,继续拆分直到变成数组包含两个元素(若是数组长度是奇数的话,会有一个拆分数组只包含一个元素)。而后排序数组 [3, 1]
和 [2, 8]
,而后再排序数组 [1, 3, 2, 8]
,这样左边数组就排序完成,而后按照以上思路排序右边数组,最后将数组 [1, 2, 3, 8]
和 [6, 7, 9]
排序。
如下是实现该算法的代码
function sort(array) {
checkArray(array);
mergeSort(array, 0, array.length - 1);
return array;
}
function mergeSort(array, left, right) {
// 左右索引相同说明已经只有一个数
if (left === right) return;
// 等同于 `left + (right - left) / 2`
// 相比 `(left + right) / 2` 来讲更加安全,不会溢出
// 使用位运算是由于位运算比四则运算快
let mid = parseInt(left + ((right - left) >> 1));
mergeSort(array, left, mid);
mergeSort(array, mid + 1, right);
let help = [];
let i = 0;
let p1 = left;
let p2 = mid + 1;
while (p1 <= mid && p2 <= right) {
help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];
}
while (p1 <= mid) {
help[i++] = array[p1++];
}
while (p2 <= right) {
help[i++] = array[p2++];
}
for (let i = 0; i < help.length; i++) {
array[left + i] = help[i];
}
return array;
}
复制代码
以上算法使用了递归的思想。递归的本质就是压栈,每递归执行一次函数,就将该函数的信息(好比参数,内部的变量,执行到的行数)压栈,直到遇到终止条件,而后出栈并继续执行函数。对于以上递归函数的调用轨迹以下
mergeSort(data, 0, 6) // mid = 3
mergeSort(data, 0, 3) // mid = 1
mergeSort(data, 0, 1) // mid = 0
mergeSort(data, 0, 0) // 遇到终止,回退到上一步
mergeSort(data, 1, 1) // 遇到终止,回退到上一步
// 排序 p1 = 0, p2 = mid + 1 = 1
// 回退到 `mergeSort(data, 0, 3)` 执行下一个递归
mergeSort(2, 3) // mid = 2
mergeSort(3, 3) // 遇到终止,回退到上一步
// 排序 p1 = 2, p2 = mid + 1 = 3
// 回退到 `mergeSort(data, 0, 3)` 执行合并逻辑
// 排序 p1 = 0, p2 = mid + 1 = 2
// 执行完毕回退
// 左边数组排序完毕,右边也是如上轨迹
复制代码
该算法的操做次数是能够这样计算:递归了两次,每次数据量是数组的一半,而且最后把整个数组迭代了一次,因此得出表达式 2T(N / 2) + T(N)
(T 表明时间,N 表明数据量)。根据该表达式能够套用 该公式 得出时间复杂度为 O(N * logN)
快排的原理以下。随机选取一个数组中的值做为基准值,从左至右取值与基准值对比大小。比基准值小的放数组左边,大的放右边,对比完成后将基准值和第一个比基准值大的值交换位置。而后将数组以基准值的位置分为两部分,继续递归以上操做。
如下是实现该算法的代码
function sort(array) {
checkArray(array);
quickSort(array, 0, array.length - 1);
return array;
}
function quickSort(array, left, right) {
if (left < right) {
swap(array, , right)
// 随机取值,而后和末尾交换,这样作比固定取一个位置的复杂度略低
let indexs = part(array, parseInt(Math.random() * (right - left + 1)) + left, right);
quickSort(array, left, indexs[0]);
quickSort(array, indexs[1] + 1, right);
}
}
function part(array, left, right) {
let less = left - 1;
let more = right;
while (left < more) {
if (array[left] < array[right]) {
// 当前值比基准值小,`less` 和 `left` 都加一
++less;
++left;
} else if (array[left] > array[right]) {
// 当前值比基准值大,将当前值和右边的值交换
// 而且不改变 `left`,由于当前换过来的值尚未判断过大小
swap(array, --more, left);
} else {
// 和基准值相同,只移动下标
left++;
}
}
// 将基准值和比基准值大的第一个值交换位置
// 这样数组就变成 `[比基准值小, 基准值, 比基准值大]`
swap(array, right, more);
return [less, more];
}
复制代码
该算法的复杂度和归并排序是相同的,可是额外空间复杂度比归并排序少,只需 O(logN),而且相比归并排序来讲,所需的常数时间也更少。
Sort Colors:该题目来自 LeetCode,题目须要咱们将 [2,0,2,1,1,0]
排序成 [0,0,1,1,2,2]
,这个问题就能够使用三路快排的思想。
如下是代码实现
var sortColors = function(nums) {
let left = -1;
let right = nums.length;
let i = 0;
// 下标若是遇到 right,说明已经排序完成
while (i < right) {
if (nums[i] == 0) {
swap(nums, i++, ++left);
} else if (nums[i] == 1) {
i++;
} else {
swap(nums, i, --right);
}
}
};
复制代码
Kth Largest Element in an Array:该题目来自 LeetCode,题目须要找出数组中第 K 大的元素,这问题也能够使用快排的思路。而且由于是找出第 K 大元素,因此在分离数组的过程当中,能够找出须要的元素在哪边,而后只须要排序相应的一边数组就好。
如下是代码实现
var findKthLargest = function(nums, k) {
let l = 0
let r = nums.length - 1
// 得出第 K 大元素的索引位置
k = nums.length - k
while (l < r) {
// 分离数组后得到比基准树大的第一个元素索引
let index = part(nums, l, r)
// 判断该索引和 k 的大小
if (index < k) {
l = index + 1
} else if (index > k) {
r = index - 1
} else {
break
}
}
return nums[k]
};
function part(array, left, right) {
let less = left - 1;
let more = right;
while (left < more) {
if (array[left] < array[right]) {
++less;
++left;
} else if (array[left] > array[right]) {
swap(array, --more, left);
} else {
left++;
}
}
swap(array, right, more);
return more;
}
复制代码
堆排序利用了二叉堆的特性来作,二叉堆一般用数组表示,而且二叉堆是一颗彻底二叉树(全部叶节点(最底层的节点)都是从左往右顺序排序,而且其余层的节点都是满的)。二叉堆又分为大根堆与小根堆。
堆排序的原理就是组成一个大根堆或者小根堆。以小根堆为例,某个节点的左边子节点索引是 i * 2 + 1
,右边是 i * 2 + 2
,父节点是 (i - 1) /2
。
如下是实现该算法的代码
function heap(array) {
checkArray(array);
// 将最大值交换到首位
for (let i = 0; i < array.length; i++) {
heapInsert(array, i);
}
let size = array.length;
// 交换首位和末尾
swap(array, 0, --size);
while (size > 0) {
heapify(array, 0, size);
swap(array, 0, --size);
}
return array;
}
function heapInsert(array, index) {
// 若是当前节点比父节点大,就交换
while (array[index] > array[parseInt((index - 1) / 2)]) {
swap(array, index, parseInt((index - 1) / 2));
// 将索引变成父节点
index = parseInt((index - 1) / 2);
}
}
function heapify(array, index, size) {
let left = index * 2 + 1;
while (left < size) {
// 判断左右节点大小
let largest =
left + 1 < size && array[left] < array[left + 1] ? left + 1 : left;
// 判断子节点和父节点大小
largest = array[index] < array[largest] ? largest : index;
if (largest === index) break;
swap(array, index, largest);
index = largest;
left = index * 2 + 1;
}
}
复制代码
以上代码实现了小根堆,若是须要实现大根堆,只须要把节点对比反一下就好。
该算法的复杂度是 O(logN)
每一个语言的排序内部实现都是不一样的。
对于 JS 来讲,数组长度大于 10 会采用快排,不然使用插入排序 源码实现 。选择插入排序是由于虽然时间复杂度不好,可是在数据量很小的状况下和 O(N * logN)
相差无几,然而插入排序须要的常数时间很小,因此相对别的排序来讲更快。
对于 Java 来讲,还会考虑内部的元素的类型。对于存储对象的数组来讲,会采用稳定性好的算法。稳定性的意思就是对于相同值来讲,相对顺序不能改变。
工厂模式分为好几种,这里就不一一讲解了,如下是一个简单工厂模式的例子
class Man {
constructor(name) {
this.name = name
}
alertName() {
alert(this.name)
}
}
class Factory {
static create(name) {
return new Man(name)
}
}
Factory.create('yck').alertName()
复制代码
固然工厂模式并不只仅是用来 new 出实例。
能够想象一个场景。假设有一份很复杂的代码须要用户去调用,可是用户并不关心这些复杂的代码,只须要你提供给我一个接口去调用,用户只负责传递须要的参数,至于这些参数怎么使用,内部有什么逻辑是不关心的,只须要你最后返回我一个实例。这个构造过程就是工厂。
工厂起到的做用就是隐藏了建立实例的复杂度,只须要提供一个接口,简单清晰。
在 Vue 源码中,你也能够看到工厂模式的使用,好比建立异步组件
export function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// 逻辑处理...
const vnode = new VNode(
`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
data, undefined, undefined, undefined, context,
{ Ctor, propsData, listeners, tag, children },
asyncFactory
)
return vnode
}
复制代码
在上述代码中,咱们能够看到咱们只须要调用 createComponent
传入参数就能建立一个组件实例,可是建立这个实例是很复杂的一个过程,工厂帮助咱们隐藏了这个复杂的过程,只须要一句代码调用就能实现功能。
单例模式很经常使用,好比全局缓存、全局状态管理等等这些只须要一个对象,就能够使用单例模式。
单例模式的核心就是保证全局只有一个对象能够访问。由于 JS 是门无类的语言,因此别的语言实现单例的方式并不能套入 JS 中,咱们只须要用一个变量确保实例只建立一次就行,如下是如何实现单例模式的例子
class Singleton {
constructor() {}
}
Singleton.getInstance = (function() {
let instance
return function() {
if (!instance) {
instance = new Singleton()
}
return instance
}
})()
let s1 = Singleton.getInstance()
let s2 = Singleton.getInstance()
console.log(s1 === s2) // true
复制代码
在 Vuex 源码中,你也能够看到单例模式的使用,虽然它的实现方式不大同样,经过一个外部变量来控制只安装一次 Vuex
let Vue // bind on install
export function install (_Vue) {
if (Vue && _Vue === Vue) {
// ...
return
}
Vue = _Vue
applyMixin(Vue)
}
复制代码
适配器用来解决两个接口不兼容的状况,不须要改变已有的接口,经过包装一层的方式实现两个接口的正常协做。
如下是如何实现适配器模式的例子
class Plug {
getName() {
return '港版插头'
}
}
class Target {
constructor() {
this.plug = new Plug()
}
getName() {
return this.plug.getName() + ' 适配器转二脚插头'
}
}
let target = new Target()
target.getName() // 港版插头 适配器转二脚插头
复制代码
在 Vue 中,咱们其实常用到适配器模式。好比父组件传递给子组件一个时间戳属性,组件内部须要将时间戳转为正常的日期显示,通常会使用 computed
来作转换这件事情,这个过程就使用到了适配器模式。
装饰模式不须要改变已有的接口,做用是给对象添加功能。就像咱们常常须要给手机戴个保护套防摔同样,不改变手机自身,给手机添加了保护套提供防摔功能。
如下是如何实现装饰模式的例子,使用了 ES7 中的装饰器语法
function readonly(target, key, descriptor) {
descriptor.writable = false
return descriptor
}
class Test {
@readonly
name = 'yck'
}
let t = new Test()
t.yck = '111' // 不可修改
复制代码
在 React 中,装饰模式其实随处可见
import { connect } from 'react-redux'
class MyComponent extends React.Component {
// ...
}
export default connect(mapStateToProps)(MyComponent)
复制代码
代理是为了控制对对象的访问,不让外部直接访问到对象。在现实生活中,也有不少代理的场景。好比你须要买一件国外的产品,这时候你能够经过代购来购买产品。
在实际代码中其实代理的场景不少,也就不举框架中的例子了,好比事件代理就用到了代理模式。
<ul id="ul">
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<script> let ul = document.querySelector('#ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script>
复制代码
由于存在太多的 li
,不可能每一个都去绑定事件。这时候能够经过给父节点绑定一个事件,让父节点做为代理去拿到真实点击的节点。
发布-订阅模式也叫作观察者模式。经过一对一或者一对多的依赖关系,当对象发生改变时,订阅方都会收到通知。在现实生活中,也有不少相似场景,好比我须要在购物网站上购买一个产品,可是发现该产品目前处于缺货状态,这时候我能够点击有货通知的按钮,让网站在产品有货的时候经过短信通知我。
在实际代码中其实发布-订阅模式也很常见,好比咱们点击一个按钮触发了点击事件就是使用了该模式
<ul id="ul"></ul>
<script>
let ul = document.querySelector('#ul')
ul.addEventListener('click', (event) => {
console.log(event.target);
})
</script>
复制代码
在 Vue 中,如何实现响应式也是使用了该模式。对于须要实现响应式的对象来讲,在 get
的时候会进行依赖收集,当改变了对象的属性时,就会触发派发更新。
若是你对于如何实现响应式还有疑问,能够阅读我以前的文章 深度解析 Vue 响应式原理
若是你认为还有什么好的题目能够贡献,也能够在评论中提出
你可能会疑问我怎么写出 25K 的文字的,其实不少面试题均可以在个人万星项目中找到答案,如下是 项目地址
若是你想学习到更多的前端知识、面试技巧或者一些我我的的感悟,能够关注个人公众号一块儿学习
了解掘金秋招求职征文活动更多信息👉秋招求职时,写文就有好礼相送 | 掘金技术征文