1.
EC(Execution Context):函数执行环境(或执行上下文),执行上下文中存放的是当前函数所需要的一些信息(如:变量等)。
ECS(Execution Context Stack):执行环境栈。
每个函数执行时,js引擎都会为其创建一个EC,而这个EC就存放在ECS中。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>函数执行上下文</title>
</head>
<body>
<script>
// 代码执行前,就会立即创建一个全局的执行上下文Global Execution Context,并压栈(ECS)
function f1(){
console.log("f1:");
}
function f2(){
console.log("f2:");
f3(); // 调用函数f3
}
function f3(){
console.log("f3:");
f4(); // 调用函数f4
}
function f4(){
console.log("f4:");
}
f1(); // 代码进入执行f1函数,函数内的代码在执行前,js执行引擎立即创建一个f1的执行环境(f1 Execution Context),并压栈(ECS)
// f1函数执行完后,出栈
// f2函数执行前,创建f2 EC,并压栈(ECS)
f2(); // f2函数内部调用了f3(在f3函数执行前,创建f3 EC,并压栈(ECS)),f3函数内部调用了f4 (在f4函数执行前,创建f4 EC,并压栈(ECS))
// f4执行完,f4的EC出栈;f3执行完,f3的EC出栈;f2执行完,f2的EC出栈。
</script>
</body>
</html>
讲解:
2.执行上下文的生命周期
- 创建阶段:
- 创建变量对象
ps:VO和AO就是一回事,白话:VO全局的。 - 创建作用域链(Scope Chain)
- 创建变量对象
- 执行阶段:执行变量赋值、代码执行
- 回收阶段:执行上下文出栈等待虚拟机垃圾回收。
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>函数执行上下文的讲解</title>
</head>
<body>
<script>
// 变量声明
var a1 = 19,
a2 = 20,
a3 = 'sss',
b1 = {name: 'lili'};
// 函数调用
a1 = f1(a1, a2); // 函数为什么可以先调用后声明:因为整个代码在执行前,有一个全局EC被创建,其中有一步就是扫描声明的函数。
// f1函数执行完毕,出栈,垃圾回收
console.log(a1); // 打印a1的值
// 函数声明
function f1(a, b){
// f1函数的执行上下文的创建阶段:
// step1:扫描参数, a = 19, b = 20
// step2:扫描函数声明 f2 = function(){}
// step3:扫描变量声明 t = undefined, m = undefined,i = undefined
// 执行阶段:变量赋值,代码执行。
var t = 0,
m = 10;
for(var i = 0; i<a; i++){
console.log(i);
}
function f2(){
console.log("f2");
}
return a + b;
}
</script>
</body>
</html>
讲解:
ps:
- 创建阶段:this:(待后续)(全局的this 指向的是window)
- 创建时第一步扫描申明:先扫描函数,再扫描变量,变量都是先赋值为undefined。执行阶段才有变量赋值。
3.JavaScript作用域
1. JavaScript的解释和执行
2.函数变量的作用域
ps:
- 变量声明没有带var就是全局变量!!
- js只有函数作用域!
- 变量声明和函数声明,同时拥有一个名字时,函数优先级高。
(声明阶段函数优先级高,执行时先执行的第一个打印,输出为function;继续执行b为9,覆盖前面的,故第二次执行输出9)
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>变量的作用域</title>
</head>
<body>
<script>
// alt+shift+f 快速格式化我们的代码
var t = 9; // 全局作用域,在js中的任何地方都可以访问
function f1(){ // f1 函数全局作用域
var t2 = 10; // t2是f1函数内部的变量,只有f1内部可以访问。f1局部作用域
console.log(t);
function f2(){ // f2 f1函数作用域
var t3 = 200; // t3 只能在f2中访问。f2局部作用域
console.log(t2);
return t2 * t3; // f2函数可以访问f1函数的作用域的变量及f2自己内部的变量。
}
return f2(); // 调用f2,并返回f2的返回值
}
var m = f1(); // 调用f1
console.log(m);
</script>
</body>
</html>
3.变量提升(白话:遵守js作用域规则)
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>函数变量提升</title>
</head>
<body>
<script>
var a = 10; // 全局变量
// !js代码解释阶段:已然确定了作用域规则
// 所以js好的编程习惯:把函数内部所有的变量声明都放在函数的头部。
function f1(){
// 函数变量提升,因为在函数执行之前,先创建了函数的EC,
// 复习:在创建EC的时候已经把函数里面声明的变量都已经初始化为undefined (即:EC创建阶段的扫描变量声明!)
console.log(a); // undefined
var a = 19; // EC创建阶段,a为局部变量。给局部变量a赋值
console.log(a) // 局部变量a的值:19
}
f1();
console.log(a); // 全局变量啊的值:10
console.log("******");
function f2(){
console.log(a); // 10
a = 29; // EC创建阶段,a为全局变量。
console.log(a); // 29
}
f2();
console.log(a); // 29
</script>
</body>
</html>
4.作用域链
4.函数的四种调用模式与this
1.方法调用模式
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>this 方法调用模式</title>
</head>
<body>
<script>
// js的灵活性足见一般!
// 在ES6中有class关键字了。
// 此处是ES3,函数当做构造函数来使用。
function Dog(dogName){
this.name = dogName;
this.age = 0;
this.run = function(){
console.log(this.name + "is running...");
};
// 如果函数当做构造函数调用,并没有返回任何数据,默认就会返回 新对象(this)
}
var d = new Dog('gorg'); // !创建一个实例对象
d.run(); // 实例对象调用run方法
// 在方法调用模式中,方法内部的this指向当前调用者对象
// 故此处 this 指向d
</script>
</body>
</html>
2.构造器调用模式
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>构造器调用模式</title>
</head>
<body>
<script>
// 构造器调用模式
// 关键字:new
function Cat(){
// step1:创建一个空对象(新对象)
// step2:给函数上下文赋值 新对象 this = 新对象
this.age = 19;
this.name = "cat"; // 在构造函数内部定义的this的所有属性,都会返回给新对象
this.run = function(){
console.log(this.name + "is running...");
}
// 如果构造函数没有返回值,那么就返回this(新对象)
// return 3; // 如果返回值类型为简单类型,那么会被忽略!
return { // 注意 别用!
name: "aliex",
run: function(){
console.log("aliex is singing!");
}
}; // 但如果返回一个对象,即一个引用类型,那么就返回这个引用类型,(注意!即this的相关被覆盖了,故下面cat的输出为"aliex is singing!",但是cat的age输出还是20,被覆盖的只有name和run。)请避免使用这种方式!
}
var cat = new Cat(); // 构造函数调用模式
// 如果使用new关键字+构造函数,就触发了构造函数执行模式
cat.age = 20;
cat.name = "serty";
cat.run(); // 方法调用模式
// console.log(cat.age);
</script>
</body>
</html>
3.函数调用模式
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>函数调用模式</title>
</head>
<body>
<script>
function f(a, b){
console.log(a + " "+ b);
this.a = 19; // 这里的this指向window
console.log('a' in window); // 故这里输出为true
console.log(this); // 这里输出window 但如果是严格模式:undefined
}
f(1, 2); // 直接调用函数:f():函数调用模式
//小记:
console.log("********");
function Dog(){
this.age = 1;
console.log(this);
}
Dog(); // 函数调用模式,this为window
var d = new Dog(); // 构造器调用模式, this 为d
</script>
</body>
</html>
4.apply/call调用模式(借用方法模式)
示例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>apply/call调用模式</title>
</head>
<body>
<script>
function sum(a, b){
console.log(this);
console.log(arguments);
return a + b;
}
sum(1, 2) // 函数调用模式, this 为window
console.log("*****");
// call调用模式
var t = {
name: "lili"
} ;
// f.call(fContext, p1, p2, ....)
var m = sum.call(t, 2, 3) // 第一个参数t相当于给了sum内部的this, 2和3 分别给了a和b。注意:除了第一个参数,后面的参数都是放到arguments里
console.log(m);
console.log("*****");
// apply调用模式
var m2 = sum.apply(t, [2, 3]); // apply的第二个参数,是传的一个数组
console.log(m2);
console.log("*****");
// 注意:如果传递的参数是简单类型,那么
// 如果传递的第一个参数是:null、undefined 则转为window
// 如果是number、string、boolean 则转为包装类型
sum.call(null, 3, 5);
console.log("*****");
sum.call(undefined, 3, 5);
console.log("*****");
sum.call(3, 3, 5);
</script>
</body>
</html>
运行结果: