目录
1.变量
1.1原始值与引用值
原始值:最简单的数据,前面我们提到六种原始值,undefined、null、Boolean、string、number,symbol。
引用值:由多个值构成的对象
在把一个值赋给一个变量时,JavaScript引擎首先必须确定这个值是原始值还是引用值。保存原始值的变量是按值访问的,我们操作的就是保存在变量中的实际值,而保存引用值的变量是按引用访问 ,我们实际上操作的是对该对象的引用而非对象实际本身
1.1-1动态属性
对于引用值而言,可以随时添加,删除和修改其属性和方法。比如下面的例子
let person=new Object();
person.name='小明';
console.log(person.name);//小明
原始值不能有属性,尽管尝试给他添加属性不会报错
let name='小明';
name.age=17;
console.log(name.age);//undefined
原始类型初始化可以使用的只有原始字面量形式,如果使用new关键字,则JavaScript会创建一个object类型的实例,简单地说就是用new关键字创建的变量实际上属于object类型
let name1='程咬金';
let name2=new String("屌丝");
name1.age=17;
name2.age=18;
console.log(name1.age);//undefined
console.log(name2.age);//18
console.log(typeof name1);//string
console.log(typeof name2);object
1.1-2复制值
原始值的复制值:
主本与副本之间互不干扰,各自独立
let age1=18;
let age2=age1;
console.log(age1,age2);//18,18
age2=17;//修改age2的值不影响age1
console.log(age1.age2);//18,17
引用值复制值:
引用值的复制值实际上是一个指针,它指向存储在堆内存中的对象。操作完成之后,两个变量实际上指向同一个指针,因此一个对象上的变化会在另一个对象上反映出来
let obj1=new Object();
let obj2=obj1;
obj1.name='李哈哈';
//对obj1设置name属性可以通过obj2反映出来
console.log(obj2.name);//李哈哈
1.1-3传递参数
原始值传递参数:
当我们把一个原始值作为参数传递给函数的形参时,其实就是把变量在栈空间的值复制了一份给形参,那么在方法内部对形参做任何修改,都不会影响到外部的变量
function abc(obj){
obj+=90;
console.log(obj);//100
}
let name=10;
abc(name);
//函数内部的变动不影响外部
console.log(name);//10
引用值传递参数:
当我们把一个引用值作为参数传递给函数的形参时,其实就是把变量在栈空间里面保存的堆地址复制给了形参,形参和实参其实保存的是同一个堆地址,所以操作的是同一个对象
function abc(obj){
obj.name='梁志超'
}
let name={};
abc(name);
console.log(name);//{name: '梁志超'}
传递参数的概念不能与变量作用域混淆一滩,在JavaScript中,函数的参数是局部变量,可不要想当然的说我要创建一个全局变量当参数传递!!!
1.1-4确定类型
typeof操作符可以判断一个变量是什么原始类型(原始值),但是对引用值的用处不大,遇到object或null只会返回object,通常我们不关心一个值是不是对象,而是想知道它是什么类型的对象,ECMA script提供了instanceof操作符,用于判断一个变量是否为给定的引用类型
语法:变量( variable ) instanceof 构造函数(constructor) // 返回值为布尔值
console.log(obj1 instanceof Array);
//变量obj1是Array类型吗,是返回true,不是返回false
console.log(obj1 instanceof Object);
//变量obj1是Object类型吗,是返回true,不是返回false
console.log(obj1 instanceof RegExp);
//变量obj1是RegExp类型吗,是返回true,不是返回false
按照定义,所有引用值都是 Object 的实例,因此通过 instanceof 操作符检测任何引用值和 Object 构造函数都会返回 true,类似的 instanceof 检测原始值,会返回 false,因为原始值不是对象
2.执行上下文与作用域
上下文:JS代码执行环境;
Js代码在引擎中是以“一段一段”的方式来执行的,而非一行一行来分析执行的。而这“一段一段”的可执行代码无非三种:Global code(全局执行上下文), Function code(函数执行上下文), Eval code(Eval函数执行上下文)。这些可执行代码在执行的时候会创建一个一个的执行上下文。例如,当执行到一个函数的时候,js引擎会做一些“准备工作”,而这个“准备工作”,我们称其为执行上下文。那么随着我们执行上下文数量的增加,js引擎又如何管理这些执行上下文呢?这时便有了执行上下文栈。
简单来说,当浏览器执行程序时,大部分情况他会首先将全局上下文首先推入上下文栈中,当遇到函时,就会将函数上下文也推入到上下文栈中,覆盖之前的上下文并取代之前上下文的控制权,当函数退出时,再将控制权返还给之前的上下文,以此一步一步执行
这里我们用一段贯穿全文的例子来讲解执行上下文的执行过程
var scope = 'global scope'
function checkScope(s) {
var scope = 'local scope'
function f() {
return scope
}
return f()
}
checkScope('scope')
当js引擎去解析代码的时候,最先碰到的就是global code,所以一开始初始化的时候便将全局上下文推入执行上下文栈,并且只有整个应用程序执行完毕的时候,全局上下文才会推出执行上下文栈。(比如关闭网页或退出浏览器)
这里我们用ECS(Excution Context Stack)来模拟执行上下文栈,用glocalContext来表示全局执行上下文栈
// 1.在执行函数之前只有全局上下文
ECS = {
globalContext
}
// 2.当代码执行fn函数的时候,会创建fn函数的执行上下文,并将其压入执行上下文栈
ECS = {
fnContext,
globalContext
}
// 3.当代码执行f函数时,会创建f函数的执行上下文,并将其压入执行上下文栈
ECS = {
fContext,
fnContext,
globalContext
}
// 4.f函数执行完毕后,f函数的执行上下文出栈,随后fn函数执行完毕,fn函数的执行上下文出栈
ECS = {
// fContext 出栈
fnContext,
globalContext
}
ECS = {
// fnContext 出栈
globalContext
}
执行上下文的三个重要属性:
- 变量对象(VO)
- 作用域链(scope chain)
- this
2.1变量对象
变量对象是与执行上下文相关的数据作用域,储存了在上下文中定义的变量和函数声明。并且不同的执行上下文也有着不同的变量对象,这里分为全局上下文中的变量对象和函数执行上下文中的变量对象。
2.1-1全局上下文中的变量对象 (VO)
全局上下文中包含的变量对象其实就是全局对象,也就是所有的window对象。我们可以通过this来访问全局对象,并且在浏览器环境中this=window,在node环境中this=global。
2.1-2函数上下文中的变量对象 (AO)
在函数上下文中的变量对象,我们用活动对象来表示(activation object,这里简称AO),为什么称其为活动对象呢,因为只有进入一个执行上下文中,这个执行上下文的变量对象才会被激活,并且只有被激活的变量对象,其属性才能被访问。
注意:
在初学JavaScipt中,总会有同学对变量对象与活动对象弄混,为此解释一下这两个名词。在执行上下文
建立的阶段会创建变量对象,变量对象中保存arguments,function声明的函数,var声明的变量,具体
的第二节已经介绍,在此不做过多阐述。当执行上下文创建过程的结束,就开始了代码执行阶段,在此时
JavaScript会将变量对象(VO)转换成活动对象(AO)。在未进入到执行阶段之前,变量对象中的属性都不
能访问,但是进入执行阶段后,由于变量对象被转换成了活动对象,里面的属性就可以被访问了,随后开
始执行代码。因此在第二节的例子上,其实输出的都是活动对象的属性(但变量对象与活动对象中包含的
属性与值都相同,所以姑且认为是变量对象)
再总结一下,其实变量对象与活动对象都是同一个对象,只是它们处于不同的执行上下文生命周期。
在函数执行之前,会为当前函数创建上下文,并且在此时,会创建变量对象:
- 根据函数arguments属性初始化arguments对象;【参数对象,当某个函数接收参数的时候,会将参数封装成arguments对象】
- 根据函数声明生成对应的属性,其值为一个指向内存中函数的引用指针。如果函数名称已经存在,则覆盖【后定义的同名函数会覆盖原来定义的同名函数成为这个上下文的变量对象】
- 检查当前上下文的变量声明,对于每一个变量声明,变量对象会以变量名建立一个属性,属性值为undefined。如果变量名属性已存在,则会直接跳过,原属性值保持不变(这样是为了防止将同名的函数名覆盖)
我们来写一个代码方便大家理解:
function method(num1, num2) {
console.log(arguments);
console.log(num1, num2);
console.log(vara, func);
console.log(func());
var vara = 10;
console.log(vara);
function func() {
return 1;
}
function func() {
return 2;
}
}
method(1, 2);
输出结果如下
{ '0': 1, '1': 2 }
1 2
undefined [Function: func]
2
10
首先输出的是arguments对象,通过与第二行输出num1,num2对比可知,argument对象是一种key-value格式的对象,其中key从0开始逐步递增,value依次对应着传入参数。
随后根据变量声明与函数声明的规则,由于变量声明,会在执行上下文创建阶段为变量赋值undefined,而函数声明,会在执行上下文创建阶段为函数变量赋值指向该函数的引用地址。故第三行输出结果为 undefined 与 [Function:func]
又由于代码中声明了两个func函数,为了验证是否存在函数覆盖现象,打印输出func()的结果,输出结果为2,则意味着,第二个定义的func函数,在执行上下文阶段,覆盖了一开始的func函数。
最后给vara赋值10,随即输出vara,得到10
总结:上下文的变量对象就是其里面定义的变量与函数,函数上下文的变量对象多一个arguments对象【全局上下文没有】
2.2作用域链
当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直查到全局上下文的变量对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链
一个上下文中的变量对象只包含该上下文里面定义的变量与函数,但是不包含其内创建的函数内的变量对象,就像函数外部无法访问函数内的局部变量一样,但是上下文的作用域链中包含的对象有它自身及其往上所有级的变量对象,也就是在函数内可以访问到函数外的变量。有些读者看到这里时会有些疑惑。不着急,我们来看看下面的代码及其解析。
var color="blue";
function changeColor(){
let anotherColor="red";
function swapColors(){
let tempColor=anotherColor;
//这里可以访问color,anotherColor和tempColor
}
//这里可以访问color、anotherColor,但是不能访问tempColor
swapColors();
}
//这里只可以访问color
changeColor();
这段代码中,涉及到三个上下文:全局上下文 、changeColor()函数上下文、swapColor()函数上下文。
全局上下文包含的变量对象有:color、changeColor函数,作用域链中包含的变量对象(可以访问的变量)有color,changeColor函数
changeColor函数上下文包含的变量对象有:anotherColor、函数swapColor、arguments(arguments是所有函数上下文默认创建变量,全局上下文不会默认创建), 作用域链包含的变量对象:anotherColor、color,swapColor()函数
swapColor函数上下文包含的对象:tempColor。作用域链包含的对象有:tempColor、anotherColor、color
总的来说,内部上下文可以通过作用域链访问外部上下文的一切,但是外部上下文无法访问内部上下文的任何东西
2.3this
在这里this绑定也分为全局执行上下文和函数执行上下文
- 在全局执行上下文中,this的值指向全局对象。(浏览器中this指向window,node中指向global)
- 在函数执行上下文中,this的值取决于该函数如何被调用的。如果它被一个对象调用,那么this会被设置成这个对象,否则this的值被设置为全局对象或者undefined(在严格模式下)
总结起来就是谁调用,this指向谁。
实例分析:
var name = 'window'
var obj1 = {
name: 'obj1',
fn1: function() {
console.log(this.name)
},
fn2: () => {
console.log(this.name)
},
fn3: function() {
return function() {
console.log(this.name)
}
},
fn4: function() {
return () => {
console.log(this.name)
}
}
}
var obj2 = {
name: 'obj2'
}
obj1.fn1() // obj1
// 执行过程上下文为
//fn1Context = {
// scope: [VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: obj1
//}
// 由上下文可知this指向obj1,所以结果为obj1.name = 'obj1'
obj1.fn1.call(obj2) // obj2
// fn1Context = {
// scope: [VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: obj2
// }
// 可知this指向obj2,所以结果为obj2.name = 'obj2'
obj1.fn2() // window
obj1.fn2.call(obj2) // window
// 因为箭头函数没有自己的this,他的this永远指向父级执行上下文的this,那为什么上层执行上下文是globalContext.VO呢?有上下文可知javascript中的上下文分为全局执行上下文,函数执行上下文域eval执行上下文。而不管是全局执行上下文或函数执行上下文,大致都包含创建VO,确认作用域链,确认this指向三步。也就是说,this属于上下文中的一部分,很明显对象obj1并不是一个函数,它并没有权利创建自己的上下文,所以没有自己的this,那么他的外层是谁呢?当然是全局window了,所以这里的this指向window。箭头函数的this由外部环境决定,且一旦绑定无法通过call,apply或者bind再次改变箭头函数的this。所以这里虽然使用了call方法依旧无法修改,指向window
// fn2Context = {
// scope: [VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: globalContext.VO
// }
obj1.fn3()() // window
obj1.fn3().call(obj2) // obj2
obj1.fn3.call(obj2)() // window
// fn3返回一个闭包, 而它的this指向它的调用者,即obj1.fn3()返回的函数的调用者
// fn3returnFnContext = {
// scope: [VO, fn3Context.VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: 返回函数的调用者
// }
obj1.fn4()() // obj1
obj1.fn4().call(obj2) // obj1
obj1.fn4.call(obj2)() // obj2
// fn4返回一个闭包,只是这个闭包是一个箭头函数,而箭头函数没有自己的this,继承于父级this,所以返回函数的this指向fn4的this,即fn4的调用者
// fn4ReturnFnContext = {
// scope: [VO, fn4Context.VO, globalContext.VO],
// VO: {
// arguments: {
// length: 1
// }
// },
// this: fn4的调用者
// }
总结:创建上下文只有三中方式,全局上下文/函数上下文/eval上下文。对象不是函数,不具备创建上下文的能力
2.4作用域链的增
虽然执行上下文主要有全局上下文和函数上下文两种(eval()调用内部存在第三种上下文),但是有其他方式增强作用域链,某些语句会导致在作用域链前端添加一个上下文,这个上下文在代码执行后会被删除。通常在这两种情况下会出现这个现象
1.try/catch语句的catch块
2.with语句
对于with语句来说,会向作用域链前端添加指定对象;对catch语句来说,则会创建一个新的变量对象,看看下面的例子 。
function abc(){
let qs="?debug=true";
with(location){
let url1=href+qs;
}
return url1;
}
这里,with语句将location对象作为上下文,当with语句中的代码引用变量href时,实际上引用的是location.href,也就是自己变量对象的属性
2.5 object.freeze()
教材看到这里,想给大家补充一点前面没讲到的知识点,(害,原因是书上这里才写)
我们知道定义的对象是可以随时更改其属性值的,那么如何才能让整个对象不能修改呢,可以使用object.freeze()
let obj1={
name:"小明"
}
obj1.name="不是小明"
console.log(obj1.name);//不是小明
let obj2=Object.freeze({
name:"小红"
})
obj2.name="不是小红";
console.log(obj2.name);//小红
3.垃圾回收
3.1什么是js垃圾回收机制 ?
js的垃圾回收机制就是定时回收闲置资源的一种机制 , 每隔一段时间, 执行环境都会清理内存中一些没用的变量释放它所占用的内存 .
核心思想 : 找到没用的变量, 释放它们的内存
3.2两种主要的回收策略
- 标记清除
- 引用计数
3.2-1. 标记清除
标记清除是现在最常使用的垃圾回收策略, 使用标记清除作为垃圾回收机制的浏览器会在垃圾回收程序进行时会做如下几步 :
- 标记内存中所有的变量
- 把在上下文(全局作用域, 脚本作用域)中声明的变量,以及在全局被引用的变量的标记删除掉, 剩下的所有带标记的变量就被视为要删除的变量, 垃圾回收执行时释放它们占用的内存
- 内存清理, 清除垃圾
代码示例:
// 变量 color,dog 在全局环境下声明, 不会被清除
const color = 'red';
var dog = '金毛';
{
let cat = 'kitty'; // 变量 cat 在块作用域中声明, 且没有被全局所引用, 所以会在下一次垃圾
回收执行时, 释放其内存
}
3.2-2.引用计数
引用计数是一种不常用的垃圾回收策略, 主要核心思路就是记录值被引用的次数, 一个值被赋给变量,引用次数+1, 这个变量在某个时刻重新赋了一个新值, 旧值的引用次数-1变为了0, 在下次垃圾回收程序进行时就会释放它的内存
引用计数存在的问题 : 循环引用
代码示例:
function fn() {
const obj1 = new Object() // new Object 在堆内存中创建了一个对象1 {} 这个值被赋值给obj1 于是引用次数 + 1
const obj2 = new Object() // new Object 在堆内存中创建了一个对象2 {} 这个值被赋值给obj2 于是引用次数 + 1
obj1.a = obj2; // obj2 被赋值给 obj1的a属性 于是对象1的引用次数 1+1 = 2
obj2.a = obj1; // obj1 被赋值给 obj2的a属性 于是对象2的引用次数 1+1 = 2
}
// 此时两个对象之间相互引用 且如果函数多次调用, 又会重新执行多次函数体, 又会多了n个相互引用的对象占用内存
为了避免循环引用的问题 : 我们可以手动将其设置为null
obj1.a = null;
obj2.a = null;
// 通过设置为null 可以切断两者之间的引用, 在下次回收时就会清理释放掉
但是引用计数这种方法已经淘汰
3.3如何提升网页的性能
3.3-1接触引用
将不在必要的数据设置为null
functiona abc(){
let add=123;
return add;
}
let der=abc();
der=null;//解除引用
这种方法非常适合全局变量和全局对象属性,因为垃圾回收机是不会回收全局变量占用的内存的
3.3-2通过const和let声明提升性能
性能:垃圾回收程序为了提升网页性能而生,但是频繁且长时间的回收程序也会让网页性能下降,应该尽早的结束垃圾回收程序
因为let与const是块级作用域,在块级不属于全局变量,所以这样会让垃圾程序更早的介入
3.3-3局部变量超出作用域会被自动解除引用
离开作用域的值会被自动标记为可回收,然后在下次垃圾回收期间被删除,但是不是一个值接触引用了就会被回收,应该确保它和全局上下文没有关系了才会被回收
functiona abc(){
let add=123;
return add;
}
let der=abc();//这里add超出了作用域,自动接触引用,但是全局变量der引用了他得值,所以下次回收不会删除它
der=null;//这里下次回收时会删除add与der
如果这里看不懂,返回3.3-1
这里不要把作用域链 与作用域等同
3.3-4内存泄露
可能这里就有小伙伴疑惑了,这个和性能有什么关系吗,毋庸置疑大有关系,我们来看看下面的代码
function abc(){
name="小明";
}
abc();
这里我们在函数内创建了一个全局变量name,那么此时解析器就会把他 当作window对象来创建。可想而知,在window对象上(全局上下文)创建的属性,只要window本身不被清理就不会被回收