JavaScript 变量提升、函数提升、 this 指向、 作用域/闭包

94 阅读10分钟

变量声明提升函数提升

  • 通过var定义(声明)的变量,在定义语句之前就可以访问到;值:undefined
  • 通过function声明的函数,在声明之前就可以直接调用;值:函数定义(对象)
var a = 3
function fn () {
    console.log(a) // undefined 变量提升
    var a = 4
     /*
        相当于 
        var a ;
        console.log(a)
        a = 4
    */ 
}
fn()

console.log(b) // undefined 变量提升
fn2() // fn2() 可调用 函数提升 
fn3() // 不可调用 遵循的是变量提升
var b = 3
function fn2(){
   console.log('fn2()')
}

var fn3 = function() {
   console.log('fn2()')
}

执行上下文

  1. 代码分类(位置)
  • 全局代码
  • 函数(局部)代码
  1. 全局执行上下文
  • 在执行全局代码前将window确定为全局执行上下文
  • 对全局数据进行预处理
    1. var定义的全局变量 ==> undefined,添加为window的属性
    2. function声明的全局函数 ==> 赋值(fun),添加为window的方法
    3. this==> 赋值(window)
  • 开始执行全局代码
// 全局执行上下文
console.log(a1,window.a1) // undefined,undefined
a2() // 'a(2)'
console.log(this) // window

var a1 = 3
function a2(){
    console.log('a2')
}
console.log(a1) // 3
  1. 函数执行上下文
  • 在调用函数,准备执行函数体之前,创建对应的函数执行上下文对象(虚拟的,存在于栈中)
  • 对局部数据进行预处理
    1. 形参变量 ==> 赋值(实参)==> 添加为执行上下文的属性
    2. arguments ==> 赋值(实参列表),添加为执行上下文的属性
    3. var定义的局部变量 ==> undefined,添加为执行上下文的属性
    4. function声明的函数 ==> 赋值(fun),添加为执行上下文的方法
    5. this赋值(调用函数的对象)
  • 开始执行函数体代码
// 函数执行上下文
function fn(a1){
   console.log(a1) // 2
   console.log(a2) // undefined
   a3() // a(3)
   console.log(this) // window
   console.log(arguments) // 伪数组(2,3)
   
   var a2 = 3
   function a3(){
       console.log('a(3)')
   }
}

fn(2,3)

执行上下文栈

  1. 在全局代码执行前,JS引擎就会创建一个栈来存储管理所有的执行上下文对象
  2. 在全局执行上下文(window)确定后,将其添加到栈中(压栈)
  3. 在函数执行上下文创建后,将其添加到栈中(压栈)
  4. 在当前函数执行完之后,将栈顶的对象移除(出栈)
  5. 当所有的代码执行完之后,栈中就只剩下window
// 全局上下文 
var a = 10
var bar = function(x) {
    var b = 5
    foo(x + b) // foo函数的上下文
}
var foo = function(y){
    var c = 5
    console.log(a + c + y)
}
bar(10) // bar 函数的上下文
/*
    先执行变量提升,再执行函数提升
*/
function a(){}
var a
console.log(typeof a) // 'function'

if(!(b in window)){
    var b = 1
}
console.log(b) // undefined

var c = 1
function c(c) {
    console.log(c)
}
c(2)
/*   相当于
    var c 
    function c(c) {
        console.log(c)
    }
   c = 1
   c(2)
*/

This指向规则

this和函数定义的位置没有关系,只和调用者有关系 this是在运行时被绑定的

//定义一个函数
 function foo(){
    console.log(this) // obj对象
 }
 //1.调用方式一:直接调用
 foo() // window
 
 //2.调用方式二:将foo放到一个对象中,再调用
 var obj1 = {
    name:'wangjianguo',
    foo:foo
 }
 obj1.foo() // obj对象
 
 //3.调用方式三:通过call或者apply调用
 foo.call("wangjianguo") // String{"wangjianguo"} 对象

隐式绑定

通过对象调用函数绑定this

谁直接调用foo()(换而言之,谁离foo()更近),那么foo的this就指向谁。

   //obj 调用了foo()方法。因此,this会隐式的被绑定到obj对象上。
   function foo(){
       console.log(this) // obj对象
   }
   var obj1 = {
       name:'wangjianguo',
       foo:foo
   }
   obj1.foo()
   
   var obj2 = {
       name:'wangjianguo',
       foo:foo,
       obj1:obj
   }
   // 谁直接调用foo(),那么foo()中的this就指向谁
   obj2.obj1.foo()

显式绑定

call函数

function foo(){
    console.log(this)
}

foo.call(window) // window
foo.call({name:"wangjianguo"}) // {name:"wangjianguo"}

bind、apply、call的区别

共同点;功能一致

可以改变this指向

语法:函数.call()、函数.apply()、函数.bind()

区别:

  1. call,apply可以立即执行。bind不会立即执行,因为bind返回的是一个函数需要加入()执行
  2. 参数不同:apply第二个参数是数组。call和bind有多个参数需要挨个写
var str = '你好'
var obj = {str:'这是obj对象内的str'}

function fun(
    Console.log(this, this.str)
)
fun.call(obj) // call立即执行
fun.apply(obj) // apply立即执行
fun.bind(obj) // bind不会立即执行,因为bind返回的是函数

场景
1.用apply的情况
var arr1 = [1,2,4,5,7,3,321]
console.log(Math.max.apply(null,arr1))

2.用bind的情况
var btn = document.getElementById('btn')
var h1s = document.getElementById('h1s')
btn.onClick = function (){

    Console.log(this.id)

}.bind(h1s)

使用new关键字绑定this

function Student(name) {
    console.log(this) // Student {}
    this.name = name // Student {name:"wangjianguo"}
}

var s = new Student("wangjianguo")
console.log(s)

通过new 关键字创建一个新对象的步骤是什么/构造函数是如何创建对象的?

new操作符具体做了什么

  1. 创建了一个空对象
  2. 将空对象的隐式原型,指向于构造函数的显式原型
  3. 将空对象作为构造函数的上下文(改变this指向)
  4. 对构造函数有返回值的处理判断,如果return的是对象,则直接返回该对象,如果返回的是基本类型,则return语句无效,仍难返回我们创建的对象。
function create(fn,…args){
    console.log(args)
   // 1.创建了一个空对象
    var obj = {}
  //2.将空对象的原型,指向于构造函数的原型
    Object.setPrototypeOf(obj,fn.prototype)
    //3.将空对象作为构造函数的上下文(改变this指向)
    var result = fn.apply(obj,args)
    //4.对构造函数有返回值的处理判断
    return result instanceof Object ? result : obj
}

console.log(create(Fun,18,'zhangsan'))

优先级顺序

  • 显式绑定优先级高于隐式绑定
  • new绑定优先级高于隐式绑定
  • new绑定优先级高于bind

new绑定 > 显式绑定(bind) > 隐式绑定

function foo(){
    console.log(this)
}

var ob1 ={
    name:'obj1',
    foo:foo
}

var ob2 ={
    name:'obj2',
    foo:foo
}
// 隐式绑定
obj1.foo() // obj1
obj2.foo() // obj2

//隐式绑定和显式绑定同时存在
obj1.foo.call(obj2) // obj2 ,显式绑定优先级更高

把一个函数地址,赋值给另一个对象属性,属于间接引用。

function foo(){
    console.log(this)
}

var ob1 ={
    name:'obj1',
    foo:foo
}

var ob2 ={
    name:'obj2'
}
(obj2.foo = obj1.foo)()// window,相当于直接执行foo(),既没隐式绑定,又没显示绑定,就相当于直接使用函数调用,打印出来就是window。
var name = "全局window"
var person = {
    name:"person",
    sayName:function(){
        console.log(this.name)
    }
}
function sayName(){
    var fun = person.sayName
    fun()//全局window
    person.sayName()//person
    (b = person.sayName)()//全局window
}
sayName()

JS中关于this指向的问题

  1. 全局对象中的this指向,指向的是window
  2. 全局作用域或者普通函数中的this,指向全局window
  3. this永远指向最后调用它的那个对象,在不是箭头函数的情况下
  4. new 关键字改变了this的指向
  5. apply,call,bind 可以改变this的指向,不是箭头函数
  6. 箭头函数中的this 它的指向在定义的时候就已经确定了,箭头函数它没有this,看外层是否有函数,有就是外层函数的this,没有就是window
  7. 匿名函数中的this,永远指向了window,匿名函数的执行环境具有全局性,因此this指向window
function Foo() {
    getName = function(){console.log(1)} // 注意是全局的window
    return this
}

Foo.getNamefunction(){console.log(2)} // 
Foo.prototype.getNamefunction(){console.log(3)} // 
var getName = function(){console.log(4)} // 
function getName () {
    console.log(5)
}
Foo.getName() // 2 在本身去找
getName() // 4 // var getName = function(){console.log(4)} 和 function getName 变量声明的优先级大于普通函数的优先级
Foo().getName() // 执行Foo函数,把var getName = function(){console.log(4)} 替换了 getName = function(){console.log(1)} // 注意是全局的window 打印为1
getName() //当执行getName 去 window上找,getName 函数已经替换,执行函数为1
new Foo.getName() //3 在构造函数上找 ,打印为3

闭包

作用域链

当我们访问一个变量时,JavaScript 引擎首先会在当前作用域寻找这个变量。如果当前作用域没有这个变量,就会去上一层作用域寻找。如果上一层作用域找不到,就去上上层寻找。直到全局作用域都找不到时,返回undefined.

var windowVar = "windowVar"
function outer() {
    var outerVar = "outer"
    function inner () {
        var innerVar = "inner"
        console.log(outerVar) // outerVar
        console.log(innerVar) // innerVar
        console.log(windowVar) // windowVar
    }
}

闭包

闭包指的是:即便外部函数已经不存在,也可以获取作用域链上变量的函数。

应用场景:

  1. 防抖截流 
  2. 库的封装(保证数据的私有性)
function outer(){
    const a = 1
    function f(){
        console.log(a)
    }
    // 不一定要return 内部函数,才会形成闭包,内部调用也可以
    // f()
    return f
}

let f = outer()
f() // 1

闭包的优点:

  1. 内部函数可以访问到外部函数的局部变量
  2. 闭包可以解决的问题
    var lis = document.getElementsByTagName('li')
    for (var I =0;i < lis.length;i++){
        (function(i){
            lis[I].conClick = function(){
                alert(i)
            }
            lis[I] = null
        })(i)
    }

闭包的缺点:

  1. 变量会驻留内存中,造成内存损耗问题。解决:把闭包的函数设置为null
  2. 内存泄露【ie】==》 可说可不说,如果说就一定要提到ie
function fun(){
    var element = document.getElementById("button")
    console.log(element)
    var someResource = new Array(1000).join("*")
    
    element.addEventListener("click",()=>{
        console.log(someResource)
    })
    
    // 注意 要移除监听
     element.removeEventListener("click")
}
fun()

JS作用域考题

  1. 除了函数外,js 是没有块级作用域
for (var i = 0;i<10;i++){
}
console.log(i) //10 

除了函数外,js 是没有块级作用域,相当于声明i声明在外面
var i;
for (i = 0;i<10;i++){}
if (true){
    var a = 10
}
console.log(a) // 10

// 相当于
var a 
if (true){
   a = 10
}
console.log(a) // 10
  1. 作用域链:内部可以访问外部的变量,但是外部不能访问内部的变量。注意:如果内部有,优先查找内部,如果内部没有就查找外部的
  2. 注意声明变量是用var还是没有写(没有写就是 window)
(function(){
    var a = b = 10
    /*
    相当于
    var a = 10 // a是函数作用域,不能在外部访问。
    b = 10,没有写var,声明在全局window
    */
})
// 相当于
var b = 10 //b 声明在全局window
(function(){
    var a = 10 //a是函数作用域,不能在外部访问。 
})
console.log(a) // error undefined a
console.log(b) // 10
  1. 注意:js有变量提升的机制【变量悬挂声明】
  2. 优先级:声明变量 》 声明普通函数 〉 参数 》 变量提升

规则:

  1. 本层作用域有没有此变量【注意变量提升】
  2. 注意:js除了函数外没有块级作用域
  3. 普通声明函数是不看写函数的时候顺序
//普通声明函数是不看写函数的时候顺序
function fun(){
    console.log(a) // f a(){}
    var a = 10
    function a(){}
}

示例

示例1:
function c(){
    Var b = 1
    function a() {
        // var b = undefined 变量提升
        console.log(b) // undefined
        var b = 2
        console.log(b) // 2
    }
    a()
    console.log(b) // 1
}
c()

示例2
var name = 'a'
(function (){
    //变量提升  var name = undefined 
    if (typeof name == 'undefined'){
        var name = 'b'
        console.log('111' + name)// '111b'
    }else {
        console.log('222' + name)
    }
})()

示例3
function fun(a){
    var a =10 // 声明变量 大于 普通函数 大于 参数的优先级
    function a(){}
    console.log(a) // 10
}

示例4
function fun(){
    a = 10 // 相当于在函数外部定义 ,a 的作用域是 window
    var a = 20
    console.log(a) // 20,本层有,优先用本层的
}

示例5
function fun(){
    a = 10 
    console.log(a) // 10,a 有变量提升,a = 10 相当于再次赋值。
    var a = 20
}

示例6
var o = {
    a:10,
    b:{
        fn:function(){
            console.log(this.a) // undefined
            console.log(this) //b
        }
    }
}

示例7
window.name = 'ByteDance'
function A(){
    this.name = 123
}

A.prototype.getA = function (){
    console.log(this)
    return this.name + 1
}

let a = new A()
let funcA = a.getA// 相当于把一个函数赋值给了funcA
console.log(funcA())  // funcA在全局作用域下执行,打印 ‘ByteDance1’


示例8
var length = 10
function fn(){
    return this.length  + 1
}

var obj = {
    length:5,
    test1:function(){
        return fn()
    }
}

obj.test2 = fn
console.log(obj.test1()) // 11 这里是一个闭包,返回fn函数, 相当于执行fn(),
console.log(fn() === obj.test2()) // false fn() 执行为11,obj.test2()执行为6
console.log(obj.test1() == obj.test2()) // false obj.test1() 相当于fn(),结果为11,obj.test2() 执行为6