js面试题+解析
以下面试题是我认为比较有代表性的几道题,每道题目都配有我自己的见解,可能部分习题的讲解存在偏差,欢迎大家多多交流指正
第六题
var x=8;
var objA = {
x:'good',
y:32
}
function add(x,y){
console.log(x.y+y);
}
function fn(x,y){
x.y=5;
y(x,3);
}
fn(objA,add);
console.log(objA);
答:结果是依次打印8 {x:“good”,y:5};
首先编译器对代码进行编译,先提升两个函数表达式,然后提升声明x,objA,所以代码片段会被引擎理解为如下形式:
function add(x,y){
console.log(x.y+y);
}
function fn(x,y){
x.y=5;
y(x,3);
}
var x;
var objA;
x=8;
objA={
x:‘good’,
y:32
}
fn(objA,add);
console.log(objA);
然后引擎运行代码fn(objA,add),objA和add是实参被传入函数fn中,所以相当于运行函数fn(objA,add)。在函数内部出现x.y=5。首先对象取属性操作的优先级最高,其次访问对象属性有两种方法,一种是本题中使用的点表示法,这也是很多面向对象语言中通用的语法。不过,在JavaScript也可以使用方括号表示法来访问对象的属性。在使用方括号语法时,应该将要访问的属性以字符串的形式放在方括号中,从功能上看,这两种访问对象属性的方法没有任何区别。但方括号语法的主要优点是可以通过变量来访问属性。而点表示法 不能使用变量来访问属性,所以本题中的x.y=5等同于objA.y=5,即在全局作用域中找到objA并把其y属性改变为5。而y(x,3)就等同于add(objA,3),即运行函数add,并为形参x,y传入实参objA和3,而且同上面所讲对象取属性优先级更高,所以函数add内部可以看为console.log(objA.y+3),对objA进行RHS查询,并得到它的y属性值为5,所以打印值为5。
若本题中改为:
var x=8;
var objA = {
x:'good',
y:32
}
function add(x,y){
console.log(x.y+y);
}
function fn(x,y){
x[y]=5;//改变去属性表示方法
y(x,3);
}
fn(objA,add);
console.log(objA);
则会第一个打印是38,因为x[y]=5等同于objA[add]=5,即给objA添加里一个add属性,而objA[y]还是等于35,所以console.log(x.y+y)等同于console.log(objA.y+3)即35+3等于38,所以会打印38.
第七题
function changeObjectProperty (o) {
o.siteUrl = "http://www.csser.com/";
o = new Object();
o.siteUrl = "http://www.popcg.com/";
}
var CSSer = new Object();
changeObjectProperty(CSSer);
console.log(CSSer.siteUrl);
答:打印http://www.csser.com/
首先说明一点ECMAScript中所有函数的参数都是按值传递的。也就是说,把函数外部的值复制给函数内部的参数,就和把值从一个变量复制到另一个变量一样。基本类型值的传递如同基本类型变量的复制一样,而引用类型值的传递,则如同引用类型变量的复制一样。访问变量有按值和按引用两种方式,而参数只能按值传递。
在向参数传递基本类型的值时,被传递的值会被复制给一个局部变量(即命名参数,或者用ECMAScript的概念来说,就是arguments对象中的一个元素)。在向参数传递引用类型的值时,会把这个值在内存中的地址复制给一个局部变量,因此这个局部变量的变化会反映在函数的外部。请看下面这个例子:
function addTen(num) {
num += 10;
return num;
}
var count = 20;
var result = addTen(count);
alert(count); //20,没有变化
alert(result); //30
这里的函数addTen()有一个参数num,而参数实际上是函数的局部变量。在调用这个函数时,变量count作为参数被传递给函数,这个变量的值是20。于是,数值20被复制给参数num以便在addTen()中使用。在函数内部,参数num的值被加上了10,但这一变化不会影响函数外部的count变量。参数num与变量count互不相识,它们仅仅是具有相同的值。假如num是按引用传递的话,那么变量count的值也将变成30,从而反映函数内部的修改。但如果使用对象,那么情况会有一点复杂。再举一个例子:
function setName(obj) {
obj.name = "Nicholas";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
以上代码中创建一个对象,并将其保存在了变量person中。然后,这个变量被传递到setName()函数中之后就被复制给了obj。在这个函数内部,obj和person引用的是同一个对象。换句话说,即使这个变量是按值传递的,obj也会按引用来访问同一个对象。于是,当在函数内部为obj添加name属性后,函数外部的person也将有所反映;因为person指向的对象在堆内存中只有一个,而且是全局对象。有很多人错误地认为:在局部作用域中修改的对象会在全局作用域中反映出来,就说明参数是按引用传递的。为了证明对象是按值传递的,可以再看一看下面这个经过修改的例子:
function setName(obj) {
obj.name = "Nicholas";
obj = new Object();
obj.name = "Greg";
}
var person = new Object();
setName(person);
alert(person.name); //"Nicholas"
这个例子与前一个例子的唯一区别,就是在setName()函数中添加了两行代码:一行代码为obj重新定义了一个对象,另一行代码为该对象定义了一个带有不同值的name属性。在把person传递给setName()后,其name属性被设置为”Nicholas”。然后,又将一个新对象赋给变量obj,同时将其name属性设置为”Greg”。如果person是按引用传递的,那么person就会自动被修改为指向其name属性值为”Greg”的新对象。但是,当接下来再访问person.name时,显示的值仍然是”Nicholas”。这说明即使在函数内部修改了参数的值,但原始的引用仍然保持未变。实际上,当在函数内部重写obj时,这个变量引用的就是一个局部对象了。而这个局部对象会在函数执行完毕后立即被销毁。
当实参是数组时,情况也有一些特殊,例如:
var a=[1,2,3];
function foo(a){
a.push(4); //调用引用类型方法,改变了形参a,也改变了全局变量a
console.log(a); // [1,2,3,4] 此时的a是形参变量的值
a=[5,6,7]; // 形参重新赋值不会改变全局变量a
console.log(a); // [5,6,7] 形参变量a
};
foo(a);
console.log(a); // [1,2,3,4]
对照下面代码:
var a=[1,2,3];
function foo(a){
a=[5,6,7]; // 形参a被重新赋值,不会改变全局a
a.push(4); // 此时只改变了形参a,不会改变全局a
console.log(a); // [5,6,7,4]
};
foo(a);
console.log(a); // [1,2,3]
综上所述,在从一个变量向另一个变量复制基本类型值和引用类型值时,存在不同。如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,调用属性或方法改变其中一个变量,就会影响另一个变量,如下面的例子所示:对引用数据类型,会发现在函数里,形参被赋值或重新声明之前,对形参调用引用数据类型的属性(或方法)时,不仅会改变形参,还会改变全局变量。
所以对于本题中代码片段,实参CSSer是一个在对象,在函数changeObjectProperty中把CSSer值复制给o,然后对形参调用siterUrl属性,不仅改变了形参o,也改变了CSSer,所以此时CSSer包含一个siteUrl属性,并且属性值为http://www.csser.com/,然后又将一个新对象赋给变量o,此时这个变量引用的就是一个局部对象了,而这个局部对象会在函数执行完毕后立即被销毁。所以在o.siteUrl = “http://www.popcg.com/“;这一个操作里,只会改变o的属性,而不会改变外部CSSer的属性。所以最后打印结果为http://www.csser.com/。
第八题
var num=5;
function func1(){
var num=3;
var age =4;
function func2(){
console.log(num);
var num ='ivan';
function func3(){
age =6;
}
func3();
console.log(num);
console.log(age);
}
func2();
}
func1();
答:结果为依次打印 undefined ivan 6
根据本套面试题第一题及第二题中所写的声明提升等知识可以得首先引擎在解释JacaScript代码之前首先对其编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。引擎会在全局作用域顶端声明函数func1,然后声明全局变量num。在函数func1的作用域顶端先声明函数func2。在函数func2内部作用域的顶端声明函数func3,然后声明变量num。引擎所理解的代码格式如下:
function func1(){
function func2(){
function func3(){
age =6;
}
var num;
console.log(num);//undefined
num ='ivan';
func3();
console.log(num);//ivan
console.log(age);//6
}
var num;
var age;
num =3;
age =4;
func2();
}
var num;
num=5;
func1();
然后引擎开始从上到下执行代码,首先执行num=5,对num进行LHS查询,在全局作用域中找到变量num并将值赋给它。
然后运行函数func1,依次对变量num,age进行LHS查询,查询规则为从里到外,即从自己的作用域依次查询嵌套它的外部作用域。所以这两个变量直接在自己作用域内找到已经被声明的变量空间,然后把值3 ,4依次赋值给它们。
然后运行函数func2,首先运行console.log(num);对num进行RHS查询,在func2作用域中找到被声明的变量num,但是该变量并未赋值,所以打印结果为undefined。然后运行num=’ivan’;对num进行LHS查询,在func2作用域中找到num并赋值为ivan。
然后运行函数func3,运行age=6;对age进行LHS查询,在func3作用域内没有找到该变量,然后到包裹该作用域的func2作用域中查找该变量,找到该变量,并对其重新赋值为6。
函数func3内部代码运行完后,再接着运行func2内部代码console.log(num),对num进行RHS引用,在其所在作用域内找到被赋值为ivan的变量num,然后把得到的值传给console.log(),即打印出ivan。
接着执行代码console.log(age);和上步同理,对age进行RHS查询,在其所在作用域内没有找到变量age,然后向上级作用域接着查找,找到已被重新赋值为6的变量age,并把值传递给console.log(),所以打印结果为6。
第九题
var fn1 = 'ivan';
var name ='good';
var fn1 = function(y){
y();
}
function fn1(x){
x(name);
}
function fn2(x){
console.log(x);
console.log(name);
var name = 'hello';
console.log(name);
}
fn1(fn2);
答:结果为依次打印 undefined undefined hello
根据本套面试题第一题及第二题中所写的声明提升等知识可以得首先引擎在解释JacaScript代码之前首先对其编译,编译阶段中的一部分工作就是找到所有的声明,并用合适的作用域将它们关联起来。所以引擎会在全局作用域顶端先声明函数fn1,然后声明函数fn2,在fn2内部作用域的顶端声明name。在声明fn2下面声明变量fn1,因为与函数fn1声明重复,所以忽略该声明,然后声明变量name,然后声明fn1,与之前声明重复被忽略。最后结果如下:
function fn1(x){
x(name);
}
function fn2(x){
var name;
console.log(x);//undefined
console.log(name);//undefined
name = 'hello';
console.log(name);//hello
}
// var fn1; 声明被忽略
var name;
// var fn1; 声明被忽略
fn1 = 'ivan';
name ='good';
fn1 = function(y){
y();
}
fn1(fn2);
然后引擎开始执行代码,首先对fn1进行LHS查询,在全局作用域中找到该变量,然后对其重新赋值为ivan,(需要说明的是在JavaScript中可以通过改变变量的值来改变变量的属性)。
然后对name进行LHS查询,在全局作用域内找到该变量,并赋值为good。
然后对fn1进行LHS查询,在全局作用域内找到该变量,并把function(y){y();}赋值给它,并且该变了fn1的类型。
然后运行函数fn1,其中y为形参,fn2为实参,对y(隐式的)进行LHS查询,把fn2赋给y。
然后运行函数y(),即运行函数fn2(),fn2中存在形参x,首先对x(隐式的)进行LHS查询,但并未查询到所对应的实参,所以x为空。然后运行代码console.log(x);即打印undefined。
然后运行console.log(name),对name进行LHS查询,在其所在作用域中找到没有赋值的变量name,所以打印undefined。
然后运行name=“hello”,对nameRHS查询并赋值。
然后再运行console.log(name),对name进行LHS查询,在其所在作用域中找到被赋值为hello的变量name,所以打印hello。
第十题
var buttons = [{name:'b1'},{name:'b2'},{name:'b3'}];
function bind(){
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = function() {
console.log(i);
}
}
};
bind();
buttons[0].onclick();//3
buttons[1].onclick();//3
buttons[2].onclick();//3
答:运行结果为依次打印3 3 3
上面的代码在循环里包含着一个闭包,闭包可以简单理解为:当函数可以记住并访问所的词法作用域时,就产生了闭包,即使函数是在当前词法作用域之外执行。在for循环里面的匿名函数执行 console.log(i)语句的时候,由于匿名函数里面没有i这个变量,所以这个i他要从父级函数中寻找i,实际情况是尽管循环中的五个函数是在各个迭代中分别定义的,
但是它们都被封闭在一个共享的全局作用域中,因此实际上只有一个i,当找到这个i的时候,是for循环完毕的i,也就是3,所以这个bind()得到的是一三个相同的函数:
function(){console. log(3)}
所以当运行buttons[0].onclick();和其他两个程序时时都会打印3。
如果要想实现理想中的打印0 1 2的效果,需要更多的闭包作用域,特别是在循环的过程中每个迭代都需要一个闭包作用域。而函数自调用会通过声明立即执行一个函数来创建作用域。
例如:
var buttons = [{name:'b1'},{name:'b2'},{name:'b3'}];
function bind(){
for (var i = 0; i < buttons.length; i++) {
buttons[i].onclick = (function (i){
console.log(i)
}(i));
}
};
bind();
buttons[0].onclick;//0
buttons[1].onclick;//1
buttons[2].onclick;//2
第十一题
function fun(n,o) {
console.log(o)
return {
fun:function(m){
return fun(m,n);
}
};
}
var a = fun(0); a.fun(1); a.fun(2); a.fun(3);
var b = fun(0).fun(1).fun(2).fun(3);
var c = fun(0).fun(1); c.fun(2); c.fun(3);
答:结果为依次打印undefined 0 0 0 undefined 0 1 2 undefined 0 1 1
var a =fun1(0);o没有被赋值打印undefined,a等于返回的一个对象
{fun:function(m){oturn fun1(m,0)};
a.fun(1);fun1(1,0)打印0,返回一个对象
{fun:function(m){oturn fun1(m,1)}}
同理,a.fun(2);fun1(2,0)打印0
同理,a.fun(3);fun1(3.0)打印0
var b=fun1(0).fun(1).fun(2).fun(3);o没有被赋值打印undefined,b等于返回的一个对象
{fun:function(m){oturn fun1(m,0)};
.fun(1)=function(1){oturn fun1(1,0)} 打印0
返回一个对象 {fun:function(m) {oturn fun1(m,1)}}
.fun(2)=function(2) {oturn fun1(2,1)} 打印1
返回一个对象 {fun:function(m) {oturn fun1(m,2)}}
.fun(3)=function(3) {oturn fun1(3,2)} 打印2
var c=fun1(0).fun(1);
o没有被赋值打印undefined,c等于返回的一个对象{fun:function(m){oturn fun1(m,0)};
.fun(1)=function(1){oturn fun1(1,0)} 打印0
返回一个对象{fun:function(m){oturn fun1(m,1)};也就是等于c
c.fun(2);
c={fun:function(m){oturn fun1(m,1)};
所以fun(2)=fun1(2,1),打印1
c.fun(3);
c={fun:function(m){oturn fun1(m,1)};
所以fun(3)=fun1(3,1),打印1
第十二题
var name = 'lili';
var obj = {
name: 'liming',
prop: {
name: 'ivan',
getname: function() {
return this.name;
}
}
};
console.log(obj.prop.getname());//ivan
var test = obj.prop.getname;
console.log(test()); //lili
答:结果为依次打印ivan lili
在从一个变量向另一个变量复制基本类型值和引用类型值时,存在不同。如果从一个变量向另一个变量复制基本类型的值,会在变量对象上创建一个新值,然后把该值复制到为新变量分配的位置上。当从一个变量向另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到为新变量分配的空间中。不同的是,这个值的副本实际上是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此,调用属性或方法改变其中一个变量,就会影响另一个变量。
在本题中,在全局作用域中声明了一个变量test,并把obj.prop.getname;赋值给它,那么test便指向函数function() {return this.name; }。
当引擎运行到console.log(obj.prop.getname());时对obj.prop.getname进行LHS查询,得到函数function() {return this.name; },然后运行函数,
首先说明this并不是指向函数自身或是函数的词法作用域,this的绑定和函数声明的位置没有任何关系,只取决于函数的调用方式。当一个函数被调用时,会创建一个活动记录(有时候也称为执行上下文)。这个记录会包含函数在哪里被调用(调用栈)、函数的调用方法、传入的参数等信息。this就是记录的其中一个属性,会在函数执行的过程中用到。简单来说,this是指调用包含this最近的函数的对象。
而要找到this代表什么,首先要找到调用位置,调用位置就是函数在代码中被调用的位置(而不是声明的位置)。首先最是要分析调用栈(就是为了到达当前执行位置所调用的所有函数)。调用位置就在当前正在执行的函数的前一个调用中。在代码 console.log(obj.prop.getname());
中,this所在函数是被prop调用的,即调用位置是obj的属性prop,所以this.name可以看成obj.prop.name,即ivan。
声明在全局作用域中的变量(var test = obj.prop.getname; )就是全局对象的一个同名属性。它们本质上就是同一个东西,并不是通过复制得到的,就像一个硬币的两面一样。接下来我们可以看到当调用test()时,this.name被解析成了全局变量name。因为在本题中,函数调用时应用了this的默认绑定,因此this指向全局对象。换句话说就是test的调用位置在全局作用域,所以this.name就在全局作用域中匹配,得到name=lili。
JS面试题上下已完结,上一篇链接:JavaScript经典面试题(下),所写详解的大部分知识来自于宝书《JavaScript高级程序设计》,强烈建议还没有看过的程序猿们阅读本书。可能后续还会更新更多内容,敬请期待!