Javascript底层原理总结

 

文档持续更新~

目录

基础

数据结构

JS堆栈的概念

作用域链的理解

变量提升、函数提升、浏览器解析变量的机制

理解上下文和作用域

定义一个变量到这个变量被回收做了什么

进程与线程、什么是单线程?和异步有何关系

理解MVVM、MVC

理解AMD、commonjs

虚拟内存及缓冲区溢出

null和undefined的区别

ajax/axios/fetch的区别

Promise的原理

instanceof的原理

typeof的原理

数组的扁平化

xhr对象

ES6 Proxy的概念

闭包?运行时上下文里面包括什么?

结合作用域链看闭包

三栏布局

BFC布局原理

事件

even loop事件循环

DOM事件

js事件流

js防抖和节流

事件委托原理以及优缺点

回流/重绘

浏览器相关

在地址栏输入url到最终展示界面期间发生了什么

http、https的区别

同源策略、跨域及原理

缓存及更新问题

新一代的前端存储方案--indexedDB

webview与原生应用交互

DOM

获取DOM节点的几个方法

如何给DOM节点上添加事件

服务器端知识


基础

  • 数据结构

:一种遵从先进后出 (LIFO) 原则的有序集合;新添加的或待删除的元素都保存在栈的末尾,称作栈顶,另一端为栈底。在栈里,新元素都靠近栈顶,旧元素都接近栈底。只是对原有数据进行了一次封装而已。而封装的结果是:并不去关心其内部的元素是什么,只是去操作栈顶元素,这样的话,在编码中会更可控一些

// 定义一个栈
class Stack {
    constructor() {
        this.items = []
    }
    push(element) { // 入栈
         this.items.push(element)
    }
    pop() {    // 出栈 
        return this.items.pop()
    }
    get peek() {    // 末位
        return this.items[this.items.length - 1]
    }
    get isEmpty() {    // 是否为空栈
        return !this.items.length
    }
    get size() {    // 尺寸
        return this.items.length
    }
    clear() {    // 清空栈
        this.items = []
    }
    print() {    // 打印栈数据
        console.log(this.items.toString())
    }
}
const stack = new Stack()
console.log(stack.isEmpty) // true

// 添加元素
stack.push(5)
stack.push(8)

// 读取属性再添加
console.log(stack.peek) // 8
stack.push(11)
console.log(stack.size) // 3
console.log(stack.isEmpty) // false

 

队列:与上相反,一种遵循先进先出 (FIFO / First In First Out) 原则的一组有序的项;队列在尾部添加新元素,并从头部移除元素。最新添加的元素必须排在队列的末尾。例如日常生活中的购物排队。与栈类比,栈仅能操作其头部,队列则首尾均能操作,但仅能在头部出尾部进

class Queue {
    constructor(items) {
        this.items = items || []
    }
    enqueue(element){ // 向队列尾部添加一个(或多个)新的项
        this.items.push(element)
    }
    dequeue(){ // 移除队列的第一(即排在队列最前面的)项,并返回被移除的元素
        return this.items.shift()
    }
    head(){ // 返回队列第一个元素,队列不做任何变动
        return this.items[0]
    }
    clear(){ // 清空队列
        this.items = []
    }
    get size(){ // 返回队列内元素个数
        return this.items.length
    }
    get isEmpty(){ // 队列内无元素返回 true,否则返回 false
        return !this.items.length
    }
    print() {
        console.log(this.items.toString())
    }
}

链表:存储有序的元素集合,但不同于数组,链表中的元素在内存中并不是连续放置的;每个元素由一个存储元素本身的节点和一个指向下一个元素的引用(指针/链接)组成。

集合:由一组无序且唯一(即不能重复)的项组成;这个数据结构使用了与有限集合相同的数学概念,但应用在计算机科学的数据结构中。

字典:以 [键,值] 对为数据形态的数据结构,其中键名用来查询特定元素,类似于 Javascript 中的Object

散列:根据关键码值(Key value)直接进行访问的数据结构;它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度;这个映射函数叫做散列函数,存放记录的数组叫做散列表。

:由 n(n>=1)个有限节点组成一个具有层次关系的集合;把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的,基本呈一对多关系,树也可以看做是图的特殊形式。

:图是网络结构的抽象模型;图是一组由边连接的节点(顶点);任何二元关系都可以用图来表示,常见的比如:道路图、关系图,呈多对多关系。

数据结构详细地址点这里

 

  • JS堆栈的概念

(heap)用于复杂数据类型(引用类型)分配空间,例如数组对象、object对象;它是运行时动态分配内存的,因此存取速度较慢。

(stack)中主要存放一些基本类型的变量和对象的引用,(包含池,池存放常量),其优势是存取速度比堆要快,并且栈内的数据可以共享,但缺点是存在栈中的数据大小与生存期必须是确定的,缺乏灵活性,先进后出,后进先出原则,所以 push 优于 unshift。

js的数据类型主要分为两种:基本类型值引用类型值

基本类型值 有6种:undefined,null,boolean,number,string,symbol。这六种数据类型是按值访问的,是存放在栈内存中的简单数据段,数据大小确定,内存空间大小可以分配。基本类型值的复制是值的传递,赋值以后二者再无关联,修改其中一个不会影响另一个。

引用类型值: 5种基本类型值以外的数据类型都可以看做是引用类型值,比如array,object等,是保存在堆内存中的对象。js不允许直接访问堆内存中的位置,也就是说不能直接操作对象的内存空间。在操作对象时,实际是在操作对象的引用而不是实际的对象,是按地址访问的。

Javascript底层原理总结_第1张图片

可以看到,直接传递引用类性值的时候,传递的只是引用,二者指向同一块内存,所以修改其中一个,必然会引起另一个变量的变化。 在日常的使用中,我们把对象赋值给一个变量时,通常希望得到的是一个跟原对象无关的副本,修改新的变量不影响原对象,因此就有了浅拷贝深拷贝。 

 

  • 作用域链的理解

作用域链是由于js的变量都是对象的属性,而该对象可能又是其它对象的属性,而所有的对象都是window对象的属性,所以这些对象的关系可以看作是一条链

javascript查找与变量相关联的值时,会遵循一定的规则,也就是沿着作用域链从当前函数作用域内逐级的向上查找,直到顶层全局作用域结束,若找到则返回该值,若无则返回undefined,这个链条是基于作用域的层次结构的,一旦当代码在坏境中执行时,会自动的创建一个变量对象的作用域链,其作用域链的用途也就是保证对执行坏境的全局变量和具有访问权限函数内的局部变量定制特殊的规则,由内到外有序的对变量或者函数进行访问,作用域链包含了在坏境栈中的每个执行坏境对应的变量对象,通过作用域链可以决定变量的访问与标识符的解析

作用域就是变量与函数的可访问范围,即作用域控制着变量与函数的可见性和生命周期。在JavaScript中,变量的作用域有全局作用域和局部作用域两种。

全局作用域:可以在代码的任何地方访问,一般来说,下面情况的对象会在全局作用域中:

              最外层函数和在最外层函数外面定义的变量。

              没有通过关键字"var"声明的变量。

              浏览器中,window对象的属性。

局部作用域:函数作用域(Function scope),所有的变量和函数只能在作用域内部使用

 

  • 变量提升、函数提升、浏览器解析变量的机制

JS预解析:

    1.当浏览器加载html页面的时候,首先会提供一个全局JS代码执行的环境(全局作用域)

    2.预解析(变量提升,浏览器的加载机制)

    在当前的作用域中,js代码执行之前,浏览器首先会默认把所有带var和function的进行提前的声明或者定义

    注意:对于变量只是进行了变量提前声明,而函数是提前声明并且定义

var num = 1;
// 理解声明和定义
//    声明(declare):var num; --> 告诉浏览器在全局作用域中有一个num的变量了,如果一个变量只是声明了但是没有赋值,默认的值是undefined。
//    定义(defined):num = 1; --> 给变量进行赋值
//    var ->在预解释的时候只是提前的声明
//    function ->在预解释的时候提前的声明+定义都完成了
console.log(number);  // num is not defined
console.log(num); // undefined
var num = 1;
console.log(num); // 1

console.log(fn); // 打印出函数体
function fn (){
    console.log('fn')
};
console.log(fn); // 打印出函数体

 变量和函数重名时:变量只是提前声明了,函数声明并且定义了,所以先打印的fn,然后开始执行时,变量开始赋值,函数不进行赋值。

console.log(fn) // fn(){console.log(4)}
function fn(){
    console.log(2)
}
console.log(fn) //  // fn(){console.log(4)}
var fn = 3
console.log(fn) // 3
function fn(){
    console.log(4)
}
console.log(fn) // 3

    函数表达式调用必须写到函数表达式的下面

fun(); // fun is not a function
var fun = function () {
    console.log(22);
}
相当于执行
var fun;
fun();
fun = function () {
    console.log(22);
}

    3.预解析只发生在当前的作用域(全局作用域/局部作用域)下,例如:开始只对window下的进行预解释,只有函数执行的时候才会对函数中的进行预解析

var a = 10;
function fn (){
    console.log(a); // undefined
    var a  = 11;
    console.log(a); // 11
}
fn()
console.log(a); // 10
相当于
var a = 10; // 全局变量
function fn (){
    var a; // 局部变量
    console.log(a); // undefined;
    var a  = 11;
    console.log(a); // 11;
}
fn()
console.log(a); // 10
  • 理解上下文和作用域

上下文作用域是两个不同的概念,有时我自己也经常混淆,把它们视为是同一个东西,我们知道函数的每次调用都会有与之紧密相连的作用域和上下文,从本质上说,作用域其实是基于函数的,而上下文基于对象的,也就是说作用域是涉及到它所被调用函数中的变量访问,而调用方法和访问属性又存在着不同的调用场景(4种调用场景,函数调用,方法调用,构造器函数调用,call(),apply()间接调用),而上下文始终是this所代表的,它是拥有控制当前执行代码的对象的引用

 

  • 定义一个变量到这个变量被回收做了什么

  • 进程与线程、什么是单线程?和异步有何关系

  • 理解MVVM、MVC

  • 理解AMD、commonjs

  • 虚拟内存及缓冲区溢出

  • null和undefined的区别

  • ajax/axios/fetch的区别

  • Promise的原理

  • instanceof的原理

  • typeof的原理

  • 数组的扁平化

  • xhr对象

  • ES6 Proxy的概念

  • 闭包?运行时上下文里面包括什么?

  • 结合作用域链看闭包

  • 三栏布局

  • BFC布局原理

 

 

事件

  • even loop事件循环

     even loop总结点这里

 

  • DOM事件

DOM的级别Level DOM0:不是W3C规范。

DOM0事件绑定的原理
给当前元素的某一私有属性(onXXX)赋值的过程;(之前属性默认值是null,如果我们赋值了一个函数,就相当于绑定了一个方法)
当我们赋值成功(赋值一个函数),此时浏览器会把DOM元素和赋值的的函数建立关联,以及建立DOM元素的行为监听,当某一行为被用户触发,浏览器会把赋值的函数执行;
DOM0事件绑定的特点:
只有DOM元素天生拥有这个私有属性(onxxx事件私有属性),我们赋值的方法才叫事件绑定,否则属于设置自定义属性
移除事件绑定的时候,我们只需要赋值为null;
在DOM0事件绑定中,只能给当前元素的某一个事件行为绑定一个方法,绑定多个方法,最后一次的绑定的会替换前面绑定的

DOM0分为两个事件:在标签内写onclick事件、在JS写onlicke=function(){}函数




Javascript底层原理总结_第2张图片

打印一下root,root的onclick为null

Javascript底层原理总结_第3张图片

给root添加onclick事件再打印 打印结果为fn(){}

Javascript底层原理总结_第4张图片

 

DOM1:开始是W3C规范。专注于HTML文档和XML文档。

DOM2:对DOM1增加了样式表对象模型

2级DOM 监听方法:addEventListener()和removeEventListener(),IE下的DOM2事件是attachEvent和 detachEvent。

addEvenetListener()、removeEventListener() 有三个参数:
第一个参数是事件名(如click, IE是 onclick);
第二个参数是事件处理程序函数;
第三个参数如果是true则表示在捕获阶段调用,为false表示在冒泡阶段调用。

addEventListener(‘onclick’, handle):可以为元素添加多个监听事件,触发时会按照添加顺序依次调用。

attachEvent 执行事件的顺序是从后往前的,跟addEventListener 刚好相反。

只有2级DOM包含3个事件:事件捕获阶段处于目标阶段事件冒泡阶段, DOM0 不包含

DOM0 与DOM2区别

区别:

  1. 如果定义了两个dom0级事件,dom0级事件会覆盖
  2. dom2不会覆盖,会依次执行
  3. dom0和dom2可以共存,不互相覆盖,但是dom0之间依然会覆盖
  4. DOM0是私有属性赋值,DOM2是事件池的事件机制

DOM3:对DOM2增加了内容模型 (DTD 、Schemas) 和文档验证。

DOM3事件类型:UI事件、焦点事件、鼠标事件、滚轮事件、文本事件、键盘事件、合成事件、变动事件。

同时DOM3级事件也允许开发人员自定义一些事件。

 

  • js事件流

定义:"DOM2级事件"规定的事件流包括三个阶段:事件捕获阶段处于目标阶段事件冒泡阶段

首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是时间冒泡阶段,可以在这个阶段对事件做出响应。以前面的例子,则会按下图顺序触发事件。

在DOM事件流中,事件的目标在捕获阶段不会接受到事件。这意味着在捕获阶段,事件从document到p后就定停止了。

下一个阶段是处于目标阶段,于是事件在p上发生,并在事件处理中被看成冒泡阶段的一部分。然后,冒泡阶段发生,事件又传播回document。

Javascript底层原理总结_第5张图片

 

  • js防抖和节流

函数防抖(debounce):在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

场景:给按钮加函数防抖防止表单多次提交、对于输入框连续输入进行AJAX验证时,用函数防抖能有效减少请求次数。

生活中的例子:坐电梯等。

函数节流(throttle):规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

场景:游戏中的刷新率、DOM元素拖拽、Canvas画笔功能。

生活中的例子:播放动画每一帧的频率等。

手动实现防抖/节流点这里

 

  • 事件委托原理以及优缺点

事件委托就是基于js的事件流产生的,事件委托是利用事件冒泡,将事件加在父元素或者祖先元素上,触发该事件。


    
    

 上面的代码就是一个典型的事件委托案例。利用的原理就是事件冒泡,将事件加载父元素上,通过event参数来区别按钮的不同

优点:

  1. 减少事件注册,节省内存。比如,

    • 在table上代理所有td的click事件。
    • 在ul上代理所有li的click事件。
  2. 简化了dom节点更新时,相应事件的更新。比如

    • 不用在新添加的li上绑定click事件。
    • 当删除某个li时,不用移解绑上面的click事件。

缺点:

  1. 事件委托基于冒泡,对于不冒泡的事件不支持。
  2. 层级过多,冒泡过程中,可能会被某层阻止掉。
  3. 理论上委托会导致浏览器频繁调用处理函数,虽然很可能不需要处理。所以建议就近委托,比如在table上代理td,而不是在document上代理td。
  4. 把所有事件都用代理就可能会出现事件误判。比如,在document中代理了所有button的click事件,另外的人在引用改js时,可能不知道,造成单击button触发了两个click事件。

 

  • 回流/重绘

回流(reflow):当DOM元素的结构或者位置发生改变都会引发回流,所谓回流,就是浏览器抛弃原有计算的结构和样式,从新进行DOM TREE或者RENDER TREE,非常非常非常...消耗性能。

回流会从html这个根节点开始递归往下,依次计算所有可见节点的几何信息,且回流是无可避免的

例子:容器尺寸的改变等、增加、删除 DOM节点

 

重绘(repaint):当某一个DOM元素样式更改浏览器会重新渲染这个元素。

例子:改变元素颜色、改变元素背景色 ……

注意:回流一定重绘,重绘不一定回流

关于display:none和visibility:hidden

  • display:none 的节点是不可见的,因此不会被加入Render Tree的,而visibility:hidden的节点会被加入Render Tree
  • display:none 改为 display:block 时,算是增加了一个可见节点,因此会重新渲染,所以触发回流,而visibility:hidden,节点是已经在Render Tree中的,所以会触发重绘

解决:采用虚拟dom、集中样式更改、元素批量修改、documentFragment文档碎片

 

浏览器相关

  • 在地址栏输入url到最终展示界面期间发生了什么

  • http、https的区别

  • 同源策略、跨域及原理

  • 缓存及更新问题

  • 新一代的前端存储方案--indexedDB

  • webview与原生应用交互

 

DOM

  • 获取DOM节点的几个方法

  • 如何给DOM节点上添加事件

 

服务器端知识

 

你可能感兴趣的:(JavaScript,javascript,前端面试,底层原理)