前端必备进阶基础(2)

258 阅读21分钟

前言

文中主要介绍浏览器运行html过程、BFC机制、原型链于与js生产工厂、执行上下文以及相关的代码示例等。

html构建相关知识

DOM和BOM

DOM

文档对象模型 (DOM) 是HTML和XML文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合。简言之,它会将web页面和脚本或程序语言连接起来。
简单来理解就是浏览器不仅提供很多方法,可以改变html的布局、样式和文本,并且将html文档解析成节点。也可以说是文档构建成对象之后的模型。

可操作DOM常用API

您在DOM编程时,通常使用的最多的就是 Document 和 window 对象。简单的说, window 对象表示浏览器中的内容,而 document 对象是文档本身的根节点。

  • document.getElementById(id)
  • document.getElementsByTagName(name)
  • document.querySelector(class)
  • document.createElement(name)
  • parentNode.appendChild(node)
  • element.innerHTML
  • element.style.left
  • element.setAttribute()
  • element.getAttribute()
  • element.addEventListener()

BOM

JS 浏览器对象模型(Browser Object Model, BOM)被广泛应用于 Web 开发之中,主要用于客户端浏览器的管理。

BOM API

  • window 是 JS 的最顶层对象,其他的 BOM 对象都是 window 对象的属性
  • document 文档对象
  • location 浏览器当前URL信息
  • navigator 浏览器本身信息
  • screen 客户端屏幕信息
  • history 浏览器访问历史信息

DOM和BOM的联系

DOM的根本对象是document,而document是window下的对象。document对象既属于BOM又属于DOM,BOM是浏览器相关,DOM是文档相关,他们之间是存在交集的。其实也就是说相关文档调用的api是隶属BOM的。

HTML构成元素和几个重要的标签

构成元素

html构成元素可分为三类

  • 行级元素:行内元素不能独占一行,与其他行内元素排成一行。不能设置宽高等
  • 块级元素:块级元素独占一行,当没有设置宽高时,它默认设置为100%。可以设置宽高等
  • 行内块元素:能设置宽高等,可以和其他元素同行 其实除去几个特殊标签,使用div和span就可以实现html大部分页面

特殊元素

有几个特殊块级元素只能包含内联元素或者可变元素,不能包含块级元素。这几个特殊标签是 h1~h6、p、dt。

DOCTYPE(⽂档类型)

DOCTYPE是HTML5中一种标准通用标记语言的文档类型声明,它的目的是告诉浏览器(解析器)应该以什么样(html或xhtml)的文档类型定义来解析文档,不同的渲染模式会影响浏览器对 CSS 代码甚⾄ JavaScript 脚本的解析。它必须声明在HTML⽂档的第⼀⾏。

meta元信息

meta 标签由 name和 content属性定义,用来描述网页文档的属性,比如网页的作者,网页描述,关键词等,除了HTTP标准固定了一些name作为大家使用的共识,开发者还可以自定义name。
meta 元素定义的元数据的类型包括以下几种:

  • 如果设置了 name属性,meta 元素提供的是文档级别(document-level)的元数据,应用于整个页面。
  • 如果设置了 http-equiv属性,meta 元素则是编译指令,提供的信息与类似命名的HTTP头部相同。
  • 如果设置了 charset属性,meta 元素是一个字符集声明,告诉文档使用哪种字符编码。
  • 如果设置了itemprop 属性,meta 元素提供用户定义的元数据。 其中name=viewport 可用于移动端的适配,其content可配置其类型 其中,content 参数有以下几种:
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">\
  • width viewport :宽度
  • height viewport :高度
  • initial-scale :初始缩放比例
  • maximum-scale :最大缩放比例
  • minimum-scale :最小缩放比例
  • user-scalable :是否允许用户缩放

DTD规范

文档类型定义(DTD)可定义合法的XML文档构建模块。它使用一系列合法的元素来定义文档的结构。 DTD 可被成行地声明于 XML 文档中,也可作为一个外部引用。

渲染机制和阻塞

浏览器渲染网页的过程:

  • 构建DOM树:词法分析然后解析成DOM树(dom tree),是由dom元素及属性节点组成,树的根是document对象
  • 构建CSS规则树:生成CSS规则树(CSS Rule Tree)
  • 构建render树:Web浏览器将DOM和CSSOM结合,并构建出渲染树(render tree)
  • 布局(Layout):计算出每个节点在屏幕中的位置
  • 绘制(Painting):即遍历render树,并使用UI后端层绘制每个节点

阻塞

图中除了异步步骤,同步的步骤任一步未执行,后面的步骤都将会被阻塞。比如说在css未加载完之前,在浏览器里面是查不到相关dom节点的。

image.png

css相关

link和@import的区别

  • 二个都是外部引用文件的方式,link除了可以加载css文件,还可以定义rss等事务时间,@impoert只能加载css。
  • link引用时,页面加载css同时加载。@import网页完全加载完后加载。
  • @import底部本不支持

Webkit CSS 引擎分析

blog.sina.com.cn/s/blog_4ba2…

一些自动生成css3动画的网站

物理像素逻辑像素和css像素

  • 物理像素:设备像素(物理像素),顾名思义,显示屏是由一个个物理像素点组成的,通过控制每个像素点的颜色,使屏幕显示出不同的图像,屏幕从工厂出来那天起,它上面的物理像素点就固定不变了,单位pt。
  • 逻辑像素:也叫“设备独立像素”(Device Independent Pixel,DIP),可以理解为反映在CSS/JS程序里面的像素点,也就是说css像素是逻辑像素的一种。
  • css像素:CSS像素是Web编程的概念,指的是CSS样式代码中使用的逻辑像素。在CSS规范中,长度单位可以分为两类,绝对(absolute)单位以及相对(relative)单位。px是一个相对单位,相对的是设备像素(device pixel)。
  • 设备像素比dp:dpr = 物理像素/逻辑像素

适配大屏和手机端的样式方案

大屏

  • 设计二套样式,一个用于大屏。一个用于普通屏幕。这样不用考虑每个屏幕的字体、美观、溢出等问题,但缺点也很明显,扩展性不强,如果换一个场景,又得重新设配样式。
  • 使用room对屏幕进行等比例的缩放,但前提是图表字体使用百分比。但这样缩放会引起,节点点击位置错位等问题。如果只是语音操作可以考虑这种方式。
  • 使用@medio做多套方案,其实这个和第一种方案大同小异。

手机端

  • 其实安卓端大部分的样式只要布局设计好,基本不用做适配。主要是对弹出键盘、特殊边(苹果)、缩放、不同设备等做兼容。

BFC机制

BFC(Block Formatting Context)即是盒模型格式化上下文,是Web页面中盒模型布局的CSS渲染模式。

css定位机制

普通流

除非专门指定,否则所有框都在普通流中定位。普通流中元素框的位置由元素在(X)HTML中的位置决定。块级元素从上到下依次排列,框之间的垂直距离由框的垂直margin计算得到。行内元素在一行中水平布置。

浮动

盒称为浮动盒(floating boxes); 它位于当前行的开头或末尾;这导致常规流环绕在它的周边,除非设置 clear 属性;

定位

相对定位、绝对定位都脱离了常规流,不会影响常规流的布局。所以它可以覆盖页面上的其他元素,可以通过设置Z-Iindex属性来控制这些框的堆放次序。

创建BFC

  • 绝对定位定位值为absolute或者fixed
  • 浮动,除去float:none
  • 设置了overflow,除去overflow:visible
  • 行内块元素
  • 表格单元格,元素的display: table-cell,HTML表格单元格默认属性
  • 弹性布局,display值为flex活着liline-flex
  • body元素,以及body的父元素

BFC的范围

一个BFC包含创建该上下文元素的所有子元素,但不包括创建了新BFC的子元素的内部元素。

BFC的特性

  • 内部的盒会在垂直方向一个接一个排列(可以看作BFC中有一个的常规流);
  • 处于同一个BFC中的元素相互影响,可能会发生margin collapse;
  • 每个元素的margin box的左边,与容器块border box的左边相接触(对于从左往右的格式化,否则相反)。即使存在浮动也是如此;
  • BFC就是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素,反之亦然;
  • 计算BFC的高度时,考虑BFC所包含的所有元素,连浮动元素也参与计算;
  • 浮动盒区域不叠加到BFC上;

BFC的例子

例子1:解决margin塌陷和margin合并

<style>
.demo{
    overflow: hidden;
}
.demo div{
    height: 50px;
    border: 1px solid gold;
    margin: 20px;
    overflow: hidden;
}
.demo>div:first-child{
    margin: 50px;
}
</style>
<body>
   <div class="demo">
       <div>1</div>
       <div>2</div>
       <div>3</div>
       <div>4</div>
       <div>5</div>
   </div>
</body>

在demo元素不加overflow属性时,你会发现此时demo的margin-top不见了,而且神奇的是demo外边距加到body上。这是由于margin塌陷和margin合并。
margin塌陷现象:在垂直方向如果有两个元素的外边距有相遇,在浏览器中加载的真正的外边距不是两个间距的加和,而是两个边距中值比较大的,边距小的塌陷到了边距值大的值内部。
此时demo的外上边距为8,body外上边距为42px。

image.png

image.png 加上属性后,此时demo距离浏览器顶部成58px,且与body间隔50px。其实这是由于demo创建了一个BFC,而body也有一个BFC。此时二个元素间相互隔离,避免了margin塌陷和margin合并

image.png

例子2:融合高度、隐藏溢出

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        .box {
            border: 1px solid yellow;
            width: 250px;
            overflow: hidden;
        }

        .block {
            height: 100px;
            width: 200px;
            opacity: 0.3;
            background: black;
        }

        .other {
            height: 90px;
            width: 50px;
            float: left;
            background: blue;
        }
    </style>
</head>

<body>
    <div class="box">
        <div class="block"></div>
        <div class="other">
            aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
            aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
            aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
        </div>
    </div>
</body>

在box未有bfc机制前,元素会溢出并且box不会包含所有子元素的高度。


image.png 有bfc机制后,元素相互隔离,计算高度是会把浮动元素也计算进去。


image.png

JS相关内容

原型链

从原型工厂到生产各类型的对象

原型链

JavaScript 只有一种结构:对象。每个实例对象(object)都有一个私有属性(称之为 __proto__ )指向它的构造函数的原型对象prototype。该原型对象也有一个自己的原型对象(__proto__),层层向上直到一个对象的原型对象为 null。根据定义,null 没有原型,并作为这个原型链中的最后一个环节。
另外隐式原型和显式原型都是协助构成原型链的。protoType关联函数而__proto__ 关联对象。我想分为显式和隐式的原因应该是prototype为最终指向的实例,而__proto__为协助指向。

Object实例

在JavaScript中,几乎所有的对象都是Object类型的实例,它们都会从Object.prototype继承属性和方法。Object 构造函数为给定值创建一个对象包装器。Object构造函数,会根据给定的参数创建对象。

解图

这张图不知出处,但在我看过的大多数文章中都出现了,基本没有把它说清楚的。总是附图一张,然后自说自的。我想看了这多遍,原型链。除了官方的解释让人醍醐灌顶,其他的都讲的模糊不清。 image.png

首先要看明白这个图要把原型链和对象实例的概念了解清楚。

  1. 图中分为三列:

    • 第一列:实际应用中的对象或者函数,如let f1 = new Foo();中的f1;let o1 = new Object()中的o1;(即是工厂产生的最终产品)
    • 第二列: 对象的构造函数,函数的构造函数。(工厂的加工机器)
    • 第三列:构造函数的原型。(工厂最终原材料)。而在第三列中所有材料的最终材料是Object实例,Object实例之上则为空
  2. 生产函数的产品线:

    • 从f1往下看。f1的隐式原型指向f1构造函数(Foo)的原型对象(Foo.protoType)

    • Foo.protoType因是对象,所以拥有私有属性__proto__,它的__proto__指向对象实例(Object.protoType)

    • 而实例对象的尽头Object.protoType.__proto__则为null,null没有原型

  3. 生产构造函数Foo的产品线:

    • Foo的隐式原型指向,Function构造函数的原型。
    • Function.protoType指向对象实例
    • Function是成产function的机器,即为所有的函数的构造函数。
  4. 生产对象的产品线:o1的隐式原型指向其构造函数Object的原型(即指向原型终点)。但是Object()的隐式原型是指向Function的。看图中的关系,Object()、Function.protoType、Object.protoType是一个闭环。Object.protoType为其圈的终点,先有Object.protoType,再有Function.protoType,再有Object()。

  5. 梳理一遍逻辑,Object.protoType是神(神之上是一片虚无),神下面有皇帝(Function.protoType)负责所有事务,皇帝有二个大臣(二个构造函数Object()和Function()),分别负责生产对象和函数,从此构建了整个js王朝根基。

具体应用

例1:属性查找

// Object.prototype.hasOwnProperty()
console.log(obj.hasOwnProperty('son'));//true

例2: instanceof && typeof

function instanceof(L, R) {
 let O = L.__proto__
 const P = R.prototype
 while(O) {
  if (O === P) return true
  O = O.__proto__
 }
 return false
}

function typeof(value) { 
    return Object.prototype.toString.call(value).slice(8, -1) 
}

实现new()

function _new(constructor, ...arg) {
  var obj = {}; 
  obj.__proto__ = constructor.prototype; 
  var res = constructor.apply(obj, arg); 
  return Object.prototype.toString.call(res) === '[object Object]' ? res : obj; 
}

迭代协议

作为 ECMAScript 2015 的一组补充规范,迭代协议并不是新的内置实现或语法,而是协议。这些协议可以被任何遵循某些约定的对象来实现。迭代协议具体分为两个协议:可迭代协议和迭代器协议。

可迭代协议

可迭代协议允许 JavaScript 对象定义或定制它们的迭代行为,例如,在一个 for..of 结构中,哪些值可以被遍历到。一些内置类型同时是内置可迭代对象,并且有默认的迭代行为,比如 Array 或者 Map,而其他内置类型则不是(比如 Object))。

要成为可迭代对象, 一个对象必须实现  @@iterator 方法。这意味着对象(或者它原型链上的某个对象)必须有一个键为 @@iterator 的属性,可通过常量 Symbol.iterator 访问该属性。 当一个对象需要被迭代的时候(比如被置入一个 for...of 循环时),首先,会不带参数调用它的 @@iterator 方法,然后使用此方法返回的迭代器获得要迭代的值。
值得注意的是调用此零个参数函数时,它将作为对可迭代对象的方法进行调用。 因此,在函数内部,this关键字可用于访问可迭代对象的属性,以决定在迭代过程中提供什么。
此函数可以是普通函数,也可以是生成器函数,以便在调用时返回迭代器对象。 在此生成器函数的内部,可以使用yield提供每个条目。

迭代器协议

迭代器协议定义了产生一系列值(无论是有限个还是无限个)的标准方式。当值为有限个时,所有的值都被迭代完毕后,则会返回一个默认返回值。 只有实现了一个拥有以下语义(semantic)的 next()  方法,一个对象才能成为迭代器:

  1. 属性: next
  2. :一个无参数或者一个参数的函数,返回一个应当拥有以下两个属性的对象:
    • done(boolean)如果迭代器可以产生序列中的下一个值,则为 false。(这等价于没有指定  done 这个属性。)如果迭代器已将序列迭代完毕,则为 true。这种情况下,value 是可选的,如果它依然存在,即为迭代结束之后的默认返回值。
    • value迭代器返回的任何 JavaScript 值。done 为 true 时可省略。next() 方法必须返回一个对象,该对象应当有两个属性: done 和 value,如果返回了一个非对象值(比如 false 或 undefined),则会抛出一个TypeError异常

定义一个简单的迭代器

let iterator = function (array) {
    let index = 0;
    return {
        next: function() {
            return index < array.length ? {
                value: array[index++],
                done: false
            }: {
                done: true
            }
        },
        done: false 
    }
}
let p = new iterator([1,2,3,4])
p.next() //{value: 1, done: false}
p.next() //{value: 2, done: false}
p.next() //{value: 3, done: false}
p.next() //{value: 4, done: false}
p.next() //{done: true}

定义总结

可迭代协议是用于内置是内置可迭代对象,并且有默认的迭代行为的值,比如array、map;而迭代器协议一般用于普通对象。只有拥有协议二者之一的对象都是可被迭代的。String、Array、TypedArray、Map和Set都是内置迭代器的。

for...in、for...of、forEach

for in遍历的是元素的索引(即键名),而for of遍历的是元素的value

for...in

  • 在遍历对象时会出现乱序的现象,因为对象的值以hash值的方式去存储的,当你去遍历读取对象的值时,不会按你定义对象时的上下顺序去读取,一般有数字先排列数字,在排列顺序排列其他,所以遍历时不能保证顺序。
  • for in 会遍历数组所有的可枚举属性,包括原型。
  • 遍历原型:
    let obj = {
        b: 1,
        12: 2,
        1: 3
    }
    obj.__proto__.a = 4
    for(let i in obj){
        console.log(i) // 1,12,a,b
    }

for...of

  • 内部使用迭代器实现,只要对象可被迭代(对象拥有方法Symbol.iterator),即可被迭代。
  • for of不会迭代原型
  • 简单实现对象迭代器:
    let obj = {
        b: 1,
        12: 2,
        1: 3
    }
    obj[Symbol.iterator] = function () {
        console.log(this)
        let array = Object.keys(this)
        let index = 0
        return {
            next: function () {
                return index < array.length ? {
                    value: array[index++],
                    done: false
                } : {
                    done: true
                }
            },
            done: false
        }
    }
    obj.__proto__.a = 4
    for (let i of obj) {
        console.log(i) // 1,12,a
    }

forEach

foeEach其实是比较特殊的。不同于其他,它是不能跳出循环的。

  • 下面简单实现一下forEach代码
    let arr = [1, 2, 3, 4, 5, 6]
    Array.prototype.forEach = function (callback, thisArg) {
        var len = this.length;
        for (var i = 0; i < len; i++) {
            // 可以让callback根据返回值判断是否返回
            // if(callback() === 'break') break
            callback.call(thisArg, this[i] + '--', i, this)
        }
       
    }
    arr.forEach(e => {
        console.log(e) //1--,2--......
        // throw new Error("StopIteration") 也可使用throw强制跳出循环
    })

闭包

描述

一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。

应用

这些代码都是找的例子,今天没时间了,拖了很久了。先发布吧。

例1:返回一个函数

    function test() {
        var n = 0
        function f() {
            n++;
            console.log(n)
        }
        return f
    }
    let fn = test()
    fn() 

例2:循环赋值

    for (var i = 0; i < 10; i++) {
        (function (j) {
            setTimeout(function () {
                console.log(j)
            }, 1000)
        })(i)
    }

例3:实现柯里化

    function curry(fn, len = fn.length) {
        return _curry(fn, len)
    }

    function _curry(fn, len, ...arg) {
        return function (...params) {
            let _arg = [...arg, ...params]
            if (_arg.length >= len) {
                return fn.apply(this, _arg)
            } else {
                return _curry.call(this, fn, len, ..._arg)
            }
        }
    }

    let fn = curry(function (a, b, c, d, e) {
        console.log(a + b + c + d + e)
    })

    fn(1, 2, 3, 4, 5)  // 15
    fn(1, 2)(3, 4, 5)
    fn(1, 2)(3)(4)(5)
    fn(1)(2)(3)(4)(5)

例4:防抖节流

    // 节流
    function throttle(fn, timeout) {
        let timer = null
        return function (...arg) {
            if (timer) return
            timer = setTimeout(() => {
                fn.apply(this, arg)
                timer = null
            }, timeout)
        }
    }

    // 防抖
    function debounce(fn, timeout) {
        let timer = null
        return function (...arg) {
            clearTimeout(timer)
            timer = setTimeout(() => {
                fn.apply(this, arg)
            }, timeout)
        }
    }

事件循环与primise

描述

JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。这个模型与其它语言中的模型截然不同,比如 C 和 Java。在往期中介绍过了。 juejin.cn/post/698911…

上下文环境

执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。执行上下文 为我们的可执行代码块提供了执行前的必要准备工作,例如变量对象的定义、作用域链的扩展、提供调用者的对象引用等信息。

执行上下文的三种环境

  • 全局执行上下文:这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文
  • 函数执行上下文:每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤
  • eval函数执行上下文:执行在 eval 函数内部的代码也会有它属于自己的执行上下文

执行上下文内容

  • 变量对象:全局上下文中的变量对象就是全局对象,以浏览器环境来说,就是 window 对象。函数执行上下文中的变量对象内部定义的属性,是不能被直接访问的,只有当函数被调用时,变量对象(VO)被激活为活动对象(AO)时,我们才能访问到其中的属性和方法。所以调用函数时this总是指向调用者。
  • 活动对象:函数进入执行阶段时,原本不能访问的变量对象被激活成为一个活动对象,自此,我们可以访问到其中的各种属性。
  • 作用域链:作用域 规定了如何查找变量,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。
  • 调用者信息:如果当前函数被作为对象方法调用或使用 bind call applyAPI 进行委托调用,则将当前代码块的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。

ES5 执行上下文过程

  • 程序启动,全局上下文被创建

    1. 创建全局上下文的 词法环境

      1. 创建 对象环境记录器 ,它用来定义出现在 全局上下文 中的变量和函数的关系(负责处理 letconst 定义的变量)
      2. 创建 外部环境引用,值为 null
    2. 创建全局上下文的 变量环境

      1. 创建 对象环境记录器,它持有 变量声明语句 在执行上下文中创建的绑定关系(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
      2. 创建 外部环境引用,值为 null
    3. 确定 this 值为全局对象(以浏览器为例,就是 window

  • 函数被调用,函数上下文被创建

    1. 创建函数上下文的 词法环境

      1. 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 letconst 定义的变量)
      2. 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
    2. 创建函数上下文的 变量环境

      1. 创建 声明式环境记录器 ,存储变量、函数和参数,它包含了一个传递给函数的 arguments 对象(此对象存储索引和参数的映射)和传递给函数的参数的 length。(负责处理 var 定义的变量,初始值为 undefined 造成声明提升)
      2. 创建 外部环境引用,值为全局对象,或者为父级词法环境(作用域)
    3. 确定 this

  • 进入函数执行上下文的执行阶段:

    1. 在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。

执行上下文栈

当一段脚本运行起来的时候,可能会调用很多函数并产生很多函数执行上下文,那么问题来了,这些执行上下文该怎么管理呢?为了解决这个问题,javascript 引擎就创建了 “执行上下文栈” (Execution context stack 简称 ECS)来管理执行上下文。

顾名思义,执行上下文栈是栈结构的,因此遵循 LIFO(后进先出)的特性,代码执行期间创建的所有执行上下文,都会交给执行上下文栈进行管理。

当 JS 引擎开始解析脚本代码时,会首先创建一个全局执行上下文,压入栈底(这个全局执行上下文从创建一直到程序销毁,都会存在于栈的底部)。

每当引擎发现一处函数调用,就会创建一个新的函数执行上下文压入栈内,并将控制权交给该上下文,待函数执行完成后,即将该执行上下文从栈内弹出销毁,将控制权重新给到栈内上一个执行上下文。

例子

// 例1
var foo = function () {
    console.log('foo1');
}

foo();

var foo = function () {
    console.log('foo2');
}

foo();
// 结果:fool、fool2
// 解析:变量声明时foo全部赋值为undefined,代码执行时,从上往下执行。

// 例2
foo();

var foo = function foo() {
    console.log('foo1');
}

function foo() {
    console.log('foo2');
}

foo();

// 结果:foo2、fool1
// 解析:函数变量和变量声明是同步的,且同名发送函数声明提升,此事一foo()打印fool2
然后开始执行代码,foo的变量被赋予一个函数,后面的function不会再次执行。

// 例3
var foo = 1;
function bar () {
    console.log(foo);
    var foo = 10;
    console.log(foo);
}

bar();
// 结果:undefined、10
// 解析:函数内存在变量,先找函数作用域。

// 例4
ar a = 1;

function foo () {
    var a = 2;
    return function () {
        console.log(this.a);
    }
}

var bar = foo().bind(this);
bar();

下期内容

  • webpack相关
  • ts相关

参考

developer.mozilla.org/zh-CN/
juejin.cn/post/684490…
juejin.cn/post/693746…