前端--JavaScript

1,560 阅读34分钟

目前 JavaScript 仍是前端开发的灵魂,各种层出不穷的框架其实都是与底层相关。

开始之前,借前端三元同学的灵魂发问自测一下掌握了多少:

原生JS灵魂之问, 请问你能接得住几个?(上)

原生JS灵魂之问(中),检验自己是否真的熟悉JavaScript?

原生JS灵魂之问(下), 冲刺进阶最后一公里

数据类型

  • 简单数据类型(栈内存)

string

number

boolean

null

undefined

symbol(创建后独一无二且不可变的数据类型,常用于解决全局变量命名冲突、创建私有变量)

bigint(操作超出JS安全范围的大整数)

ES6规范不建议用new来创建基本类型的包装类,用new 新建 symbol bigint会报错。

  • 引用数据类型(堆内存)

Object Array Function

类型判断

typeof

不能区分Object, Array, null,都会返回object,null在设计之初就是对象。

用法 typeof variable

instanceof

原理:检查右边构造函数的prototype属性,是否在左边对象的原型链上。

JS中一切皆对象,每个对象(除了null和undefined)都有自己的原型 __proto__ ,指向对应的构造函数的 prototype 属性,只有函数有 prototype 属性。

用法 variable instanceof Object

只能用于对象,适合用于判断自定义的类实例对象,能够区分Array、Object和Function,但是Array和Function也可以是Object。

有一种特殊情况,当左边对象的原型链上只有null对象,instanceof判断会失真。

/**
 * @description instanceof
 * 检测左边对象在其原型链中是否存在构右边函数的 prototype 属性
 * 若是简单数据类型或null直接返回false,原型链的尽头是null
 * @param {*} left 
 * @param {*} right 
 */
function myInstanceof(left, right) {
    if (typeof left !== 'object' || left === null) return false;
    let proto = Object.getPrototypeOf(left);
    while (proto !== null) {
        if (proto === right.prototype) return true
        proto = Object.getPrototypeOf(proto);
    }
    return false;
}
console.log(myInstanceof("111", String)); //false
console.log(myInstanceof(new String("111"), String));//true

Object.prototype.toString.call()

精准判断数据类型。

var type = function (o){
  var s = Object.prototype.toString.call(o);
  return s.match(/\[object (.*?)\]/)[1].toLowerCase();
};
type({}); // "object"
type([]); // "array"
type(5); // "number"
type(null); // "null"
type(); // "undefined"
type(/abcd/); // "regex"
type(new Date()); // "date"

类型转换

其他类型转字符串

null / undefined: null -> 'null', undefined -> 'undefined'。

boolean:true -> 'true', false -> 'false'。

number:直接转换,极大或极小值可能用指数形式。

symbol:只允许显示强制类型转换。

Object:对普通对象来说,除非自定义toString()方法,否则会调用Object.prototype.toString()方法返回内部属性[[class]]。Array有自己的toString()方法。

'1'.toString();  // 1 会先转成对象,然后对象转字符串,并不是三元说的null啊
1.toString();    // 报错 .被认为是小数点
(1).toString();   // "1"

其他类型转数值

null:0

undefined: NaN

boolean:true -> 1, false -> 0

string:相当于Number()方法,空字符串为0,包含非数字字符则为NaN。

symbol:不能转换为数字。

Object:首先被转换为相应的基本类型值,如果返回的是非数字的基本类型值,则再遵循以上规则将其强制转换为数字。

解析字符串,如 parseInt() ,允许含有非数字字符,按从左到右的顺序解析,如果遇到非数字字符就停止。

转换字符串,如 Number(),不允许出现非数字字符,否则会失败并返回 NaN。

其他类型转布尔值

假值:undefined, null, false, +0, -0, NaN, ""

所有对象(包括空对象)的转换结果都是true,甚至连false的布尔对象new Boolean(false)也是true

==

===严格相等,要求数据类型相同;==相等,会转换为同一类型再进行比较;

  1. 两者类型为null / undefined:true
  2. 一者类型为null / undefined:false
  3. 两者类型为string 和 number:将string转为 number
  4. 一者类型为boolean:将 boolean 转为 number
  5. 一者类型为object,其另一者类型为string, number 或 symbol,将 object 转为原始类型。
  6. 两者都为引用类型(对象、数组、函数):比较是否指向**同一个地址,**两个空对象、两个空数组、两个空函数指向不同的内存地址。
console.log({a: 1} == true);  //false
console.log({a: 1} == "[object object]");  //true
console.log([1] == 1);  //true 相当于调用valueOf()方法
console.log([1] == '1');  //true

[] != [] 是 true,那么**[] == ![] 为什么是true**

  1. 右边:运算符的优先级更高,![] = !true = false;boolean需要转换为number,false = 0。
  2. 左边,此时一方为object且另一方为number,将object转换为原始类型,[] = '';此时两者类型为string和number,将string转换为number, ''=0。

对象转原始类型

对象转原始类型,会调用内置的[ToPrimitive]函数,对于该函数而言,其逻辑如下:

  1. 如果Symbol.toPrimitive()方法,优先调用再返回
  2. 调用valueOf(),如果转换为原始类型,则返回
  3. 调用toString(),如果转换为原始类型,则返回
  4. 如果都没有返回原始类型,会报错
var a = {
  value: 0,
  valueOf: function() {
    this.value++;
    return this.value;
  }
};
console.log(a == 1 && a == 2);//true

null和undefined

undefined类型只有一个值,即undefined,表示在作用域中声明但还没有赋值,在转换为数值时是NaN,用法:

  • 变量声明了,但是还没有赋值,默认为undefined,如new Array(n);
  • 调用函数时,没有提供必需的参数,该参数等于undefined;
  • 对象没有赋值的属性,该属性的值为undefined;
  • 函数没有返回值时,默认返回undefined;

null类型也只有一个值,即null,用来表示尚未存在的对象,在转换为数值时是0,用法:

  • 作为函数的参数,表示该函数的参数不是对象;
  • 作为原型链的终点;

Number

浮点数精度

JavaScript 只有一种数字类型Number,所有数字都是以64位浮点数形式储存,因此设计小数的比较与运算要很小心。

JavaScript 内部,11.0是是同一个数,存在2个0:一个是+0,一个是-0,区别就是64位浮点数表示法的符号位不同。唯一的区别在于,+0-0当作分母,返回的值是不相等的。

JS中浮点数精度问题

0.1+0.2 !=0.3 怎么处理 把需要计算的数字升级(乘以10的n次幂)成计算机能够精确识别的整数,等计算完成后再进行降级(除以10的n次幂),即:

(0.1*10 + 0.2*10)/10 == 0.3 //true

toFixed在不同浏览器下的四舍五入情况不太一致,可以重写toFixed()函数统一。

安全整数

在安全范围内的整数,在二进制转换时不会出现精度丢失的情况。

JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的:

第1位:符号位,0表示正数,1表示负数 第2位到第12位(共11位):指数部分 第13位到第64位(共52位):小数部分(即有效数字)

精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-2- 1到2,都可以精确表示。超出会自动转换成 Infinity 或 -Infinity 。

使用字面量直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制。

  • 十进制:没有前导0的数值。
  • 八进制:有前缀0o0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值。
  • 十六进制:有前缀0x0X的数值。
  • 二进制:有前缀0b0B的数值。

在安全整数范围内,可通过 parseInt()方法进行进制转换。

JavaScript 提供Number对象的MAX_VALUEMIN_VALUE属性,返回可以表示的具体的最大值和最小值。

NaN

NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number)。

NaN是唯一一个非自反的值,不等于任何值,包括它本身,通常用Number.isNaN()函数判断。

  • isNaN()

会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true 。

  • Number.isNaN()

会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,对于 NaN 的判断更为准确。

函数

函数定义

函数和变量同名会如何?

函数定义优先于变量提升。

函数声明

// es5
function getSum(){}
function (){}  // 匿名函数
// es6
() => {}

函数表达式

// es5
var getSum = function(){}
// es6
const getSum = () => {}

构造函数

const getSum = new Function('a', 'b', 'return a+b')

高阶函数

参数值为函数或者返回值为函数。例如map,reduce,filter,sort方法就是高阶函数。 编写高阶函数,就是让函数的参数能够接收别的函数。

map(callback([item, index, array])[, thisArg]) reduce(callback([prevSum, currVal, array])[, originalVal]) filter(callback(item)) sort(callback(a,b)) 不传函数参数时,默认将值转换为字符串,根据字母unicode值进行升序排序。

闭包

变量的作用域:在ES5中,只有全局作用域和函数作用域。

变量的生命周期:函数作用域内的局部变量,会随着函数调用的结束而被销毁。

在ES5时代,作用域通信常用的解决方式就是闭包,但是对性能有负面影响(多执行了一个函数,多一个内存指向)。

  • 概念

简单来说,闭包(Closure)可以理解成“从内部函数访问外部函数作用域”。函数作用域内的局部变量,在父级作用域内声明,函数调用结束后仍然保留在内存里。

JavaScript 语言特有的"链式作用域"结构(chain scope),当访问一个变量时,解释器会首先在当前作用域查找标示符,如果没有找到,会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

  • 应用

封装异步操作,如 setTimeout() , onClick ; 封装变量; 延长局部变量的生命周期;

  • 内存管理

如果闭包的作用域链里包含了DOM节点,容易造成内存泄漏,但这本质上是垃圾回收机制的循环引用问题。 解决方案是在不需要使用变量后,设为 null

for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}  // 宏任务,闭包,全部输出6

for(var i = 1;i <= 5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    }, 0)
  })(i)
}   // 立即执行函数表达式,在循环时把i作为变量传入,依次输出 1 ~ 5

for(let i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}  // 块级作用域,依次输出 1 ~ 5

/**
 * @description 闭包实现计数
 */
var counter = (function() {
    var i = 0;
    return function myPrint() {
        i += 1;
        console.log(i);
        return i;
    }
})();
counter();  // 1
counter();  // 2
counter();  // 3

柯里化

函数柯里化指的是把接受多个参数的一个函数转换成一系列接受单个参数的函数。

curry 的这种用途可以理解为:参数复用、提前返回和延迟执行。典型的应用场景是求和。

/**
 * @description curry 把接受多个参数的函数转换为一系列接受单个参数的函数
 * @param {Function} fn 
 * @param  {...any} args 
 */
function curry(fn, ...args) {
    return fn.length <= args.length ? fn(...args) : curry.bind(null, fn, ...args);
}
function curry(fn, args) {
    args = args || [];
    return function () {
        let newArgs = args.concat([...arguments]);
        if (fn.length <= newArgs.length) {
            return fn.apply(this, newArgs);
        } else {
            return curry.call(this, fn, newArgs);
        }
    }
}
function multiFn(a, b, c) {
    console.log(a, b, c)
    return a * b * c;
}
var multi = curry(multiFn);
multi(2)(3)(4);

纯函数

概念:相同的输入永远会得到相同的输出,而且没有任何可观察的副作用。

例子: slice 提取目标数组的一部分不改变原数组,是纯函数;而 splice 返回原数组被删除的部分元素,并可以在删除的位置添加新的数组成员,会改变原数组,不是纯函数。

偏函数

概念:使用一个函数,应用其中一个或多个参数但不是全部参数,在这个过程中创建一个新函数,新函数用于接受剩余的参数去完成功能。

防抖和节流

函数被触发的频率太高,出于性能考虑,不希望回调函数被频繁调用。 如window.onresize事件,mousemove事件,上传进度,频繁提交表单,输入搜索联想等。

防抖(debounce)

函数被触发执行后,如果单位时间内又被触发,不会执行,且重新计时。

  • 非立即执行版,至少等待n秒后执行
/**
 * @description debounce 非立即执行版 适用场景:resize, input search
 * @param {Function} fn
 * @param {Number} interval
 */
const debounce = (fn, interval) => {
    let timer = null;
    // 箭头函数没有arguments,需要手动调用...args
    return (...args) => {
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn(...args);
        }, interval);
    }
}

function debounce (fn, interval) {
    let timer = null;
    return function () {
        let context = this;
        let args = arguments;
        if (timer) clearTimeout(timer);
        timer = setTimeout(() => {
            fn.apply(context, args);
        }, interval);
    }
}
  • 立即执行版,触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。

节流(throttle)

稀释函数的执行频率,单位时间内只执行一次。

  • 时间戳版
function throttle (fn, delay) {
  	let previous = 0;
    return function() {
        let now = Date.now();
        let _this = this;
        let args = arguments;
        if (now - previous > delay) {
            fn.apply(_this, args);
            previous = now;
        }
    }
}
  • 定时器版
/**
 * @description throttle
 * @param {Function} fn
 * @param {Number} interval
 */
const throttle = (fn, interval) => {
    let timer = null;
    return (...args) => {
        if (!timer) {
            timer = setTimeout(() => {
                timer = null;
                fn(...args);
            }, interval);
        }
    }
}

function throttle(fn, interval) {
    let timer = null;
    return funtion () {
      let context = this;
      let args = arguments;
      if (!timer) {
          timer = setTimeout(() => {
             timer = null;
             fn.apply(this, args);
          }, interval);
      }
    }
}

ES6新语法

面试官会根据你的回答扩散发问。

变量声明与作用域

  • var

全局作用域(ES5只有全局作用域和函数作用域)。

存在变量提升(即变量可以在声明之前使用,值为undefined),函数声明优先于变量提升。

// 内层变量会覆盖外层变量,内层存在变量提升
var tmp = new Date();
function f() {
  console.log(tmp);
  if (false) {
    var tmp = 'hello world';
  }
}
f(); // undefined
  • let

块级作用域:该语句所在的代码块内。

不存在变量提升,但是可能发生临时死区(ReferenceError)。

在ES5中,可通过立即执行函数表达式来模拟块级作用域。

  • const

只读常量,指的是变量指向的内存地址不得改动,对于简单数据类型,等同于常量;对于引用数据类型,变量指向的内存地址保存的只是一个指向实际值的指针, const只能保证这个指针是固定的(即总是指向另一个固定的地址)。

在ES5中,可通过 Object.defineProperty设置 writable 和 configurable 属性为false来模拟const。

箭头函数

  • 箭头函数的的this,就是定义时所在的对象;
  • 一旦绑定了上下文,就不可改变箭头函数内部 this 的指向(call、apply、bind 都不能改变);
  • 由于this函数的指向问题,箭头函数不能作为构造函数,不能使用new 命令;
  • 箭头函数没有arguments,需要手动使用...args参数代替;
  • 箭头函数不能用作generator函数;

解构赋值

数组、对象、字符串、数值、布尔值、函数参数

ES5 生成实例对象的传统方法是通过构造函数。

  • class声明提升
  • class声明内部会启用严格模式
  • class的所有方法都是不可枚举的
  • constructor方法是类的默认方法,constructor方法默认返回实例对象(即this),但是也可以指定返回另外一个对象:
  • class的所有方法都没有原型对象prototype
  • class定义的类不能被当做函数调用
  • 静态方法(static 关键字)不会被实例继承,而是直接通过类来调用

继承

  • ES5的继承

new命令会先创建一个子类的实例对象,再执行构造函数的代码,把父类的属性和方法绑定到this上。

  • ES6的继承

虽然本质上还是基于原型链的继承,但是会先执行super(),把父类实例的属性和方法绑定到this上,再通过子类的构造函数修改this。

模块

Node和ES6模块

Node, CommonJS

  1. 运行时加载(require)。
  2. 单值导出(加载一个模块就是加载对应的一个文件,一个模块被多次加载但只执行一次,放在缓存中)。
  3. 模块输出的是值拷贝(基本数据类型:值复制,引用数据类型:浅拷贝)。
  4. this是当前模块。

ES6, Module

  1. 编译时加载,效率更高(ES2020提案引入import()函数,支持动态加载模块,webpack文档中有)。
  2. 可以导出多个值。
  3. 模块输出的是值动态只读引用。
  4. this是undefined。

异步编程

回调函数

回调地狱(异步操作成功或失败的多层嵌套),代码的可读性和维护性差,异常处理复杂。 回调函数,内部使用了发布-订阅模式。

Promise

为了解决回调地狱,Promise采用了

  • 回调函数延迟绑定

回调函数不是直接声明的,而是在通过后面的 then 方法传入的,即延迟传入。

  • 返回值穿透

把多层嵌套的回调,包装成链式调用。

  • 错误冒泡

Promise 对象的错误具有“冒泡”性质,会一直向后传递直到被捕获为止。也就是说,错误总是会被下一个catch语句捕获。如果不处理错误,Promise 内部的错误不会影响到 Promise 外部的代码,通俗来说就是“Promise 会吃掉错误”。

Promise.prototype.then(onFulfilled, onRejected) 报错用的第二个参数;Promise.prototype.catch(onRejected) 报错只有一个参数,也会捕获.then()中回调函数的错误。

Promises/A+ 规范

Promises/A+ 规范是 JavaScript Promise 的标准,规定了一个 Promise 所必须具有的特性:

  • 状态机

Promise 实例有三种状态,pending、fulfilled 和 rejected,分别代表进行中、已成功和已失败。状态只能由 pending 转变 fulfilled 或者 rejected 状态,并且状态变更是不可逆的。

  • 构造函数

接收一个函数作为参数,函数接受两个参数resolve和reject,返回一个 Promise 实例。 resolve 将状态从 pending 变成 fulfilled,并返回成功的结果 value。 reject 将状态从 pending 变成 rejected,并返回失败的原因 reason。

  • 原型方法 then()

then 方法,接受两个参数 onFulfilled 和 onRejected,分别表示promise成功或失败后的回调函数。then() 方法会返回一个promise,支持链式调用。 为了更好地处理异常,ES6中还定义了 catch() 和finally() 方法。

Promise 的执行顺序是 then收集回调 -》异步操作完成触发resolve / reject => resolve / reject 执行回调,类似于 收集依赖 =》 触发通知 =》 取出依赖执行 的发布-订阅模式。

const STATUS = {
    PENDING: 'pending',
    FULFILLED: 'fulfilled',
    REJECTED: 'rejected',
}

class MyPromise {
    constructor(executor) {
        this.status = STATUS.PENDING;
        this.value = undefined;
        this.reason = undefined;
        this.onResolvedCbs = [];
        this.onRejectedCbs = [];
        
        const resolve = (value) => {
            if (this.status === STATUS.PENDING) {
                this.status = STATUS.FULFILLED;
                this.value = value;
                this.onResolvedCbs.forEach(fn => fn());
            }
        }

        const reject = (reason) => {
            if (this.status === STATUS.PENDING) {
                this.status = STATUS.REJECTED;
                this.reason = reason;
                this.onRejectedCbs.forEach(fn => fn());
            }
        }

        try {
            executor(resolve, reject);
        } catch(e) {
            reject(e);
        }
    }

    then(onFulfilled, onRejected) {
        return new MyPromise((fulfill, reject) => {
            if (this.status === STATUS.FULFILLED) {
                fulfill(onFulfilled(this.value));    
            }
            if (this.status === STATUS.REJECTED) {
                reject(onRejected(this.reason));
            }
            if (this.status === STATUS.PENDING) {
                this.onResolvedCbs.push(() => {
                    fulfill(onFulfilled(this.value))
                });
                this.onResolvedCbs.push(() => {
                    reject(onRejected(this.reason))
                });
            }
        })
    } 
}

Promise的静态方法

  • Promise.resolve()
  1. 参数为一个promise,直接返回一个promise对象。
  2. 参数为一个thenable对象,返回的promise会跟把这个对象的状态作为自己的状态。
  3. 参数为一个定值,返回以该值为valude的成功状态promise。
  • Promise.reject()

参数作为reason,返回一个带有失败原因的Promise对象。

  • Promise.prototype.finally()
  • Promise.all()
  1. 参数为空的可迭代对象,直接进行resolve()。
  2. 参数中所有promise的状态都变成resolved,将所有的返回值以数组形式传给回调函数,执行resolve(),返回的promise对象成功。
  3. 参数中只要有一个promise的状态变成rejected,将该返回值以数组形式传给回调函数,执行reject(),返回的promise对象失败。
  • Promise.race()

只要其中一个promise的状态发生改变,直接执行resolve(),将返回值传给回调函数。

  • Promise.allSettled()

只有当所有promise的状态都改变,不论是成功或失败,将所有的返回值以数组形式传给回调函数。

  • Promise.any() 提案阶段

和race()很像,但不会因为一个promise的状态变成rejected而结束。

  • try(),提案阶段

模拟try代码块,就像promise.catch模拟的是catch代码块。

有时候不论是同步或异步操作都想用promise处理,但是同步任务会变成微任务,解决方法是定义立即执行的匿名函数:

// async
const f = () => console.log('now');
(async () => f())();  // 立即执行的匿名函数,但是会吃掉f()抛出的错误
console.log('next');

// Promise
const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');

Promise 实现 sleep

const sleep = (t) => new Promise((resolve, reject) => {
  setTimeout(() => resolve(), t * 1000)
});
sleep(5).then(() => console.log('awake'));

(async function () {
     console.log(Date.now());
     const res = await sleep(5);
     console.log(res);
     console.log(Date.now());
 })();

Promise实现并行

Promise.allSettled()

let n = 3;
let arr = new Array(n);
for (let i = 0; i < n; i++) {
  arr[i] = n - i;
}
function asyncPromise (id, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(id), delay * 1000)
  });
}
let myPromises = arr.map((item, index) => asyncPromise(index, item));

// all
Promise.all(myPromises).then(res => {
  console.log('all success', res);
}).catch(err => {
  console.log('one error', err);
});

// allSettled
Promise.allSettled(myPromises).then(res => {
  console.log('all done', res);
}).catch(err => {
  console.log('error', err);
});

Promise实现串行

let n = 5;
let arr = new Array(n);
for (let i = 0; i < n; i++) {
  arr[i] = n - i;
}
function asyncPromise (id, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(id), delay * 1000)
  });
}
let myPromises = arr.map((item, index) => asyncPromise(index, item));

// reduce
function serial (myPromises) {
  let result = [];
  return myPromises.reduce((prev, curr, index) => prev.then(res => {
    return curr.then(res => {
      console.log(res);
      result.push(res);
      return index == myPromises.length - 1 ? result : curr;
    })
  }), Promise.resolve());
}

async function serial(myPromises) {
  let result = [];
  for (let p of myPromises) {
    let res = await p;
    console.log(res);
    result.push(res);
  }
  myPromises.forEach(async p => {
    let res = await p;
    console.log(res);
    result.push(res);
  });  // 经测试,forEach不能保证异步代码的顺序执行,因而不能用来实现串行
  return result;
}

serial(myPromises).then(res => {
  console.log('serial done', res);
});

Promise实现并发控制的串行

队列里始终有K个promises正在执行

let n = 10;
let arr = new Array(n);
for (let i = 0; i < n; i++) {
  arr[i] = n - i;
}
function asyncPromise (id, delay) {
  return new Promise((resolve, reject) => {
    setTimeout(() => resolve(id), delay * 1000)
  });
}
let myPromises = arr.map((item, index) => asyncPromise(index, item));

function parallelK (myPromises, limit) {
  return new Promise((resolve, reject) => {
    let result = [];
    let i = 0;
    let running = 0;
    add();
    function add () {
      while (running < limit && i < myPromises.length) {
        running += 1;
        myPromises[i++].then(res => {
          console.log(res);
          result.push(res);
        }).catch(err => {
          console.log(err);
          result.push(err);
        }).finally(() => {
          running -= 1;
          if (i < myPromises.length) {
            add();
          } else if (running == 0) {
            resolve(result);  // 确保最后一个异步请求也完成了才能resolve()
          }
        });
      }
    }
  });
}
parallelK(myPromises, 5).then(res => {
  console.log('parallel k done', res);
});

Promise 防抖

  • cancel 之前的request,XMLHttpRequest abort();用 fetch 封装实现

  • redux saga,takelatest

  • 设置一个变量 counter,每次请求加 1,只有当前值匹配 counter 时才处理 response。或者用函数包装 promise,如果不匹配counter,直接 reject() 中断Promise链。

  • Promise.race(), 只要有一个 promise 的状态发生改变,就返回值。

生成器Generator

尽管Promise通过链式回调取代了回调嵌套,但过多的链式调用可读性仍然不强。 通常与co库结合,处理异步操作。

co(function* () {
  const r1 = yield readFilePromise('1.json');
  const r2 = yield readFilePromise('2.json');
  const r3 = yield readFilePromise('3.json');
  const r4 = yield readFilePromise('4.json');
})

生成器的执行流程:

  1. 调用生成器函数后,程序阻塞,不会执行任何语句;
  2. 调用next()方法后,程序继续执行,直到遇到yield关键字暂停;
  3. 暂停后,返回一个包含value和done属性的对象,value表示当前 yield后的结果,done 表示是否执行完,return语句会使得done变为true。

async/await

async / await 利用协程Promise实现了同步方式编写异步代码的效果,被称为JS中的异步终极解决方案。

  • 优点

    优雅处理 then 链式调用的问题,async 是一个通过异步执行并隐式返回 Promise 作为结果的函数。

  • 缺点

    如果多个异步代码没有依赖性却使用了 await 会导致性能上的降低。async 函数内部所有 await 的 promise 对象执行完,返回的 promise 对象才会发生状态改变,除非遇到 return 语句或者抛出错误。

async函数执行的时候,一旦遇到await相当于执行Promise.resolve(),不论 await关键字后面返回的是不是promise, resolve()任务进入微任务队列,JS 引擎将暂停当前协程的运行,把线程的执行权交给父协程,父协程对 await 返回的 promise 调用 then 来监听异步操作的状态改变,然后继续往下执行。

forEach...中使用 async / await 并不能保证异步的有序执行, for...of... 可以,因为采用的是迭代器遍历。

对象

深拷贝和浅拷贝

  • 浅拷贝

浅拷贝,拷贝的只是对象的引用,即内存地址。 如果属性是对象,浅拷贝都是引用。

解构赋值、Object.assign()都是浅拷贝。

/**
 * @description shallow copy
 * @param {Object} obj 
 * @returns {Object}
 */
const copy = (obj) => {
    if (obj === null || typeof obj !== 'object') return obj;
    let newObj = new obj.constructor;  // 可能是数组
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {  // 过滤原型链上的属性
            newObj[key] = obj[key];
        } 
    }
    return newObj;
}
// 对象
let newObj = {...obj};
let newObj = Object.assign({}, obj);
// 数组
let newArr = [...arr];
  • 深拷贝

遍历对象中的每一个属性,拷贝值的副本。 JSON.parse(JSON.stringify(obj)) 能覆盖大部分的情况,但存在以下问题:

  • 无法解决循环引用,会无限递归,深拷贝的解决方案是用Map标记已经拷贝过的对象。
  • 无法处理特殊对象,如RegExp, Date, Set, Map,深拷贝的解决方案是用构造函数。
  • 无法拷贝函数
/**
 * @description deep copy
 * @param {Object} obj 
 * @returns {Object}
 */
const deepCopy = (obj, map = new Map()) => {
    if (map.has(obj)) return obj;
    if (obj === null || typeof obj !== 'object') return obj;
    let newObj = new obj.constructor;
    map.set(obj, true);
    for (let key in obj) {
        if (obj.hasOwnProperty(key)) {
            newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key], map) : obj[key];
        }
    }
    return newObj;
}

let obj = {a: 1, b: 2};
obj.c = obj;
// let obj = new Date();
let newObj = deepCopy(obj);
console.log(newObj, Object.getPrototypeOf(newObj));

新建

  1. {} 字面创建

  2. new Object()

回顾new命令的创建过程,会发现字面量创建更高效一些,少了__proto__指向赋值和this绑定的操作。

  1. Object.create(proto[, propertiesObject])

使用现有对象的原型proto对象及其属性propertiesObject去创建一个新的对象; 如果proto参数是null,那新对象就是个空对象,没有继承Object.prototype上的任何属性和方法,如hasOwnProperty()、toString()等。

采用继承(原型继承、构造函数继承、组合继承、寄生继承、寄生组合继承)的方式创建,本质上也是Object.create()

Object.myCreate = function(proto, properties) {
  function F() {};
  F.prototype = proto;
  let newObj = new F();
  if (properties) {
    Object.defineProperties(newObj, properties);
  }
  return newObj;
}
let myNewObj = Object.myCreate({a: 1}, {b: {value: 2}});  // F {b: 2}
console.log(newObj.__proto__);  // {a: 1}
let newObj = Object.myCreate({a: 1}, {b: {value: 2}});  // {b: 2}

冻结

  • Object.preventExtensions()

禁止修改原型,禁止添加属性

  • Object.freeze()

相当于执行了 Object.preventExtensions() 禁止添加属性,configurable: false 禁止删除属性,writable: false 禁止修改属性。

但是只冻结一层,如果属性是对象,该对象属性的属性可以修改,彻底冻结需要递归;

  • Object.seal()

与 Object.feeeze() 不同的是,writable: true,可修改属性值。

遍历

  • for...in...

遍历对象的可枚举属性(symbol属性不可枚举),会获取到原型链上的属性,用 hasOwnProperty 过滤。

  • Object.keys()

只返回对象可枚举的属性,不会获取到原型链上的属性,会获取到原型方法,在ES6类内部定义的方法是不可枚举的。

Object.getOwnPropertyNames() 方法可以返回不可枚举的属性名。

  • for...of...

可迭代数据类型:原生具有[Symbol.iterator]属性数据类型为可迭代数据类型。 可遍历所有具备 Iterator 接口的数据结构,原生具备 Iterator 接口的数据结构如下:

  • Array
  • Map
  • Set
  • String
  • TypedArray
  • 类数组对象
  • forEach

遍历Array, Map, Set,但不能中断,不能return。

对于异步代码,即使用 async / await,也不能保证异步的顺序执行。

JSON

JSON 是一种基于文本的轻量级的数据交换格式。它可以被任何的编程语言读取和作为数据格式来传递。 JSON语法基于JS,但不同于JS里的对象,更为严格。

  • JSON.stringify()

遇到undefined和函数的时候都会跳过

  • JSON.parse()

数组

类数组对象

具有length属性、可以通过下标访问的对象,即可以被迭代,但不具有数组的方法。如arguments和DOM collections。 转换为数组的方法:

  1. Array.from(arrayLike)
  2. [...arrayLike]
  3. Array.prototype.slice.call(arrayLike)
  4. Array.prototype.splice.call(arrayLike, 0) 

注意,String没有Splice方法

  1. Array.prototype.concat.apply([], arrayLike)

查找元素

  • arr.indexOf(val)

  • arr.includes(val)

  • arr.find(val)

  • arr.findIndex(val)

类型判断

  • instaceof
  • 构造函数检查
  • 原型检查Object.getPrototypeOf()
  • Object.prototype.toString.call()
  • Array.isArray()
let arr = [1, 2, [3,4]];
let obj = {};
console.log(arr instanceof Array, obj instanceof Array);
console.log(arr.constructor === Array, obj.constructor === Array);
console.log(Object.getPrototypeOf(arr) === Array.prototype, Object.getPrototypeOf(obj) === Array.prototype);
console.log(Object.prototype.toString.call(arr) === '[object Array]', Object.prototype.toString.call(obj) === '[object Array]');
console.log(Array.isArray(arr), Array.isArray(obj));

元素去重

  • Set去重
  • 考虑元素是对象的情况,保持原先的先后顺序

利用Map键值唯一的性质

  • 保留较大的value

先按value排序

let arr = [{key: 'fe', value: 19}, {key: 'ml', value: 20}, {key: 'fe', value: 17}];
const unique = (arr, key, value) => {
    arr.sort((a, b) => a[value] - b[value]);
    return [...new Map(arr.map(item => [item[key], item])).values()];   
}
console.log(unique(arr, 'key', 'value'));

数组拍平

  • flat()方法
  • 字符串转换

toString() join()

  • reduce() 递归
  • Array.prototype.some()
let arr = [1,2,[2,3,4],[2,3,[4,5]],0,6,4];
let res = myFlat(arr);
console.log(res, res.length);

function myFlat(arr) {
    let res = [];
    // res = arr.flat(Infinity);
    // res = arr.join().split(",").map(Number);
    // res = arr.toString().split(",").map(Number);
    // res = arr.reduce((prev, curr) => {
    //     return prev.concat(Array.isArray(curr) ? myFlat(curr) : curr)
    // }, [])
    // return res;
    while (arr.some(Array.isArray)) {
        arr = [].concat(...arr);
    }
    return arr;
}

数组排序

V8引擎里的sort()函数,假设数组长度为n 当 n <= 10 时,采用插入排序 当 n > 10 时,采用三路快速排序

  • 10 < n <= 1000, 采用中位数作为哨兵元素
  • n > 1000, 每隔 200~215个元素挑出一个元素,放到一个新数组,然后对它排序,找到中间位置的数,以此作为中位数
  • 冒泡排序

两两比较相邻元素,第i趟遍历使得当前最大的元素冒泡到第n-i个位置。

  • 插入排序

每趟遍历把当前元素插入到已经有序的子序列里。子序列中比当前元素大的元素,依次往后移动腾位置。

  • 选择排序(不稳定排序)

第i躺遍历,在未排序序列中找出最小的元素,与第i个元素交换位置。

  • 堆排序(不稳定排序)

选择排序的升级,将未排序序列的末尾元素,与堆顶元素交换位置,再调整堆。
堆是一种特殊的二叉树,用数组存储,a[i]的子元素是a[2i]和a[2i+1],父元素是a[i/2]。
堆化,n/2到n的元素是叶子节点,不需要堆化。
插入堆,未排序序列从后往前插入堆,从上往下堆化。
堆排序数据访问的方式没有快速排序友好。
在排序过程中,堆排序算法的数据交换次数要多于快速排序。

  • 归并排序

把长度为N的序列看作N个长度为1的子序列,对相邻子序列两两合并,直到得到长度为N的序列。
需要额外的存储空间。

  • 快速排序(不稳定排序)

冒泡排序的升级,每躺排序选择一个分割点,把待排序数组分割为两部分,其中一部分的值小于另一部分,再分别对这两部分递归排序。
分割点选的不合理,最坏情况下时间复杂度是 O(n^2)。
理想的分割点,应该使两部分的元素数量差不多。因此再实际应用中常选取中点, 衍生出三数、五数取中等。

详见数据结构。

组合数组

let list = [['热', '冷', '冰'], ['大', '中', '小'], ['重辣', '微辣'], ['重麻', '微麻']];
let options = compose(list);
console.log(options, options.length);
// 输出所有的维度组合
function compose(arr) {
    let res = arr.reduce((result, items) => {
        return items.reduce((prev, curr) => prev.concat(
            result.map(group => [].concat(group, curr))
        ), []);
    });
    return res.map(item => item.join("+"));
}

面向对象

构造函数

  • 函数内部使用this关键字,代表所要生成的对象实例;
  • 生成对象必须使用new命令;
  • 常见的构造函数,ES6规范不建议用new来创建基本数据类型的包装类。

Boolean() Number() String() Array() Date() Function() RegExp() Error() Object()

new命令

  1. 创建一个空对象;
  2. 新对象的原型__proto__指向构造函数的 prototype 属性;
  3. 绑定 this 和新对象;
  4. 执行构造函数内部的代码;
  5. 返回新对象
/**
 * @description new命令
 * 创建一个新的空对象
 * 将新对象的原型指向构造函数的prototypr属性
 * 绑定新对象到构造函数的this,并传递参数
 * @param {Function} constructor 
 * @param  {...any} args 
 * @returns {Object}
 */
function createNew(constructor, ...args) {
    if (typeof constructor !== 'function') {
        throw 'not a function';
    }
    // let obj = {};
    // Object.setPrototypeOf(obj, constructor.prototype);
    let obj = Object.create(constructor.prototype);
    let result = constructor.apply(obj, ...args);
    return result instanceof Object ? result : obj;
}

this关键字

概念: this就是函数运行时所在的对象(即环境)。由于JS支持环境动态切换,this的指向是动态的。this的设计目的就是在函数体内部,指向函数当前的运行环境。

  1. 全局上下文

默认this指向window, 严格模式下指向undefined。

  1. 函数调用

当一个函数不是一个对象的属性时,直接作为函数调用,this指向全局对象window。

  1. 对象的方法调用

当一个函数作为一个对象的方法来调用时,this指向该对象。

  1. 构造调函数用

当一个函数用new命令调用时,函数执行时会创建一个新对象,this指向所要生成的实例对象。

  1. apply / call / bind 绑定
  2. 箭头函数

定义之后,this指向不可改变。

  1. DOM事件绑定

onclik, addEventListener 默认指向绑定事件的元素。

call / apply / bind

三个函数的作用都是将函数绑定到上下文中,用来切换/固定函数中this的指向(不能用于箭头函数),常用于借用函数(类数组借用数组实例方法),构造函数的继承。

  • call方法接受的是若干个参数。
  • apply接收的是一个包含多个参数的数组,apply 在运行前要对作为参数的数组进行一系列检验和深拷贝,所以会比 call 慢,但非常适合返回数组的一类操作。
  • bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数。
/**
 * @description fun.call(thisArg, arg1, arg2...) 
 * 判断调用对象是否为函数
 * 传入的上下文对象如果不存在,则默认为全局对象window
 * 将函数设为上下文对象的方法
 * 传入给定参数,并通过上下文对象调用执行函数
 * 删除刚才新增的属性
 * 返回结果
 * @param {Object} context
 */
Function.prototype.myCall = function () {
    if (typeof this !== 'function') {
        throw new TypeError('not a function');
    }
    let context = arguments[0] || window;  // 在Node中没有全局对象window
    let fn = Symbol('fn');
    context.fn = this;
    let args = [...arguments].slice(1);  // 获取剩余参数
    console.log('myCall', context, args);
    let result = context.fn(...args);
    delete context.fn;
    return result;
}

/**
 * @description fun.apply(thisArg, [arg1, arg2...]) 
 * @param {Object} context
 */
Function.prototype.myApply = function() {
    if (typeof this !== 'function') {
        return new TypeError('not a function');  // 在Node中没有全局对象window
    }
    let context = arguments[0] || window;
    let fn = Symbol('fn');
    context.fn = this;
    let result;
    if (arguments[1]) {
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn()
    }
    delete context.fn;
    return result;
}


/**
 * @description fn.bind(thisArg, arg1, arg2...)()
 * 判断调用对象是否为函数
 * 保存当前函数的引用,获取其余传入的参数值
 * 创建一个函数返回
 * 内部使用apply来绑定函数调用
 * 需要判断函数作为构造函数的情况(传入当前函数的this),其余情况都传入指定的上下文对象
 * @param {Object} context
 * @returns {Function}
 */
Function.prototype.myBind = function () {
    if (typeof this !== 'function') {
        return new TypeError('not a function');
    }
    let context = arguments[0] || window;  // 在Node中没有全局对象window
    let args = [...arguments].slice(1);
    let self = this;
    console.log('myBind', context, args, self);
    return function fn() {
        return self.apply(
            this instanceof fn ? this : context,
            args.concat(...arguments)
        );
    }
}

let base = new Number(0);
let arr = [1, 2, 3];
// let res1 = Math.max.call(base, arr[0], arr[1], arr[2]);
// let res2 = Math.max.apply(base, arr);
// let res3 = Math.max.apply(base);
// let myMax = Math.max.bind(base, arr[0]);
// let res4 = myMax(arr[1], arr[2]);
let res1 = Math.max.myCall(base, arr[0], arr[1], arr[2]);
let res2 = Math.max.myApply(base, arr);
let res3 = Math.max.myApply(base);
let myMax = Math.max.myBind(base, arr[0]);
let res4 = myMax(arr[1], arr[2]);
console.log(res1, res2, res3, res4);

原型链

  1. 使用构造函数创建对象,在对象内部包含一个指针,这个指针指向构造函数的prototype属性,称为对象的原型;
  2. 每个构造函数内部都有一个prototype属性,prototype也是一个对象,这个对象包含了可以由该构造函数所有实例共享的属性和方法;
  3. 当我们访问一个对象的属性,如果对象内部不存在这个属性,就回去原型对象里寻找对应的属性,原型对象又有自己的原型,也就是原型链的概念,原型链的尽头是null;

Object.prototype.hasOwnProperty()函数,在执行对象查找时,永远不会去查找原型。

获取原型的方法

  1. p. __proto__ 不是规范规定的,但是在浏览器中有实现。
  2. p.constructor.prototype
  3. Object.getPrototypeOf(p)

继承

ES5继承

// 父类
function Animal(name, colors) {
  this.name = name || 'Jack'
  this.colors = colors || ['white']
  this.makeSound = function(animal) {
    animal.sound();
  }
}
Animal.prototype.eat = function (food) {
  console.log(this.name + ' is eating ' + food)
}

原型继承

  • 基于原型链,让一个构造函数的 prototype指向父类实例。
  • 原型对象的所有属性和方法,都能被实例对象共享。既是父类的实例,也是子类的实例。
  • 缺点:1. 无法实现多继承;2. 子类的多个实例将共享父类的属性,会相互影响。
//子类
function Cat(name) {
  this.name = name || 'Tom'
}
Cat.prototype = new Animal();

let cat = new Cat()
cat.colors.push('yellow')  // 继承父类的colors属性
cat.eat('fish')            // 继承父类的eat方法
console.log(cat instanceof Animal) // true
console.log(cat instanceof Cat)  // true
let cat_1 = new Cat()
console.log(cat_1.colors)  // ['white', 'yellow'] 子类的多个实例将共享父类的属性

call继承(构造函数)

  • 子类的构造函数call父类的构造函数 parent.call(this),可以实现多重继承(call多个),解决了子类多个实例共享父类属性的问题。
  • 缺点:不能继承父类原型上的属性和方法。
function Dog(name) {
  Animal.call(this)   // 动态绑定this
  this.name = name || 'Bob'
}

let dog = new Dog()
dog.colors.push('black')  // 继承父类的colors属性
dog.eat('bone')  // Uncaught TypeError: dog.eat is not a function
console.log(dog.colors)
console.log(dog instanceof Animal)  // false
console.log(dog instanceof Dog)  // true
let dog_1 = new Dog()
console.log(dog_1.colors)   // ['white']

组合继承

  • 使用构造函数继承 parent.call(this) ,可以继承父类实例属性和方法;使用原型继承 child.prototype = new parent() 可以继承父类原型属性和方法;
  • 缺点:调用了两次父类构造函数,生成了两份实例。
function Mouse(name) {
  Animal.call(this)
  this.name = name || 'Jerry'
}
Mouse.prototype = new Animal()
Mouse.prototype.constructor = Mouse

let mouse = new Mouse()
mouse.colors.push('gray')  // 继承父类的colors属性
mouse.eat('rice')     // 继承父类的原型方法 eat
console.log(mouse instanceof Animal)  // true
console.log(mouse instanceof Mouse)  // true
let mouse_1 = new Mouse()
console.log(mouse_1.colors)  // ['white']

寄生继承

  • 依托于一个对象而生的一种继承方式, Object.create()
  • 实际生产中,继承一个单例对象的场景很少。
let animal = {
  name: 'Jack',
  colors: ['white'],
  sleep: function() {
    console.log(this.name + ' is singing')
  }
}

function Bird(obj) {
  let o = Object.create(obj)
  return o
}
let bird = new Bird(animal);
bird.colors.push('red')  // 继承父类的colors属性
bird.sleep()         // 继承父类的实例方法 sleep
console.log(bird.colors)

寄生组合式继承

  • 使用构造函数继承,可以继承父类实例属性和方法 parent.call(this) ,使用寄生 Object.create(parent.prototype)和原型继承 child.prototype = parent.prototype,可以继承父类原型属性和方法,同时子类的多个实例不会共享父类的属性。
  • 子类可以传递动态参数给父类,父类的构造函数只执行了一次。
function inherit(child, parent) {
  // 继承父类的原型,合并覆盖
  // Object.setPrototypeOf(B.prototype, A.prototype);
  child.prototype = Object.assign(Object.create(parent.prototype), child.prototype)
  // 重写被污染的子类的构造函数
  child.prototype.constructor = child
}

function Pokemon(name) {
  Animal.call(this)
  this.name = name || 'pika'
}
inherit(Pokemon, Animal)
Pokemon.prototype.sound = function() {
  console.log('pika pika');
}

let pokemon = new Pokemon()
pokemon.colors.push('yellow')
console.log(pokemon.colors)  // 继承父类的colors属性
pokemon.makeSound(pokemon)         // 继承并改写父类的实例方法 makeSound
pokemon.eat('unknow')     // 继承父类的原型方法 eat
console.log(pokemon instanceof Animal)  // true
console.log(pokemon instanceof Pokemon)  // true
let pokemon_1 = new Pokemon()
console.log(pokemon_1.colors)  // ['white']

ES6继承

  • ES5的继承,new命令会先创建一个子类的实例对象,再执行构造函数的代码,把父类的属性和方法添加到this上 parent.apply(this)
  • ES6的继承,先执行 super(),把父类实例对象的属性和方法添加到this上,然后再用子类的构造函数修改this指向。因此ES6允许原生构造函数的继承;父类的static 静态方法,也会被子类继承;
class ColorPoint extends Point {
  constructor(x, y, color) {
    super(x, y);   // 调用父类的构造函数,否则得不到this对象,相当于Point.prototype.constructor.call(this);
    this.color = color;
  }
  toString() {
    return this.color + super.toString();   // 调用父类的toString()
  }
}

Object.getPrototypeOf(ColorPoint) === Point;  // true
ColorPoint.__proto__ === Point;  // true
ColorPoint.prototype.__proto__ === Point.prototype;  // true

// B 的实例继承 A 的实例
Object.setPrototypeOf(B.prototype, A.prototype);
// B 继承 A 的静态属性
Object.setPrototypeOf(B, A);

use strict

  • 全局变量显示声明
  • 禁止删除变量
  • 静态绑定
  • 增强的安全措施

禁止this关键字指向全局对象 禁止在函数内部遍历调用栈

  • 显示报错
  • 重名错误
  • arguments对象的限制
  • 函数必须声明在顶层

DOM操作

DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。

操作方法

let newElem = document.createElement("span");
parentElem.appendChild(newElem);
parentElem.insertBefore(newElem, parentElem.lastChild());
parentElem.removeChild(oldNode);
parentElem.replaceChild(newNode, oldNode);
element.setAttribute(key, value);
document.getElementsByName("myname");  // array-like
document.getElementsByTagName("span");  // array-like
document.getElementsByClassName("myclass");   // array-like
document.getElementById("myid");  // one

element.parentNode();
element.previousSibling();
element.nextSibling();
parentElem.attributes();
parentElem.childNodes(); // array-like
parentElem.firstChild();
parentElem.lastChild();

Mutation Observer API

Mutation Observer API用来监视DOM变动,比如节点的删减、属性的变动、文本内容的变动等。

  1. 异步触发,要等到所有DOM操作都结束才触发,为了应对DOM频繁变更。
  2. 把DOM变动记录封装成数组进行处理,而不是逐条个别处理。
  3. 既可以观察DOM的所有类型变动,也可以指定只观察某一类变动。
// 构造函数
const observer = new MutationObserver((mutations, observer) => {
  callback();
});

// 实例方法
// 所要观察的DOM节点,和所要观察的特定变动
const domNode = document.querySelector('article');
const options = {
  attributes: true,
  characterData: true,
  childList: true,
  subtree: true,  // 是否将观察者应用于该节点的后代所有节点
  attributeOldValue: true,  //表示观察attributes变动时,是否需要记录变动前的属性值
  characterDataOldValue: true,
  attributeFilter: ['class','src'],  // 数组,表示需要观察的特定属性
};
// 开始观察
observer.observe(domNode, options); 
// 停止观察。调用该方法后,DOM 再发生变动,也不会触发观察器
observer.disconnect(); 
// 清除变动记录,即不再处理未处理的变动。该方法返回变动记录的数组。
observer.takeRecords();

事件

EventTarget

// addEventListener(type, listener[, useCapture]) useCapture默认为false, 只在冒泡阶段被触发
// target.addEventListener(type, listener[, options]);  once只触发一次; passive使preventDefault()失效
function hello() {
  console.log('Hello world');
}
const button = document.getElementById('btn');
button.addEventListener('click', hello, false); 

// removeEventListener() 移除事件监听, 参数必须与addEventListener完全一致
const event = new Event('click');
// dispatchEvent() 触发事件
button.dispatchEvent(event);

事件模型

事件驱动编程,通过监听函数对事件作出反应。

事件传播

  • DOM0事件模型
  • IE事件模型
  • DOM2事件模型

事件捕获(capture phase):事件开始由不太具体的节点接收,从windows对象向下传播到目标节点。

目标阶段(target phase): 在目标节点上触发。

事件冒泡(bubbling phase):从目标节点逐级向上传播到较为不具体的节点或文档,div -> body -> html -> document -> window

事件代理(委托)

事件的代理(delegation):由于事件在冒泡阶段会向上传播到父节点,因此可以把子节点的监听函数定义在父节点上,由父节点的监听函数统一处理,可以:

  • 提高事件的处理速度,减少内存的占用,比如列表的事件绑定。
  • 不需要因为元素改动而修改事件绑定。

eventTarget,事件的原始触发节点。 currentEventTarget,事件当前所在的节点。

ul.addEventListener('click', function(e){
     if(e.target.tagName.toLowerCase() === 'li'){
         fn() // 执行某个函数
     }
 })

编译原理

高级程序设计语言 =》汇编语言 =》机器语言

  • 静态编译

简称AOT(Ahead of time,提前编译),通常为静态类型语言,在编译时就能提前发现错误。 以 Angular 为例,AOT 不需要在客户端导入体积庞大的编译器。

  • 动态解释

简称JIT(Just in time,即时编译),通常为动态类型语言,没有类型判断。

AOTJIT
编译平台ServerBrowser
编译时机BuildRuntime
包大小SmallLarge
执行性能Better
启动时间Quicker

现代编译器的主要工作流程:

源代码 source code =》 预处理器 preprocessor =》编译器 compiler =》 汇编程序 assembler =》目标代码 object code =》链接器 linker =》可执行文件 executables

核心阶段:

  • 解析 Parsing

    词法分析Tokenizer 和语法分析,将原始代码字符串解析成抽象语法树(Abstract Syntax Tree, AST);

  • 转换 Transformation

    利用遍历器,对AST做转换处理操作;

  • 生成代码 Code Generation

    将转换之后的AST对象生成目标语言代码字符串;

/**
 * @params {String} input
 * @returns {String}
 */
function compiler(input) {
  let tokens = tokenizer(input);
  let ast = parser(tokens);  // 括号匹配,栈
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);
  return output;
}