这一小节比较晦涩难懂,目前简单的了解一下就行,有需要再深入研究
转载于 https://github.com/mqyqingfeng/Blog/issues/7

当JavaScript代码执行一段可执行代码(executable code)时,会创建对应的执行上下文(execution context)。

对于每个执行上下文,都有三个重要属性

  • 变量对象(Variable object,VO)
  • 作用域链(Scope chain)
  • this

今天重点讲讲 this,然而不好讲。

……

因为我们要从 ECMAScript5 规范开始讲起。

先奉上 ECMAScript 5.1 规范地址:

英文版:http://es5.github.io/#x15.1

中文版:http://yanhaijing.com/es5/#115

让我们开始了解规范吧!

Types

首先是第 8 章 Types:

Types are further subclassified into ECMAScript language types and specification types.

An ECMAScript language type corresponds to values that are directly manipulated by an ECMAScript programmer using the ECMAScript language. The ECMAScript language types are Undefined, Null, Boolean, String, Number, and Object.

A specification type corresponds to meta-values that are used within algorithms to describe the semantics of ECMAScript language constructs and ECMAScript language types. The specification types are Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, and Environment Record.

我们简单的翻译一下:

ECMAScript 的类型分为语言类型和规范类型。

ECMAScript 语言类型是开发者直接使用 ECMAScript 可以操作的。其实就是我们常说的Undefined, Null, Boolean, String, Number, 和 Object。

而规范类型相当于 meta-values,是用来用算法描述 ECMAScript 语言结构和 ECMAScript 语言类型的。规范类型包括:Reference, List, Completion, Property Descriptor, Property Identifier, Lexical Environment, 和 Environment Record。

没懂?没关系,我们只要知道在 ECMAScript 规范中还有一种只存在于规范中的类型,它们的作用是用来描述语言底层行为逻辑。

今天我们要讲的重点是便是其中的 Reference 类型。它与 this 的指向有着密切的关联。

Reference

那什么又是 Reference ?

让我们看 8.7 章 The Reference Specification Type:

The Reference type is used to explain the behaviour of such operators as delete, typeof, and the assignment operators.

所以 Reference 类型就是用来解释诸如 delete、typeof 以及赋值等操作行为的。

抄袭尤雨溪大大的话,就是:

这里的 Reference 是一个 Specification Type,也就是 “只存在于规范里的抽象类型”。它们是为了更好地描述语言的底层行为逻辑才存在的,但并不存在于实际的 js 代码中。

再看接下来的这段具体介绍 Reference 的内容:

A Reference is a resolved name binding.

A Reference consists of three components, the base value, the referenced name and the Boolean valued strict reference flag.

The base value is either undefined, an Object, a Boolean, a String, a Number, or an environment record (10.2.1).

A base value of undefined indicates that the reference could not be resolved to a binding. The referenced name is a String.

==这段讲述了 Reference 的构成,由三个组成部分,分别是==:

  • base value
  • referenced name
  • strict reference

可是这些到底是什么呢?

我们简单的理解的话:

base value 就是属性所在的对象或者就是 EnvironmentRecord,它的值只可能是 undefined, an Object, a Boolean, a String, a Number, or an environment record 其中的一种。

referenced name 就是属性的名称。

举个例子:

1
2
3
4
5
6
7
8
var foo = 1;

// 对应的Reference是:
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};

再举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var foo = {
bar: function () {
return this;
}
};

foo.bar(); // foo

// bar对应的Reference是:
var BarReference = {
base: foo,
propertyName: 'bar',
strict: false
};

而且规范中还提供了获取 Reference 组成部分的方法,比如 GetBase 和 IsPropertyReference。

这两个方法很简单,简单看一看:

1.GetBase

GetBase(V). Returns the base value component of the reference V.

返回 reference 的 base value。

2.IsPropertyReference

IsPropertyReference(V). Returns true if either the base value is an object or HasPrimitiveBase(V) is true; otherwise returns false.

简单的理解:如果 base value 是一个对象,就返回true。

3.GetValue

除此之外,紧接着在 8.7.1 章规范中就讲了一个用于从 Reference 类型获取对应值的方法: GetValue。

简单模拟 GetValue 的使用:

1
2
3
4
5
6
7
8
9
var foo = 1;

var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};

GetValue(fooReference) // 1;

GetValue 返回对象属性真正的值,但是要注意:

调用 GetValue,返回的将是具体的值,而不再是一个 Reference

这个很重要,这个很重要,这个很重要。

如何确定this的值

关于 Reference 讲了那么多,为什么要讲 Reference 呢?到底 Reference 跟本文的主题 this 有哪些关联呢?如果你能耐心看完之前的内容,以下开始进入高能阶段:

看规范 11.2.3 Function Calls:

这里讲了当函数调用的时候,如何确定 this 的取值。

只看第一步、第六步、第七步:

1.Let ref be the result of evaluating MemberExpression.

6.If Type(ref) is Reference, then

1
a.If IsPropertyReference(ref) is true, then
1
i.Let thisValue be GetBase(ref).
1
b.Else, the base of ref is an Environment Record
1
i.Let thisValue be the result of calling the ImplicitThisValue concrete method of GetBase(ref).

7.Else, Type(ref) is not Reference.

1
a. Let thisValue be undefined.

==让我们描述一下:==

1.计算 MemberExpression 的结果赋值给 ref

2.判断 ref 是不是一个 Reference 类型

1
2
3
4
5
2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

2.3 如果 ref 不是 Reference,那么 this 的值为 undefined

具体分析

让我们一步一步看:

  1. 计算 MemberExpression 的结果赋值给 ref

什么是 MemberExpression?看规范 11.2 Left-Hand-Side Expressions:

MemberExpression :

  • PrimaryExpression // 原始表达式 可以参见《JavaScript权威指南第四章》
  • FunctionExpression // 函数定义表达式
  • MemberExpression [ Expression ] // 属性访问表达式
  • MemberExpression . IdentifierName // 属性访问表达式
  • new MemberExpression Arguments // 对象创建表达式

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function foo() {
console.log(this)
}

foo(); // MemberExpression 是 foo

function foo() {
return function() {
console.log(this)
}
}

foo()(); // MemberExpression 是 foo()

var foo = {
bar: function () {
return this;
}
}

foo.bar(); // MemberExpression 是 foo.bar

所以简单理解 MemberExpression 其实就是()左边的部分

2.判断 ref 是不是一个 Reference 类型。

关键就在于看规范是如何处理各种 MemberExpression,返回的结果是不是一个Reference类型。

举最后一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}

//示例1
console.log(foo.bar());
//示例2
console.log((foo.bar)());
//示例3
console.log((foo.bar = foo.bar)());
//示例4
console.log((false || foo.bar)());
//示例5
console.log((foo.bar, foo.bar)());

foo.bar()

在示例 1 中,MemberExpression 计算的结果是 foo.bar,那么 foo.bar 是不是一个 Reference 呢?

查看规范 11.2.1 Property Accessors,这里展示了一个计算的过程,什么都不管了,就看最后一步:

Return a value of type Reference whose base value is baseValue and whose referenced name is propertyNameString, and whose strict mode flag is strict.

我们得知该表达式返回了一个 Reference 类型!

根据之前的内容,我们知道该值为:

1
2
3
4
5
var Reference = {
base: foo,
name: 'bar',
strict: false
};

接下来按照 2.1 的判断流程走:

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

该值是 Reference 类型,那么 IsPropertyReference(ref) 的结果是多少呢?

前面我们已经铺垫了 IsPropertyReference 方法,如果 base value 是一个对象,结果返回 true。

base value 为 foo,是一个对象,所以 IsPropertyReference(ref) 结果为 true。

这个时候我们就可以确定 this 的值了:

1
this = GetBase(ref),

GetBase 也已经铺垫了,获得 base value 值,这个例子中就是foo,所以 this 的值就是 foo ,示例1的结果就是 2!

唉呀妈呀,为了证明 this 指向foo,真是累死我了!但是知道了原理,剩下的就更快了。

(foo.bar)()

看示例2:

1
console.log((foo.bar)());

foo.bar 被 () 包住,查看规范 11.1.6 The Grouping Operator

直接看结果部分:

Return the result of evaluating Expression. This may be of type Reference.

NOTE This algorithm does not apply GetValue to the result of evaluating Expression.

实际上 () 并没有对 MemberExpression 进行计算,所以其实跟示例 1 的结果是一样的。

(foo.bar = foo.bar)()

看示例3,有赋值操作符,查看规范 11.13.1 Simple Assignment ( = ):

计算的第三步:

3.Let rval be GetValue(rref).

因为使用了 GetValue,所以返回的值不是 Reference 类型,

按照之前讲的判断逻辑:

2.3 如果 ref 不是Reference,那么 this 的值为 undefined

this 为 undefined,非严格模式下,this 的值为 undefined 的时候,其值会被隐式转换为全局对象。

(false || foo.bar)()

看示例4,逻辑与算法,查看规范 11.11 Binary Logical Operators:

计算第二步:

2.Let lval be GetValue(lref).

因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined

(foo.bar, foo.bar)()

看示例5,逗号操作符,查看规范11.14 Comma Operator ( , )

计算第二步:

2.Call GetValue(lref).

因为使用了 GetValue,所以返回的不是 Reference 类型,this 为 undefined

揭晓结果

所以最后一个例子的结果是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1

注意:以上是在非严格模式下的结果,严格模式下因为 this 返回 undefined,所以示例 3 会报错。

补充

最最后,忘记了一个最最普通的情况:

1
2
3
4
5
function foo() {
console.log(this)
}

foo();

MemberExpression 是 foo,解析标识符,查看规范 10.3.1 Identifier Resolution,会返回一个 Reference 类型的值:

1
2
3
4
5
var fooReference = {
base: EnvironmentRecord,
name: 'foo',
strict: false
};

接下来进行判断:

2.1 如果 ref 是 Reference,并且 IsPropertyReference(ref) 是 true, 那么 this 的值为 GetBase(ref)

因为 base value 是 EnvironmentRecord,并不是一个 Object 类型,还记得前面讲过的 base value 的取值可能吗? 只可能是 undefined, an Object, a Boolean, a String, a Number, 和 an environment record 中的一种。

IsPropertyReference(ref) 的结果为 false,进入下个判断:

2.2 如果 ref 是 Reference,并且 base value 值是 Environment Record, 那么this的值为 ImplicitThisValue(ref)

base value 正是 Environment Record,所以会调用 ImplicitThisValue(ref)

查看规范 10.2.1.1.6,ImplicitThisValue 方法的介绍:该函数始终返回 undefined。

所以最后 this 的值就是 undefined。

多说一句

尽管我们可以简单的理解 this 为调用函数的对象,如果是这样的话,如何解释下面这个例子呢?

1
2
3
4
5
6
7
8
9
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}
console.log((false || foo.bar)()); // 1

此外,又如何确定调用函数的对象是谁呢?在写文章之初,我就面临着这些问题,最后还是放弃从多个情形下给大家讲解 this 指向的思路,而是追根溯源的从 ECMASciript 规范讲解 this 的指向,尽管从这个角度写起来和读起来都比较吃力,但是一旦多读几遍,明白原理,绝对会给你一个全新的视角看待 this 。而你也就能明白,尽管 foo() 和 (foo.bar = foo.bar)() 最后结果都指向了 undefined,但是两者从规范的角度上却有着本质的区别。

此篇讲解执行上下文的 this,即便不是很理解此篇的内容,依然不影响大家了解执行上下文这个主题下其他的内容。所以,依然可以安心的看下一篇文章。

转载

一、浏览器缓存基本认识

分为强缓存和协商缓存

  1. 浏览器在加载资源时,先根据这个资源的一些http header判断它是否命中强缓存,强缓存如果命中,浏览器直接从自己的缓存中读取资源,不会发请求到服务器。比如某个css文件,如果浏览器在加载它所在的网页时,这个css文件的缓存配置命中了强缓存,浏览器就直接从缓存中加载这个css,连请求都不会发送到网页所在服务器
  2. 当强缓存没有命中的时候,浏览器一定会发送一个请求到服务器,通过服务器端依据资源的另外一些http header验证这个资源是否命中协商缓存,如果协商缓存命中,服务器会将这个请求返回,但是不会返回这个资源的数据,而是告诉客户端可以直接从缓存中加载这个资源,于是浏览器就又会从自己的缓存中去加载这个资源
  3. 强缓存与协商缓存的共同点是:如果命中,都是从客户端缓存中加载资源,而不是从服务器加载资源数据;区别是:强缓存不发请求到服务器协商缓存会发请求到服务器
  4. 当协商缓存也没有命中的时候,浏览器直接从服务器加载资源数据

二、强缓存的原理

2.1 介绍

当浏览器对某个资源的请求命中了强缓存时,返回的http状态为200,在chrome的开发者工具的network里面size会显示为from cache,比如京东的首页里就有很多静态资源配置了强缓存,用chrome打开几次,再用f12查看network,可以看到有不少请求就是从缓存中加载的

img

  • 强缓存是利用Expires或者Cache-Control这两个http response header实现的,它们都用来表示资源在客户端缓存的有效期。
1
Expires`是`http1.0`提出的一个表示资源过期时间的`header`,它描述的是一个绝对时间,由服务器返回,用`GMT`格式的字符串表示,如:`Expires:Thu, 31 Dec 2037 23:55:55 GMT

2.2 Expires缓存原理

  1. 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在responeheader加上Expires,如

img

  1. 浏览器在接收到这个资源后,会把这个资源连同所有response header一起缓存下来(所以缓存命中的请求返回的header并不是来自服务器,而是来自之前缓存的header
  2. 浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,拿出它的Expires跟当前的请求时间比较,如果请求时间在Expires指定的时间之前,就能命中缓存,否则就不行
  3. 如果缓存没有命中,浏览器直接从服务器加载资源时,Expires Header在重新加载的时候会被更新
1
Expires`是较老的强缓存管理`header`,由于它是服务器返回的一个绝对时间,在服务器时间与客户端时间相差较大时,缓存管理容易出现问题,比如随意修改下客户端时间,就能影响缓存命中的结果。所以在`http1.1`的时候,提出了一个新的`header`,就是`Cache-Control`,这是一个相对时间,在配置缓存的时候,以秒为单位,用数值表示,如:`Cache-Control:max-age=315360000

2.3 Cache-Control缓存原理

  1. 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在responeheader加上Cache-Control,如:

img

  1. 浏览器在接收到这个资源后,会把这个资源连同所有response header一起缓存下来
  2. 浏览器再请求这个资源时,先从缓存中寻找,找到这个资源后,根据它第一次的请求时间和Cache-Control设定的有效期,计算出一个资源过期时间,再拿这个过期时间跟当前的请求时间比较,如果请求时间在过期时间之前,就能命中缓存,否则就不行
  3. 如果缓存没有命中,浏览器直接从服务器加载资源时,Cache-Control Header在重新加载的时候会被更新
  • Cache-Control描述的是一个相对时间,在进行缓存命中的时候,都是利用客户端时间进行判断,所以相比较ExpiresCache-Control的缓存管理更有效,安全一些。
  • 这两个header可以只启用一个,也可以同时启用,当response header中,ExpiresCache-Control同时存在时,Cache-Control优先级高于Expires

img

2.4 cache-control 补充

img

img

三、强缓存的管理

前面介绍的是强缓存的原理,在实际应用中我们会碰到需要强缓存的场景和不需要强缓存的场景,通常有2种方式来设置是否启用强缓存

  1. 通过代码的方式,在web服务器返回的响应中添加ExpiresCache-Control Header
  2. 通过配置web服务器的方式,让web服务器在响应资源的时候统一添加ExpiresCache-Control Header

比如在javaweb里面,我们可以使用类似下面的代码设置强缓存

1
2
3
4
java.util.Date date = new java.util.Date();    
response.setDateHeader("Expires",date.getTime()+20000); //Expires:过时期限值
response.setHeader("Cache-Control", "public"); //Cache-Control来控制页面的缓存与否,public:浏览器和缓存服务器都可以缓存页面信息;
response.setHeader("Pragma", "Pragma"); //Pragma:设置页面是否缓存,为Pragma则缓存,no-cache则不缓存

还可以通过类似下面的java代码设置不启用强缓存

1
2
3
response.setHeader( "Pragma", "no-cache" );   
response.setDateHeader("Expires", 0);
response.addHeader( "Cache-Control", "no-cache" );//浏览器和缓存服务器都不应该缓存页面信息
  • nginxapache作为专业的web服务器,都有专门的配置文件,可以配置expirescache-control,这方面的知识,如果你对运维感兴趣的话,可以在百度上搜索nginx 设置 expires cache-controlapache 设置 expires cache-control 都能找到不少相关的文章。
  • 由于在开发的时候不会专门去配置强缓存,而浏览器又默认会缓存图片,cssjs等静态资源,所以开发环境下经常会因为强缓存导致资源没有及时更新而看不到最新的效果,解决这个问题的方法有很多,常用的有以下几种

处理缓存带来的问题

  1. 直接ctrl+f5,这个办法能解决页面直接引用的资源更新的问题
  2. 使用浏览器的隐私模式开发
  3. 如果用的是chrome,可以f12network那里把缓存给禁掉(这是个非常有效的方法)

img

  1. 在开发阶段,给资源加上一个动态的参数,如css/index.css?v=0.0001,由于每次资源的修改都要更新引用的位置,同时修改参数的值,所以操作起来不是很方便,除非你是在动态页面比如jsp里开发就可以用服务器变量来解决(v=${sysRnd}),或者你能用一些前端的构建工具来处理这个参数修改的问题
  2. 如果资源引用的页面,被嵌入到了一个iframe里面,可以在iframe的区域右键单击重新加载该页面,以chrome为例

img

  1. 如果缓存问题出现在ajax请求中,最有效的解决办法就是ajax的请求地址追加随机数
  2. 还有一种情况就是动态设置iframesrc时,有可能也会因为缓存问题,导致看不到最新的效果,这时候在要设置的src后面添加随机数也能解决问题
  3. 如果你用的是gruntgulpwebpack这种前端工具开发,通过它们的插件比如grunt-contrib-connect来启动一个静态服务器,则完全不用担心开发阶段的资源更新问题,因为在这个静态服务器下的所有资源返回的respone header中,cache-control始终被设置为不缓存

img

四、强缓存的应用

强缓存是前端性能优化最有力的工具,没有之一,对于有大量静态资源的网页,一定要利用强缓存,提高响应速度。通常的做法是,为这些静态资源全部配置一个超时时间超长的ExpiresCache-Control,这样用户在访问网页时,只会在第一次加载时从服务器请求静态资源,其它时候只要缓存没有失效并且用户没有强制刷新的条件下都会从自己的缓存中加载,比如前面提到过的京东首页缓存的资源,它的缓存过期时间都设置到了2026

img

然而这种缓存配置方式会带来一个新的问题,就是发布时资源更新的问题,比如某一张图片,在用户访问第一个版本的时候已经缓存到了用户的电脑上,当网站发布新版本,替换了这个图片时,已经访问过第一个版本的用户由于缓存的设置,导致在默认的情况下不会请求服务器最新的图片资源,除非他清掉或禁用缓存或者强制刷新,否则就看不到最新的图片效果

这个问题已经有成熟的解决方案,具体内容可阅读知乎这篇文章详细了解:https://www.zhihu.com/question/20790576

文章提到的东西都属于理论上的解决方案,不过现在已经有很多前端工具能够实际地解决这个问题,由于每个工具涉及到的内容细节都有很多,本文没有办法一一深入介绍。有兴趣的可以去了解下grunt gulp webpack fis 还有edp这几个工具,基于这几个工具都能解决这个问题,尤其是fisedp是百度推出的前端开发平台,有现成的文档可以参考:

http://fis.baidu.com/fis3/api/index.html

http://ecomfe.github.io/edp/doc/initialization/install/

强缓存还有一点需要注意的是,通常都是针对静态资源使用,动态资源需要慎用,除了服务端页面可以看作动态资源外,那些引用静态资源的html也可以看作是动态资源,如果这种html也被缓存,当这些html更新之后,可能就没有机制能够通知浏览器这些html有更新,尤其是前后端分离的应用里,页面都是纯html页面,每个访问地址可能都是直接访问html页面,这些页面通常不加强缓存,以保证浏览器访问这些页面时始终请求服务器最新的资源

五、协商缓存的原理

5.1 介绍

当浏览器对某个资源的请求没有命中强缓存,就会发一个请求到服务器,验证协商缓存是否命中,如果协商缓存命中,请求响应返回的http状态为304并且会显示一个Not Modified的字符串,比如你打开京东的首页,按f12打开开发者工具,再按f5刷新页面,查看network,可以看到有不少请求就是命中了协商缓存的

img

查看单个请求的Response Header,也能看到304的状态码和Not Modified的字符串,只要看到这个就可说明这个资源是命中了协商缓存,然后从客户端缓存中加载的,而不是服务器最新的资源

img

5.2 Last-Modified(response),If-Modified-Since(request)控制协商缓存

  1. 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在responeheader加上Last-Modifiedheader,这个header表示这个资源在服务器上的最后修改时间

img

  1. 浏览器再次跟服务器请求这个资源时,在requestheader上加上If-Modified-Sinceheader,这个header的值就是上一次请求时返回的Last-Modified的值

img

  1. 服务器再次收到资源请求时,根据浏览器传过来If-Modified-Since和资源在服务器上的最后修改时间判断资源是否有变化,如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。当服务器返回304 Not Modified的响应时,response header中不会再添加Last-Modifiedheader,因为既然资源没有变化,那么Last-Modified也就不会改变,这是服务器返回304时的response header

img

  1. 浏览器收到304的响应后,就会从缓存中加载资源
  2. 如果协商缓存没有命中,浏览器直接从服务器加载资源时,Last-Modified Header在重新加载的时候会被更新,下次请求时,If-Modified-Since会启用上次返回的Last-Modified

Last-ModifiedIf-Modified-Since】都是根据服务器时间返回的header,一般来说,在没有调整服务器时间和篡改客户端缓存的情况下,这两个header配合起来管理协商缓存是非常可靠的,但是有时候也会服务器上资源其实有变化,但是最后修改时间却没有变化的情况,而这种问题又很不容易被定位出来,而当这种情况出现的时候,就会影响协商缓存的可靠性。所以就有了另外一对header来管理协商缓存,这对header就是【ETagIf-None-Match】。它们的缓存管理的方式是

5.3 ETag(response)、If-None-Match(request)控制协商缓存

  1. 浏览器第一次跟服务器请求一个资源,服务器在返回这个资源的同时,在responeheader加上ETagheader,这个header是服务器根据当前请求的资源生成的一个唯一标识,这个唯一标识是一个字符串,只要资源有变化这个串就不同,跟最后修改时间没有关系,所以能很好的补充Last-Modified的问题

img

  1. 浏览器再次跟服务器请求这个资源时,在requestheader上加上If-None-Matchheader,这个header的值就是上一次请求时返回的ETag的值

img

  1. 服务器再次收到资源请求时,根据浏览器传过来If-None-Match和然后再根据资源生成一个新的ETag,如果这两个值相同就说明资源没有变化,否则就是有变化;如果没有变化则返回304 Not Modified,但是不会返回资源内容;如果有变化,就正常返回资源内容。与Last-Modified不一样的是,当服务器返回304 Not Modified的响应时,由于ETag重新生成过,response header中还会把这个ETag返回,即使这个ETag跟之前的没有变化

img

  1. 浏览器收到304的响应后,就会从缓存中加载资源。

六、协商缓存的管理

协商缓存跟强缓存不一样,强缓存不发请求到服务器,所以有时候资源更新了浏览器还不知道,但是协商缓存会发请求到服务器,所以资源是否更新,服务器肯定知道。大部分web服务器都默认开启协商缓存,而且是同时启用【Last-ModifiedIf-Modified-Since】和【ETagIf-None-Match】,比如apache:

img

如果没有协商缓存,每个到服务器的请求,就都得返回资源内容,这样服务器的性能会极差。

  • Last-ModifiedIf-Modified-Since】和【ETagIf-None-Match】一般都是同时启用,这是为了处理Last-Modified不可靠的情况。

有一种场景需要注意

  • 分布式系统里多台机器间文件的Last-Modified必须保持一致,以免负载均衡到不同机器导致比对失败;
  • 分布式系统尽量关闭掉ETag(每台机器生成的ETag都会不一样);
  • 京东页面的资源请求,返回的repsones header就只有Last-Modified,没有ETag

img

协商缓存需要配合强缓存使用,你看前面这个截图中,除了Last-Modified这个header,还有强缓存的相关header,因为如果不启用强缓存的话,协商缓存根本没有意义

七、相关浏览器行为对缓存的影响

如果资源已经被浏览器缓存下来,在缓存失效之前,再次请求时,默认会先检查是否命中强缓存,如果强缓存命中则直接读取缓存,如果强缓存没有命中则发请求到服务器检查是否命中协商缓存,如果协商缓存命中,则告诉浏览器还是可以从缓存读取,否则才从服务器返回最新的资源。这是默认的处理方式,这个方式可能被浏览器的行为改变:

  • ctrl+f5强制刷新网页时,直接从服务器加载,跳过强缓存和协商缓存;
  • f5刷新网页时,跳过强缓存,但是会检查协商缓存

总结图片

强缓存

协商缓存

// TODO
半成品 半成品 先别看了
下面结合实例代码探索html、js、css的阻塞关系,浏览器chrome 版本92

  • html监听了DCL和load事件,拥有两个具有文字内容(可以观测到FCP、LCP)的div
  • js css 带有sleep3000-的前缀,表示服务器会阻塞3000毫秒后返回,方便观测

js css并存 测试变量太多3*4=12种情况,感觉意义不大、、

首先是html内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script>
document.addEventListener('DOMContentLoaded',function(){
console.log('DOMContentLoaded');
});
window.addEventListener('load',function(){
console.log('load');
});
</script>
<style>
div {
width: 100px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div>的时光飞逝东莞市地方刮大风</div>
<div>asfklasjf</div>
</body>
</html>

css 将div背景设置为blue,html中为green

1
2
3
div {
background: blue!important;
}

js1 打印获取的div

1
2
const div1 = document.querySelector('div');
console.log(div1);

Js2 执行复杂操作后 打印div

1
2
3
4
5
6
7
const arr = [];
for (let i = 0; i < 10000000; i++) {
arr.push(i);
arr.splice(i % 3, i % 7, i % 5);
}
const div = document.querySelector('div');
console.log(div);

首先仅测试css

外链css放在head内

图上dcl只是被挡住了,几乎是立刻执行,然后等待css下载完成后,触发load时间,然后进行fp、fcp、lcp

image-20210812171549248

页面上前五秒并未显示div,但是此时在dom中已经存在div,css并不能阻塞dom的解析,五秒后显示蓝色div

![Kapture 2021-08-12 at 17.34.48](assets/阻塞关系/Kapture 2021-08-12 at 17.34.48.gif)

外链css放在body最前部

放在body最前部,浏览器会等待css下载完毕后,才会触发dcl及一系列绘制

image-20210812174254853

很明显,放在body前的外链css阻塞的dom的解析,当css下载完毕后,触发了一系列关键事件

![Kapture 2021-08-12 at 17.46.42](assets/阻塞关系/Kapture 2021-08-12 at 17.46.42.gif)

外链css放在body内,两个div之间

浏览器会先绘制并渲染css之前的div,待css下载完毕后,会渲染另一个div,并显示蓝色背景,触发dcl及load

image-20210812175156074

![Kapture 2021-08-12 at 17.55.14](assets/阻塞关系/Kapture 2021-08-12 at 17.55.14.gif)

css外链放在body内最后

performance和放在div之间一样,实际页面表现有一些差异

![Kapture 2021-08-12 at 18.03.58](assets/阻塞关系/Kapture 2021-08-12 at 18.03.58.gif)

外链css放在body之后

会被浏览器修复到body内最后

仅测试js

js放在head内、body头部

很明显,js的下载和解析(执行)都会阻塞dom元素的解析和渲染,直到js下载解析之后,才会继续解析渲染dom,触发dcl、fp等等

image-20210813140650543

![Kapture 2021-08-12 at 21.36.22](assets/阻塞关系/Kapture 2021-08-12 at 21.36.22.gif)

js放在div之间

js会阻塞js之后的dom解析和渲染,对于js之前的dom会正常解析渲染,并触发fp等,但是dcl需要等待js下载解析(执行)完成后

image-20210813140535630

![Kapture 2021-08-12 at 21.41.08](assets/阻塞关系/Kapture 2021-08-12 at 21.41.08.gif)

js放body尾部

image-20210813141225951

![Kapture 2021-08-12 at 21.46.00](assets/阻塞关系/Kapture 2021-08-12 at 21.46.00.gif)

js、css并存

css处于head

js处于dom前

image-20210813152617775

js处于dom中

image-20210813152844462

image-20210813152950225

js处于dom后

image-20210813153113295

image-20210813153206303

css和js文件对html的阻塞

  • CSS 不会(仅限head内)阻塞 DOM 的解析(document存在),但会阻塞其后的 DOM 渲染。

  • 在js前的css会阻塞js的解析(不会阻止下载),继而阻止dom的解析和渲染

  • JS 阻塞 DOM 解析,但浏览器会”偷看”DOM,预先下载相关资源。

DCL的结论

除了放在head里的css,html中的js、css都会阻塞dcl事件

导言

众所众知,JavaScript是一种弱类型、动态语言。这意味着:

  • 弱类型,意味着你不需要告诉 JavaScript 引擎这个或那个变量是什么数据类型,JavaScript 引擎在运行代码的时候自己会计算出来。

  • 动态,意味着你可以使用同一个变量保存不同类型的数据。

其实 JavaScript 中的数据类型一种有 8 种,它们分别是:

image-20210720140409634

我们把前面的 7 种数据类型称为原始类型,把最后一个对象类型称为引用类型,之所以把它们区分为两种不同的类型,是因为它们在内存中存放的位置不一样

内存空间

要理解 JavaScript 在运行过程中数据是如何存储的,你就得先搞清楚其存储空间的种类。下面是JavaScript 的内存模型,可以参考:

image-20210720140604983

在 JavaScript 的执行过程中, 主要有三种类型内存空间,分别是代码空间、栈空间堆空间

栈空间和堆空间

这里的栈空间就是我们之前反复提及的调用栈,是用来存储执行上下文的。为了搞清楚栈空间是如何存储数据的,我们还是先看下面这段代码:

1
2
3
4
5
6
7
function foo(){
var a = " 极客时间 "
var b = a
var c = {name:" 极客时间 "}
var d = c
}
foo()

最终分配好内存的示意图如下所示:

image-20210720140826201

可以认为简单类型的值都是存放在栈空间中,对象类型是存放在堆空间的,在栈空间中只是保留了对象的引用地址

为什么一定要分堆和栈两个存储空间

​ 这是因为 JavaScript 引擎需要用栈来维护程序执行期间上下文的状态,如果栈空间大了话,所有的数据都存放在栈空间里面,那么会影响到上下文切换的效率,进而又影响到整个程序的执行效率。

​ 下面简单介绍一下上下文的切换,比如文中的 foo 函数执行结束了,JavaScript 引擎需要离开当前的执行上下文,只需要将指针下移到上个执行上下文的地址就可以了,foo 函数执行上下文栈区空间全部回收。

image-20210720141334853

再谈闭包

这里简单的探讨下闭包的内存模型,看下述代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function foo() {
var myName = " 极客时间 "
let test1 = 1
const test2 = 2
var innerBar = {
setName:function(newName){
myName = newName
},
getName:function(){
console.log(test1)
return myName
}
}
return innerBar
}
var bar = foo()
bar.setName(" 极客邦 ")
bar.getName()
console.log(bar.getName())

由闭包知识我们可以知道:了当 foo 函数的执行上下文销毁时,由于 foo 函数产生了闭包,所以变量 myName 和 test1 并没有被销毁,而是保存在内存中,那么应该如何解释这个现象呢?

要解释这个现象,我们就得站在内存模型的角度来分析这段代码的执行流程。

  • 当 JavaScript 引擎执行到 foo 函数时,首先会编译,并创建一个空执行上下文。

  • 在编译过程中,遇到内部函数 setName,JavaScript 引擎还要对内部函数做一次快速的词法扫描,发现该内部函数引用了 foo 函数中的 myName 变量,由于是内部函数引用了外部函数的变量,所以 JavaScript 引擎判断这是一个闭包,于是在堆空间创建换一个“closure(foo)”的对象(这是一个内部对象,JavaScript 是无法访问的),用来保存 myName 变量。

  • 接着继续扫描到 getName 方法时,发现该函数内部还引用变量 test1,于是JavaScript 引擎又将 test1 添加到“closure(foo)”对象中。这时候堆中的“closure(foo)”对象中就包含了 myName 和 test1 两个变量了。

  • 由于 test2 并没有被内部函数引用,所以 test2 依然保存在调用栈中。

  • 当foo执行上下文销毁了,foo函数中的对closure(foo)的引用也断开了,但是setName和getName里面又重新建立起来了对closure(foo)引用

    • 打开“开发者工具”
    • 在控制台执行上述代码
    • 然后选择“Memory”标签,点击”take snapshot” 获取V8的堆内存快照。
    • 然后“command+f”(mac) 或者 “ctrl+f”(win),搜索“setName”,然后你就会发现setName对象下面包含了 raw_outer_scope_info_or_feedback_metadata,对闭包的引用数据就在这里面。(实际上也看不到闭包的数据)

image-20210720143331806

GZIP压缩

启用keep alive

默认http1.1以后默认开启

  • keepalive-timeout 保持时间 s
  • keepalive-requests 链接数

http 缓存

  • cache control 1.1 / expires 1.0
  • Last-modified if-modified-since 1.0
  • Etag if-none-match 1.1

一般设置:html 不缓存, css、js缓存过期时间可以设置的很长,因为一般使用hash命名文件

图片、字体不经常更换的话,缓存时间设置长一点

server-work和HTTP2 都依赖https,可以生成自签名的证书

http2

  • 二进制传输(http1.1是基于文本的,传输效率慢且不安全)
  • 请求响应多路复用
  • server push (服务器直接推送,没有请求过程)
  • 只能部署在https
  • 适合较高的请求量

资源优先级

  • 浏览器默认安排资源加载优先级
  • 使用preload,prefetch调整优先级
  • preload 提前加载较晚出现,但对于当前页面非常重要的资源
    • 就是通过标签显式声明一个高优先级资源,强制浏览器提前请求资源,同时不阻塞文档正常onload
    • preload link必须设置as属性来声明资源的类型(font/image/style/script等),否则浏览器可能无法正确加载资源。
  • prefetch 提前加载后继路由需要的资源,优先级低

文章写的相当好
pre系列讲解
同上,防止链接挂掉

接口缓存策略

  1. ajax/fetch缓存
    • 前端请求的时间带上cache,依赖浏览器本身缓存机制
  2. 本地缓存
    • 异步接口数据优先使用本地localStorage中的缓存数据
  3. 多次请求
    • 接口数据本地无localStorage缓存数据,重新再次发出ajax请求

使用CDN

CDN优点

cdn回源

回源是指浏览器访问cdn集群上静态文件时,文件缓存过期,直接穿透cdn集群而访问源站机器的行为。(发生这种情况后,cdn会更新文件及缓存标记)

cdn缓存

cdn缓存

cdn灰度发布

减少http请求

减少http请求

在V8原理中 22小节 有一些增量知识–回收效率

在JavaScript中产生的垃圾数据是由垃圾回收器来释放的,并不需要手动通过代码来释放。

我们知道JavaScript的数据分为两类,原始数据类型是存储在栈空间中的,引用类型的数据是存储在堆空间中的

垃圾回收机制

栈中的数据是如何回收的

在blog中有关于介绍**执行上下文(调用栈)**的详细介绍,栈中的垃圾回收,我们通过一段代码来看一下

1
2
3
4
5
6
7
8
9
10
function foo(){
var a = 1
var b = {name:" 极客邦 "}
function showName(){
var c = " 极客时间 "
var d = {name:" 极客时间 "}
}
showName()
}
foo()

当执行到第 6 行代码时,其调用栈和堆空间状态图如下所示:

image-20210720151826588

当showName执行完毕,上下文需要切换至foo时,有一个记录当前执行状态的指针(称为 ESP)会下移指向foo函数的执行上下文,虽然showName的执行上下文未被摧毁,但是已经是无效内存了。因为如果当foo 函数再次调用另外一个函数时,这块内容会被直接覆盖掉,用来存放另外一个函数的执行上下文

image-20210720152137737

结论

在栈空间中,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文,顺带完成了栈空间的垃圾回收

堆中的数据是如何回收的

继续上边代码的执行,当上面那段代码的 foo 函数执行结束之后,ESP 应该是指向全局执行上下文的,那这样的话,showName 函数和 foo 函数的执行上下文就处于无效状态了,不过保存在堆中的两个对象依然占用着空间,如下图所示:

image-20210720152420782

要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了

代际假说

代际假说(The Generational Hypothesis),这是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的。

代际假说有以下两个特点:

  • 第一个是大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;

  • 第二个是不死的对象,会活得更久。

分代收集

在 V8 中会把堆分为新生代老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象

  • 副垃圾回收器,主要负责新生代的垃圾回收。

  • 主垃圾回收器,主要负责老生代的垃圾回收。

统一的回收流程

不论什么类型的垃圾回收器,它们都有一套共同的执行流程

  • 标记空间中活动对象和非活动对象。所谓活动对象就是还在使用的对象,非活动对象就是可以进行垃圾回收的对象。

  • 回收非活动对象所占据的内存。其实就是在所有的标记完成之后,统一清理内存中所有被标记为可回收的对象。

  • 第三步是做内存整理。一般来说,频繁回收对象后,内存中就会存在大量不连续空间,我们

    把这些不连续的内存空间称为内存碎片。(副垃圾回收器不会产生内存碎片

副垃圾回收器

副垃圾回收器主要负责新生区的垃圾回收。大多数小的对象都会被分配到新生区,所以说这个区域虽然不大,但是垃圾回收还是比较频繁的。

新生代中用Scavenge 算法来处理。所谓 Scavenge 算法,是把新生代空间对半划分为两个区域,一半是对象区域,一半是空闲区域,如下图所示:

image-20210720153841493

新加入的对象都会存放到对象区域,当对象区域快被写满时,就需要执行一次垃圾清理操作。

在垃圾回收过程中,首先要对对象区域中的垃圾做标记;标记完成之后,就进入垃圾清理阶段,副垃圾回收器会把这些存活的对象复制到空闲区域中,同时它还会把这些对象有序地排列起来,所以这个复制过程,也就相当于完成了内存整理操作,复制后空闲区域就没有内存碎片了。

完成复制后,对象区域与空闲区域进行角色翻转,这样就完成了垃圾对象的回收操作,同时这种角色翻转的操作还能让新生代中的这两块区域无限重复使用下去

复制操作需要时间成本,为了执行效率,一般新生区的空间会被设置得比较小

对象晋升策略

JavaScript 引擎采用了对象晋升策略,也就是经过两次垃圾回收依然还存活的对象,会被移动到老生区中

主垃圾回收器

主垃圾回收器主要负责老生区中的垃圾回收。除了新生区中晋升的对象,一些大的对象会直接被分配到老生区。因此老生区中的对象有两个特点,一个是对象占用空间大,另一个是对象存活时间长

由于老生去对象比较大,不适合新生去的回收机制,主垃圾回收器是采用标记-清除算法

标记 - 清除

首先是标记过程阶段。标记阶段就是从一组根元素开始,递归遍历这组根元素(调用栈),在这个遍历过程中,能到达的元素称为活动对象,没有到达的元素就可以判断为垃圾数据

接下来就是垃圾的清除过程。它和副垃圾回收器的垃圾清除过程完全不同,你可以理解这个过程是清除掉红色标记数据的过程,可参考下图大致理解下其清除过程:

image-20210720154746371

不过对一块内存多次执行标记 - 清除算法后,会产生大量不连续的内存碎片,于是又产生了另外一种算法——标记 - 整理(Mark-Compact)

这个标记过程仍然与标记 - 清除算法里的是一样的,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。你可以参考下图:

image-20210720154921297

全停顿

JavaScript 是运行在主线程之上的,一旦执行垃圾回收算法,都需要将正在执行的JavaScript 脚本暂停下来,待垃圾回收完毕后再恢复脚本执行。我们把这种行为叫做全停顿(Stop-The-World)

image-20210720154954477

一般来说是老生代的垃圾回收占用时间较长,新生代可以忽略不计。为了解决这个问题,V8使用了增量标记(Incremental Marking)算法

image-20210720155150795

V8 将标记过程分为一个个的子标记过程,同时让垃圾回收标记和 JavaScript 应用逻辑交替进行,直到标记阶段完成。这样当执行复杂动画效果时,就不会让用户因为垃圾回收任务而感受到页面的卡顿了。

精彩评论

image-20220720104725019

this的由来

看下述一段代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

var bar = {
myName:"time.geekbang.com",
printName: function () {
console.log(myName)
}
}
function foo() {
let myName = "极客时间"
return bar.printName
}
let myName = "极客邦"
let _printName = foo()
_printName()
bar.printName()

相信你已经知道了,在 printName 函数里面使用的变量 myName 是属于全局作用域下面的,所以最终打印出来的值都是“极客邦”

不过按照常理来说,调用bar.printName方法时,该方法内部的变量 myName 应该使用bar 对象中的,因为它们是一个整体,大多数面向对象语言都是这样设计的,以在对象内部的方法中使用对象内部的属性是一个非常普遍的需求。但是 JavaScript 的作用域机制并不支持这一点,基于这个需求,JavaScript 又搞出来另外一套this 机制

this到底是什么

this 是在运行时进行绑定的,是和执行上下文绑定的,并不是在编写时绑定,它的上下文取决于函数调用时的各种条件。this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式

image-20210720111215903

执行上下文其余相关的内容,在blog中浏览器原理中有详细介绍

this的绑定规则

默认绑定

首先要介绍的是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。
this指向window对象,在严格模式下指向 undefined

1
2
3
4
5
function foo() {      
console.log( this.a );
}
var a = 2;
foo(); // 2

foo() 是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定,无法应用其他规则。

隐式绑定

  1. 另一条需要考虑的规则是调用位置是否有上下文对象,或者说是否被某个对象拥有或者包含
1
2
3
4
5
6
7
8
9
10
function foo() {      
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

obj.foo(); // 2
  1. 对象属性引用链中只有最顶层或者说最后一层会影响调用结果,看下例
1
2
3
4
5
6
7
8
9
10
11
12
function foo() {      
console.log( this.a );
}
var obj2 = {
a: 42,
foo: foo
};
var obj1 = {
a: 2,
obj2: obj2
};
obj1.obj2.foo(); // 42
  1. 隐式丢失, 一个最常见的 this 绑定问题就是被隐式绑定的函数会丢失绑定对象,也就是说它会应用默认绑定,从而把 this 绑定到全局对象或者 undefined 上,取决于是否是严格模式
1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {      
console.log( this.a );
}

var obj = {
a: 2,
foo: foo
};

var bar = obj.foo; // 函数别名!
var a = "oops, global"; // a 是全局对象的属性
bar(); // "oops, global"

虽然 bar 是 obj.foo 的一个引用,但是实际上,它引用的是 foo 函数本身,因此此时的 bar() 其实是一个不带任何修饰的函数调用,因此应用了默认绑定

一种更微妙、更常见并且更出乎意料的情况发生在传入回调函数时

1
2
3
4
5
6
7
8
9
10
11
12
13
function foo() {      
console.log( this.a );
}
function doFoo(fn) {
// fn 其实引用的是 foo
fn(); // <-- 调用位置!
}
var obj = {
a: 2,
foo: foo
};
var a = "oops, global"; // a 是全局对象的属性
doFoo( obj.foo ); // "oops, global"

参数传递其实就是一种隐式赋值,因此我们传入函数时也会被隐式赋值,所以结果和上一个例子一样。

显式绑定

call(..)apply(..)bind(..) 方法

1
2
3
4
5
6
7
8
9
10
function foo() {      
console.log( this.a );
}

var obj = {
a:2
};

foo.call( obj ); // 2

new 绑定 (暂不深究)

在 JavaScript 中,构造函数只是一些使用 new 操作符时被调用的函数。它们并不会属于某个类,也不会实例化一个类。实际上, 它们甚至都不能说是一种特殊的函数类型,它们只是被 new 操作符调用的普通函数而已。
使用 new 来调用函数,或者说发生构造函数调用时,会自动执行下面的操作。

  1. 创建(或者说构造)一个全新的对象。
  2. 这个新对象会被执行 [[ 原型 ]] 连接。
  3. 这个新对象会绑定到函数调用的 this。
  4. 如果函数没有返回其他对象,那么 new 表达式中的函数调用会自动返回这个新对象

还有一些需要注意的示例

注意后三个示例,详细解释在 同分类下-从ECMAScript规范解读this中有解读

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var value = 1;

var foo = {
value: 2,
bar: function () {
return this.value;
}
}

//示例1
console.log(foo.bar()); // 2
//示例2
console.log((foo.bar)()); // 2
//示例3
console.log((foo.bar = foo.bar)()); // 1
//示例4
console.log((false || foo.bar)()); // 1
//示例5
console.log((foo.bar, foo.bar)()); // 1

后三个类型,方便记忆的话,可以将其理解为隐式绑定,foo.bar被隐式的赋值了,相当于

1
2
var a = foo.bar
a()

但是需要注意第二个示例

关于this补充

可以帮助理解

从ECMAScript规范解读this

极客时间

变量提升

1
2
3
4
5
6
7
8
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数 showName 被执行');
}
// 函数 showName 被执行
// undefined

最后输出的结果不太符合预期,其中的本质与js的执行过程有关,这种现象被称为变量提升

所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined。

声明部分 指 var myname = 这部分 以及 完整的函数声明

var bar = function(){} 这种与var bar = 1 并无大的区别,与function bar(){} 从编译过程来说截然不同

实际上变量和函数声明在代码里的位置是不会改变的,而且是在编译阶段被 JavaScript 引擎放入内存中

编译过程

image-20210719142421766

从上图可以看出,输入一段代码,经过编译后,会生成两部分内容:执行上下文(Execution context)和可执行代码

执行上下文也是一个特别重要的概念,会在后边具体分析

由此不难分析出打印的结果,但是如果存在相同的命名怎么处理,可以参考下述规则:

  • 如果是同名的函数,JavaScript编译阶段会选择最后声明的那个。

  • 如果变量和函数同名,那么在编译阶段,变量的声明会被忽略

执行上下文

基本概念

执行上下文(以下简称“上下文”)的概念在 JavaScript 中是颇为重要的。变量或函数的上下文决定了它们可以访问哪些数据,以及它们的行为。每个上下文都有一个关联的变量对象(variable object),而这个上下文中定义的所有变量和函数都存在于这个对象上。

执行上下文的组成代码示例:

1
2
3
4
5
const ExecutionContextObj = {
VO: window, // 变量对象
ScopeChain: {}, // 作用域链
this: window
};

执行上下文的组成图例示例:

img

**上下文基本上有三类(包括ES6)**:

  • 全局上下文 在浏览器环境下即为window
  • 函数上下文(当代码执行流进入函数时,函数的上下文被推到一个上下文栈上。在函数执行完之后,上下文栈会弹出该函数上下文,将控制权返还给之前的执行上下文)
  • eval上下文 (不考虑)

上下文中的代码在执行的时候,会创建变量对象的一个作用域链

作用域链中的下一个变量对象来自包含上下文,再下一个对象来自再下一个包含上下文。以此类推直至全局上下文;全局上下文的变量对象始终是作用域链的最后一个变量对象

函数上下文

如果上下文是函数,则其活动对象(activation object)用作变量对象,下面以函数为例具体分析一下

为什么称其为活动对象呢,因为只有到当进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问。

以下面的例子为例,结合着之前讲的变量对象和执行上下文栈,我们来总结一下函数执行上下文中作用域链和变量对象的创建过程:

1
2
3
4
5
6
7
8
9
var scope = 'global scope';
function checkscope(s) {
var scope = 'local scope';
function f() {
return scope;
}
return f();
}
checkscope('scope');

执行过程如下:

1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

1
2
3
checkscope.[[scope]] = [
globalContext.VO
];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

1
2
3
4
ECStack = [
checkscopeContext,
globalContext
];

3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

1
2
3
checkscopeContext = {
Scope: checkscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

1
2
3
4
5
6
7
8
9
10
11
12
checkscopeContext = {
AO: {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope', // 传入的参数
f: pointer to function f(),
scope: undefined, // 此时声明的变量为undefined
},
Scope: checkscope.[[scope]],
}

5.第三步:将活动对象压入 checkscope 作用域链顶端

1
2
3
4
5
6
7
8
9
10
11
12
checkscopeContext = {
AO: {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope', // 传入的参数
f: pointer to function f(),
scope: undefined, // 此时声明的变量为undefined
},
Scope: [AO, [[Scope]]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

1
2
3
4
5
6
7
8
9
10
11
12
checkscopeContext = {
AO: {
arguments: {
0: 'scope',
length: 1,
},
s: 'scope', // 传入的参数
f: pointer to function f(),
scope: 'local scope', // 变量赋值
},
Scope: [AO, [[Scope]]]
}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

1
2
3
ECStack = [
globalContext
];

执行上下文的维护

说在执行 JavaScript 时,可能会存在多个执行上下文,那么 JavaScript 引擎是如何管理这些执行上下文的呢?

答案是通过一种叫栈的数据结构来管理的

JavaScript 引擎正是利用栈的这种结构来管理执行上下文的。在执行上下文创建好后,JavaScript 引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文栈,又称调用栈

看如下代码

1
2
3
4
5
6
7
8
9
10
var a = 2
function add(b,c){
return b+c
}
function addAll(b,c){
var d = 10
result = add(b,c)
return a+result+d
}
addAll(3,6)

第一步,创建全局上下文,并将其压入栈底

image-20210719153100763

第二步是调用 addAll 函数。当调用该函数时,JavaScript 引擎会编译该函数,并为其创建一个执行上下文,最后还将该函数的执行上下文压入栈中,如下图所示:

image-20210719153303513

第三步,当执行到 add 函数调用语句时,同样会为其创建执行上下文,并将其压入调用栈,如下图所示:

image-20210719153352832

当 add 函数返回时,该函数的执行上下文就会从栈顶弹出,并将 result 的值设置为 add函数的返回值,也就是 9。如下图所示:

image-20210719153501404

紧接着 addAll 执行最后一个相加操作后并返回,addAll 的执行上下文也会从栈顶部弹出,此时调用栈中就只剩下全局上下文了。最终如下图所示:

image-20210719153551914

至此,整个 JavaScript 流程执行结束了。

调用栈的跟踪可以借助chorme调试工具中的call stack 或者借助 console.trace()

解决变量提升的弊端

ES6 通过 let const 解决了变量提升的问题,那么 ES6 又是如何在函数级作用域的基础之上,实现对块级作用域的支持呢?

变量对象应该包括 变量环境 和 词法环境

下面我们来看如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function foo() {
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a)
console.log(b)
}
console.log(b)
console.log(c)
console.log(d)
}
foo()

第一步是编译并创建执行上下文,下面是我画出来的执行上下文示意图,你可以参考下:

image-20210719180147793

通过上图,我们可以得出以下结论:

  • 函数内部通过 var 声明的变量,在编译阶段全都被存放到变量环境里面了。

  • 通过 let 声明的变量,在编译阶段会被存放到词法环境(Lexical Environment)中。

  • 在函数的作用域内部,通过 let 声明的变量并(暂时)没有被存放到词法环境中。

接下来,第二步继续执行代码,当执行到代码块里面时,变量环境中 a 的值已经被设置成了 1,词法环境中 b 的值已经被设置成了 2,这时候函数的执行上下文就如下图所示:

image-20210719180806940

​ 从图中可以看出,当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量,比如在作用域外面声明了变量 b,在该作用域块内部也声明了变量 b,当执行到作用域内部时,它们都是独立的存在。

​ 其实,在词法环境内部,维护了一个小型栈结构,栈底是函数最外层的变量,进入一个作用域块后,就会把该作用域块内部的变量压到栈顶;当作用域执行完成之后,该作用域的信息就会从栈顶弹出,这就是词法环境的结构。需要注意下,我这里所讲的变量是指通过 let 或者 const 声明的变量。

​ 再接下来,当执行到作用域块中的console.log(a)这行代码时,就需要在词法环境和变量环境中查找变量 a 的值了,具体查找方式是:沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。

image-20210719181101068

当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文

如下图所示:

image-20210719181919678

通过上面的分析,想必你已经理解了词法环境的结构和工作机制,块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了。

查缺补漏–优秀blog文章

  1. JavaScript深入之执行上下文栈

  2. JavaScript深入之变量对象

  3. JavaScript深入之作用域链

该作者其余文章 https://github.com/mqyqingfeng/Blog

Tips 为了防止链接失效,请看 深入执行上下文

html的优化

  • 减少iframes的使用
    • 必须使用的时候 延迟加载,动态赋予src
  • 压缩空白符
  • 避免节点深层次嵌套
  • 避免使用table布局(已经没人用了)
  • 删除注释
  • css&js 尽量外链
  • 删除元素默认属性
  • 语义化标签

借用工具进行优化

  • html-minifier (webpack已经集成)

css优化

  • 降低css对渲染的阻塞
  • 利用gpu完成动画绘制
  • 使用contain属性
  • 使用font-display属性
0%