js基础

211 阅读11分钟

浏览器

1.回流与重绘

回流必将引起重绘,重绘不一定会引起回流。

1.1回流

Render Tree中部分或全部元素的尺寸、结构、或某些属性发生改变时,浏览器重新渲染部分或全部文档的过程称为回流。

会导致回流的操作:

  • 页面初始渲染,这是开销最大的一次重排;
  • 添加/删除可见的DOM元素;
  • 改变元素位置;
  • 改变元素尺寸,比如边距、填充、边框、宽度和高度等;
  • 改变元素内容,比如文字数量,图片大小等;
  • 改变元素字体大小;
  • 改变浏览器窗口尺寸,比如resize事件发生时;
  • 激活CSS伪类(例如::hover);
  • 设置 style 属性的值,因为通过设置style属性改变结点样式的话,每一次设置都会触发一次reflow;
  • 查询某些属性或调用某些计算方法:offsetWidth、offsetHeight等,除此之外,当我们调用getComputedStyl方法,或者IE里的 currentStyle 时,也会触发重排,原理是一样的,都为求一个“即时性”和“准确性”;

一些常用且会导致回流的属性和方法:

  • clientWidthclientHeightclientTopclientLeft
  • offsetWidthoffsetHeightoffsetTopoffsetLeft
  • scrollWidthscrollHeightscrollTopscrollLeft
  • scrollIntoView()scrollIntoViewIfNeeded()
  • getComputedStyle()
  • getBoundingClientRect()
  • scrollTo()

1.2 重绘

当页面中元素样式的改变并不影响它在文档流中的位置时(例如:colorbackground-colorvisibility等),浏览器会将新样式赋予给元素并重新绘制它,这个过程称为重绘。

  • color
  • border-style
  • border-radius
  • text-decoration
  • box-shadow
  • outline
  • background

2. 浏览器的垃圾回收机制

「硬核 JS」你真的了解垃圾回收机制吗

有两种垃圾回收策略:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 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();

b04314c0bfd75b09fceca4aebcf88c8.png 总结:

  • 原型:每一个 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 200;  
  fn();  
}  
  
const 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 实现

callbind方法接受的是参数列表apply方法接受的是一个参数数组bind 方法不立即执行,而applycall 立即执行。

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(startend, arr) {  
  if (start < end) {  
    const mid = sort(startend, arr);  
    quickSort(start, mid - 1, arr);  
    quickSort(mid + 1end, arr);  
  }  
}  
  
function sort(startend, 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 === nullreturn 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.prototypereturn 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;
  });
}