我的八股

221 阅读15分钟

JS中的数据类型

基本数据类型和引用数据类型

基本类型5+2:UndefinedNullBooleanNumberStringSymbolBigInt。 在内存中占据固定大小,保存在栈内存中。(Symbol、BigInt)
每个从Symbol()返回的symbol值都是唯一的,一个symbol值能作为对象属性的标识符,这是该数据类型仅有的目的。BigInt 只用来表示整数,没有位数的限制,任何位数的整数都可以精确表示。

引用类型: ObjectFunction等,引用类型的值是对象,保存在堆内存中,栈内存存储的是对象的变量标识符以及对象在堆内存中的存储地址。其他还有Array、Date、RegExp、特殊的基本包装类型(String、Number、Boolean) 以及单体内置对象(Global、Math)等。

数据类型检测的方法

1.typeof
优点:能够快速区分基本数据类型
缺点:不能将Object、Array和Null区分,都返回object

console.log(typeof 1);               // number
console.log(typeof true);            // boolean
console.log(typeof 'mc');            // string
console.log(typeof Symbol)           // function
console.log(typeof function(){});    // function
console.log(typeof console.log());   // function
console.log(typeof []);              // object 
console.log(typeof {});              // object
console.log(typeof null);            // object
console.log(typeof undefined);       // undefined

2.instanceof
优点:能够区分Array、Object和Function,适合用于判断自定义的类实例对象
缺点:Number,Boolean,String基本数据类型不能判断

console.log(1 instanceof Number);                    // false
console.log(true instanceof Boolean);                // false 
console.log('str' instanceof String);                // false  
console.log([] instanceof Array);                    // true
console.log(function(){} instanceof Function);       // true
console.log({} instanceof Object);                   // true

3.Object.prototype.toString.call()
优点:精准判断数据类型
缺点:写法繁琐不容易记,推荐进行封装后使用

var toString = Object.prototype.toString;
console.log(toString.call(1));                      //[object Number]
console.log(toString.call(true));                   //[object Boolean]
console.log(toString.call('mc'));                   //[object String]
console.log(toString.call([]));                     //[object Array]
console.log(toString.call({}));                     //[object Object]
console.log(toString.call(function(){}));           //[object Function]
console.log(toString.call(undefined));              //[object Undefined]
console.log(toString.call(null));                   //[object Null]

var && let && const

  • var 声明的范围是函数作用域,let 和 const 声明的范围是块作用域
  • var 和 let 用来定义变量,const 用来定义常量,使用时必须初始化且不能被修改
  • var 声明存在变量提升,可以先使用后声明,let 和 const 声明不会变量提升,会产生暂时性死区
  • var 声明允许在同一个作用域中重复声明,let 和 const 不允许
  • 在全局作用域中,使用 var 声明的变量会成为window 对象的属性,let 和 const 声明的变量则不会

数组的常用方法

arr.push(), arr.unshift(), arr.pop(), arr.shift(), arr.reverse(), arr.sort(), arr.splice()

arr.slice(), arr.reduce(reducer(pre, cur, curIndex, arr), initialVal)
arr.concat(拼接的内容), arr.toString(), arr.join()
arr.indexOf()返回第一个匹配的元素的索引, arr.lastIndexOf(), arr.find(fn(item, index, arr){item>2})返回第一个匹配的元素
arr.includes(), arr.some(), arr.every()
arr.forEach(), arr.map(), arr.filter()

作用域与作用域链,静态作用域(词法作用域)

作用域是决定了在程序运行时代码中的变量、函数和对象的可访问性。
作用域又分为全局作用域和局部作用域。在ES6之前,局部作用域只包含函数作用域,ES6通过新增命令let和const提供了块级作用域。

当要使用一个变量的时候,首先Javascript引擎会尝试在当前作用域下去寻找该变量,如果没找到,再到它的上层作用域寻找,直到找到该变量或是已经到了全局作用域,如果在全局作用域里仍然找不到该变量,它就会直接报错,这就是作用域链

JavaScript是静态作用域的设计,解释阶段便会确定作用域规则,因此作用域在函数定义时就已经确定了,而不是在函数调用时确定,作用域访问的变量是根据编写代码的结构确定的。
(JavaScript的执行分为解释执行两个阶段:
(解释阶段:词法分析、语法分析、作用域规则确定
(执行阶段:创建执行上下文、执行函数代码、垃圾回收

作用域与执行上下文区别

JavaScript是静态作用域的设计,作用域在函数定义时就已经确定了,而不是在函数调用时确定,作用域访问的变量是根据编写代码的结构确定的。
但是执行上下文是函数执行之前创建的,最明显的表示就是this的指向是执行时确定的。
所以作用域和执行上下文之间最大的区别是: 作用域在定义时就确定,并且不会改变;执行上下文在运行时确定,随时可能改变;

(一个作用域下可能包含若干个上下文环境。有可能从来没有过上下文环境(函数从来就没有被调用过);有可能有过,现在函数被调用完毕后,上下文环境被销毁了;有可能同时存在一个或多个(闭包)。同一个作用域下,不同的调用会产生不同的执行上下文环境,继而产生不同的变量的值。)

执行上下文与执行栈(译)

每当 Javascript 代码在运行的时候,它都是在执行上下文中运行的。JavaScript 中有三种类型的执行上下文:
全局执行上下文 — 这是默认的上下文,一个程序中只会有一个全局执行上下文。
函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。
Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文。

执行栈,被用来存储代码运行时创建的所有执行上下文。当 JavaScript 引擎第一次遇到一个脚本时,它会创建一个全局的执行上下文并且压入当前执行栈。每当引擎遇到一个函数调用,它会为该函数创建一个新的执行上下文并压入栈的顶部。当该函数执行结束时,执行上下文从栈中弹出。引擎总会执行执行上下文位于栈顶的函数。

闭包

闭包是指有权访问另一个函数作用域中变量的函数,内部的函数存在外部作用域中变量的引用就会导致闭包。

设计闭包是因为JavaScript 是静态作用域的设计,所以需要考虑到子函数晚于父函数销毁这种情况,一般函数执行完后,执行上下文就被销毁,但是如果子函数存在对父函数中变量的引用,闭包会保存这种引用,即便父函数执行上下文被销毁,但闭包仍可以访问父函数中变量。

通过闭包,可以实现方法和属性的私有化延长外部变量的生命周期

闭包存在对外部作用域的引用,会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄漏。(闭包中的变量存储的位置是堆内存。)

闭包的经典使用场景

变量提升(彻底解决 JS 变量提升的面试题)

原型和原型链

image.png

每个对象都有一个__proto__属性指向它的构造函数的原型对象。
每个函数对象都有一个prototype 属性指向函数的原型对象,原型对象的constructor属性默认指向函数本身。
每个函数对象都是 Function 的实例对象,所以函数对象的__proto__属性指向Function.prototype
Function.prototype.__proto__ = Object.prototype
Object.prototype.__proto__ = null, 表示原型链顶端。

原型链指当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型对象,于是就这样一直找下去。原型链的尽头一般来说都是Object.prototypeObject.prototype.__proto__ = null

this的绑定规则

  • 默认绑定:通常是独立函数调用,非严格模式下this指向全局对象,严格模式下this指向undefined。
  • 隐式绑定:函数的调用是在某个对象上触发的,即调用位置上存在上下文对象。典型的形式为 XXX.fun()。
  • 显式绑定:通过 call,apply,bind 的方式,显式的指定 this 所指向的对象。如果我们将 null 或者是 undefined 作为 this 的绑定对象传入 call、apply 或者是 bind,这些值在调用时会被忽略,实际应用的是默认绑定规则。
  • new绑定:使用new来调用函数的时候,会创建一个新对象,将新对象绑定到这个函数的this上。
  • 箭头函数:箭头函数没有自己的this,this对象继承的是外层代码块的this(外层第一个普通函数的this)。

call apply bind

都是函数内置的api,用来改变函数执行时的this指向。

apply的第一个参数是需要指定的this,第二个参数是函数的参数,以数组的形式传入;
call 和 bind 的第一个参数是需要指定的this,之后若干个参数都是函数的参数。
call 和 apply 改变了函数的this后马上执行该函数,返回该函数的执行结果;
bind则是返回该函数的拷贝,指定了this指向保存了参数,不执行该函数。

call apply 使用场景:
参数数量/顺序确定就用call,不确定的话就用apply。
考虑可读性:参数数量不多就用call。
参数集合已经是一个数组的情况,用apply,比如获取数组最大值/最小值。

//call
Function.prototype.myCall = function (context, ...args) {
    // context = context || window这种写法不可,context为'',0,false的情况下,本该context=context,但这么写会导致context=window
    if (context === null || context === undefined) {
        context = window  // 指定为 null 和 undefined 的 this 值会自动指向 window
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    
    const key = Symbol()  //给context新增一个独一无二的属性以免覆盖原有属性
    context[key] = this  //这里this是调用myCall的那个方法
    const result = context[key](...args)  //通过隐式绑定的方式调用函数
    delete context[key]  //删除添加的属性
    return result  //返回函数调用的返回值
}
//apply
Function.prototype.myApply = function (context, args) {
    if (context === null || context === undefined) {
        context = window  // 指定为 null 和 undefined 的 this 值会自动指向 window
    } else {
        context = Object(context) // 值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的实例对象
    }
    args = args ? args : []
    const key = Symbol() //给context新增一个独一无二的属性以免覆盖原有属性
    context[key] = this  //这里this是调用myApply的那个方法
    args = Array.from(args)  //把类数组变成数组
    const result = context[key](...args)  //通过隐式绑定的方式调用函数
    delete context[key]  //删除添加的属性
    return result  //返回函数调用的返回值
}
//bind//还没懂
Function.prototype.myBind = function(context, ...args) {
    const fn = this  //在此保存this
    let newFn = function(...newArgs) {
        const isNew = this instanceof newFn  //this是否是newFun的实例 也就是返回的fToBind是否通过new调用 
        const newContext = isNew ? this : Object(context)  //new调用就绑定到this上,否则就绑定到传入的context上 
        return fn.call(newContext, ...args, ...newArgs)  //用call调用源函数绑定this的指向并传递参数,返回执行结果 
    }
    //复制源函数的prototype给newFn,一些情况下函数没有prototype,比如箭头函数 
    if (fn.prototype) {
        newFn.prototype = Object.create(fn.prototype);  
    } 
    return newFn; // 返回拷贝的函数
}

new

使用new来调用函数,会自动执行下面的操作:

  1. 创建一个空对象
  2. 这个新对象继承构造函数的原型链(target.__proto__ = constructor.prototype)
  3. 将构造函数的 this 指向这个新对象,执行构造函数方法,属性和方法被添加到新对象中
  4. 如果构造函数中没有返回其它对象,那么返回这个新对象,否则返回构造函数返回的对象。
function _new() {
    let target = {};  //创建的新对象
    let [constructor, ...args] = [...arguments];  //第一个参数是构造函数
    target.__proto__ = constructor.prototype;  //执行[[原型]]连接;target 是 constructor 的实例
    let result = constructor.apply(target, args);  //执行构造函数,构造函数中的this指向这个空对象,将属性或方法添加到创建的空对象上
    
    //如果构造函数执行的结构返回的是一个对象,那么返回这个对象
    if (result && (typeof (result) == "object" || typeof (result) == "function")) {
        return result;
    }
    //如果构造函数返回的不是一个对象,返回创建的新对象
    return target;
}

箭头函数

  • 相比普通函数,箭头函数有更简洁的语法,只包含一个表达式时连 {}return 都省略掉了,包含多条语句时就不能省略 {}return
  • 箭头函数没有自己的 this,this 对象继承的是外层代码块的 this(外层第一个普通函数的 this )。
  • 正是因为它没有this,所以也就不能用作构造函数,不可以使用 new 命令,否则会抛出一个错误。
  • 使用 call()、apply()和bind() 调用,对 this 没影响, this 已经在词法层面完成了绑定。
  • 不可以使用 arguments 对象,该对象在函数体内不存在。想要在箭头函数中以类似数组的形式取得所有参数,可以利用展开运算符来接收参数
const testFunc = (...args)=>{
    console.log(args) //数组形式输出参数
}

js的运行机制

进程和线程的区别

进程是操作系统分配资源的最小单位,线程是程序执行的最小单位。
一个进程由一个或多个线程组成,线程可以理解为是一个进程中代码的不同执行路线。
进程之间相互独立,但同一进程下的各个线程间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号)。
调度和切换:线程上下文切换比进程上下文切换要快得多。

为什么js是单线程?

js作为主要运行在浏览器的脚本语言,js主要用途之一是操作DOM。如果js同时有两个线程,同时对同一个dom进行操作,这时浏览器应该听哪个线程的,如何判断优先级?为了避免这种问题,js必须是一门单线程语言,并且在未来这个特点也不会改变。

渲染进程Renderer的主要线程

GUI渲染线程
  • 负责渲染浏览器界面,解析HTML,CSS,构建DOM树和RenderObject树,布局和绘制等

    • 解析html代码(HTML代码本质是字符串)转化为浏览器认识的节点,生成DOM树,也就是DOM Tree
    • 解析css,生成CSSOM(CSS规则树)
    • 把DOM Tree 和CSSOM结合,生成Rendering Tree(渲染树)
  • 当我们修改了一些元素的颜色或者背景色,页面就会重绘(Repaint)

  • 当我们修改元素的尺寸,页面就会回流(Reflow)

  • 当页面需要Repaing和Reflow时GUI线程执行,绘制页面

  • 回流(Reflow)比重绘(Repaint)的成本要高,我们要尽量避免Reflow和Repaint

  • GUI渲染线程与JS引擎线程是互斥的

    • 当JS引擎执行时GUI线程会被挂起(相当于被冻结了)
    • GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
JS引擎线程(主线程)
  • JS引擎线程就是JS内核,负责处理Javascript脚本程序(例如V8引擎)

  • JS引擎线程负责解析Javascript脚本,运行代码

  • JS引擎一直等待着任务队列中任务的到来,然后加以处理

    • 浏览器同时只能有一个JS引擎线程在运行JS程序,所以js是单线程运行的
    • 一个Tab页(renderer进程)中无论什么时候都只有一个JS线程在运行JS程序
  • GUI渲染线程与JS引擎线程是互斥的,js引擎线程会阻塞GUI渲染线程

    • 就是我们常遇到的JS执行时间过长,造成页面的渲染不连贯,导致页面渲染加载阻塞(就是加载慢)
    • 例如浏览器渲染的时候遇到<script>标签,就会停止GUI的渲染,然后js引擎线程开始工作,执行里面的js代码,等js执行完毕,js引擎线程停止工作,GUI继续渲染下面的内容。所以如果js执行时间太长就会造成页面卡顿的情况
事件触发线程

用来控制事件循环,并且管理着一个事件队列(task queue)

定时触发器线程

setIntervalsetTimeout所在线程,通过单独线程来计时并触发定时(计时完毕后,添加到事件触发线程的事件队列中,等待JS引擎空闲后执行)。
W3C在HTML标准中规定,规定要求setTimeout中低于4ms的时间间隔算为4ms

异步http请求线程

当执行到一个http异步请求时,就把异步请求事件添加到异步请求线程,等收到响应(准确来说应该是http状态变化),再把回调函数添加到事件队列,等待js引擎线程来执行

浏览器中的事件循环

image.png

事件循环是 js 的执行机制。首先,整体的script代码(作为第一个宏任务)开始执行的时候,为防止发生阻塞,会把所有代码分为同步任务异步任务两部分,同步任务会直接进入主线程依次执行,异步任务会再分为宏任务和微任务,宏任务指定的事件完成就把它的回调加到宏任务队列,微任务指定的事件完成就把它的回调加到微任务队列。当前宏任务执行完成,主线程为空时,会查看微任务队列并全部执行,然后渲染;然后再查看宏任务队列,执行下一项宏任务,再次查看微任务队列并清空,然后渲染,如此反复,就是事件循环

因为宏任务队列的执行时间较长,所以微任务队列要优先于宏任务队列。微任务队列的代表就是Promise.thenprocess.nextTickAsync/Await(实际就是promise)MutationObserver(html5新特性),宏任务的话就是script(整体代码)setTimeoutsetIntervalsetImmediate

这道题,执行宏任务,然后执行该宏任务产生的微任务,若微任务在执行过程中产生了新的微任务,则继续执行微任务,微任务执行完毕后,再回到宏任务中进行下一轮循环。

异步

回调函数Promise、Generator 再到 Async/Await。表面上只是写法的变化,但本质上则是语言层的一次次抽象。让我们可以用更简单的方式实现同样的功能,而不需要去考虑代码是如何执行的。
异步代码执行顺序判断习题

回调函数、Promise、Async/Await改进之路

当使用多个层级的的回调函数时,会产生回调地狱(callback hell),Promise使用then将它们链接起来,可读性更高。 async/await 可以采用同步的方式调用Promise函数,提高异步代码的可读性。

async 相较于 Promise 的优势

  • 相比于 Promise,async 能更好地处理 then 链
  • 需要用到中间值时,async 更加优雅
  • async 相比于 Promise 更易于调试,因为调试器只能跟踪同步代码的每一步。
  • async 让 try/catch 可以同时处理同步和异步错误。

async 相较于 Generator 的优势

  • 比起星号和yield,更好的语义

Promise阮一峰

Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。

Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve函数和reject函数,由 JavaScript 引擎提供。
resolve函数在异步操作成功时调用,并将异步操作的结果,作为参数传递出去。(将Promise对象的状态从 pending 变为 resolved);reject函数在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去。(将Promise对象的状态从 pending 变为 rejected)。

Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数,then方法可以接受两个回调函数作为参数,then方法返回的是一个新的Promise实例。
一般来说,不要在then()方法里面定义 Reject 状态的回调函数(即then的第二个参数),总是使用catch方法。理由是catch写法可以捕获前面then方法执行中的错误,也更接近同步的写法(try/catch)。跟传统的try/catch代码块不同的是,如果没有使用catch()方法指定错误处理的回调函数,Promise 对象抛出的错误不会传递到外层代码,即不会有任何反应。

const promise = new Promise(function(resolve, reject) {
  // ... some code
  if (/* 异步操作成功 */){
    resolve(value);
  } else {
    reject(error);
  }
});

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

Promise 新建后立即执行,所以首先输出的是Promise。然后,then方法指定的回调函数,将在当前脚本所有同步任务执行完才会执行。

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

async/await

async函数声明一个异步方法,隐式返回 Promise 对象。
await用来等待异步方法执行,后面的函数执行完毕时直接跳出async函数,把await行后面的代码阻塞(加入到微任务队列),先执行async外面的同步代码,执行完毕后,再回到async函数去执行之前阻塞的代码。

//例 1
async function async1() {
    console.log('async1 start')
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('settimeout')
})
async1()
new Promise(function (resolve) {
    console.log('promise1')
    resolve()
}).then(function () {
    console.log('promise2')
})
console.log('script end')

//script start、async1 start、async2、promise1、script end、async1 end、promise2、settimeout

分析过程:

  1. 执行整段代码,遇到 console.log('script start') 直接打印结果,输出 script start
  2. 遇到定时器了,它是宏任务,先放着不执行
  3. 遇到 async1(),执行 async1 函数,先打印 async1 start,下面遇到await怎么办?先执行 async2,打印 async2,然后阻塞下面代码(即加入微任务列表),跳出去执行同步代码
  4. 跳到 new Promise 这里,直接执行,打印 promise1,下面遇到 .then(),它是微任务,放到微任务列表等待执行
  5. 最后一行直接打印 script end,现在同步代码执行完了,开始执行微任务,即 await下面的代码,打印 async1 end
  6. 继续执行下一个微任务,即执行 then 的回调,打印 promise2
  7. 上一个宏任务所有事都做完了,开始下一个宏任务,就是定时器,打印 settimeout

AJAX

AJAX全称(Async Javascript and XML),即异步的JavaScript 和 XML,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页。

Ajax 的原理简单来说是通过 XmlHttpRequest 对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面。

具体来说,AJAX 包括以下几个步骤。

  1. 创建 XMLHttpRequest 对象
  2. 发出 HTTP 请求
  3. 接收服务器传回的数据
  4. 更新网页数据
//封装一个ajax请求
function ajax(options) {
    //创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest()

    //初始化参数的内容
    options = options || {}
    options.type = (options.type || 'GET').toUpperCase()
    options.dataType = options.dataType || 'json'
    const params = options.data

    //发送请求
    if (options.type === 'GET') {
        xhr.open('GET', options.url + '?' + params, true)
        xhr.send(null)
    } else if (options.type === 'POST') {
        xhr.open('POST', options.url, true)
        xhr.send(params)

    //接收请求
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            let status = xhr.status
            if (status >= 200 && status < 300) {
                options.success && options.success(xhr.responseText, xhr.responseXML)
            } else {
                options.fail && options.fail(status)
            }
        }
    }
}

使用方式如下

ajax({
    type: 'post',
    dataType: 'json',
    data: {},
    url: 'https://xxxx',
    success: function(text,xml){//请求成功后的回调函数
        console.log(text)
    },
    fail: function(status){////请求失败后的回调函数
        console.log(status)
    }
})

XMLHttpRequest实例属性 XMLHttpRequest实例方法

Fetch

Fetch 是一个基于 promise 的 API。 采用.then 链式调用的方式处理结果,这样不仅利于代码的可读,而且也解决了回调地狱的问题。

fetch('./api/some.json')
  .then(
    function(response) {
      if (response.status !== 200) {
        console.log('Looks like there was a problem. Status Code: ' +
          response.status);
        return;
      }

      // Examine the text in the response
      response.json().then(function(data) {
        console.log(data);
      });
    }
  )
  .catch(function(err) {
    console.log('Fetch Error :-S', err);
  });

//我们先判断response的status码,如果是200我们才将response解析为JSON

用async来优化上面的代码,减少回调,使其更加语义化、容易理解:

async function geturl(){
	try{
		let res = await fetch('./api/some.json')
		if(res.status == 200){
			console.log(await res.text())
		}
	} catch(err){
		console.log(err)
	}
}

Axios

Axios是基于 promise 对 XMLHttpRequest 的封装而成的网络请求库,而Fetch是一种新的获取资源的接口方式并不是对XMLHttpRequest的封装。(它们最大的不同点在于Fetch是浏览器原生支持,而Axios需要引入Axios库。)

事件、事件冒泡、事件捕获、事件代理

javascript中的事件,可以理解就是一种交互操作,比如加载事件、鼠标事件。

由于DOM是一个树结构,如果在父子节点绑定事件时候,当触发子节点的时候,就存在一个顺序问题,这就涉及到了事件流的概念。事件流会经历三个阶段:捕获阶段(capture phase)、目标阶段(target phase)、冒泡阶段(bubbling phase)。

  • 捕获阶段是指事件响应从最外层的Window开始,逐级向内层前进,直到具体事件目标元素。在捕获阶段,不会处理响应元素注册的冒泡事件。
  • 目标阶段指触发事件的最底层的元素。
  • 冒泡阶段与捕获阶段相反,事件的响应是从最底层开始一层一层往外传递到最外层的Window。

image.png

事件代理就是利用事件冒泡或事件捕获的机制把一系列的内层元素事件绑定到外层元素。

<ul id="item-list">
	<li>item1</li>
	<li>item2</li>
	<li>item3</li>
	<li>item4</li>
</ul>

var items = document.getElementById('item-list');
//事件捕获实现事件代理
items.addEventListener('click', (e) => {console.log('捕获:click ',e.target.innerHTML)}, true);
//事件冒泡实现事件代理
items.addEventListener('click', (e) => {console.log('冒泡:click ',e.target.innerHTML)}, false);

//阻止事件冒泡
//给子级加 event.stopPropagation( )
//在事件处理函数中返回 false
//if(event.target==event.currentTarget),让触发事件的元素等于绑定事件的元素,也可以阻止事件冒泡

//阻止默认事件
//event.preventDefault( )
//return false

DOM 和 BOM

DOM,文档对象模型,提供操作页面元素的方法和属性,任何XMLHTML文档都可以用DOM表示为一个由节点构成的层级结构。
BOM,Browser Object Model,浏览器对象模型,提供了独立于内容与浏览器窗口进行交互的对象。

image.png

document 和 window 之间的区别

1653535476.png window 对象指浏览器中打开的窗口,document指页面,documentwindow的一个子对象。 window是一个全局对象,可以从浏览器中运行的任何JS代码直接访问,如:

window.alert('Hello world'); // Shows an alert
window.document; // The current page

因为这些属性和方法也是全局的,所以也可以这样访问它们

alert('Hello world'); // Shows an alert
document;// The current page

DOM 常用方法

创建节点Node

// 创建一个html元素,这里以创建h3元素为例
document.createElement("h3")

获取节点

// 通过id号来获取元素,返回一个元素对象
document.getElementById(idName) 
      
// 通过name属性获取id号,返回元素对象数组 
document.getElementsByName(name)  
   
// 通过class来获取元素,返回元素对象数组
document.getElementsByClassName(className)   

// 通过标签名获取元素,返回元素对象数组
document.getElementsByTagName(tagName)      

// 传入任何有效的`css` 选择器,即可选中单个 `DOM`元素(首个):如果页面上没有指定的元素时,返回 `null`
document.querySelector('.element')
document.querySelector('#element')
document.querySelector('div')

//返回一个包含节点子树内所有与之相匹配的`Element`节点列表,如果没有相匹配的,则返回一个空节点列表
const notLive = document.querySelectorAll("p");

获取/设置元素的属性值

// 括号传入属性名,返回对应属性的属性值
element.getAttribute(attributeName)

// 传入属性名及设置的值
element.setAttribute(attributeName,attributeValue)

增添节点

// 往element内部最后面添加一个节点,参数是节点类型
element.appendChild(Node);

// 在element内部的中在existingNode前面插入newNode
elelment.insertBefore(newNode,existingNode); 

删除节点

//删除当前节点下指定的子节点,删除成功返回该被删除的节点,否则返回null
element.removeChild(Node)

DOM 常用属性

获取当前元素的父节点

// 返回当前元素的父节点对象
element.parentNode

获取当前元素的子节点

// 返回当前元素所有子元素节点对象,只返回HTML节点
element.chlidren

// 返回当前元素多有子节点,包括文本,HTML,属性节点。(回车也会当做一个节点)
element.chilidNodes

// 返回当前元素的第一个子节点对象
element.firstChild

// 返回当前元素的最后一个子节点对象
element.lastChild

获取当前元素的同级元素

// 返回当前元素的下一个同级元素 没有就返回null
element.nextSibling

// 返回当前元素上一个同级元素 没有就返回 null
element.previousSibling

获取当前元素的文本

// 返回元素的所有文本,包括html代码
element.innerHTML

// 返回当前元素的自身及子代所有文本值,只是文本内容,不包括html代码
element.innerText

获取当前节点的节点类型

// 返回节点的类型,数字形式(1-12)
// 常见几个1:元素节点,2:属性节点,3:文本节点。
node.nodeType   

设置样式

// 设置元素的样式时使用style
element.style.color=“#eea”;

垃圾回收机制

WHAT:垃圾是指程序不用的内存或者是之前用过以后不会再用的内存,而 GC 就是负责回收这些垃圾的。

WHY:程序的运行需要内存,那么持续运行的服务进程必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。

HOW:简单来说就是定期找出那些不再用到的内存然后释放。

  • 标记清除(最常用):垃圾回收器在运行时会给内存中的所有变量都加上一个标记。假设内存中所有对象都是垃圾,全标记为 0 ;然后开始遍历把不是垃圾的节点改成 1 ;垃圾回收器会销毁那些带 0 标记的值并回收它们所占用的内存空间;最后,把所有内存中对象标记修改为 0 ,等待下一轮垃圾回收。优点是实现比较简单,缺点是会导致内存碎片化内存分配速度慢
  • 查找引用,浏览器不定时去查找当前内存的引用,如果没有被占用了,浏览器会回收它。
  • 引用计数:当前内存被引用一次,计数累加1次,移除引用就减1,减到0时就回收它。(引用计数算法的优点是可以立即回收垃圾,对象引用值为 0 的那一刻就会被回收;缺点是需要一个计数器,计数器需要占很大的位置,因为被引用数量的上限不确定,还有就是无法解决循环引用的问题。)

内存泄漏

当不再用到的对象内存,没有及时被回收时,我们叫它 内存泄漏(Memory leak)

内存泄漏的常见原因有:

  • 不正当的使用闭包可能会造成内存泄漏,在函数调用后,把外部的引用关系置空就好了。
  • 因为 没有声明 和 函数中this指向window 的问题会造成的隐式全局变量,这两个变量使用后不会被回收。当我们在使用全局变量存储数据时,要确保使用后将其置空或者重新分配。
  • 当我们使用变量缓存 DOM 节点引用后删除了节点,如果不将缓存引用的变量置空,依然进行不了 GC,也就会出现内存泄漏。
  • 当不需要 interval 或者 timeout 时,最好调用 clearInterval 或者 clearTimeout来清除,不然回调函数里的变量以及回调函数本身都无法被回收。
  • 事件监听器在组件内挂载相关的事件处理函数,而在组件销毁时不主动将其清除时,其中引用的变量或者函数都被认为是需要的而不会进行回收。
  • 在控制台能看到数据输出是因为浏览器保存了我们输出对象的引用,因此未清理的 console 如果输出了对象也会造成内存泄漏。

赋值和深/浅拷贝的区别

这三者的区别如下,不过比较的前提都是针对引用类型

  • 当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,另一个也会变化。 深、浅拷贝都是为了解决引用类型赋值时只传递引用,导致两个变量指向同一块内存,会从堆内存中开辟一个新的区域存放新对象。
  • 但浅拷贝只拷贝一层,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。
  • 深拷贝会对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。

浅拷贝的实现方式

1.Object.assign()

Object.assign() 方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。

let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);///////////
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }
2.函数库lodash的_.clone方法
var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.clone(obj1);//////////
console.log(obj1.b.f === obj2.b.f);// true
3.展开运算符...
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}/////////////
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }
4.Array.prototype.concat()
let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();//////////////
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]
5.Array.prototype.slice()
let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();///////////
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

深拷贝的实现方式

1.JSON.parse(JSON.stringify())
let arr = [1, 3, {
    username: ' kobe'
}];
let arr4 = JSON.parse(JSON.stringify(arr));/////////////
arr4[2].username = 'duncan'; 
console.log(arr, arr4)

这也是利用JSON.stringify将对象转成JSON字符串,再用JSON.parse把字符串解析成对象,一去一来,新的对象产生了,而且对象会开辟新的栈,实现深拷贝。

2.函数库lodash的_.cloneDeep方法

该函数库也有提供_.cloneDeep用来做 Deep Copy

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);//////////
console.log(obj1.b.f === obj2.b.f);// false
复制代码
3.jQuery.extend()方法

jquery 有提供一個$.extend可以用来做 Deep Copy

$.extend(deepCopy, target, object1, [objectN])//第一个参数为true,就是深拷贝
var $ = require('jquery');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = $.extend(true, {}, obj1);///////////
console.log(obj1.b.f === obj2.b.f); // false
4.手写递归方法

递归方法实现深度克隆原理:遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

有种特殊情况需注意就是对象存在循环引用的情况,即对象的属性直接的引用了自身的情况,解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系,当需要拷贝当前对象时,先去存储空间中找,有没有拷贝过这个对象,如果有的话直接返回,如果没有的话继续拷贝,这样就巧妙化解的循环引用的问题。关于这块如有疑惑,请仔细阅读ConardLi大佬如何写出一个惊艳面试官的深拷贝?这篇文章。

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

cookie、session、token

HTTP是无状态的协议,也就是说每次客户端和服务端会话完成时,服务端不会保存任何会话信息,每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器为了能确认前后两个请求是否来自同一浏览器,就必须主动的去维护一个状态,这个状态通过 cookie 或者 session 去实现。

cookie

cookie是服务器为了辨别用户身份而储存在客户端上的数据。cookie在每次请求中都会被发送,如果不使用 HTTPS并对其加密,其保存的信息很容易被窃取,导致安全风险。

HTML5提供了两种本地存储的方式 sessionStorage 和 localStorage; image.png

session

session是服务器为了辨别用户身份而储存在服务器端上的数据,sessionId会被存储到客户端的cookie

session 认证流程:

  1. 用户第一次请求服务器的时候,服务器根据用户提交的相关信息,创建对应的 Session。
  2. 浏览器接收到服务器返回的 SessionID 信息后,会将此信息存入到 Cookie 中,同时 Cookie 记录此 SessionID 属于哪个域名。
  3. 当用户第二次访问服务器的时候,请求会自动判断此域名下是否存在 Cookie 信息
  4. 如果存在自动将 Cookie 信息也发送给服务端,服务端会从 Cookie 中获取SessionID,再根据 SessionID 查找对应的 Session 信息,如果没有找到说明用户没有登录或者登录失效,如果找到 Session 证明用户已经登录可执行后面操作。

SessionID 是连接 Cookie 和 Session 的一道桥梁,大部分系统也是根据此原理来验证用户登录状态。

Cookie 和 Session 的区别

  • 安全性: Session 比 Cookie 安全,Session 是存储在服务器端的,Cookie 是存储在客户端的。
  • 存取值的类型不同:Cookie 只支持存字符串数据,Session 可以存任意数据类型。
  • 有效期不同: Cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,Session 一般失效时间较短,客户端关闭或者 Session 超时都会失效。
  • 存储大小不同: 单个 Cookie 保存的数据不能超过 4K,Session 可存储数据远高于 Cookie,但是当访问量过多,会占用过多的服务器资源。

session扩展性不好,如何实现单点登录?

  • Nginx ip_hash 策略,服务端使用 Nginx 代理,每个请求按访问 IP 的 hash 分配,这样来自同一 IP 固定访问一个后台服务器,避免了在服务器 A 创建 Session,第二次分发到服务器 B 的现象。
  • Session复制:任何一个服务器上的 Session 发生改变(增删改),该节点会把这个 Session 的所有内容序列化,然后广播给所有其它节点。
  • 共享Session:服务器端无状态化,将用户的session等信息使用缓存中间件(如Redis)统一管理,保障分发到每一个服务器的响应结果都一致。
  • 使用token。

token

token 的身份验证流程:数字签名这里不明白...

  1. 客户端使用用户名跟密码请求登录。
  2. 服务端收到请求,去验证用户名与密码。
  3. 验证成功后,服务端会签发一个 token 并把这个 token 发送给客户端。
  4. 客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里。
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 token。
  6. 服务端收到请求,然后去验证客户端请求里面带着的 token(放到 HTTP 的 Header 里),如果验证成功,就向客户端返回请求的数据。

Token和Session的区别...

Session是一种HTTP储存机制, 为无状态的HTTP提供持久机制; Token就是令牌, 比如你授权(登录)一个程序时,它就是个依据,判断你是否已经授权该软件;

Session和Token并不矛盾,作为身份认证Token安全性比Session好,因为每一个请求都有签名还能防止监听以及重放攻击,而Session就必须依赖链路层来保障通讯安全了。如上所说,如果你需要实现有状态的回话,仍然可以增加Session来在服务端保存一些状态。

防抖、节流

(链接里还讲了重绘、回流,浏览器解析url过程,dns,tcp三次握手四次挥手,浏览器渲染页面)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>防抖</title>
</head>
<body>
  <button id="debounce">点我防抖!</button>

  <script>
    window.onload = function() {
      // 1、获取这个按钮,并绑定事件
      var myDebounce = document.getElementById("debounce");
      myDebounce.addEventListener("click", debounce(sayDebounce, 1000));
    }

    function debounce(fn, delay) {
        let timeout = null
        return function(){
            clearTimeout(timeout)
            timeout = setTimeout(() => {
                fn.call(this, arguments)
            }, delay)
        }
    }
    // 2、防抖功能函数,接受传参
    // function debounce(fn,delay) {
    //   // 4、创建一个标记用来存放定时器的返回值
    //   let timeout = null;
    //   return function(args) {
    //     // 5、每次当用户点击/输入的时候,把前一个定时器清除
    //     clearTimeout(timeout);
    //     // 6、然后创建一个新的 setTimeout,
    //     // 这样就能保证点击按钮后的 interval 间隔内
    //     // 如果用户还点击了的话,就不会执行 fn 函数
    //     timeout = setTimeout(() => {
    //       fn.call(this, args);
    //     }, delay);
    //   };
    // }

    // 3、需要进行防抖的事件处理
    function sayDebounce() {
      // ... 有些需要防抖的工作,在这里执行
      console.log("hhhhhhhh");
    }

  </script>
</body>
</html>
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=no">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>节流</title>
</head>

<body>

    <button id="throttle">点我节流!</button>

    <script>
        window.onload = function () {
            // 1、获取按钮,绑定点击事件
            var myThrottle = document.getElementById("throttle");
            myThrottle.addEventListener("click", throttle(sayThrottle, 1000));
        }

        function throttle(fn, delay) {
            let lock = 1
            return function () {
                if (!lock) {
                    return
                }
                lock = 0
                setTimeout(() => {
                    fn.call(this, arguments)
                    lock = 1
                }, delay)

            }
        }
        // // 2、节流函数体
        // function throttle(fn) {
        //   // 4、通过闭包保存一个标记
        //   let canRun = true;
        //   return function() {
        //     // 5、在函数开头判断标志是否为 true,不为 true 则中断函数
        //     if(!canRun) {
        //       return;
        //     }
        //     // 6、将 canRun 设置为 false,防止执行之前再被执行
        //     canRun = false;
        //     // 7、定时器
        //     setTimeout( () => {
        //       fn.call(this, arguments);
        //       // 8、执行完事件(比如调用完接口)之后,重新将这个标志设置为 true
        //       canRun = true;
        //     }, 1000);
        //   };
        // }

        // 3、需要节流的事件
        function sayThrottle() {
            console.log("节流成功!");
        }

    </script>
</body>

</html>

重绘、回流

事件绑定

给同一个DOM元素绑定多个on + '事件类型'(onclick)事件,后边的绑定事件覆盖前边的。

给同一个DOM元素绑定多个addEventListener事件,是不会互相覆盖的,彼此顺序执行。

给同一个DOM元素即使用onclick绑定事件,又使用addEventListener绑定事件,他们之间是不会覆盖的。如果onclick事件是直接绑定行内绑定在DOM标签上的,会优先执行onclick绑定事件,接下来执行addEventListener事件;如果onclick事件是在js代码中注册的,则绑定事件顺序执行。

on事件解绑需要将其置为none,而addEventListener需要使用removeEventListener。

// on 绑定
window.onresize = function () { // ... }
// on 解绑
window.onresize = none

// addEventListener 绑定
window.addEventListener('resize', function () { // ... }, false)
// addEventListener 解绑
window.removeEventListener('resize', function () { // ... }, false)

精度丢失

JavaScript 在计算时,会先将10进制转为二进制,再进行计算。

0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

这里得到的结果0.0100110011001100110011001100110011001100110011001100111再转换成十进制就是0.30000000000000004

解决办法:

  1. 把小数转成整数后再运算。以加法为例:
/**
 * 精确加法
 */
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}
  1. 还可以使用第三方库 Decimal
x = new Decimal(0.1)
y = x.plus(0.2) 

bignumber

x = new BigNumber(0.1)
y = x.plus(0.2)    

apply获取数组最大值最小值

apply直接传递数组做要调用方法的参数,也省一步展开数组,比如使用Math.maxMath.min来获取数组的最大值/最小值:

const arr = [15, 6, 12, 13, 16];
const max = Math.max.apply(Math, arr); // 16
const min = Math.min.apply(Math, arr); // 6

为啥不用__proto__而是使用 Object.getPrototypeOf 

__proto__ 并不是语言本身的特性,这是各大厂商具体实现时添加的私有属性,虽然目前很多现代浏览器的 JS 引擎中都提供了这个私有属性,但依旧不建议在生产中使用该属性,避免对环境产生依赖。生产环境中,我们可以使用 Object.getPrototypeOf 方法来获取实例对象的原型,然后再来为原型添加方法/属性。

http2实现了多路复用,http1.1为什么不能多路复用?

HTTP/1.1是基于文本分割解析的协议,也没有序号,如果多路复用会导致顺序错乱,http2则用帧的方式,等于切成一块块,每一块都有对应的序号,所以可以实现多路复用。