金九银十~JS精选回顾宝典

前言

在这个金九银十的日子中,为大家奉上JS精选复习宝典一份,望各位看官笑纳!

普通函数和箭头函数的 this

function fn() {
  console.log(this); // 1. {a: 100}
  var arr = [1, 2, 3];

  (function() {
    console.log(this); // 2. Window
  })();

  // 普通 JS
  arr.map(function(item) {
    console.log(this); // 3. Window
    return item + 1;
  });
  // 箭头函数
  let brr = arr.map(item => {
    console.log("es6", this); // 4. {a: 100}
    return item + 1;
  });
}
fn.call({ a: 100 });
复制代码

其实诀窍很简单,常见的基本是 3 种情况:es5 普通函数、es6 的箭头函数以及通过bind改变过上下文返回的新函数。

① es5 普通函数:

函数被直接调用,上下文一定是window 函数作为对象属性被调用,例如:obj.foo(),上下文就是对象本身obj 通过new调用,this绑定在返回的实例上

② es6 箭头函数:

它本身没有this,会沿着作用域向上寻找,直到 window。请看下面的这段代码:

function run() {
  const inner = () => {
    return () => {
      console.log(this.a);
    };
  };

  inner()();
}

run.bind({ a: 1 })(); // Output: 1
复制代码

③ bind 绑定上下文返回的新函数:就是被第一个 bind 绑定的上下文,而且 bind 对“箭头函数”无效。请看下面的这段代码:

function run() {
  console.log(this.a);
}

run.bind({ a: 1 })(); // output: 1

// 多次bind,上下文由第一个bind的上下文决定
run.bind({ a: 2 }).bind({ a: 1 })(); // output: 2
复制代码

最后,再说说这几种方法的优先级:new > bind > 对象调用 > 直接调用

原始数据类型和判断方法

题目:JS 中的原始数据类型?

ECMAScript 中定义了 7 种原始类型:

Boolean

String

Number

Null

Undefined

Symbol(es6)

注意:原始类型不包含 Object 和 Function

题目:常用的判断方法?

在进行判断的时候有typeof、instanceof。对于数组的判断,使用Array.isArray():

  • typeof:

  • typeof 基本都可以正确判断数据类型

  • typeof null和typeof [1, 2, 3]均返回"object"

  • ES6 新增:typeof Symbol()返回"symbol"

  • instanceof:

专门用于实例和构造函数对应

function Obj(value) {
  this.value = value;
}
let obj = new Obj("test");
console.log(obj instanceof Obj); // output: true
复制代码

判断是否是数组:[1, 2, 3] instanceof Array

Array.isArray():ES6 新增,用来判断是否是'Array'。Array.isArray({})返回false。

事件流

事件冒泡事件捕获

事件流分为:冒泡捕获,顺序是先捕获再冒泡。

事件冒泡:子元素的触发事件会一直向父节点传递,一直到根结点停止。此过程中,可以在每个节点捕捉到相关事件。可以通过stopPropagation方法终止冒泡。

事件捕获:和“事件冒泡”相反,从根节点开始执行,一直向子节点传递,直到目标节点。

addEventListener给出了第三个参数同时支持冒泡与捕获:默认是false,事件冒泡;设置为true时,是事件捕获。

<div id="app" style="width: 100vw; background: red;">
  <span id="btn">点我</span>
</div>
<script>
  // 事件捕获:先输出 "外层click事件触发"; 再输出 "内层click事件触发"
  var useCapture = true;
  var btn = document.getElementById("btn");
  btn.addEventListener(
    "click",
    function() {
      console.log("内层click事件触发");
    },
    useCapture
  );

  var app = document.getElementById("app");
  app.onclick = function() {
    console.log("外层click事件触发");
  };
</script>
复制代码

DOM0 级DOM2 级 DOM2 级:前面说的addEventListener,它定义了DOM事件流,捕获 + 冒泡。

DOM0 级:

直接在 html 标签内绑定on事件 在 JS 中绑定on系列事件 注意:现在通用DOM2级事件,优点如下:

可以绑定 / 卸载事件 支持事件流 冒泡 + 捕获:相当于每个节点同一个事件,至少 2 次处理机会 同一类事件,可以绑定多个函数

ES5 继承

方法一:绑定构造函数

缺点:不能继承父类原型方法/属性

function Animal() {
  this.species = "动物";
}

function Cat() {
  // 执行父类的构造方法, 上下文为实例对象
  Animal.apply(this, arguments);
}

/**
 * 测试代码
 */
var cat = new Cat();
console.log(cat.species); // output: 动物
复制代码

方法二:原型链继承

缺点:无法向父类构造函数中传递参数;子类原型链上定义的方法有先后顺序问题。 注意:js 中交换原型链,均需要修复prototype.constructor指向问题。

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function() {
  console.log("Animal");
};

function Cat() {}
/**
 * func方法是无效的, 因为后面原型链被重新指向了Animal实例
 */
Cat.prototype.func = function() {
  console.log("Cat");
};

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat; // 修复: 将Cat.prototype.constructor重新指向本身

/**
 * 测试代码
 */
var cat = new Cat();
cat.func(); // output: Animal
console.log(cat.species); // undefined
复制代码

方法 3:组合继承

结合绑定构造函数和原型链继承 2 种方式,缺点是:调用了 2 次父类的构造函数。

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function() {
  console.log("Animal");
};

function Cat() {
  Animal.apply(this, arguments);
}

Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;

/**
 * 测试代码
 */
var cat = new Cat("cat");
cat.func(); // output: Animal
console.log(cat.species); // output: cat
复制代码

方法4: 寄生组合继承 改进了组合继承的缺点,只需要调用 1 次父类的构造函数。这是目前最推荐的继承方式

/**
 * 寄生组合继承的核心代码
 * @param {Function} sub 子类
 * @param {Function} parent 父类
 */
function inheritPrototype(sub, parent) {
  // 拿到父类的原型
  var prototype = Object.create(parent.prototype);
  // 改变constructor指向
  prototype.constructor = sub;
  // 父类原型赋给子类
  sub.prototype = prototype;
}

function Animal(species) {
  this.species = species;
}
Animal.prototype.func = function() {
  console.log("Animal");
};

function Cat() {
  Animal.apply(this, arguments); // 只调用了1次构造函数
}

inheritPrototype(Cat, Animal);

/**
 * 测试代码
 */

var cat = new Cat("cat");
cat.func(); // output: Animal
console.log(cat.species); // output: cat
复制代码

原型和原型链

  • 所有的引用类型(数组、对象、函数),都有一个__proto__属性
  • 所有的函数,都有一个 prototype 属性,属性值也是一个普通的对象
  • 所有的引用类型(数组、对象、函数),__proto__属性值指向它的构造函数的 prototype 属性值
  • 注:ES6 的箭头函数没有prototype属性,但是有__proto__属性。

如何理解 JS 中的原型?

原型就是用来继承属性和方法的。

当试图得到一个对象的某个属性时,如果这个对象本身没有这个属性,那么会去它的__proto__(即它的构造函数的prototype)中寻找

如何理解 JS 中的原型链?

__proto__是每个对象都有的属性,因为prototype也是对象,所以他也具有__proto__属性,__proto__将每个对象之间串联起来,形成了链条,这就叫做原型链。

作用域和作用域链

如何理解 JS 的作用域和作用域链?

① 作用域

ES5 有”全局作用域“和”函数作用域“。ES6 的let和const使得 JS 用了”块级作用域“。

② 作用域链

当前作用域没有找到定义,继续向父级作用域寻找,直至全局作用域。这种层级关系,就是作用域链

事件循环

  • 执行一个宏任务(栈中没有就从事件队列中获取)

  • 执行过程中如果遇到微任务,就将它添加到微任务的任务队列中

  • 宏任务执行完毕后,立即执行当前微任务队列中的所有微任务(依次执行)

  • 当前宏任务执行完毕,开始检查渲染,然后GUI线程接管渲染

  • 渲染完毕后,JS线程继续接管,开始下一个宏任务(从事件队列中获取)

 

 

 

JS 是单线程的,其上面的所有任务都是在两个地方执行:执行栈和任务队列。前者是存放同步任务;后者是异步任务有结果后,就在其中放入一个事件。 当执行栈的任务都执行完了(栈空),js 会读取任务队列,并将可以执行的任务从任务队列丢到执行栈中执行。 这个过程是循环进行的,所以称作EventLoop

执行上下文

JS执行上下文分为全局执行上下文函数执行上下文

①全局执行上下文

解析 JS 时候,创建一个 全局执行上下文 环境。把代码中即将执行的(内部函数的不算,因为你不知道函数何时执行)变量、函数声明都拿出来。未赋值的变量就是undefined。

下面这段代码输出:undefined;而不是抛出Error。因为在解析 JS 的时候,变量 a 已经存入了全局执行上下文中了。

console.log(a);
var a = 1;
复制代码

②函数执行上下文

和全局执行上下文差不多,但是多了this和arguments和参数。

在 JS 中,this是关键字,它作为内置变量,其值是在执行的时候确定(不是定义的时候确定)。

闭包

定义:外部函数调用之后其变量对象本应该被销毁,但闭包的存在使我们仍然可以访问外部函数的变量对象,这就是闭包的重要概念

如何解决内存泄漏?

解决方法是显式对外暴露一个接口,专门用以清理变量:

function mockData() {
  const mem = {};

  return {
    clear: () => (mem = null), // 显式暴露清理接口

    get: page => {
      if (page in mem) {
        return mem[page];
      }
      mem[page] = Math.random();
    }
  };
}
复制代码

实现千分位分隔符

一句话版本:

(123456789).toLocaleString('en-US');//"123,456,789"
复制代码

手写call

要点:如果一个函数作为一个对象的属性,那么通过对象的.运算符调用此函数,this就是此对象

let obj = {
  a: "a",
  b: "b",
  test: function(arg1, arg2) {
    console.log(arg1, arg2);
    // this.a 就是 a; this.b 就是 b
    console.log(this.a, this.b);
  }
};

obj.test(1, 2);
复制代码

知道了实现关键,下面就是我们模拟的call:

Function.prototype.call2 = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  // 默认上下文是window
  context = context || window;
  // 保存默认的fn
  const { fn } = context;

  // 前面讲的关键,将函数本身作为对象context的属性调用,自动绑定this
  context.fn = this;
  const args = [...arguments].slice(1);
  const result = context.fn(...args);

  // 恢复默认的fn
  context.fn = fn;
  return result;
};

// 以下是测试代码
function test(arg1, arg2) {
  console.log(arg1, arg2);
  console.log(this.a, this.b);
}

test.call2(
  {
    a: "a",
    b: "b"
  },
  1,
  2
);
复制代码

实现 apply

apply和call实现类似,只是传入的参数形式是数组形式,而不是逗号分隔的参数序列。

因此,借助 es6 提供的...运算符,就可以很方便的实现数组和参数序列的转化。

Function.prototype.apply2 = function(context) {
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }

  context = context || window;
  const { fn } = context;

  context.fn = this;
  let result;
  if (Array.isArray(arguments[1])) {
    // 通过...运算符将数组转换为用逗号分隔的参数序列
    result = context.fn(...arguments[1]);
  } else {
    result = context.fn();
  }

  context.fn = fn;
  return result;
};

/**
 * 以下是测试代码
 */

function test(arg1, arg2) {
  console.log(arg1, arg2);
  console.log(this.a, this.b);
}

test.apply2(
  {
    a: "a",
    b: "b"
  },
  [1, 2]
);
复制代码

手写深拷贝

function deepClone(src, target) {
  const keys = Reflect.ownKeys(src);
  let value = null;

  for (let key of keys) {
    value = src[key];

    if (Array.isArray(value)) {
      target[key] = cloneArr(value, []);
    } else if (typeof value === "object") {
      // 如果是对象而且不是数组, 那么递归调用深拷贝
      target[key] = deepClone(value, {});
    } else {
      target[key] = value;
    }
  }

  return target;
}
复制代码

手写EventEmitter

实现思路:这里涉及了“订阅/发布模式”的相关知识。参考addEventListener和removeEventListener的具体效果来实现即可。

// 数组置空:
// arr = []; arr.length = 0; arr.splice(0, arr.length)
class Event {
  constructor() {
    this._cache = {};
  }

  // 注册事件:如果不存在此种type,创建相关数组
  on(type, callback) {
    this._cache[type] = this._cache[type] || [];
    let fns = this._cache[type];
    if (fns.indexOf(callback) === -1) {
      fns.push(callback);
    }
    return this;
  }

  // 触发事件:对于一个type中的所有事件函数,均进行触发
  trigger(type, ...data) {
    let fns = this._cache[type];
    if (Array.isArray(fns)) {
      fns.forEach(fn => {
        fn(...data);
      });
    }
    return this;
  }

  // 删除事件:删除事件类型对应的array
  off(type, callback) {
    let fns = this._cache[type];
    // 检查是否存在type的事件绑定
    if (Array.isArray(fns)) {
      if (callback) {
        // 卸载指定的回调函数
        let index = fns.indexOf(callback);
        if (index !== -1) {
          fns.splice(index, 1);
        }
      } else {
        // 全部清空
        fns = [];
      }
    }
    return this;
  }
}

// 以下是测试函数

const event = new Event();
event
  .on("test", a => {
    console.log(a);
  })
  .trigger("test", "hello");
复制代码

Promise

  • 三个状态:pending、fulfilled、rejected
  • Promise实例一旦被创建就会被执行
  • Promise过程分为两个分支:pending=>resolved和pending=>rejected
  • Promise状态改变后,依然会执行之后的代码:
const warnDemo = ctx => {
  const promise = new Promise(resolve => {
    resolve(ctx);
    console.log("After resolved, but Run"); // 依然会执行这个语句
  });
  return promise;
};

warnDemo("ctx").then(ctx => console.log(`This is ${ctx}`));
复制代码

Promise性质

状态只改变一次

Promise 的状态一旦改变,就永久保持该状态,不会再变了。

错误冒泡

Promise 对象的错误具有“冒泡”性质,会一直向后传递,直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获

"吃掉错误"机制

Promise会吃掉内部的错误,并不影响外部代码的运行。所以需要catch,以防丢掉错误信息。

async/await

async函数返回一个Promise对象,可以使用then方法添加回调函数。

当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

这也是它最受欢迎的地方:能让异步代码写起来像同步代码,并且方便控制顺序。

可以利用它实现一个sleep函数阻塞进程:

function sleep(millisecond) {
  return new Promise(resolve => {
    setTimeout(() => resolve, millisecond);
  });
}

/**
 * 以下是测试代码
 */
async function test() {
  console.log("start");
  await sleep(1000); // 睡眠1秒
  console.log("end");
}

test(); // 执行测试函数
复制代码

虽然方便,但是它也不能取代Promise,尤其是我们可以很方便地用Promise.all()来实现并发,而async/await只能实现串行(用不好会产生性能问题哦)。

EsModule 和 CommonJS 的比较

目前 js 社区有 4 种模块管理规范:AMD、CMD、CommonJS 和 EsModule。 ES Module 是原生实现的模块化方案,与 CommonJS 有以下几个区别:

CommonJS 支持动态导入,EsModule目前不支持 CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而EsModule是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响 commonJs 输出的是值的浅拷贝,esModule 输出值的引用 ES Module 会编译成 require/exports 来执行的

浏览器渲染过程

根据HTML代码生成DOM树

根据CSS生成CSSDOM

将 DOM 树和 CSSOM 整合成 RenderTree

根据 RenderTree 开始渲染和展示

遇到script标签,会阻塞渲染

为什么遇到script标签,会阻塞渲染

浏览器中常见的线程有:渲染线程、JS 引擎线程、HTTP 线程等等。

例如,当我们打开一个 Ajax 请求的时候,就启动了一个 HTTP 线程。

同样地,我们可以用线程的只是解释:为什么直接操作 DOM 会变慢,性能损耗更大?因为 JS 引擎线程和渲染线程是互斥的。而直接操作 DOM 就会涉及到两个线程互斥之间的通信,所以开销更大。

毕竟 JS 是可以修改 DOM 的,如果 JS 执行的时候 UI 也工作,就有可能导致不安全(不符合预期)的渲染结果。

onload和DOMContentLoaded触发的先后顺序是什么?

DOMContentLoaded是在onload前进行的。

DOMContentLoaded事件在 DOM 树构建完毕后被触发,我们可以在这个阶段使用 js 去访问元素。

async和defer的脚本可能还没有执行。

图片及其他资源文件可能还在下载中。

load事件在页面所有资源被加载完毕后触发,通常我们不会用到这个事件,因为我们不需要等那么久。

document.addEventListener("DOMContentLoaded", () => {
  console.log("DOMContentLoaded");
});
window.addEventListener("load", () => {
  console.log("load");
});
复制代码

跨域

  • JSONP:通过script标签实现,但是只能实现GET请求
  • 反向代理:nginx的proxy
  • CORS:后端允许跨域资源共享

实现JSONP

// 定义回调函数
const Response = data => {
  console.log(data);
};

// 构造 <script> 标签
let script = document.createElement("script");
script.src =
  "http://xxx.com?callback=Response";

// 向document中添加 <script> 标签,并且发送GET请求
document.body.appendChild(script);
复制代码

结语

愿大家在金九银十这个季节能找到满意的工作!


作者:_Dreams
链接:https://juejin.im/post/5d77acb66fb9a06af50ff525
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值