前端每日一题整理(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)执行其他代码
销毁出栈

浙公网安备 33010602011771号