摘要
本篇学习总结简要概述JavaScript语言代码预解析与执行的过程,以及在这过程中内存的分配与释放。
本文通过浏览器对JS代码在代码段内与代码段间的执行特点,引出代码执行的过程,运用《蛋与杯子的存放和取出》的通俗而又生动的比喻,归纳总结代码从被浏览器预解析,形成执行上下文EC==>入栈==>执行==>出栈(释放内存)的主要路线,以及在执行阶段执行上下文的内数据的指向、作用域对数据的限制特点、作用域链发挥的用处。最后运用大量习题反复巩固知识点,形成系统知识体系,并引出闭包和原型链知识点的思考。
关键字:JavaScript、代码段、预解析、行上下文EC、作用域、作用域链。
一、JavaScript代码段的概念
1. 什么是代码段:
在JavaScript(下面一律称作JS)中,一对<script> </script> 标签之间就是一个代码段。浏览器对一个代码段内JS代码预解析完成后,按从上到下的顺序执行代码。
2. 代码段内部代码执行的特点: 在一个代码段内,代码出现错误,浏览器就会在控制台(Console),打印出错误类型及错误位置,并停止错误位置以下的代码执行,开始下一个代码段内代码的预解析与执行。
3. 代码段间代码执行的特点: 每一个代码段之间是彼此独立的,浏览器在执行代码时,如果上面的代码段报错,浏览器会跳到下一段代码,上一段代码不会影响下面的代码段的执行。
4. 一个页面中可以有多个代码段
<script>
var a = 110;
console.log(a);//110
// 使用了没有声明的变量
console.log(c);
// 报错:ReferenceError 引用错误
// 同一个代码码段如报了引用错误,这个代码段的错误位置以下的代码停止执行
console.log(a);//不执行
</script>
<!-- 一张页面中可以有多个代码段 -->
<script>
var b = 220;
console.log(b);
// 上面的代码段中定义数,在下面的代码中可以执行
console.log(a);
</script>
控制台打印结果:
二、浏览器中JavaScript语言的执行
在代码段内,JS代码不是随意访问的,而是遵循严格的规则:全局作用域下的代码可以在代码段内被全局访问,局部作用域下的代码只能在本作用域内访问。
在浏览器中,JS代码执行过程可分解为两个阶段:预解析阶段(也称:预编译阶段)、代码执行阶段。
浏览器预解析完(声明提升),马上执行代码(赋值或赋值或者赋地址),是一个连续的过程。
2.1 浏览器对JS代码预解析时都做了些什么?预解析与代码执行的过程具体是怎样的?
1. 声明提升: 加var的变量 提升的是声明,没有赋值; function声明的函数要整体提升到代码段的最前面。
<script>
// 预解析提升:
// 相当于var a ;
// 相当于 function fn() {console.log('我是一个函数');}
//执行代码:
console.log(a); //undefined 预解析并未提升变量值,所以返回undefined
var a = 110;//执行到此步 相当于给 变量a赋值110;
console.log(a);//110
fn(); //'我是一个函数'
//浏览器预解析时,函数整体都提升到了最前面
function fn() {
console.log('我是一个函数');
}
</script>
控制台打印结果:
将函数体赋值给变量的情况
<script>
// 相当于 var g = undefined;
g(); //Uncaught TypeError: g is not a function 输入错误
// 加var的变量仅提升声明 不提升赋值,所以此变量的值一开始是undefined
// g 此时是值为undefined的变量 而不是一个函数function ,所以不可以用g()调用
// 函数表达式
// 本质是一个变量, var 用来声明变量
// 这个变量的值是函数 函数也是一种数据
var g = function() {
console.log('g....');
}
</script>
控制台打印结果:
2. 声明提升与执行的具体过程: 全局变量提升到页面window最上面, 如果在函数内部的(var声明)局部变量,就提升到函数内部的最前面。
<script>
// 1、最外层预解析提升:
// var a ;
// 2、函数整体提升与内部预解析:
// function fn() {
// var b ;
// c = 111;
// console.log(b);
// console.log('我是一个函数');}
//3、执行代码:
console.log(a); //undefined 预解析并未提升变量值,所以返回undefined
var a = 110; //执行到此步 相当于给 变量a赋值110
console.log(a); //110
fn();
//浏览器预解析时,函数整体都提升到了最前面
function fn() {
console.log(b); //undefined
var b = 220; //执行到此步 相当于给 变量b赋值220
c = 111; //不是var 声明的全局变量
console.log(b);
console.log('我是一个函数');
}
console.log(c); //111
</script>
控制台打印结果:
3. 特别注意:
加 var 的变量,仅仅是提升声明;函数提升的不仅只有声明,还有赋值。
三、数据与数据储存(执行的JS代码数据在内存区的分配)
3.1 数据的容器 栈(stack)和 堆(heap)
JS代码是运行在计算机(或者手机等)硬件设备上的,以计算机来说,计算机储存分为RAM内存和ROM外存(也叫硬盘),计算机的RAM内存相当于一个中转站,计算机所需调取数据是从RAM内存中调取,RAM内存的特点是数据传输速度快,而RAM内存的数据又是从ROM外存中调取的,本次重点理解RAM内存中的两个区 栈(stack)、堆(heap)。
3.2 数据的分类
数据可以分为两类:原始类型(也叫:简单数据类型、基本数据类型)、对象Object(也叫:复杂数据类型)。
原始类型包括:Number(数值型)、String(字符串型)、undefined、null(空)、Boolean(布尔型)、Symbol、BigInt(超大整数,使用的时候后面必须加n,一个可以表示任意精度的新的数字原始类型)。----------uSSNnBB
对象Object包括:对象、函数、数组、内置对象:window ,document(Dom), Math数学对象,Date日期对象等。除了基础类型以外的,其它都属于复杂数据类型。
3.3 JS全局代码、局部代码、执行上下文
3.3.1 JS全局代码与局部代码
全局代码:全局作用域下的代码,默认进入<script> </script> 标签,就会执行全局代码。
局部代码:局部作用域下的代码,一个函数就是一个局部代码。
<script>
// a、b位于全局代码中
var a = 1;
var b = 2;
function fn() {
// c d位于局部代码中
var c = 3;
var d = 4;
}
</script>
3.3.2 JS代码执行上下文(EC)、执行上下文栈(ECS)
全局代码执行时,就会产生全局执行上下文 ( Execution Context Global )===>ECG。
每当调用一个函数,就会产生一个局部的执行上下文EC,调用100个函数,就会产生100个执行上下文EC。
执行上下文的产生,都需要放到栈中,这个栈叫做 执行上下文栈(Execution Context Stack) ===>ECS。
当函数调用完毕,函数的EC就要出栈,当ECG执行完毕,ECG也要出栈。
执行上下文出入栈以及堆内GO 通俗理解
1)当全局代码执行,产生ECG (第一个是鸭蛋);
2)当函数执行,产生EC(fn),每调用一次就产生一个EC(后面都是鸡蛋);
3)ECG或EC(fn)都需要放到ECS中,ECS是执行上下文栈(一个杯子),先放进去的执行上下文后
拿出来;
4)当函数调用完毕,就出栈(内存释放)。(所谓出栈就是把蛋从杯子中拿出来);
JS在执行代码时,肯定先执行全局代码,就会产生EC(G),这个EC(G)就要入栈。当我们调用一个函
数,就会产生一个局部执行上下文EC(fn),此时,这个局部执行上下文EC(fn)也要入栈,当函数调
用完毕后,这个 EC(fn)在堆内没有引用就要出栈(被删除)释放内存,调用其它函数又进入其他EC
执行完毕后又,出以此类推,当全局代码执行完后,EC(G)也要出栈。
EC 的作用:给代码提供数据,代码中需要的数据,都要从EC中找。
JS代码在执行时,会在 堆内存 中创建一个去全局对象,Global Object 简称GO;在浏览器
中,这个GO 说白了就是window,是一个全局对象,是属性的无序集合。
BOM API就是window属性,如:Data、Array、String、Number、setTimeout,aleart...
声明的全局变量和在全局代码中写的函数,都会挂在GO上
↓↓ 以下代码结合图片有助于更深入理解 ↓↓
<script>
var a = 110;
var b = 220;
console.log(window.a);//110
console.log(window.b);//220
function fn() {
console.log('fn.....');//'fn.....'
//没有return
}
console.log(window.fn());//undefined fn()没有返回任何数据所以是undefined
</script>
控制台打印结果:
<script>
// GO:内置的很多属性,和我们自己定义的全局变量和全局函数
// EC:EC给代码提供数据
var n = 110;
console.log(n); //110 找n,去ECG中找 ECG中有 VO说白了就是GO
console.log(window.n); //110 直接去GO中找,有,找到
var m = 220; //
console.log(m); //220 找m 去ECG中找 GO中有m 找到了220
// console.log(x); // 找x 去ECG中找,找不到,报错,x is not defined 报错不执行该行代码段报错开始以下代码
console.log(name);// (为空字符串 “”) name是Object内置属性之一
console.dir(window); //name是Object内置属性之一
</script>
控制台打印结果:
<script>
function fn(a) {
console.log(a); //100
}
fn(100);
console.log(a); //报错 引用错误 a is not defined
</script>
控制台打印结果:
↓↓ 执行上下文练习题 ↓↓
<script>
// 加var 的同名变量智慧提升第一个 后面的相当于赋值
var a = 1;
var a = 2;
var a = 3;
console.log(a);
// 经浏览器预解析 提升后
var a;
a = 1;
a = 2;
a = 3;
console.log(a); //3
</script>
<script>
var a = 1;
function a() {
console.log('a...');
}
console.log(a); //1
// 分析
// 浏览器预编译后:
// var a; //=und 函数整体提升,a变成函数a()
// 编译后执行:
// 对a执行赋值1
// a = 1;
</script>
↓此段代码解释函数声明优先级高于变量声明 ↓
<script>
// 在JS中函数的优先级高
function a() {
console.log('a...');
}
var a;
console.log(a); //function a() { console.log('a...')}
</script>
<script>
var a = 10;
var b = a;
// a、 b 各占一个内存空间
console.log(a, b); //10 10
b = 1;
console.log(a, b); //10 1
</script>
<script>
var a = [1, 2];
var b = a;
console.log(a, b); //[1, 2] [1, 2]
b = [3, 4];
console.log(a, b); //[1, 2] [3, 4]
</script>
<script>
var a = [1, 2];
var b = a;
console.log(a, b); // [1, 2] [1, 2]
b[0] =110;
console.log(a, b); // [110,2] [110,2]
</script>
<script>
var a = [1, 2];
var b = [1, 2];
//ab 栈区两个的地址不一样
console.log(a==b); // false
console.log(a===b); // false
</script>
<script>
var a = 110;
var b = 110;
//ab 栈区中的数值是一样的
console.log(a==b); // true
</script>
<script>
var a = [1, 2];
var b = a;
//ab 两个栈区的地址都一样
console.log(a == b); // true
console.log(a === b); // true
</script>
<script>
console.log(a, b); //a是und b是报错ReferenceErro引用错误 ,报错的最小单位是行 所以整体是报错
var a = b = 2;
</script>
<script>
var a = {
m: 666
}
var b = a; //把a地址赋值给b
b = { //将b的值指向新的堆
m: 888
}
console.log(a.m); //666
console.log(b.m); //888
</script>
<script>
var a = {
n: 12
}
var b = a;
b.n = 13; //通过b与a的栈区的地址相同,修改了堆区的共同指向的引用对象
console.log(a.n); //13
</script>
<script>
console.log(a); //报错 Uncaught ReferenceError: a is not defined...
a = 111;
</script>
<script>
var m = 1;
n = 2;
//m n都是 全局变量,都放在栈区 的ECG 的GO
console.log(window.m); //1
console.log(window.n); //2
</script>
<script>
function fn() {
var a = 111;
}
fn();
// a.b 找a对象 走的是 作用域链
// a.b 找b属性 走的是 原型链
console.log(a); //RefrenceErro:a is not defined
console.log(window.a); //und
</script>
<script>
var a = -1;
if (++a) {
// 执行一次++a ==> a的值是0 ,++a整体的值是0;++a整体用的是新值
// 执行一次a++ ==> a的值是0 ,a++整体的值是-1;a++整体用的是旧值
// 不管++在前还是在后,a都要加1
// 此处括号内++a 值为新值0
console.log('666');
} else {
console.log('888'); //888
}
</script>
<script>
console.log(a, b); //und und
// var 不管写在哪都要提升
if (true) {
var a = 1;
} else {
var b = 2;
}
console.log(a, b); // 1 und
</script>
<script>
var obj = {
name: 'wc',
age: 18
}
// in 是一个运算符, 判断一个属性是否是一个对象的属性
// 不管是私有属性,还是公有属性
console.log('name' in obj); //true
console.log('score' in obj); //false
</script>
<script>
var a;
console.log(a); //und
if ('a' in window) {
a = 110;
}
console.log(a); //110
</script>
<script>
console.log(a); //und
if ('a' in window) {
var a = 110;
}
console.log(a); //110
</script>
<script>
var a = 100;
function fn() {
console.log(a); //und
return
var a = 110; //预编译时已经提升到函数局部作用域最前面
}
fn();
</script>
<script>
var n = 100;
function foo() {
n = 200;
}
foo();
console.log(n); //200
</script>
<script>
function fn() {
var a = b = 100; //相当于 var a = 100 ; b = 100 ;
}
fn();
console.log(a); //报错 Uncaught ReferenceError: a is not defined 下面不执行
console.log(b); //报错
</script>
<script>
var n = 100;
function fn() {
console.log(n); //100
}
function gn() {
var n = 200;
console.log(n); //200
fn();
}
gn();
console.log(n); //100
<script>
var n = 100;
function fn() {
console.log(n); //100 EC(fn)中无 n ,通过作用域链去父级(声明fn位置的父级)寻找
}
function gn() {
var n = 200;
console.log(n); //200
fn();
}
gn();
console.log(n); //100
// 控制台:
// 200
// 100
// 100
</script>