前端每日一题整理(2020.4)

1、什么是javascript的变量提升?为什么会产生变量提升?它会带来什么后果?请试着将一些可能运行错误的程序改成正确的,或是输出一些程序的运行结果。

答:因为javascript是先预编译再运行的语言,在预编译的过程中,它会提前声明所有var定义的变量,这就会产生“变量提升”和“函数提升”。

举一些例子:

a = 1; 
console.log(a);
var a; // 会将a的声明提到最顶端
此段代码等价于:
var a;
a = 1;
console.log(a);
输出结果为1
但是声明语句和赋值语句的逻辑是不同的,如下:
console.log(a);
var a = 1;
这段代码依旧会产生变量提升,但是只有声明会提升,赋值的过程并不会提升。
此段代码等价于:
var a;
console.log(a);
a = 1;
所以输出结果为undefined
a();
a = 1;
console.log(a);
var a = 2;
function a() {
  console.log("I am a function!");  
}
当一个变量同时有变量声明和函数声明时,只会将函数变量提升到最顶端,而声明变量不变,意为函数变量的优先级高于变量声明。
上述等价于:
function a() {
  console.log("I am a function!");  
}
a();  // I am a function
a = 1;
console.log(a); // 1
var a = 2;
当同一个变量同时有两个函数声明时,以后者为准。
a();
function a() {
  console.log("before");  
}
function a() {
  console.log("after");  
}
输出结果为after

而在const,let和var之间,只有var定义的变量会发生明显的变量提升,var定义的变量提前访问会返回undefined,而let和const则会抛出ReferenceError。

let和const其实也有变量提升,但let和const会在当前声明变量所在的块作用域中,从块的开始到声明的语句之间,发生“暂时性死区”,变量会在编译的过程中提前创建出来,但没有绑定词法时,试图访问会抛出异常ReferenceError。
console.log(a); // ReferenceError: Cannot access 'a' before initialization
let a = 2;

定义函数时,虽然有函数声明和函数表达式两种方法,但只有函数声明会发生变量提升:

函数声明是指通过关键字function声明一个函数,并指定名字foo1
function foo1() {
  console.log("I am a function");        
}
函数表达式是指先通过function声明一个函数,但是并未指定名字,是一个匿名函数,然后将这个匿名函数赋值给变量
var foo1 = function() {
  console.log("I am a function,too");  
}
在函数表达式后加上()可以立即执行,而函数声明不行。

变量提升在实际开发中,虽然可以让开发人员在声明前使用一个函数或者变量,但可能会造成不会再使用的变量被var声明后提升到全局,造成泄漏问题。而且后续开发时如果在不知情的情况下和全局变量重名,也会导致各种各样的问题。你可能就修改了全局的变量。

习题整理:

console.log(a); // function a
console.log(b); // undefined
var a = 1;
function a(){}
var b = function(){};
console.log(a); // 1
上述代码等价于:
function a(){}
var b;
console.log(a);
console.log(b);
var a = 1;
b = function(){}
console.log(a);
分析:变量a同时被var声明和函数声明,将函数声明提到顶端而var声明原地不动,第一次输出a时a是一个函数。b的声明提到了顶端,但赋值匿名函数并没有,所以是undefined。之后a又被重新声明赋值给1,所以最后一个输出a是1.
var a = 10;
(function () {
    console.log(a)
    a = 5
    console.log(window.a)
    var a = 20;
    console.log(a)
})()
输出结果为:undefined 10 20
上述代码等价于:
var a;
a = 10;
(function(){
   var a;
   console.log(a); // undefined
   a=5;
   console.log(window.a); // 10
   a = 20;
   console.log(a);    // 20
})()
getName(); // 5
var getName = function () { alert (4);};
getName(); // 4
function getName() { alert (5);}
getName(); // 4
上述代码等价于:
function getName() { alert (5);}
getName();
var getName = function () { alert (4);};
getName();
getName();
function foo() {
   var a = 1;        
   function b() {   
      a = 10;
      return '';
      function a() {...}
    }
    b();
    console.log(a);
}
foo();
上述代码等价于:
function foo() {
   var a;
   function b() {
       function a() {...}
       a = 10;
       return '';
  }
  a = 1;
  b();
  console.log(a);
}
foo();
输出结果为1,因为输出的a并没有被b作用域里的a污染,找到最近的a是1。如果console.log在b内,输出的就是10了。
console.log(a); // undefined
console.log(b); // ReferenceError:b is not defined
console.log(c); // function c
console.log(d); // undefined
var a = 1;
b = 2;
console.log(b); // 2
function c(){
   console.log('c');
}
var d = function(){
   console.log('d');
}
console.log(d); // function d
解析上述代码,等价于
var a;
function c(){
   console.log('c');
}
var d;
console.log(a); // undefined
console.log(b); // ReferenceError:b is not defined
console.log(c); // function c
console.log(d); // undefined
a = 1;
b = 2;
console.log(b); // 2
d = function(){
   console.log('d');
}
console.log(d); // function d
var c = 1;
function c(c) {
    console.log(c);
    var c = 3;
}
c(2);
等价于:
function c(c) {  // 被提前了,c是一个函数
    var c;
    console.log(c);
    c = 3;
}
var c = 1; // c是一个变量
c(2); // 变量不能当函数用 
输出结果为TypeError

 

2、什么是函数的闭包?为什么会产生闭包?闭包会带来什么后果和作用?如何处理闭包?

答:函数的闭包是指有权访问另一个函数作用域中变量的函数。

最直观的表现:
var a = 1;
function foo() {
    console.log(a);
}
foo();

上述代码中,函数foo内部本身并没有声明变量,但是它访问了外部的变量a,并且输出了a的值,这就是一个最简单的闭包的展现,因为子对象可以访问父对象的变量,从内部一级一级向外查找,直到全局变量,反之外部的函数就无法访问内部的函数的变量。

首先,我们知道变量分成全局变量和局部变量,全局变量的特点是可重用、但易被污染;而局部变量的特点是重用性低、但不易被污染。闭包也是一种折中的解决方法:通过外部函数内返回内部函数的方法,让内部函数自带一个不易被污染,又能被自己重用的函数作用域。

function numCount() {
    var counter = 0;
    function addCounter() {
        counter = counter + 1;
        console.log(counter);
    }
    return addCounter; 返回的是函数
}

var count1 = numCount();
var count2 = numCount();

count1(); // 1
count1(); // 2
count1(); // 3
count2(); // 1
count2(); // 2
count1(); // 4 每个计数器都会创建一个新的counter变量,并像背包一样和addCounter这个函数绑在一起

如果通过对象来对代码进行改造的话

function numCount() {
    var counter = 0;
    function addCounter() {
        counter = counter + 1;
        console.log('加法', counter);
    }
    
    function decCounter() {
        counter = counter - 1;
        console.log('减法', counter)
    }
    
    return {
        addCounter: addCounter,
        decCounter: decCounter,
    };
}

var count1 = numCount();
var count2 = numCount();

count1.addCounter(); // 1
count1.addCounter(); // 2
count1.decCounter(); // 1
count2.decCounter(); // -1
count2.addCounter(); // 0
count1.addCounter(); // 2

这样的运用是不是很类似于java语言的调用私有变量和私有方法。

闭包虽然名为“包”,但本质其实是:1、创造出一个局部变量(所以需要一个外部函数)2、将这个内部函数传递到外部(通过return或者window.func)函数b定义在函数a的内部,但外部函数c又引用了函数b,这样函数a的资源也不会被垃圾处理机制回收,这样就算平时函数a执行完了,依然会占用资源,因为函数a中的函数b运行需要占用函数a内的变量。闭包的优势是不会污染全局变量,只占用各自的变量,里面定义的变量可以作为一种缓存,从另一种意义上实现“面向对象”。而闭包也可以匿名自执行函数,减少全局变量,在其他语言中出现的类,在js里可以通过闭包来模拟实现。而闭包的缺点也很明显:函数b外的函数a一直无法释放内存,必然在资源的消耗上会有一些劣势。但内存泄漏的问题由于IE垃圾回收的机制已经变更,现在闭包并不会引起内存泄漏。

所以闭包的使用场景可以类比为闭包的父函数是一个对象,闭包是公共方法,内部变量是私有属性。

 

3、什么是作用域?什么是原型链?js在编译过程中是如何创建环境并执行的?

既然前面提到了闭包的问题,那就必定绕不开javascript的作用域和原型链,也希望能一次性将这两个知识点都融会贯通,所以就放在一起回答。

首先,从javascript的编译执行机制开始:

javascript引擎不是逐行编译代码,而是按照代码块一段一段地执行,代码块就是被<script>分隔的元素,过程有词法分析语法分析

词法分析:

e.g
var result = a - b;
通过词法分析,将会转变为下列结果:
NAME result
EQUALS
NAME A
MINUS
NAME B
SEMICOLON
将字符流转变为记号流,就像单词一个个翻译。

语法分析:

将已经通过词法分析的记号流构建成一棵语法树,如果无法构建就会报语法错误。

经过上面两轮操作后,javascript代码也不是完全按顺序执行的,就回到了问题1的变量提升,函数和var声明的变量会被提到最顶端,暂不赘述。

预编译的过程结束后,就会进入执行阶段:

在执行阶段中,执行上下文会被创建,执行上下文指的是代码运行的环境,它会形成一个作用域。执行上下文分成全局执行上下文函数执行上下文。全局执行上下文就是浏览器的window对象,this指向的就是全局执行上下文。函数执行上下文每次调用函数都会创建,可以有无数个。

因为会有很多个执行上下文,所以js使用执行上下文栈来管理这些执行上下文。最初会在栈底压入一个全局执行上下文,然后每进入到不同的运行环境,几句会创建一个新的执行上下文。栈底永远是全局执行上下文,栈顶永远是当前的函数执行上下文。

执行上下文的生命周期:

 

创建阶段:

1)在除了全局执行上下文的其他执行上下文中生成变量对象arguments,如果有函数的声明和变量的声明会提前执行,就是变量提升。

2)建立作用域链,将当前的变量对象和上层环境作用域的活动对象组成作用域链,保证了各个作用域间对变量和环境的有序访问。

3)确定this的指向

执行阶段:

1)变量赋值

2)函数引用

3)执行其他代码

销毁出栈

 

posted @ 2020-04-01 23:37  黑松露巧克力  阅读(183)  评论(0)    收藏  举报