浏览器
1.回流与重绘
回流必将引起重绘,重绘不一定会引起回流。
1.1回流
当Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。
会导致回流的操作:
- 页面初始渲染,这是开销最大的一次重排;
- 添加/删除可见的DOM元素;
- 改变元素位置;
- 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
- 改变元素内容,比如文字数量,图片大小等;
- 改变元素字体大小;
- 改变浏览器窗口尺寸,比如resize事件发生时;
- 激活CSS伪类(例如::hover);
- 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow;
- 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用getComputedStyl方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”;
一些常用且会导致回流的属性和方法:
clientWidth、clientHeight、clientTop、clientLeftoffsetWidth、offsetHeight、offsetTop、offsetLeftscrollWidth、scrollHeight、scrollTop、scrollLeftscrollIntoView()、scrollIntoViewIfNeeded()getComputedStyle()getBoundingClientRect()scrollTo()
1.2 重绘
当页面中元素样式的改变并不影响它在文档流中的位置时(例如:color、background-color、visibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。
colorborder-styleborder-radiustext-decorationbox-shadowoutlinebackground
2. 浏览器的垃圾回收机制
有两种垃圾回收策略:
- 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
- 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。
标记清除的缺点:
- 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
- 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。
解决以上的缺点可以使用 **标记整理(Mark-Compact)算法 **,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)
引用计数的缺点:
- 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
- 解决不了循环引用导致的无法回收问题。
V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。
- 针对新生区采用并行回收。
- 针对老生区采用增量标记与惰性回收。
3.浏览器内核
- Chrome: Blink
- IE:Trident
- Edge:EdgeHTML
- Safari:WebKit
- Firefox:Gecko
- 360、UC、QQ、搜狗、2345等浏览器Trident + Chromium
JS
1、数据类型
1.1 基本的数据类型有8种,分别为: Undefined、Null、Boolean、Number、String、Object、Symbol、BigInt。
- Symbol代表独一无二的值,最大的用法是用来定义对象的唯一属性名。
- BigInt可以表示任意大小的整数。
1.2 数据类型的判断
- typeof:能判断所有的值类型,函数。不可对null、对象、数组进行精准判断,因为都返回object。
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object console.log(typeof null); // object
- instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。
class People {}
class Student extends People {}
const vortesnail = new Student();
console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true
- Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"
- 判断是否为数组
Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"
1.3手写深拷贝
/**
* 深拷贝
* @param {Object} obj 要拷贝的对象
* @param {Map} map 用于存储循环引用对象的地址
*/
function deepClone(obj = {}, map = new Map()) {
if (typeof obj !== "object") {
return obj;
}
if (map.get(obj)) {
return map.get(obj);
}
let result = {};
// 初始化返回结果
if (
obj instanceof Array ||
// 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
Object.prototype.toString(obj) === "[object Array]"
) {
result = [];
}
// 防止循环引用
map.set(obj, result);
for (const key in obj) {
// 保证 key 不是原型属性
if (obj.hasOwnProperty(key)) {
// 递归调用
result[key] = deepClone(obj[key], map);
}
}
// 返回结果
return result;
}
2.原型和原型链
function Foo() {}
let f1 = new Foo();
let f2 = new Foo();
总结:
- 原型:每一个 JavaScript 对象(null 除外)在创建的时候就会与之关联另一个对象,这个对象就是我们所说的原型,每一个对象都会从原型"继承"属性,其实就是
prototype对象。 - 原型链:由相互关联的原型组成的链状结构就是原型链。
先说出总结的话,再举例子说明如何顺着原型链找到某个属性。
3.作用域与作用域链
- 作用域:规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。换句话说,作用域决定了代码区块中变量和其他资源的可见性。(全局作用域、函数作用域、块级作用域)
- 作用域链:从当前作用域开始一层层往上找某个变量,如果找到全局作用域还没找到,就放弃寻找 。这种层级关系就是作用域链。(由多个执行上下文的变量对象构成的链表就叫做作用域链,学习下面的内容之后再考虑这句话)
需要注意的是,js 采用的是静态作用域,所以函数的作用域在函数定义时就确定了。
4.执行上下文
这部分一定要按顺序连续读这几篇文章,必须多读几遍:
- JavaScript 深入之执行上下文栈;
- JavaScript 深入之变量对象;
- JavaScript 深入之作用域链;
- JavaScript 深入之执行上下文。
总结:当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:
- 变量对象(Variable object,VO);
- 作用域链(Scope chain);
- this。(关于 this 指向问题,在上面推荐的深入系列也有讲从 ES 规范讲的,但是实在是难懂,对于应付面试来说以下这篇阮一峰的文章应该就可以了:JavaScript 的 this 原理
5.闭包
根据 MDN 中文的定义,闭包的定义如下:
在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。
也可以这样说:
闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量的变量。 闭包 = 函数 + 函数能够访问的自由变量。
在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。
闭包应用: 函数作为参数被传递:
function print(fn) {
const a = 200;
fn();
}
const a = 100;
function fn() {
console.log(a);
}
print(fn); // 100
函数作为返回值被返回:
function create() {
const a = 100;
return function () {
console.log(a);
};
}
const fn = create();
const a = 200;
fn(); // 100
闭包:自由变量的查找,是在函数定义的地方,向上级作用域查找。不是在执行的地方。 应用实例:比如缓存工具,隐藏数据,只提供 API 。
function createCache() {
const data = {}; // 闭包中被隐藏的数据,不被外界访问
return {
set: function (key, val) {
data[key] = val;
},
get: function (key) {
return data[key];
},
};
}
const c = createCache();
c.set("a", 100);
console.log(c.get("a")); // 100
6、 call、apply、bind 实现
call和bind方法接受的是参数列表,apply方法接受的是一个参数数组。bind方法不立即执行,而apply、call立即执行。
var obj = {
value: "vortesnail",
};
function fn() {
console.log(this.value);
}
fn.call(obj); // vortesnail
通过 call 方法我们做到了以下两点:
call改变了 this 的指向,指向到obj。fn函数执行了。
Function.prototype.myCall = function (context) {
// 判断调用对象
if (typeof this !== "function") {
throw new Error("Type error");
}
// 首先获取参数
let args = [...arguments].slice(1);
let result = null;
// 判断 context 是否传入,如果没有传就设置为 window
context = context || window;
// 将被调用的方法设置为 context 的属性
// this 即为我们要调用的方法
context.fn = this;
// 执行要被调用的方法
result = context.fn(...args);
// 删除手动增加的属性方法
delete context.fn;
// 将执行结果返回
return result;
};
Function.prototype.myApply = function (context) {
if (typeof this !== "function") {
throw new Error("Type error");
}
let result = null;
context = context || window;
// 与上面代码相比,我们使用 Symbol 来保证属性唯一
// 也就是保证不会重写用户自己原来定义在 context 中的同名属性
const fnSymbol = Symbol();
context[fnSymbol] = this;
// 执行要被调用的方法
if (arguments[1]) {
result = context[fnSymbol](...arguments[1]);
} else {
result = context[fnSymbol]();
}
delete context[fnSymbol];
return result;
};
Function.prototype.myBind = function (context) {
// 判断调用对象是否为函数
if (typeof this !== "function") {
throw new Error("Type error");
}
// 获取参数
const args = [...arguments].slice(1),
const fn = this;
return function Fn() {
return fn.apply(
this instanceof Fn ? this : context,
// 当前的这个 arguments 是指 Fn 的参数
args.concat(...arguments)
);
};
};
7.new
- 首先创一个新的空对象。
- 根据原型链,设置空对象的
__proto__为构造函数的prototype。 - 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
- 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
function myNew(context) {
const obj = new Object();
obj.__proto__ = context.prototype;
const res = context.apply(obj, [...arguments].slice(1));
return typeof res === "object" ? res : obj;
}
8.异步
8.1 event loop、宏任务和微任务
注意:1.Call Stack 调用栈空闲 -> 2.尝试 DOM 渲染 -> 触发 Event loop。
- 每次 Call Stack 清空(即每次轮询结束),即同步任务执行完。
- 都是 DOM 重新渲染的机会,DOM 结构有改变则重新渲染。
- 然后再去触发下一次 Event loop。
宏任务:setTimeout,setInterval,Ajax,DOM 事件。 微任务:Promise async/await。
两者区别:
- 宏任务:DOM 渲染后触发,如
setTimeout、setInterval、DOM 事件、script。 - 微任务:DOM 渲染前触发,如
Promise.then、MutationObserver、Node 环境下的process.nextTick。
从 event loop 解释,为何微任务执行更早?
- 微任务是 ES6 语法规定的(被压入 micro task queue)。
- 宏任务是由浏览器规定的(通过 Web APIs 压入 Callback queue)。
- 宏任务执行时间一般比较长。
- 每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的。
8.2 Promise
Promise.all = function (promises) {
return new Promise((resolve, reject) => {
// 参数可以不是数组,但必须具有 Iterator 接口
if (typeof promises[Symbol.iterator] !== "function") {
reject("Type error");
}
if (promises.length === 0) {
resolve([]);
} else {
const res = [];
let count = 0;
const len = promises.length;
for (let i = 0; i < len; i++) {
//考虑到 promises[i] 可能是 thenable 对象也可能是普通值
Promise.resolve(promises[i])
.then((data) => {
res[i] = data;
if (++count === len) {
resolve(res);
}
})
.catch((err) => {
reject(err);
});
}
}
});
};
8.3 async/await 和 Promise 的关系
- sync/await 是消灭异步回调的终极武器。
- 但和 Promise 并不互斥,反而,两者相辅相成。
- 执行 async 函数,返回的一定是 Promise 对象。
- await 相当于 Promise 的 then。
- tr一般...catch 可捕获异常,代替了 Promise 的 catch。
10.常见的手写代码
10.1 防抖
function debounce(func, wait, immediate) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (timeout) clearTimeout(timeout);
if (immediate) {
let callNow = !timeout;
timeout = setTimeout(function () {
timeout = null;
}, wait);
if (callNow) func.apply(context, args);
} else {
timeout = setTimeout(function () {
func.apply(context, args);
}, wait);
}
};
}
10.2节流
// 使用时间戳
function throttle(func, wait) {
let preTime = 0;
return function () {
let nowTime = +new Date();
let context = this;
let args = arguments;
if (nowTime - preTime > wait) {
func.apply(context, args);
preTime = nowTime;
}
};
}
// 定时器实现
function throttle(func, wait) {
let timeout;
return function () {
let context = this;
let args = arguments;
if (!timeout) {
timeout = setTimeout(function () {
timeout = null;
func.apply(context, args);
}, wait);
}
};
}
10.3 快速排序
function sortArray(nums) {
quickSort(0, nums.length - 1, nums);
return nums;
}
function quickSort(start, end, arr) {
if (start < end) {
const mid = sort(start, end, arr);
quickSort(start, mid - 1, arr);
quickSort(mid + 1, end, arr);
}
}
function sort(start, end, arr) {
const base = arr[start];
let left = start;
let right = end;
while (left !== right) {
while (arr[right] >= base && right > left) {
right--;
}
arr[left] = arr[right];
while (arr[left] <= base && right > left) {
left++;
}
arr[right] = arr[left];
}
arr[left] = base;
return left;
}
10.4 instanceof
function myInstanceof(target, origin) {
if (typeof target !== "object" || target === null) return false;
if (typeof origin !== "function")
throw new TypeError("origin must be function");
let proto = Object.getPrototypeOf(target); // 相当于 proto = target.__proto__;
while (proto) {
if (proto === origin.prototype) return true;
proto = Object.getPrototypeOf(proto);
}
return false;
}
10.5 数组扁平化
function flat(arr, depth = 1) {
if (depth > 0) {
// 以下代码还可以简化,不过为了可读性,还是....
return arr.reduce((pre, cur) => {
return pre.concat(Array.isArray(cur) ? flat(cur, depth - 1) : cur);
}, []);
}
return arr.slice();
}
10.6 手写 reduce
// 不考虑第二个参数的初始值
Array.prototype.reduce = function (cb) {
const arr = this; //this就是调用reduce方法的数组
let total = arr[0]; // 默认为数组的第一项
for (let i = 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};
// 考虑初始值
Array.prototype.reduce = function (cb, initialValue) {
const arr = this;
let total = initialValue || arr[0];
// 有初始值的话从0遍历,否则从1遍历
for (let i = initialValue ? 0 : 1; i < arr.length; i++) {
total = cb(total, arr[i], i, arr);
}
return total;
};
10.7 带并发的异步调度器 Scheduler
class Scheduler {
constructor() {
this.waitTasks = []; // 待执行的任务队列
this.excutingTasks = []; // 正在执行的任务队列
this.maxExcutingNum = 2; // 允许同时运行的任务数量
}
add(promiseMaker) {
if (this.excutingTasks.length < this.maxExcutingNum) {
this.run(promiseMaker);
} else {
this.waitTasks.push(promiseMaker);
}
}
run(promiseMaker) {
const len = this.excutingTasks.push(promiseMaker);
const index = len - 1;
promiseMaker().then(() => {
this.excutingTasks.splice(index, 1);
if (this.waitTasks.length > 0) {
this.run(this.waitTasks.shift());
}
});
}
}
10.8 去重
// 利用 ES6 `set` 关键字:
function unique(arr) {
return [...new Set(arr)];
}
// 利用 ES5 `filter` 方法:
function unique(arr) {
return arr.filter((item, index, array) => {
return array.indexOf(item) === index;
});
}