前言
在这个金九银十的日子中,为大家奉上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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。