感谢@程序员小鹿 写的系列文章,这是我用来学习用的摘抄
变量提升
基本概念
变量还未被声明,但是却可以使用。
在代码执行前,先在词法环境中进行注册。
变量提升的根本原因就是解决函数之间互相调用的情况。
怎么提升的?
- 第一阶段:对所有函数声明进行提升(忽略表达式和箭头函数),引用类型的赋值分为三步:
- 开辟堆空间
- 存储内容
- 将地址赋值给变量
- 第二阶段:对所有变量进行提升,全部赋值为undefined,然后依次执行代码。
使用let和const的话,不能在声明之前使用变量,这叫暂时性死区。
var、let、const
三者区别
- var存在变量提升,其他两个没有
- var的全局声明的变量会挂载到window上,其他不会
- let和const作用基本一样,后者声明的变量不能再次赋值,但是能改变值,就是不能改变其栈内存指向的堆内存地址,但是你可以改堆内存里面的东西。
map、filter、reduce
map
map作用是map中传入一个函数,该函数会遍历数组,对每一个元素做变换后返回新的数组。
let arr = [1,2,3]
arr = arr.map((element,index,arr)=>{
return arr[index]+1
})//[2,3,4]
filter
filter作用也是生成一个数组,传入的函数返回值是布尔类型,返回值为true的元素放入新数组,通常用来筛选不要的元素。
let arr = [1,2,3,4]
let arr = arr.filter((element,index,arr)=>{
return element != 4
})//[1,2,3]
reduce
reduce可以将数组中的元素通过回调函数最终转换为一个值。
let arr = [1,2,3]
let sum = arr.reduce((acc,element)=>{
return acc + element
},0)
Proxy
getter和setter访问属性
作用:
- 避免意外错误发生
- 需要记录属性的变化
- 数据绑定:vue中数据的双向绑定
定义getter和setter:
- 字面量定义
- ES6的Class定义
- 使用Object.definedProperty方法
字面量定义
对象访问属性通常隐式调用getter和setter方法,属性自动关联getter和setter方法。在访问属性的时候都是立即执行的。
const collection = {
name:'william',
//读取属性
get name(){
return this.name
}
//设置属性
set name(value){
this.name = value
}
}
collection.name //隐式调用getter
collection.name = 'djp'//隐式调用setter
ES6的class定义
class William{
constructor(){
this.name = 'william'
}
get firstWilliam(){
console.log('属性已访问')
return this.name
}
set firstWilliam(value){
this.name = value
}
}
const x = new William()
x.firstWilliam
Object.defineProperty()定义
对象的字面量与类定义的getter和setter方法不是在同一作用域,因此那些希望作为私有变量属性的标量是无法实现的。
// Object.defineProperty
function xiaolu(){
let count = 0;
Object.defineProperty(this,'skillLevel',{
get:() => {
return count;
},
set:value => {
count = value;
}
})
}
let x = new xiaolu();
// 隐士的调用 get 方法
console.log(x.skillLevel)
通过Object.defineProperty()创建的get,set方法,与私有变量处于相同的作用域中,get,set方法分别创建了含有私有变量的闭包。
Proxy代理
Proxy和getter/setter的区别
代理proxy是ES6新提出的。它使我们通过代理控制对另一个对象的访问。 区别是: getter和setter仅仅控制的是单个对象属性,而proxy代理的是对象交互的通用处理,包括对象的方法。
用法:
var proxy = new Proxy(target,handler)
const obj = { name: 'will' }
const proxy = new Proxy(obj, {
get: (target, key) => {
return key in target ? target[key] : '不存在该值'
},
set: (target, key, value) => {
target[key] = value
}
})
Proxy的基本应用
- 日志记录——当访问属性时,可以在get和set中记录访问日志
- 校验值——有效避免指定属性类型错误的发生
- 定义如何计算属性值——每次访问属性值都会进行计算属性值
- 数据的双向绑定(Vue)——Vue3.0中会通过Proxy替换原来的Object.defineProperty来实现数据响应式。
ES6/7的异步编程
要解决函数回调地狱的问题
Generator生成器
如何使用,阶段状态变化
使用生成器函数可以生产一组值的序列,每个值的生成是基于每次请求的,并不同于标准函数立即生成。
调用生成器不会直接执行,而是通过叫做迭代器的对象,控制生成器执行。
function* WeaponGenerator(){
yield "1";
yield "2";
yield "3";
}
for(let item of WeaponGenerator()){
console.log(item);
}
//1
//2
//3
使用迭代器控制生成器。
- 通过调用生成器返回一个迭代器对象,用来控制生成器的执行。
- 调用迭代器的next方法向生成器请求一个值。
- 请求的结果返回一个对象,对象中包含一个value值和done布尔值,告诉我们生成器是否还会生成值。
- 如果没有可执行的代码,生成器就会返回一个undefined值,表示整个生成器已经完成。
function* WeaponGenerator() {
yield '1'
yield '2'
yield '3'
}
let weapon = WeaponGenerator()
console.log(weapon.next());
console.log(weapon.next());
console.log(weapon.next());
console.log(weapon.next());
状态变化如下:
- 每当代码执行到yield属性,就会生成一个中间值,返回一个对象。
- 每当生成一个值后,生成器就会非阻塞的挂起执行,等待下一次值的请求。
- 再次调用next方法,将生成器从挂起状态唤醒,中断执行的生成器从上次离开的位置继续执行。
- 直到遇到下一个yield,生成器又挂起。
- 当执行到没有可执行的代码,就会返回一个结果对象,value值为undefined,done为true,生成器执行完毕
PGenerator内部结构实现
生成器就像一个状态运动的状态机。
- 挂起开始状态——创建一个生成器处于未执行状态。
- 执行状态
- 挂起让渡状态——遇到第一个yield
- 完成状态——代码执行到return 全部代码进入完成状态
function* generator(action) {
yield '1'+action
yield console.log(222)
yield '3'
}
let iterator = generator('djp')
let result1 = iterator.next()
let result2 = iterator.next()
let result3 = iterator.next()
console.log(result1)
console.log(result2)
console.log(result3)
- 调用生成器之前,全局环境除了生成器变量的引用,其他变量都为undefined
- 调用生成器但没有执行函数,而是返回一个Iterator迭代器对象并指向当前生成器的上下文。
- 一般函数调用完成上下午弹出栈,然后被摧毁。当生成器的函数调用完成之后,当前生成器的上下文出栈,但是在全局的迭代器对象还保持与生成器执行上下文引用,所有生成器的词法环境还在。
- 执行next方法,一般函数会重新创建执行上下文,但是生成器会重新激活并推入栈。(所以标准函数重复调用时,会从头执行,但是生成器是会挂起然后再唤醒激活,不用从头执行。)
- 当遇到yield时,生成器上下文出栈,但是迭代器还是保持引用,处于非阻塞暂时挂起状态。
- 当遇到next,继续在原位置执行,知道遇到return,返回值结束。
Promise
Promise的原理
为什么会有Promise?
- 第一:由于JS单线程,所以执行耗时长的任务时,就会造成UI渲染的阻塞。当前解决方法是使用回调函数来解决,当任务执行完毕,会调用回调方法。
- 第二:回调函数存在缺点:
- 不能捕捉异常——回调函数的代码和开始任务的代码不在同一个事件循环内
- 回调地狱——嵌套回调
- 处理并行任务棘手——请求只见互不依赖
实现一个简单的Promise:
let promise = new Promise((resolve, reject) => {
resolve()
})
promise.then((res)=>{
console.log('回调成功');
},(err)=>{
console.log('回调失败');
})
- 通过内置的Promise构造函数可以创建一个Promise对象,构造函数传入两个函数参数:resolve和reject。两个函数的作用是:在函数内手动调用resolve时,就说明回调成功;调用reject就说明失败。通常在promise中进行耗时的异步操作,响应是否成功,就根据判断调用对应的函数。
- 调用Promise对象内置的then方法,传入两个函数,一个是成功的回调函数,一个是失败的回调函数。当在promise内部调用resolve函数时,就会回调then方法里的第一个函数,当调用reject就回调then的第二个函数。
- promise相当于一个承诺,当承诺兑现(调用resolve)就会调用then中第一个函数,在回调函数中做处理。当承诺出现未知错误或异常(调用reject),就会调用then的第二个函数,提示错误。
Promise的状态
其实Promise对象用作异步任务的一个占位符,代表暂时还没有获得但在未来获得的值.
一共三种状态,完成和拒绝都是由等待状态转变的,一旦进入完成或拒绝就不能切换了。
- 等待状态pending
- 完成状态resolve
- 拒绝状态reject
拒绝状态分两组:显式拒绝(直接调用reject)和隐式拒绝(抛出异常)
一个Promise实例:
function getJson(url) {
return Promise((resolve, reject) => {
const request = new XMLHttpRequest()
request.open('GET', url)
request.onload = function () {
try {
if (this.status == 200) {
resolve(JSON.parse(this.response))
} else {
reject(this.status + ' ' + this.statusText)
}
} catch (e) {
reject(e.message)
}
}
request.onerror = () => {
reject(this.status + ' ' + this.statusText)
}
request.send()
})
}
getJson('http://localhost:3000').then((res) => {
console.log('拿到的数据为:' + res);
}, (err) => {
console.log('错误信息为:' + err);
})
Promise链
promise可以实现链式调用,每次then之后返回的都是一个promise对象,可以紧接着使用then继续处理接下来的任务,这样就实现了链式调用。如果then中使用了return,那么return的值也会被Promise.resolve()包装。
Promise.resolve(1)
.then(res => {
console.log(res) // => 1
return 2 // 包装成 Promise.resolve(2)
})
.then(res => {
console.log(res) // => 2
})
嵌套任务处理
// 链式回调
getJson("url")
.then(n => {getJson(n[0].url)})
.then(m => {getJson(m[0].url)})
.then(w => {getJson(w[0].url)})
.catch((error => {console.log("异常错误!")}))
- then方法会返回promise对象,所以连续调用then方法可以进行链式调用promise
- 多个异步任务可能出现错误,只需要调用catch方法,并向其传入错误处理的回调函数。
并行处理任务
上述的链式调用主要处理多个异步任务之间存在依赖,如果要同时执行多个异步任务就用Promise的all方法。
Promise.all([getJson(url), getJson(url), getJson(url)]).then(res => {
if (res[0] == 1 && res[1] == 1 && res[2] == 1) {
console.log('请求成功');
}
}).catch(err => {
console.log('异常');
})
- 使用all方法将多个请求任务封装成数组进行同步请求。
- 返回的结果值会打包成一个数组,可以通过数组下标获取每个返回结果。
- 只有全部请求成功才会进入成功的方法,否则就会调用catch
- 与race方法不同,race只要其中一个返回成功,就会调用成功的方法。
如何实现一个Promise
根据Promise的执行顺序,手动实现一个
- 先执行MyPromise构造函数
- 注册then方法
- 此时promise挂起,UI非堵塞,执行其他的同步代码
- 执行回调函数
// 三种状态
const PENDING = "pending";
const RESOLVE = "resolve";
const REJECT = "reject";
// promise 函数
function MyPromise(fn){
const that = this; // 回调时用于保存正确的 this 对象
that.state = PENDING; // 初始化状态
that.value = null; // value 用于保存回调函数(resolve/reject 传递的参数值)
that.resolvedCallbacks = []; // 用于保存 then 中的回调
that.rejectedCallbacks = [];
// resolve 和 reject 函数
function resolve(value) {
if(that.state === PENDING){
that.state = 'resolve';
that.value = value;
that.resolvedCallbacks.map(cb => cb(that.value));
}
}
function reject(value) {
if (that.state === PENDING) {
that.state = 'reject'
that.value = value
that.rejectedCallbacks.map(cb => cb(that.value))
}
}
// 实现如何执行 Promise 中传入的函数
try {
fn(resolve, reject)
} catch (e) {
reject(e)
}
}
// 实现 then 函数
MyPromise.prototype.then = function(onResolved, onRejected) {
const that = this;
// 判断两个参数是否为函数类型(如果不是函数,就创建一个函数赋值给对应的参数)
onResolved = typeof onResolved === 'function' ? onResolved : v => v;
onRejected = typeof onRejected === 'function' ? onRejected : r => {throw r}
// 判断当前的状态
if (that.state === 'pending') {
that.resolvedCallbacks.push(onResolved)
that.rejectedCallbacks.push(onRejected)
}
if (that.state === 'resolve') {
onResolved(that.value)
}
if (that.state === 'reject') {
onRejected(that.value)
}
}
new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve(1)
}, 0)
}).then(value => {
console.log(value)
})
async和await
与Generator和Promise有什么区别。
ES7中的async/await就是Generator和Promise的语法糖,内部实现原理还是原来的,只不过在写法上有所改变,这些实现异步任务写起来更像是执行同步任务。
特点:
一个函数前加上async关键字,就将该函数返回一个Promise,async直接将返回值使用Promise.reslove()包裹。
await只能配套async使用,await内部实现了generator,await就是generator加上Promise的语法糖,且内部实现了自动执行generator。
优点
- 内置执行器。async函数自带执行器,所以与普通函数一样,只需要一行
- 更好的语义。async表示函数有异步操作,await表示紧跟在后面的表达式需要等待结果。
- 更广的适用性。co函数库约定,yield命令后面只能是Thunk函数或Promise对象,而async函数的await命令后面,可以跟Promise对象和原始类型的值
缺点
因为await将异步代码改造成了同步代码,如果多个异步代码没有依赖性却使用了await,就会导致性能降低。
await原理
就是将Generator函数和自动执行器包装在一个函数里面。
function spawn(genF) {
return new Promise(function(resolve, reject) {
var gen = genF();
function step(nextF) {
try {
var next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
模块化
为什么使用模块化?
模块化解决了命名冲突问题,提高代码复用和可维护性。
好处:
- 避免命名冲突
- 更好分离,按需加载
- 复用性,可维护性
方式1:函数
最起初,实现模块化的方式是使用函数进行封装。将不同功能的代码实现封装到不同的函数中。通常一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。
function a(){
// 功能二
}
function b(){
// 功能一
}
但容易发生命名冲突以及数据不安全。
方式2:立即执行函数
立即执行函数中的匿名函数中有独立的词法作用域,避免了外接访问此作用域的变量。通过函数作用域解决了命名冲突,污染全局作用域的问题。
(function (window) {
let name = 'xiaolu'
//暴露的接口来访问数据
function a() {
console.log(`name:${name}`);
}
//暴露接口
window.myModule = { a }
})(window)
方式3:CommonJS
- CommonJS的规范主要用在NodeJS中,为模块提供了四个接口:module、exports、require、global
- CommonJS用同步的方式加载模块(服务器端),在浏览器端使用的是异步加载模块
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
// 对外暴露接口
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 加载外部模块
var mod = require('./lib');
console.log(mod.counter); // 3
mod.incCounter();
// 原始类型的值被缓存,所以就没有被改变(commonJS 不会随着执行而去模块随时调用)
console.log(mod.counter); // 3
加载机制
CommonJS的加载机制是,输入的是被输出的值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
特点:
- 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载就直接读取缓存结果。要想让模块再次运行,必须清楚缓存。
- 模块加载的顺序,按照在代码出现的顺序。
方式4:AMD和CMD
AMD是RequireJS推广产出的,CMD是SeaJS推广产出的。类似的还有CommonJS
这些都是为了JavaScript的模块化开发。
区别
- 对于依赖的模块,AMD是提前执行,CMD是延迟执行
- CMD推崇依赖就近,AMD推崇依赖前置。
- AMD的API默认一个当多个用,CMD的API严格区分,职责单一。
方式5:ES6 Module
ES6实现的模块非常简单,用于浏览器和服务端。
import命令会被JavaScript引擎静态分析,在编译时,就引入模块,主要有export和import
- export用于对外接口
- import用于引入其他模块
// 指定指定的值暴露对外的接口
export let counter = 3;
export function incCounter() {
counter++;
}
// 加载模块中的某个值
import { counter, incCounter } from './lib';
console.log(counter); // 3
incCounter();
// ES6 模块不同的是,静态加载完毕之后,每执行到模块中的方法,就去模块内调用(外部的变量总是与模块进行绑定的),而且值不会被缓存。
console.log(counter); // 4
ES6模块和CommonJS模块的区别
- CommonJS模块输出的是一个值的拷贝,ES6输出的是值的引用。
- ES6模块是动态引用,不缓存值,模块内外是绑定的,而且是只读引用,不能修改值。ES6的js引擎对脚本静态分析的时候,遇到import就生成一个只读引用,当真正用到模块里面的值时,就去模块内部拿。
- CommonJS模块是运行时加载,ES6模块是编译时加载输出接口。
- 运行时加载:CommonJS模块就是对象;是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法
- 编译时加载:ES6模块不是对象,而是通过export显式指定输出的代码,import时采用静态命令的形式,在import指定加载某个输出值,而不是加载整个模块。
小结
- CommonJS主要用于服务端,加载模块是同步的,所以不适合浏览器环境,同步意味着阻塞
- AMD是异步并且可以并行加载模块,不过其开发成本高,难
- CMD与AMD相似
- ES6在语言标准的层面实现了模块功能,完全可取代CommonJS