解两道关于JS“引用类型”和“变量提升”的面试题
在前端面试中,少不了关于JS引用类型和变量提升的题目,今天就分享两道面试题并附上详解过程。供读者学习巩固基础知识。
关于引用类型的题目
var obj = { a: 1 }
function test (obj) {
obj.a = 2
obj = { a: 3 }
obj.b = 3
}
test(obj)
console.log(obj) // 求输出结果
笔者分析:
从我以往的经验来看,凡是这种一开始定义一个引用类型的变量,然后再在代码里各种修改属性,最后求输出结果的,那九成是考查"JS引用类型"的。下面我们简单说一下这个引用类型。
笔者聊基础:
关于引用类型,用我个人的白话概括就是,“引用类型的相互赋值只是单纯的把值复制了过去,而两个变量还是共享同一块内存区域,所以修改其中一个,另外一个也会跟着变化”。当然,这只是我个人的白话解释,关于更详细的定义个更官方的解释,大家自行网上学习,这里不做赘述,用一个很好的例子来说明上面的白话就是
var a = { x: 10, y: 20 }
var b = a
b.x = 100
b.y = 200
console.log(a) // { x: 100, y: 200 }
console.log(b) // { x: 100, y: 200 }
上面是关于引用类型最直观的解释。其实这里面也是有一些坑的
笔者聊坑
var a = { x: 10, y: 20 }
var b = a
b.x = 100
b.y = 200
b = { x: 1, y: 2} // 注意这里!!
console.log(a) // { x: 100, y: 200 }
console.log(b) // { x: 1, y: 2}
我们虽然知道了,引用类型之间共享内存区域的,但是一旦其中一个变量重新赋值,那么引用关系将不再存在,将中断关联!!!
也就是上面代码中展示的样子。
我们先不着急解上面的面试题,先来看这样一个例子。
var a = 1
function test (param) {
param = 2
}
test(a)
console.log(a) // 1
var a = { a: 1 }
function test (param) {
param.a = 2
}
test(a)
console.log(a) // { a: 2 }
上面的例子中,我们给函数中分别传入了基本类型和引用类型的参数,输出显示,当传入的参数是基本类型时,那么函数内部对参数的修改是不会影响原数据的,而传入的是引用类型时,函数内部对参数的操作是会影响到原来数据的,所以总结下面两点
- 函数的参数如果是简单类型,会将一个值类型的数值副本传到函数内部,函数内部不影响函数外部传递的参数变量
- 如果是一个参数是引用类型,会将引用类型的地址值复制给传入函数的参数,函数内部修改会影响传递参数的引用对象。
之所以,造成上面的问题,是因为方法的参数是一个局部变量,也就是,传参的过程中,相当于一个赋值的过程。对于引用类型,它们引用同一个地址,于是就造成了,“内部修改,外部也修改”的现象,而基本类型不会存在这种情况。
笔者解答
上面讲了这么多知识点,我想这个题的答案已经很明确了,不过还是要一步一步解答一下
var obj = { a: 1 }
function test (obj) {
// obj为一个局部变量,和外部的obj共享一个地址
obj.a = 2 // 内部obj的修改将导致外部修改为{ a: 2 }
obj = { a: 3 } // 局部变量的赋值操作,导致关联中断,指向不再和外部obj共享,故,对外部不再有影响
obj.b = 3 // 内部和外部的obj已经关联中断,此时内部obj为{ a: 3, b: 3}, 外部为 { a: 2 }
}
test(obj)
console.log(obj)
通过上面的注释分析,输出为{ a:2 }
,很明确。
避免混淆
和上面的题相似的还有一个变形,可能会容易和上面的题混淆,这里抛出来。
var obj = { a: 1 }
obj.a = 2
obj = { a: 3 }
obj.b = 3
console.log(obj) // { a: 3, b: 3 }
我们去掉了函数传参的形式,这里就只是对一个变量的操作了,没有所谓的“引用关系”后,也就结果比较明了了。
小结
上面,我们通过一个“引用类型”的面试真题,对引用类型的应用做了分析和讲解,希望对大家有所帮助,如果你对引用类型的基本知识还不够了解,那么就要加强补基础喽!
关于变量提升的题目
console.log(fn1, fn2, fn3 )
var fn1 = function () {
console.log('i am func1')
}
function fn2 () {
console.log('i am func2')
}
var fn3 = 'i am string'
console.log(fn1, fn2, fn3 )
笔者分析
根据我个人的经验,凡是在代码第一行就打出访问变量的代码的题目,考察的点八成是在"JS变量提升"。下面咱俩简单聊一下JS的变量提升。
笔者聊基础
其实这都是比较基础的知识了,官方的定义和社区的完善解释会更多,这里我们不纠结定义,想具体搞一下的,可以自己学习。这里我用自己的理解和白话来解释。想要知道什么是变量提升,我们先来看看,不是变量提升的情况。
我使用了
let
关键字声明变量,熟悉ES6的读者应该都知道let
没有变量提升,我们在一个变量还没有声明的时候,就去访问它,如果该变量还没有定义,那么就会报错,这是不具备变量提升的情况,反之,变量提升就是我们可以先访问后声明一个变量。在声明变量的关键字中只有
var
具有变量提升,let
不具备。也就是上面的错误代码,我们换成var
声明就会正确我们需要注意的是,上面的代码不再报错了,并且其打印的结果为undefined。
这里之所以强调其打印结果为
undefined
。是因为字符串的变量提升和函数的提升的效果是不同的!这里有一个小坑
笔者聊坑
说到这里,我们不得不插一句JS的“词法分析”了,这都是JS中最基础的知识,由于开发中很少注意这些细节,也被大多数开发者忽略,同事因为使用变量提升的代码,会让代码的可读性变得更差,所以开发中一般很少使用变量提升,再加上现在是ES6的时代,本身就不再有这个概念,但,作为JS使用者的前端开发人员,这是你不得不知道的知识点!
同样,我还是用自己的白话和理解来写,想看更官方更底层的解释,自行学习。词法分析就是,在JS运行的过程中,分为“编译阶段”和“执行阶段”。也就是说,我们通过var a = 1
声明的变量a,实际上是经过了两个过程
- 在内存中开辟一个区域出来,起名为a。(这一步叫变量声明)这时候他没有值,所以a = undefined
- 执行
a = 1
,将值赋给上面声明的内存区域
而 变量的声明会提升到其作用域的顶端去执行!也就是说
console.log(a)
var a = 1
这段代码实际的执行过程是
var a
console.log(a)
a = 1
注意: 只是声明被提升了,赋值不会!赋值就是普通的执行代码,顺序执行
这里之所以说有个坑,是因为函数的声明提升和其他的变量提升是不一样的,他有一个专门的名字叫“函数提升”,其实都是属于变量提升,这里函数提升有一个特点就是函数提升比变量提升优先级高!那么他是如何体现的呢?
说到函数提升,你得先搞明白,函数声明,在JS中,具名函数的声明方式有两种
- 函数声明式
function test () {
/*这里是内容*/
}
- 函数字面量式
var test = function () {
/*这里是内容*/
}
无论你是哪一种函数的声明方式,都具有变量提升!!不同的是,函数声明式会存在“函数提升”,而函数字面量式的声明和上面的变量提升结果是一样的。两种不同的函数声明方式,具有不同的提升优先级。其中,函数提升比变量提升优先级高。看代码:
看出区别来了吗?通过函数声明式声明的函数,连初始化的东西也打印了出来,也就是函数提升;而函数字面量式声明的函数也就是通过普通的变量提升,只是打印声明的结果,而初始化的过程不会被打印。这样就导致,访问同一个变量,一个在后面声明为函数,一个声明为变量,声明为函数的因为有代码块,就会优先执行,这就是其优先级更高的原因,也就是下面这个代码为什么输出1的原因
foo(); // 1
var foo;
function foo () {
console.log(1);
}
foo = function () {
console.log(2);
}
讲到这里,我想大家对于面试题的结果已经很明了了。结果不重要,享受分析的过程才重要,所以,请容我简单做个小总结。
关于变量提升的总结
- JavaScript 中,函数及变量的声明都将被提升到作用域的最顶部。其中函数会把整个代码块提升,而变量(这里叫法区别一下函数,不要纠结)只会提升其声明的过程,不会提升其初始化的过程,也就是赋值的过程。
- 函数声明比变量声明的优先级要高!
- 在开发中尽量少的使用变量提升,那将使你的代码可读性变差
- 对于大多数程序员来说并不知道 JavaScript 变量提升。
- 如果程序员不能很好的理解变量提升,他们写的程序就容易出现一些问题。
- 为了避免这些问题,通常我们在每个作用域开始前声明这些变量,这也是正常的 JavaScript 解析步骤,易于我们理解。
好了,说这么多,解题吧
笔者解答
console.log(fn1, fn2, fn3 )
// fn1 变量提升,打印undefined, fn2 函数提升,打印整个函数体f(), fn3 变量提升,同fn1
var fn1 = function () {
console.log('i am func1')
}
function fn2 () {
console.log('i am func2')
}
var fn3 = 'i am string'
console.log(fn1, fn2, fn3 )
// fn1 被初始化,打印函数体f(), fn2已经声明,打印函数体f(), fn3被初始化,打印字符串‘i am string’
结果就是这么的枯燥无味,是吧?hiahiahia~~~
避免混淆
本节,我们主要讲变量提升,不过也说一点容易混淆的事吧。这里是用来提醒新手的
var fn1 = function () {
console.log('i am func1')
}
console.log(fn1)
有时候会产生误解,“把一个函数赋给一个变量,那么这个变量不是等于这个函数的返回值吗?”哦哦哦,这里赶紧解释一下,不是的,把一个函数的执行结果给一个变量才是等于其return的值的,给函数体的话,那就是一个函数体,这里需要新手注意一下。
写在最后
本文通过两道面试题,引出了“引用类型”和“变量提升”两个JS中的基础知识点,也是在一些面试中,经常踩到的点,大家对于本文,不要把目光放在这面试题的结果上,枯燥的结果不如分析过程来的让人兴奋和收获,希望能够帮助到更多的人~