07 | 变量提升:JavaScript代码是按顺序执行的吗?
变量提升(Hoisting)
变量的声明和赋值
var myName = '极客时间'
var myName = undefined; // 声明
myName = '极客时间' // 赋值
函数的声明和赋值
function foo(){
console.log('foo');
}
function foo = function(){
console.log('foo');
}
----------------------------------
var bar = function(){
console.log('bar');
}
var bar = undefined;
bar = function(){
console.log('bar');
}
所谓的变量提升,是指在 JavaScript 代码执行过程中,JavaScript 引擎把变量的声明部分和函数的声明部分提升到代码开头的“行为”。变量被提升后,会给变量设置默认值,这个默认值就是我们熟悉的 undefined
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数showName被执行');
}
如果函数用关键字 function声明的,直接提升到顶部,如果函数用变量形式声明,则提升的是变量
// 函数的声明提升
sayHello() // 打印hello
function sayHello(){
console.log('hello');
}
sayNoFn() // 报错(sayNoFn is not a function) sayNoFn 此时是undefined
var sayNoFn = function(){
console.log('no');
}
JavaScript 代码的执行流程
先编译,再执行
showName()
console.log(myname)
var myname = '极客时间'
function showName() {
console.log('函数showName被执行');
}
1. 编译阶段
- 变量提升部分的代码
var myName = undefined;
function showName(){
console.log('函数showName被执行');
}
VariableEnvironment: // 初始变量环境
myName -> undefined
showName -> function: console.log('函数showName被执行');
2. 执行阶段
执行部分的代码
showName(); // 变量环境中有showName的应用, 执行打印 '函数showName被执行'
console.log(myName); // 变量环境中有myName, 执行打印为 undefined
myName = '极客时间' // 查找变量环境中的myName, 赋值为‘极客时间’
VariableEnvironment: // 执行后变量环境
myName -> '极客时间'
showName -> function: console.log('函数showName被执行');
总结
JS代码在执行的过程中,需要先做变量提升,之所以需要变量提升,是因为JS代码在执行之前需要先编译
- 在 编译 阶段,变量和函数会被存放在变量环境中,变量的默认值会被设置为undefined,在代码的执行阶段,JS引擎会从变量环境中去找自定义的变量和函数
- 如果在编译阶段,存在两个完全相同的函数、变量,那么最终存放在变量环境中的是最后定义的那个(覆盖之前的)
思考时间
showName()
var showName = function() {
console.log(2)
}
function showName() {
console.log(1)
}
- 编译
关于同名变量和函数的两点处理原则: 1:如果是同名的 函数 / 变量,JavaScript编译阶段会选择最后声明的那个。 2:如果变量和函数同名,那么在编译阶段,变量的声明会被忽略
VariableEnvironment
showName -> function: console.log(1)
执行
showName() // 此时变量环境中只有 showName -> function: console.log(1) 打印1
执行到第二行,赋值,此时变量环境为:
VariableEnvironment
showName -> function: console.log(2)
如果在下面再有函数调用showName,因为 showName函数的值被覆盖,此时打印 2
函数和变量同名,变量声明被忽略
- 当执行到赋值语句时,变量被重新赋值
showName() // 1
console.log('showName', showName); // showName ƒ showName() {console.log(1)}
var showName = function() {
console.log(2)
}
function showName() {
console.log(1)
}
showName() // 2
08 | 调用栈:为什么JavaScript代码会出现栈溢出?
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)
- 第一步,创建全局上下文,并将其压入栈底
- 第二步是调用 addAll 函数
- 第三步,当执行到 add 函数
- add 函数返回,该函数的执行上下文弹出
总结
调用栈
- 每调用一个函数,JavaScript 引擎会为其创建执行上下文,并把该执行上下文压入调用栈,然后 JavaScript 引擎开始执行函数代码。
- 如果在一个函数 A 中调用了另外一个函数 B,那么 JavaScript 引擎会为 B 函数创建执行上下文,并将 B 函数的执行上下文压入栈顶。
- 当前函数执行完毕后,JavaScript 引擎会将该函数的执行上下文弹出栈。当分配的调用栈空间被占满时,会引发“堆栈溢出”问题
执行上下文
改变代码的模式,避免使用递归
使用定时器的方法来吧当前的任务拆分为很多小任务
function runStack (n) {
if (n === 0) return 100;
sum +=n;
return setTimeout(()=>{
runStack( n- 2);
},0);
}
runStack(50000)
09 | 块级作用域:var缺陷以及为什么要引入let和const?
块级作用域就是通过词法环境的栈结构来实现的,而变量提升是通过变量环境来实现,通过这两者的结合,JS 支持块级作用域和变量提升同时存在
作用域scope
概念
作用域是指程序中变量定义的区域,它决定了变量和函数的生命周期,通俗来讲,作用域就是变量和函数的可访问范围,它决定了变量和函数的可见性和生命周期
分类
全局作用域:在代码的任何地方都可以访问,声明周期伴随着页面的生命周期
- 函数作用域:在函数内定义的变量或函数,只能在函数内访问,函数执行结束后会被销毁
- 块级作用域:在一个块级(函数、判断语句、循环语句、{})定义的变量或函数,只能在块内访问
变量提升所带来的问题
变量容易在不被察觉的情况下被覆盖掉
var myname = "极客时间"
function showName(){
console.log(myname); // undefined
if(0){
var myname = "极客邦"
}
console.log(myname); // "极客邦"
}
showName()
本应销毁的变量没有被销毁
function foo(){
for (var i = 0; i < 7; i++) {
}
console.log(i);
}
foo()
ES6 是如何解决变量提升带来的缺陷
JavaScript 是如何支持块级作用域的
通过变量环境实现变量提升,通过词法环境的栈结构实现块级作用域,通过这两者的结合,JavaScript 引擎也就同时支持了变量提升和块级作用域了
variableEnvironment
a --> 1
function foo(){
var a = 1
let b = 2
{
let b = 3
var c = 4
let d = 5
console.log(a) // 1
console.log(b) // 3
}
console.log(b) // 2
console.log(c) // 4
console.log(d) // d is not defined
}
foo()
- 第一步是编译并创建执行上下文(块内通过let 声明的变量并没有存放到词法环境中)
- 第二步继续执行代码(当进入函数的作用域块时,作用域块中通过 let 声明的变量,会被存放在词法环境的一个单独的区域中,这个区域中的变量并不影响作用域块外面的变量)
- 执行语句时,沿着词法环境的栈顶向下查询,如果在词法环境中的某个块中查找到了,就直接返回给 JavaScript 引擎,如果没有查找到,那么继续在变量环境中查找。
- 当作用域块执行结束之后,其内部定义的变量就会从词法环境的栈顶弹出,最终执行上下文如下图所示
思考
let myname= '极客时间'
{
console.log(myname) //Uncaught ReferenceError Cannot access 'myname' before initialization
let myname= '极客邦'
}
最终打印结果
VM6277:3 Uncaught ReferenceError: Cannot access ‘myname’ before initialization
分析原因
在块作用域内,let声明的变量被提升,但变量只是创建被提升,初始化并没有被提升,在初始化之前使用变量,就会形成一个暂时性死区。
拓展
var的创建和初始化被提升,赋值不会被提升。
let的创建被提升,初始化和赋值不会被提升。
function的创建、初始化和赋值均会被提升。
10 | 作用域链和闭包 :代码中出现相同的变量,JavaScript引擎是如何选择的?
function bar() {
console.log(myName)
}
function foo() {
var myName = "极客邦"
bar()
}
var myName = "极客时间"
foo()
分析
全局执行上下文:
变量环境
- bar: undefined
- foo: undefined
- myName: ‘极客时间’
词法环境:无
执行代码 foo() — foo 函数执行上下文:
变量环境:
- myName: ‘极客邦’
- outer —> 全局
词法环境:无
执行 bar() — bar 函数执行上下文:
变量环境
outer —> 全局
词法环境: 无
执行代码 console.log(myName)
在bar执行上下文查找 myName(无) —> 全局myName === 极客时间
作用域链
在每个执行上下文的变量环境中,都包含了一个外部引用,用来指向外部的执行上下文,我们把这个外部引用称为 outer,当一段代码使用了一个变量时,JavaScript 引擎首先会在“当前的执行上下文”中查找该变量,如果在“当前执行上下文”没有找到,就继续向外层执行上下文查找,一直到全局作用域,如果仍然没有找到,则宣布找不到,返回undefined, 我们把这个查找的链路成为“作用域链”
词法作用域
词法作用域就是指作用域是由代码中函数声明的位置来决定的,所以词法作用域是静态的作用域,通过它就能够预测代码在执行过程中如何查找标识符, 词法作用域是代码编译阶段就决定好的,和函数是怎么调用的没有关系(根据代码的位置来决定的)。
决定了函数如何按照作用域链查找变量(与函数怎么被调用没有关系,只跟代码的位置有关系)
如果一个函数在全局作用域,那如果再函数找不到变量的时候,就去全局找(即使他可能被其他函数在内部调用)
**
通过词法作用域,我们分析了在 JavaScript 的执行过程中,作用域链是已经注定好了,比如即使在 foo 函数中调用了 bar 函数,你也无法在 bar 函数中直接使用 foo 函数中的变量信息 ==> 看代码结构
foo 函数作用域—>bar 函数作用域—>main 函数作用域—> 全局作用域
块级作用域中的变量查找
function bar() {
var myName = "极客世界"
let test1 = 100
if (1) {
let myName = "Chrome浏览器"
console.log(test)
}
}
function foo() {
var myName = "极客邦"
let test = 2
{
let test = 3
bar()
}
}
var myName = "极客时间"
let myAge = 10
let test = 1
foo()
全局执行上下文
变量环境:
myName: “极客时间”
foo: ()
outer: null
词法环境
myAge: 10
test: 1
foo 执行上下文
变量环境:
myName: ‘极客帮’
outer: 全局
词法环境test: 2
test: 3
bar 执行上下文
变量环境
myName: ‘极客世界’
outer: 全局
词法环境
test1: 100
myName: Chrome浏览器
执行 console.log(test) // 1
闭包
在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。比如外部函数是 foo,那么这些变量的集合就称为 foo 函数的闭包。
function foo() {
var myName = "极客时间"
let test1 = 1
const test2 = 2
var innerBar = {
getName:function(){
console.log(test1)
return myName
},
setName:function(newName){
myName = newName
}
}
return innerBar
}
var bar = foo()
bar.setName("极客邦")
bar.getName()
console.log(bar.getName())
分析
全局执行上下文:
变量环境
bar: undefined
outer: null
词法环境
无
foo函数执行上下文:
变量环境:
myName: ‘极客时间’
innerBar: function(){}
outer:
词法环境**
test1: 1
test2: 2
执行 var bar = foo() 时,foo()函数执行,返回 innerBar
执行 bar.setName(“极客邦”), setName 函数按照作用域链查找myName, setName —> foo闭包 —> 全局
bar.getName(), 打印test1 // 1 作用域链:getName —> foo闭包 —> 全局
console.log(bar.getName()) // 1 极客邦 作用域链:getName —> foo闭包 —> 全局
闭包是怎么回收的
- 引用闭包的是全局变量:一直存在,直到页面关闭,可能导致内存泄漏
- 引用闭包的是局部变量:函数销毁后,会被垃圾回收,释放内存
思考时间
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()
分析:
全局执行上下文:
变量环境:
bar:{
myName:”time.geekbang.com”,
printName: function () {
console.log(myName)
}
}
outer:null
词法环境:
myName: ‘极客邦’
_printName:foo
foo 执行上下文:
变量环境:
outer:window
词法环境:
myName: ‘极客时间’
查找 return bar.printName 查找 bar: foo 词法 —> foo 变量 —> outer 词法 —>
outer 变量找到 bar.printName
bar.printName执行上下文
变量环境:
无
outer: window
词法环境:
无
执行 console.log(myName) —> bar.printName 词法 —> bar.printName 变量 —>
全局词法找到myName: ‘极客邦’
执行bar.printName()
bar.printName执行上下文
变量环境:
无
outer: window
词法环境:
无
执行 console.log(myName) —> bar.printName 词法 —> bar.printName 变量 —>
全局词法找到myName: ‘极客邦’
11 | this:从JavaScript执行上下文的视角讲清楚this
JavaScript 中的 this 是什么
概念
this 是和上下文绑定的,每个执行上下文都有对应的this
分类
全局执行上下文中的this
- 全局执行上下文中的this总是指向window
函数执行上下文中的this
调用一个函数,默认this也是指向windowfunction foo(){
console.log(this) // window
}
foo()
可以通过以下方式改变this的指向
通过函数的 call 方法设置
let bar = {
myName : "极客邦",
test1 : 1
}
function foo(){
this.myName = "极客时间"
}
foo.call(bar) // 将foo函数中的this更改为bar
console.log(bar) // {myName : "极客时间",test1 : 1}
console.log(myName) // myName is not defined
apply
let bar = {
myName : "极客邦",
test1 : 1
}
function foo(){
this.myName = "极客时间"
}
foo.apply(bar) // 将foo函数中的this更改为bar
console.log(bar) // {myName : "极客时间",test1 : 1}
console.log(myName) // myName is not defined
bind
let bar = {
myName : "极客邦",
test1 : 1
}
function foo(){
this.myName = "极客时间"
}
foo.bind(bar)(); // 将foo函数中的this更改为bar
console.log(bar) // {myName : "极客时间",test1 : 1}
console.log(myName) // myName is not defined
通过对象调用方法设置(使用对象来调用其内部的一个方法,该方法的 this 是指向对象本身的)
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
}
}
myObj.showThis() // ==> myObj.showThis().call(myObj)
通过构造函数中设置
function CreateObj(){
this.name = "极客时间"
}
var myObj = new CreateObj()
eval执行上下文中的this
this 的设计缺陷以及应对方案
嵌套函数中的 this 不会从外层函数中继承
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this) // this --> myObj
function bar(){console.log(this)} // this --> window
bar()
}
}
myObj.showThis()
函数 bar 中的 this 指向的是全局 window 对象,而函数 showThis 中的 this 指向的是 myObj 对象
解决方案:
把this体系转化为变量作用域体系 - 声明一个变量 self 用来保存 this,然后在 bar 函数中使用 self
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
var self = this // 保存this
function bar(){
self.name = "极客邦"
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
使用 ES6 中的箭头函数(ES6 中的箭头函数并不会创建其自身的执行上下文,所以箭头函数中的 this 取决于它的外部函数)
var myObj = {
name : "极客时间",
showThis: function(){
console.log(this)
var bar = ()=>{
this.name = "极客邦"
console.log(this)
}
bar()
}
}
myObj.showThis()
console.log(myObj.name)
console.log(window.name)
- 普通函数中的 this 默认指向全局对象 window
当函数被正常调用时
- 在严格模式下,this 值是 undefined
- 非严格模式下 this 指向的是全局对象 window
思考题
改变如下代码,使this指向正确的位置
let userInfo = {
name:"jack.ma",
age:13,
sex:male,
updateInfo:function(){
//模拟xmlhttprequest请求延时
setTimeout(function(){
this.name = "pony.ma"
this.age = 39
this.sex = female
},100)
}
}
userInfo.updateInfo()
- 箭头函数
- 存储this
- call
- bind
- apply
call 、apply、bind的对比(https://juejin.cn/post/6844903567967387656)
共同目标:改变函数执行上下文,也就是改变this的指向
let obj1 = {
name: 'wuyanbin',
sayName: function(){
console.log(this.name);
}
}
let obj2 = {
name: 'jiale'
}
obj1.sayName(); // wuyanbin
obj1.sayName.call(obj2); // jiele
obj1.sayName.apply(obj2); // jiale
let bind = obj1.sayName.bind(obj2);
bind(); // jiale
bind
执行时机:事先把fn的this改变为我们要想要的结果,并且把对应的参数值准备好,以后要用到了,直接的执行即可
apply
执行时机:马上执行改变this后的结果
参数:第一个是改变上下文的对象,第二个是以参数列表的形式传入
call
执行时机:马上执行改变this后的结果
- 参数:第一个是改变上下文的对象,第二个是以数组的形式传入
应用场景
- 转化伪数组
Array.prototype.slice.call(obj)
let obj4 = {
0: 1,
1: 'thomas',
2: 13,
length: 3 // 一定要有length属性
};
console.log(Array.prototype.slice.call(obj4)); // [1, "thomas", 13]
- 判断变量类型
Object.prototype.toString.call('变量')
let arr1 = [1,2,3];
let str1 = 'string';
let obj1 = {name: 'thomas'};
//
function isArray(obj) {
return Object.prototype.toString.call(obj) === '[object Array]';
}
console.log(fn1(arr1)); // true
// 判断类型的方式,这个最常用语判断array和object,null(因为typeof null等于object)
console.log(Object.prototype.toString.call(arr1)); // [object Array]
console.log(Object.prototype.toString.call(str1)); // [object String]
console.log(Object.prototype.toString.call(obj1)); // [object Object]
console.log(Object.prototype.toString.call(null)); // [object Null]
- 数组拼接,添加 ``` let arr1 = [1,2,3]; let arr2 = [4,5,6];
//数组的concat方法:返回一个新的数组 let arr3 = arr1.concat(arr2); console.log(arr3); // [1, 2, 3, 4, 5, 6]
console.log(arr1); // [1, 2, 3] 不变 console.log(arr2); // [4, 5, 6] 不变 // 用 apply方法 [].push.apply(arr1,arr2); // 给arr1添加arr2 console.log(arr1); // [1, 2, 3, 4, 5, 6] console.log(arr2); // 不变
- 使用call和apply做继承
function Animal(name){
this.name = name;
this.showName = function(){
console.log(this.name);
}
}
function Cat(name){
Animal.call(this, name);
}
// Animal.call(this) 的意思就是使用this对象代替Animal对象,那么
// Cat中不就有Animal的所有属性和方法了吗,Cat对象就能够直接调用Animal的方法以及属性了
var cat = new Cat(“TONY”);
cat.showName(); //TONY
- 多继承
function Class1(a,b) {
this.showclass1 = function(a,b) {
console.log(class1: ${a},${b}
);
}
}
function Class2(a,b) {
this.showclass2 = function(a,b) {
console.log(class2: ${a},${b}
);
}
}
function Class3(a,b,c) { Class1.call(this); Class2.call(this); }
let arr10 = [2,2]; let demo = new Class3(); demo.showclass1.call(this,1); // class1: 1,undefined demo.showclass1.call(this,1,2); // class1: 1,2 demo.showclass2.apply(this,arr10); // class2: 2,2
> 手写bind、apply、call
<a name="cjuNe"></a>
### 普通函数和箭头函数
[https://zhuanlan.zhihu.com/p/106675713](https://zhuanlan.zhihu.com/p/106675713)<br />**(1)箭头函数是匿名函数,不能作为构造函数,不能使用new。**<br />**(2)箭头函数不绑定arguments,取而代之用rest参数...解决**<br />**(3)this的作用域不同,箭头函数不绑定this,会捕获其所在的上下文的this值,作为自己的this值。**<br />**(4)箭头函数没有原型属性**<br />**(5)箭头函数不能当做Generator函数,不能使用yield关键字**
<a name="ZPF74"></a>
### ***new 的过程
[https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Operators/new)**`
**`new`** 关键字会进行如下的操作:
1. 创建一个空的简单JavaScript对象(即`**{}**`);
2. 链接该对象(设置该对象的**constructor**)到另一个对象 ;
3. 将步骤1新创建的对象作为`**this**`的上下文 ;
4. 如果该函数没有返回对象,则返回`**this**`。
当执行 new CreateObj() 的时候,JavaScript 引擎做了如下四件事:
- 首先创建了一个空对象 tempObj;
- 接着调用 CreateObj.call 方法,并将 tempObj 作为 call 方法的参数,这样当 CreateObj 的执行上下文创建时,它的 this 就指向了 tempObj 对象;
- 然后执行 CreateObj 函数,此时的 CreateObj 函数执行上下文中的 this 指向了 tempObj 对象;
- 最后返回 tempObj 对象。
为了直观理解,我们可以用代码来演示下
```javascript
var tempObj = {};
CreateObj.call(tempObj);
return tempObj
这样,我们就通过 new 关键字构建好了一个新对象,并且构造函数中的 this 其实就是新对象本身。