学习总览
JavaScript
- JavaScript执行机制
- 同步与异步
- 定时器
CSS
- Flex布局
- 新增属性与选择器回顾
HTML
- 元素类型
其他
- 十一月份学习内容复习问答
学习内容
JavaScript执行机制
词法作用域与动态作用域
什么是作用域?在《你不知道的JavaScript》(上)中这样描述作用域:变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量。这套规则被称为作用域。简言之,作用域就是一套用于存储以及查找变量的规则,我们可以借助作用域来找到我们需要的变量。
作用域又分为静态作用域与动态作用域,其中静态作用域又称为词法作用域,JavaScript就使用了静态作用域。这两类作用域的差别就在于静态作用域是在函数定义的时候就确定下来了,而动态作用域则是在函数调用时才能确定的。借助冴羽老师的博客示例,我们来做一个对比:
var value = 1;
function foo() {
console.log(value);
}
function bar() {
var value = 2;
foo();
}
bar();
我们已经说过JavaScript是静态作用域,所以这段代码在浏览器中会返回1,分析过程如下: 在foo定义的位置,foo函数的外层作用域就是全局作用域,所以foo函数的作用域链中其上层作用域中就已经有一个值为1的value了,所以当bar执行到foo函数时,foo执行的结果会打印其外层作用域中的1,而非其执行位置前值为2的value。所以如果JavaScript是动态作用域,此处输出的值就会变成2.
我们再来分析一个《JavaScript权威指南》中的代码示例:
// demo01
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope(); // local scope
// demo02
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()(); // local scope
两个demo打印的结果都是local scope
,原因就在于JavaScript是静态作用域,虽然这两段代码的结果一致,但是内部的执行顺序是有差异的。函数的作用域在其定义的时候就已经确定了,所以无论是先执行再返回还是先返回再执行,f函数获取的scope都是其定义位置前民的scope变量,当然这里第二个例子还涉及了闭包的概念,这里暂时按下不表,后面会详细描述。
执行上下文栈与执行上下文
冴羽老师在执行上下文栈一章中给出了两个示例,在展开描述概念之前,我们先自己动手分析一下这两个示例,如下:
// demo01
var foo = function () {
console.log('foo1');
}
foo(); // foo1
var foo = function () {
console.log('foo2');
}
foo(); // foo2
// demo02
function foo() {
console.log('foo1');
}
foo(); // foo2
function foo() {
console.log('foo2');
}
foo(); // foo2
这两个例子,我们都需要使用预编译阶段变量与函数提升的概念去理解,第一段代码中预编译阶段实际上仅捕获到一个变量foo,并且将其值初始化为undefined,所以我们改写一下demo01的代码会变成这样,如下:
var foo = undefined;
foo = function () {
console.log('foo1');
};
foo(); // foo1
foo = function () {
console.log('foo2');
};
foo(); // foo2
改写后的代码就一目了然了,逐行执行后,我们每次执行的foo函数,foo变量指向的函数体每次都会因为新的赋值,而指向不同的函数体,从而打印出不同的结果。
以此类推,我们改写一下demo02,代码如下:
function foo () {
console.log('foo1');
}
function foo() {
console.log('foo2');
}
foo();
foo();
这次的代码稍有不同,不同的点就在于demo01中所有的声明都是变量声明,而demo02中是函数声明,在前几次的学习笔记中提到过提升中函数声明优先级高于变量,而同名函数声明后面的会覆盖前面的,所以最终demo02中的第二个foo函数会覆盖第一个,因此后面两次执行结果都是foo2。 到这里,我们说完了提升与提升的优先等级这些预编译工作。 接下来,冴羽老师提出了这样一个问题:JavaScript引擎遇到一段怎样的代码时才会做的“准备工作”呢? 答:当JavaScript引擎遇到一段可执行的代码时。 那么什么是可执行的代码,在JavaScript中可执行的代码有三类:全局代码、函数代码以及eval代码。而问题中的准备工作就是“执行上下文”。
在一段JavaScript代码中,除了普通的全局代码之外,我们会创建众多的函数,那就意味着一段代码中会有非常多的执行上下文,用于管理这些上下文的工具就是执行上下文栈。 假设执行上下文栈是一个数组,当JavaScript引擎尝试着去解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候会像执行上下文栈中压入一个全局执行上下文,使用globalContext表示,当执行其他的函数时,就会按照执行的顺序依次将不同的函数执行上下文压入执行上下文栈,然后再按照先入后出的顺序依次执行完后出栈,知道只剩下全局执行上下文,如果整个应用程序结束,那么执行上下文栈会被清空。
我们来看一段代码,详细分析一下执行上下文栈:
function fun3() {
console.log('fun3')
}
function fun2() {
fun3();
}
function fun1() {
fun2();
}
fun1();
首先,加载整段脚本,我们往执行上下文栈中压入全局执行上下文,如下:
const executeStack = [
globalContext
];
然后执行fun1,并创建fun1执行上下文,压入栈:
// 遇到fun1
executeStack.push(fun1Context);
// 在fun1的内部遇到fun2
executeStack.push(fun2Context);
// 在fun2的内部遇到fun3
executeStack.push(fun3Context);
// 此时的中间状态,executeStack内部为[globalContext, fun1Context, fun2Context, fun3Context]
// 执行fun3,弹出其执行上下文
executeStack.pop();
// 执行fun2,弹出其执行上下文
executeStack.pop();
// 执行fun1,弹出其执行上下文
executeStack.pop();
根据上面这段分析,我们再来看看之前描述作用域的两段代码的执行上下文创建过程:
// demo01
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f();
}
checkscope(); // local scope
// 初始化执行上下文栈
executeStack = [globalContext]
// checkscope执行
executeStack.push(checkscopeContext);
// checkscope内部执行f函数
executeStack.push(fContext);
// f执行结束,弹出
executeStack.pop();
// checkscope执行结束,弹出
executeStack.pop();
// demo02
var scope = "global scope";
function checkscope(){
var scope = "local scope";
function f(){
return scope;
}
return f;
}
checkscope()(); // local scope
// 初始化执行上下文栈
executeStack = [globalContext]
// checkscope执行
executeStack.push(checkscopeContext);
// checkscope执行结束,弹出
executeStack.pop();
// checkscope内部返回的f函数执行
executeStack.push(fContext);
// f执行结束,弹出
executeStack.pop();
从上述两段执行上下文的创建与销毁的过程就可以看出,虽然两段代码的执行结果是相同的,但是执行上下文栈的内容变化是不同的。
变量对象
在了解到函数执行的整个执行上下文栈的变化后,我们需要详细了解一下,在执行上下文栈中进进出出的执行上下文里面又有些什么内容。
对于每一个执行上下文,都有三个重要的属性:
- 变量对象(variable object)
- 作用域链
- this
变量对象是与执行上下文相关的数据作用域,存储了在上下文中定义的变量和函数声明。
因为不同执行上下文下的变量对象稍有不同,所以我们来聊聊全局上下文下的变量对象和函数上下文下的变量对象(activation object)。
全局上下文 我们时常会听到一个词叫做“全局对象”,在解释什么叫全局对象以及全局上下文之前,我们依旧来看一段代码(运行环境是浏览器):
var a = 123;
function test() {
return 'test';
}
console.log(this.a); // 123
console.log(window.a); // 123
console.log(this.window.a); // 123
console.log(this.test()); // test
console.log(window.test()); // test
console.log(this.window.test()); // test
console.log(this instanceof Object); // true
console.log(Math.random());
console.log(this.Math.random());
总的来说,全局对象有以下几点特点:
- 可以用this来引用,在浏览器环境下this引用到的就是window对象
- 全局对象是有Object实例化的一个对象
- 预定了一些属性与方法
- 作为全局变量以及函数的宿主
- 在浏览器环境中,window属性指向自身,类似于arguments.callee指向函数自身
所以简而言之,全局对象就是全局上下文中的变量对象VO,存储着在当前上下文中声明的变量以及函数。
函数上下文 在函数上下文中使用活动对象AO来表示变量对象VO。活动对象和变量对象其实是一个东西,只是变量对象是规范上的或者说是引擎实现上的,不可在 JavaScript 环境中访问,只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,所以才叫 activation object 呐,而只有被激活的变量对象,也就是活动对象上的各种属性才能被访问。
执行上下文的代码会有两个阶段:
- 进入执行上下文
- 代码执行
当进入执行上下文的时候,变量对象被激活,其内容会包含:
- 函数的所有形参(如果是函数上下文的话)
- 形参的名称作为变量对象的属性名
- 值初始化为undefined
- 函数声明
- 函数名作为变量对象的属性
- 值初始化为函数体
- 如果变量对象中已经有相同的属性名,就将其完全覆盖掉
- 变量声明
- 变量名作为变量对象的属性
- 值初始化为undefined
- 如果变量名与变量对象中已有的形参或函数声明的属性名重复,则不对这两类属性造成影响
我们可以来分析一段代码:
function foo(a) {
var b = 2;
function c() {}
var d = function() {};
b = 3;
}
foo(1);
当进入全局执行上下文时,创建一个全局的变量对象globalVO:
globalVO = {
foo: function foo() { /* 函数体 */ }
}
当进入函数foo执行上下文时,创建一个函数的活动对象functionAO:
functionAO = {
// 形参
arguments: {
0: 1,
length: 1,
},
a: 1,
// 变量声明
b: undefined,
d: undefined,
// 函数声明
c: function c() { /* c函数体 */ },
}
当代码开始执行时,会顺序执行代码,根据代码中的赋值情况去覆盖变量对象中的属性值,所以上述代码执行完之后,AO会变成:
functionAO = {
// 形参
arguments: {
0: 1,
length: 1,
},
a: 1,
// 变量声明
b: 3,
d: function () {},
// 函数声明
c: function c() { /* c函数体 */ },
}
最后,给出三段代码来分析一下:
// demo01
function foo() {
console.log(a);
a = 1;
}
foo(); // ???
// dem02
function bar() {
a = 1;
console.log(a);
}
bar(); // ???
// demo03
console.log(foo);
function foo(){
console.log("foo");
}
var foo = 1;
先来分析一下第一段代码:
// 进入foo的执行上下文时,demo01中所有的AO
fooAO = {
arguments: {
length: 0
},
}
foo被调用时,代码逐行执行,打印a变量时,会先在当前作用域中寻找a变量,当前作用域中没有a变量的声明,所以会在全局作用域中查找,最后找不到a变量,报出一个引用错误
同样的第二段代码的AO和第一段是一样的,但是由于执行的时候并未写严格模式,所以a = 1
会在全局挂载一个全局变量a,故接下来的console.log是能打印出正确的a值的。
第三段代码,进入全局执行上下文的时候,变量对象会先收集到一个名为foo的函数,然后再收集到一个名为foo的变量,由于函数优先级比变量高,所以后面的变量foo不会影响foo函数,VO结构如下:
VO = {
foo: function foo() { /* foo函数体 */ },
}
最终执行的结果就是打印出foo函数体。
作用域链
在《JavaScript深入之变量对象》中讲到,当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。 并且在函数激活,进入函数执行上下文创建AO/VO后,就会将活动对象添加到作用域链的前端。
借助一段代码,我们将整个执行上下文、作用域链以及变量(活动)对象串起来理解一下:
var scope = "global scope";
function checkscope(){
var scope2 = 'local scope';
return scope2;
}
checkscope();
首先需要明确,在进入全局执行上下文的时候,就会将其压入执行上下文栈中,所以初始化的执行上下文栈为:
ECStack = [ globalContext ]
执行过程:
- 创建checkscope函数,由于js是静态作用域,所以在创建函数的时候就能确定函数的作用域,我们定义一个checkscope的内部属性scope,此时作用域的前端存储着全局执行上下文的变量对象,所以我们在checkscope函数中可以访问到全局作用域的变量。
checkscope.[[scope]] = [ globalContext.VO ]
- 准备执行checkscope函数,创建checkscope函数执行上下文,并将其压入执行上下文栈中
ECStack = [ checkscopeContext, globalContext ]
- 为checkscope的执行上下文创建作用域链
checkscopeContext = { scope: checkscope.[[scope]], }
- 为checkscope的执行上下文创建活动对象
checkscopeContext = { scope: checkscope.[[scope]], AO: { arguments: { length: 0, }, scope2: undefined } }
- 将AO压入当前作用域链的前端
checkscopeContext = { scope: [AO, checkscope.[[scope]]], AO: { arguments: { length: 0, }, scope2: undefined } }
- 正式开始执行函数,代码逐行执行覆盖掉AO中的属性值
checkscopeContext = { scope: [AO, checkscope.[[scope]]], AO: { arguments: { length: 0, }, scope2: 'local scope' } }
- 执行完checkscope,然后将其执行上下文从执行上下文栈中弹出
ECStack = [ globalContext ]
同步与异步
由于JavaScript是单线程,所以某些会造成阻塞的代码会在代码执行到当前行时挂起,然后先去执行不阻塞的代码,等这些代码有响应后再去处理后续的逻辑,这就是JavaScript中的异步。那么何时去处理这些异步逻辑以及如何通知主线程去处理这些逻辑就成了重中之重,所以事件循环以及消息队列应运而生。
事件循环:主线程不断地去消息队列中查找获取并执行代码的过程。 消息队列:消息队列是一个先进先出的队列,它里面存放着各种消息,这些消息暂且可以认为是注册异步任务时添加的回调函数。
在前面的几次学习总结中,我们可以总结出这么一个因果关系:单线程导致了异步的出现 –> 为了存储异步的回调,出现了消息队列 –> 为了处理异步回调,出现了事件循环。
定时器
概念 定时器主要分为两种:setTimeout以及setInterval,前者主要用于在延迟一定时长后将任务放入消息队列,后者则是在间隔一定时长后将任务放入消息队列。
在很多的博客以及文章中,都着重提到,定时器只负责在限制的时间条件下将任务放入消息队列,而不是执行任务。所以在实际使用定时器时,我们常常会发现定时器的回调并没有像我们预期的那样按时生效,相关的内容具体可以回顾一下消息队列的知识,这里不展开描述。
应用 在阮一峰老师的博客中,我收集到了以下几种应用方式,然后对这几种应用方式进行了自己的一些分析:
- 调整回调的先后顺序
- 防止用户自定义的回调函数在浏览器的默认动作之前发生,导致一些信息不能被回调捕捉
- 优化大计算量代码性能
demo01 调整回调顺序
// HTML 代码如下
// <input type="button" id="myButton" value="click">
var input = document.getElementById('myButton');
input.onclick = function A() {
setTimeout(function B() {
input.value +=' input';
}, 0)
};
document.body.onclick = function C() {
input.value += ' body'
};
首先,我们要了解两个点:事件冒泡机制以及事件回调,这里实际上,我们相当于在消息队列中插入了两个回调,当点击事件发生在子元素上时借助冒泡会先触发子元素事件回调然后再触发父元素事件回调,但是我们可以借助setTimeout(fn,0)将子元素的回调塞入消息队列的最后,也就是当消息队列空了的时候才会将回调塞入,从而在主线程去查询消息队列时再执行到这个回调。
demo02 延迟用户自定义回调函数执行
document.getElementById('input-box').onkeypress = function() {
var self = this;
setTimeout(function() {
self.value = self.value.toUpperCase();
}, 0);
}
如果,我们将setTimeout(fn, 0)去掉,那么输入框中始终只会将已经输入的小写字母转成大写,即始终都会慢一步,所以为了防止浏览器捕捉文本的默认行为晚于我们的自定义回调执行时间,setTimeout(fn, 0)还是将自定义回调移到了消息队列的最后。
demo03 优化大计算量代码性能
var div = document.getElementsByTagName('div')[0];
// 写法一
for (var i = 0xA00000; i < 0xFFFFFF; i++) {
div.style.backgroundColor = '#' + i.toString(16);
}
// 写法二
var timer;
var i=0x100000;
function func() {
timer = setTimeout(func, 0);
div.style.backgroundColor = '#' + i.toString(16);
if (i++ == 0xFFFFFF) clearTimeout(timer);
}
timer = setTimeout(func, 0);
上面第一种写法对div节点的背景颜色进行了大量的调整,从A00000到FFFFFF div的背景颜色色值被修改了六十多万次,js代码执行是比DOM操作要快的,这么进行了很多次的重绘,导致页面会被大量的DOM操作阻塞;而第二种写法则不同,我们每一次的背景颜色调整是保证在上一次DOM背景修改之后的,简而言之就是setTimeout(fn, 0)保证了本次操作会在浏览器主线程最早可得的空闲阶段去执行。
补充:
- setInterval并不会立即执行,而是延迟我们给出的时间间隔后再将回调塞入队列,例如:
setInterval()() => { // 函数体 }, 3000)
第一次执行的时候一定是距离上一个任务结束3000毫秒之后才开始,如果想先执行一次回调,然后再开始间隔调用,那就需要手动先调用一次- setInterval并不能保证两次函数调用之间差值一定是传递进去的间隔,所以日常开发需要间隔调用时多用setTimeout进行模拟 3.定时器中时常会有错误的this指向,其实说到底是因为定时器是window对象上的方法,我们调用的时候,代码实际上是这样的
window.setTimeout
或者window.setInterval
,this只有在调用的时候才能确定指向,所以定时器被window调用,自然就会指向window对象,到这里,可能表述上还是不够清楚,所以我自己写了一个例子来验证我的看法
// demo01
var a = 123;
setTimeout(function () { console.log(this.a); }, 1000); // 123
var obj = {
a: 0
};
var a = 123;
setTimeout((function () { console.log(this.a); }).bind(this), 1000); // 0
/* ======================我是分割线====================== */
// demo02
var a = 123;
var obj = {
a: 0,
sayA: function () { console.log('sayA says a', this.a); },
delaySayA: function () { setTimeout(this.sayA, 1000); }
};
obj.delaySayA(); // 123
// 第二版代码
var obj = {
a: 0,
sayA: function () { console.log('sayA says a', this.a); },
delaySayA: function () {
var _this = this;
setTimeout(this.sayA.bind(_this), 1000);
}
};
obj.delaySayA(); // 0
根据上面两段代码,实际上就已经能够验证setTimeout中this指向错误的原因了,只要适时地将setTimeout回调函数的this指向我们需要的对象就能避免引用错误。
Flex布局
2009年,W3C提出了一种新的方案—-Flex布局,可以简便、完整、响应式地实现各种页面布局。目前,它已经得到了所有浏览器的支持,这意味着,现在就能很安全地使用这项功能。(想了解更多,点击这里)
对于flex布局的概念部分不会进行太多的阐述,只进行一个罗列,主要会给出一些运用场景、优劣对比以及个人的使用观点。
什么是flex布局
flex box就是flexible box的缩写,即弹性盒子,也称作弹性盒模型。
各部分属性
整个flex布局我们可以大致分为容器与盒子部分,这两部分各自有6个属性用于控制布局。
- 容器
- flex-direction (容器内部盒子的排布方向,默认值为row,横向排列)
- flex-wrap (容器内部盒子的换行方式,默认值为no-wrap,即不折行)
- flex-flow (flex-direction + flex-wrap的简写属性,举例:
flex-flow: row no-wrap;
) - justify-content (主轴(横轴)排列盒子的对齐方式,默认值为flex-start)
- align-items (交叉轴(纵轴)排列盒子的对齐方式)
- align-content (多根交叉轴线的时候的对齐方式)
- 盒子
- order (定义项目的排列顺序。数值越小,排列越靠前,默认为0)
- flex-grow (盒子的放大比例)
- flex-basis (在分配多余空间之前,项目占据的主轴空间)
- flex-shrink (盒子的缩小比例)
- flex (flex-grow + flex-shrink + flex-basis的简写属性)
- align-self
容器属性应用
几个值得关注的点:
- align-content和align-items同时声明时,无论声明顺序如何前者优先生效
- align-content生效与否和轴线数量有关,轴线数量不能仅依靠肉眼观察是否折行而是需要根据flex-wrap的值来判定,flex-wrap为no-wrap的时候,align-content永远都不会生效,flex-wrap为wrap/wrap-reverse的时候,无论肉眼看上去是单行还是多行,这时候都是有多根轴线的,所以align-content会生效,此处可参考stackoverflow上一个高赞答案
- align-items与轴的数量无关
- 所谓的主轴就是横向轴,而交叉轴则为纵轴,flex-wrap为wrap/wrap-reverse的时候表明flex容器会有多根主轴,即多行flex盒子排列
盒子属性应用
CSS3新增属性与选择器回顾
HTML元素类型
资料参考
- Difference between microTask and macroTask within a event loop context
- Tasks, microTasks, queues ans schedules
- 如何解释Event Loop面试官才满意?