1.代码如何执行
1. 执行代码2个阶段
- 编译:把当前定义的代码移到前面(代码提升),并赋予undefine,把剩下的赋值与执行调用代码做 可执行代码,这些信息保存在执行上下文。
- 执行:开始一行一行运行可执行代码。
例子
foo() // 正常打印 xxxxx
console.log(name) //当执行到这一行 打印的是 undefined
var name = 'jason'
function foo() {
console.log('xxxxx');
}
// 等价于 拆成 = 声明代码 + 执行时代码
// 1. 编译
var name = undefined
function foo() {
console.log('xxxxx');
}
// 2. 执行代码
foo()
name = 'jason'
1.编译
声明提前(变量提升Hoisting)
- 把函数定义 和 变量的定义提到代码最前面
执行上下文(Execution context)
js是动态执行的语言,所以物理代码是不会发生变化,而是使用一个运行时的执行上下文记录和运行 声明提前的变量存储,以及编译后的可执行代码。
包含3部分: 变量环境,词法环境,可执行代码
- 变量环境
- 1.把声明的内容提前保存到变量环境
- 2.变量提升hosting:出现一样的变量与函数的声明,后面覆盖前面
- 词法环境
- 1.存放const 和 let定义的变量
2.可执行代码
- 一行行执行,主要进行赋值操作
- 遇到函数,会进入函数,并再进行编译和执行代码,创建一个新的执行上下文
例子
1.函数表达式
var b = function foo() {console.log('xxxxx');}
这是一个表达式,var b 是定义,b = function foo() {console.log('xxxxx');} 是赋值。
b() //
var b = function foo() {
console.log('xxxxx');
}
// 等价于
//1. 声明提前
var b = undefined
//2. 执行代码
b() //这里会报错,找不到该方法
b = function foo() {
console.log('xxxxx');
}
提示:
// Uncaught TypeError: b is not a function at <anonymous>:1:1
2.相同方法定义时
function foo() {
console.log('xxxxx');
}
foo() //最终打印 yyyy 后面覆盖前面定义
function foo() {
console.log('yyyy');
}
foo() //最终打印 yyyy 后面覆盖前面定义
// 等价于
// 1.编译
function foo() { // 重复定义 后面直接覆盖前面
console.log('yyyy');
}
// 2.执行
foo() // 打印 yyyy
foo() // 打印 yyyy
3.相同变量定义时
相同变量定义都一样,都是undefined,没有覆盖的说法,只声明一次。
console.log(x) //打印undefined
var x = 100
var x = 200
console.log(x)
// 等价于
// 1.编译
var x = undefined
// 2.执行
console.log(x) //undefined
x = 100;
x = 200;
console.log(x) //200
4. 变量与函数同名
最终都以后面定义的覆盖前面
foo()
var foo = function() {
console.log('xxx')
}
function foo() {
console.log('yyy')
}
//等价于
// 1.编译
var foo = undefined
function foo() { // foo 把上面var foo = undefined给覆盖了
console.log('yyy')
}
// 2.执行
foo() // 输出yyy
2.调用栈
3种执行上下文
- 全局
- 函数
- eval
全局
js执行全局代码时,会编译全局代码,创建全局执行上下文与可执行代码,整个页面的生存周期内,全局执行上下文只有一份。
函数
调用一个函数时,会进行编译,创建函数执行上下文与可执行代码,当函数结束后,函数执行上下文与可执行代码会被销毁。
eval
动态执行与动态销毁
调用栈
函数调用
使用函数名+() 进行调用 会自动创建 执行上下文 + 可执行代码
栈结构
- 行为:入栈与出栈
- 连续空间
- 如果递归调用不结束,会栈溢出
- 后进先出
JavaScript 的调用栈
js引擎会将执行上下文压入栈中,管理执行上下文的栈称为执行上下文栈,又称调用栈。
例子
var g1 = 'g1'
main()
function main(){
var m1 = 'm1'
a(m1)
}
function a(para) {
var a1 = 'a1'
console.log("aaa",para)
}
其他
console.trace()调试
栈溢出
当递归没有终止的条件,会出现栈溢出
function test(a,b){
return test(a,b)
}
console.log(test(1,2))
//提示
Uncaught RangeError: Maximum call stack size exceeded
at test (<anonymous>:2:5)
尾递归调用
当函数出现递归时,如果次数过多,效率会十分低,并且栈有可能被挤爆。
所以编译器会针对return 的内容是方法调用,而不是表达式时候,就不压栈,直接调用完就销毁方法。
例子1 , 求1加到5
1+2+3+4+5=15
function sum(n) {
if (n <= 1) return n;
return n + sum(n-1);
}
sum(5) // 1+2+3+4+5=15
//尾递归实现
function sum2(n,total = 0) {
console.log("n",n," tatal",total)
if (n == 0) return total;
return sum2(n-1,n+total);//
}
//打印的结果
// n 5 tatal 0
// n 4 tatal 5
// n 3 tatal 9
// n 2 tatal 12
// n 1 tatal 14
// n 0 tatal 15
例子2 斐波那契数列
1,1,2,3,5,8,13,21,34,55,89
前面两个之和等于第三个数
//转化成的逻辑
F(4) = F(3) + F(2)
= [F(2) + F(1)] + [F(1) + F(1)]
= [[F(1) + F(1)] + F(1)] + [F(1) + F(1)]
= 5
实现的代码
function mysum(n) {
if (n == 1 || n == 0) {
return 1
}
return mysum(n-1) + mysum(n-2)
}
mysum(4) //从4开始
入栈& 出栈
上面的代码由于是嵌套递归,所有复杂度是 O(2^n) 指数阶
会依次把所有递归的方法压入栈,然后依次出栈,返回。
尾代码优化
把return 的内容改成方法调用,每次压入栈,都会立刻出栈。每次方法的空间都是立即释放销毁。
所以我们要做的是把每一次计算的结果保留下来,并传递给下个执行的方法。
'use strict'
/**
*
* @param {*} n 代表到第几位结束
* @param {0} total1 代表每次循环的合计结果 包含n-1
* @param {1} total2 代表每次循环的合计结果 包含n-1 + n-2
* @returns
*/
function mysum(n, total1 = 0, total2 = 1) {//
console.log(`n:${n} total1:${total1} total2:${total2} `)
if ( n == 0) {
return total2
}
return mysum(n-1,total2,total1+total2)
}
注意:只有在es6开启严格模式才生效
3.作用域
作用域:变量可以被访问的范围
1.词法作用域
词法作用域:代码阶段就决定好的,和函数是怎么调用的没有关系
例子
var name = 'global'
main()
function main(){
var name = 'main'
a()
}
function a() {
console.log(name) //这打印的是 全局的 global ,而不是main函数里面的 main
}
let name = 'global'
function a() {
let name = 'a'
function b(){
let name = 'b'
function c(){
let name = 'c'
console.log(name) //这里会依次查找 c -> b -> a -> 全局
}
}
}
//输出 c
//当没有定义 let name = 'c' name 打印的是 b
2.动态作用域
同样的例子,如果是动态作用域时,是实时访问所在的运行嵌套关系访问作用域
例子
var name = 'global'
main()
function main(){
var name = 'main'
a()
}
function a() {
console.log(name) //如果是动态作用域的语言,这是打印的是 main
}
2.作用域链
- 执行上下文中包含 outer引用,指向上一级的指向上下文
- 访问顺序先访问当前执行上下文的变量,没有再去outer 上一级上下文的变量中找
- outer的指向基于词法作用域,即代码定义时的位置已经决定了outer的指向
访问顺序
当前执行上下文中的 词法环境 -> 变量环境 -> outer -> 词法环境 -> 变量环境
例子
function b() {
var name = "b"
let num2 = 100
if (1) {
let name = "bb"
console.log(num) //这里在 b上下文找不到,会去全局找到 1
}
}
function a() {
var name = "a"
let num = 2
{
let num = 3 // 这里使用的是栈结构,num=3 是最后压入,所以最先取出
b()
console.log(num) // 这里在a 上下文找到num,直接打印 3
}
}
var name = "global"
let num = 1
a()
js中的块级作用域
- js没有块级作用域的问题
- 变量提升的缺点
- 块里面的代码变量提升到函数顶部或全局,导致变量污染 和 变量覆盖
例子
//场景1
for (var i =0; i < 10; i++)
//场景2
var a = 10 ;
function fun(){
console.log(a)
if(false) {
var a = 100
}
}
- 这里a访问了的是变量提升后的100,而不是全局的10
使用const 和let 实现块级作用域
- 普通变量存储在变量环境
- 块级作用域的变量 存储在词法环境,压入以栈的形式压入
- 访问变量的顺序:
- 1.先访问词法环境,从栈顶到栈底,
- 2.再访问变量环境
//1.先访问词法环境,从栈顶到栈底,
//2.再访问变量环境
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()
//词法环境依次压入 b = 2 , b =3, d = 5,访问的时候从栈顶开始,所以括号内的 b取得是3
编程语言的好坏
- 没有好坏之分,语言只是开发实现功能的工具,是否有完善的框架和社区,好的生态,非常多的应用,以及可以给开发实实在在的收益
- js借鉴了 java的虚拟机,新引入了协程,迭代器,作用域,一直往好的方向发展
4.闭包
内部函数引用外部函数的变量,则形成闭包。外部函数已经执行结束,执行上下文已经销毁,但是由于内部函数已经引用者,所以外部函数的变量不会被销毁。
function myFun() {
var name = "jason"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){ // 在词法作用中,他的outer是myFun函数
console.log(test1)
return name
},
setName:function(newName){// 在词法作用中,他的outer是myFun函数
name = newName
}
}
return innerBar
}
var myFunObj = myFun() //这里返回的是一个对象,同时 myFun执行完应该被销毁,包括执行上下文
myFunObj.setName("jason2") // 这里setName方法里面还引用着外部的name变量,name变量形成了闭包
myFunObj.getName() // 这里getName 方法里面还引用着外部的test1变量,test1 变量形成闭包
console.log(myFunObj.getName()) // 再次调用 访问的依然是统一闭包变量。
//最终 name 和 test1 会被 closeure(myFun) 闭包集合保留下来
//当调用 setName方法,调用栈 栈顶 setName, closeure(myFun) , 全局,
//所以查找变量的时候,依次查询顺序 setName -> closeure(myFun) ,找到变量
var myFunObj = myFun()
系统会创建一个集合来保存这些closure(fun),内容都存放在堆里
myFunObj.setName("jason2")
当调用引用闭包的函数时,该函数的执行上下文已经 引用着closure(fun)的集合。作用域访问依次顺序是 Local–>Closure(myFun)–>Global
myFunObj.getName() 个setName一样,中间共用Closure(myFun)
- 垃圾回收问题
- 如果引用的是局部变量,则会被垃圾回收
- 如果是全局变量将会永久保存
使用闭包的准则:如果该闭包会一直使用,那么它可以作为全局变量而存在;但如果使用频率不高,而且占用内存又比较大的话,那就尽量让它成为一个局部变量**。
5.this
包含在执行上下文中
- 全局执行上下文
- 指向window
- 函数中的执行上下文
- 默认指向window
- 当使用对象调用函数
- js会隐式调用 call方法改变this指向 (js框架自动处理了)
- 主要调用call方法 设置this
- eval执行上下文
//主动调用call ,传入this的对象
let obj = {
name : "jason",
}
function myfun(){
this.name = "sinky"
}
myfun.call(obj) //传入对象
console.log(obj.name) // 打印 sinky
//对象调用方法改变this
let obj2 = {
name : "jason",
myfun : function(){
this.name = "sinky"
}
}
obj2.myfun() // 这里等价于 隐式调用了 myfun.call(obj2)
console.log(obj2.name) // 打印 sinky
特殊情况, 赋给全局对象var myfun1,等同于window调用
//
var myObj = {
name : "jason",
myfun: function(){
this.name = "sinky" //这里修改的是 window.name ,不是当前对象
console.log(this) //这里打印的是window
}
}
var myfun1 = myObj.myfun //这里的var myfun1 等价于 window.myfun1
myfun1() //等价于 window.myfun1() 所以等同于: myfun1.call(window)
例子
var obj = {
name:"jason",
printName: function () {
console.log(name) //这里读取的是 outer 的 name,和对象里定义的 name: "jason" 没有半毛钱关系
}
}
function myFun() {
let name = "myFun"
return obj.printName
}
let name = "global"
let printNameFun = myFun() //这里拿到的是方法
printNameFun() //调用方法,最终还是调用 obj.printName,printName 函数的词法环境outer指向全局,访问name是global
obj.printName() // 和上面同理访问name是global
- 在全局环境中调用一个函数,函数内部的 this 指向的是全局变量 window。
- 通过一个对象来调用其内部的一个方法,该方法的执行上下文中的 this 指向对象本身。
构造函数中的this
function CreateObj(){
this.name = "jason"
}
var myObj = new CreateObj()
console.log(myObj.name) //打印jason
// 原理:
var myObj = new CreateObj()
//等价于
var myObj = {}
CreateObj.call(myObj) //由于系统会自动加入设置的这一行
this.name = "jason" //当执行这一行中的this 变成了上面的 myObj = {}这个对象 ,myObj就设置了 name属性。
return myObj
this的缺陷
嵌套中的this,不会继承与外层
var myObj = {
name : "jason",
showThis: function(){
console.log(this) //这里打印{ name : "jason" }是因为 myObj.showThis() 被系统转化为 myObj.showThis.call(myObj)
function bar(){
console.log(this) // 这里的this 访问的是全局的this
}
bar()
}
}
myObj.showThis()
//打印this是window
解决方案
1. 使用 self = this,临时存储,变量会根据作用域链访问到 self
var myObj = {
name : "jason",
showThis: function(){
console.log(this) //这里打印{ name : "jason" }是因为 myObj.showThis() 被系统转化为 myObj.showThis.call(myObj)
let self = this
function bar(){
console.log(self) // 这里的this 访问的是全局的this
}
bar()
}
}
myObj.showThis() //{name: 'jason', showThis: ƒ}
2. 使用箭头函数,因为箭头函数不会产生执行上下文
箭头函数没有自己的执行上下文,所以它会继承调用函数中的 this。
var myObj = {
name : "jason",
showThis: function(){
console.log(this) //这里打印{ name : "jason" ... }是因为 myObj.showThis() 被系统转化为 myObj.showThis.call(myObj)
bar = () => {
console.log(this) // 这里打印{ name : "jason" ... }
}
bar()
}
}
myObj.showThis()
3.普通函数的中的this 指向window
可以通过严格模式,this只会指向undefined,从而限制this指向window