几道javascript练习题

走在前端的大道上javascript

问题1: 做用域(Scope)

考虑如下代码:css

(function() {
   var a = b = 5;
})();
 
console.log(b);

控制台(console)会打印出什么?html

答案前端

上述代码会打印出5。java

这个问题的陷阱就是,在当即执行函数表达式(IIFE)中,有两个赋值,可是其中变量a使用关键词var来声明。这就意味着a是这个函数的局部变量。与此相反,b被分配给了全局做用域(译注:也就是全局变量)。node

这个问题另外一个陷阱就是,在函数中没有使用”严格模式” ('use strict';)。若是 严格模式开启,那么代码就会报错 ” Uncaught ReferenceError: b is not defined” 。请记住,若是这是预期的行为,严格模式要求你显式地引用全局做用域。因此,你须要像下面这么写:web

(function() {
   'use strict';
   var a = window.b = 5;
})();
 
console.log(b);

问题2: 建立 “原生(native)” 方法

在 String 对象上定义一个 repeatify 函数。这个函数接受一个整数参数,来明确字符串须要重复几回。这个函数要求字符串重复指定的次数。举个例子:面试

console.log('hello'.repeatify(3));

应该打印出hellohellohello.算法

答案数组

一个可行的作法以下:

String.prototype.repeatify = String.prototype.repeatify || function(times) {
   var str = '';
 
   for (var i = 0; i < times; i++) {
      str += this;
   }
 
   return str;
};

这个问题测试了开发人员对 javascript 中继承及原型(prototype)属性的知识。这也验证了开发人员是否有能力扩展原生数据类型功能(虽然不该该这么作)。

在这里,另外一个关键点是,看你怎样避免重写可能已经定义了的方法。这能够经过在定义本身的方法以前,检测方法是否已经存在。

String.prototype.repeatify = String.prototype.repeatify || function(times) {/* code here */};

当你被问起去扩展一个Javascript方法时,这个技术很是有用。

另外一个:重复输出一个给定的字符串(str第一个参数)n 次 (num第二个参数),若是第二个参数num不是正数的时候,返回空字符串。

function repeatStringNumTimes(str, num) {
  return str;
}
repeatStringNumTimes("abc", 3);

提供测试状况:

repeatStringNumTimes("*", 3) //应该返回 "***".
repeatStringNumTimes("abc", 3) //应该返回 "abcabcabc".
repeatStringNumTimes("abc", 4) //应该返回 "abcabcabcabc".
repeatStringNumTimes("abc", 1) //应该返回 "abc".
repeatStringNumTimes("*", 8) //应该返回 "********".
repeatStringNumTimes("abc", -2) //应该返回 "".

解题思路:

三种方法:

使用 while 循环
使用递归
使用ES6 repeat()

方法1:经过 while 循环重复输出一个字符串

这多是最常规的解题思路。while 语句只要指定的条件计算结果为true的时候,就执行其语句。while 语句结构大概是这样的:

while (condition)
  statement

在每次经过循环以前计算条件结果。若是条件为true,则执行语句。若是条件为false,则执行继续 while 循环以后的任何语句。

只要条件为true,语句就会执行。 这里是解决方案:

function repeatStringNumTimes(string, times) {
  // 第1步. 常见一个空字符,用来寄存重复的字符串
  var repeatedString = "";
 
  // 第2步. 设置 while 循环的条件为(times > 0) 做为检查
  while (times > 0) { // 只要 times 大于 0, 语句就会执行
    // 执行语句 statement
    repeatedString += string; // 等价于 repeatedString = repeatedString + string; 
    times--; // 递减,等价于 times = times - 1; 
  }
  /* while循环逻辑
          条件        T/F    repeatedString += string   结果          次数
    1th   (3 > 0)    true    "" + "abc"                "abc"          2
    2th   (2 > 0)    true    "abc" + "abc"             "abcabc"       1
    3th   (1 > 0)    true    "abcabc" + "abc"          "abcabcabc"    0
    4th   (0 > 0)    false
    }
  */
  
  // 第3步. 返回重复字符串
  return repeatedString; // "abcabcabc"
}
 
repeatStringNumTimes("abc", 3);

去掉注释后:

function repeatStringNumTimes(string, times) {
  var repeatedString = "";
  while (times > 0) {
    repeatedString += string;
    times--;
  }
  return repeatedString;
}
repeatStringNumTimes("abc", 3);

好,轻松完成!不过这里还能够有几个变种:

对于老前端来讲,首先一个可能会将字符串拼接,修改成 数组join()拼接字符串,例如:

function repeatStringNumTimes(string, times) {
  var repeatedArr = []; //
  while (times > 0) {
    repeatedArr.push(string);
    times--;
  }
  return repeatedArr.join("");
}
repeatStringNumTimes("abc", 3)

不少老前端都有用数组join()拼接字符串的“情怀”,由于很早之前广泛认为数组join()拼接字符串比字符串+拼接速度要快得多。不过如今未必,例如,V8 下+拼接字符串,要比数组join()拼接字符串快。我用这两个方法测试了3万次重复输出,只相差了几毫秒。

另外一个变种能够用 for 循环:

function repeatStringNumTimes(string, times) {
  var repeatedString = "";
  for(var i = 0; i < times ;i++) {
    repeatedString += string;
  }
  return repeatedString;
}
repeatStringNumTimes("abc", 3)

方法2:经过条件判断和递归重复输出一个字符串

递归是一种经过重复地调用函数自己,直到它达到达结果为止的迭代操做的技术。为了使其正常工做,必须包括递归的一些关键特征。

第一种是基本状况:一个语句,一般在一个条件语句(如if)中,中止递归。

第二种是递归状况:调用递归函数自己的语句。

这里是解决方案:

function repeatStringNumTimes(string, times) {
  // 步骤1.检查 times 是否为负数,若是为 true 则返回一个空字符串 
  if (times < 0) {
    return "";
  }
  
  // 步骤2.检查times是否等于1,若是是,返回字符串自己。
  if (times === 1) {
    return string;
  }
  
  // 步骤3. 使用递归
  else {
    return string + repeatStringNumTimes(string, times - 1); // return "abcabcabc";
  }
  /* 
    递归方法的第一部分你须要记住,你不会只调用一次,您将有好几个嵌套调用
                 times       string + repeatStringNumTimes(string, times - 1)
      1st call   3           "abc" + ("abc", 3 - 1)
      2nd call   2           "abc" + ("abc", 2 - 1)
      3rd call   1           "abc" => if (times === 1) return string;
      4th call   0           ""   => if (times <= 0) return "";
    递归方法的第二部分
      4th call will return      ""
      3rd call will return     "abc"
      2nd call will return     "abc"
      1st call will return     "abc"
    最后调用是串联全部字符串
    return "abc" + "abc" + "abc"; // return "abcabcabc";
  */
}
repeatStringNumTimes("abc", 3);

去掉注释后:

function repeatStringNumTimes(string, times) {
  if(times < 0) 
    return "";
  if(times === 1) 
    return string;
  else 
    return string + repeatStringNumTimes(string, times - 1);
}
repeatStringNumTimes("abc", 3);

方法3:使用ES6 repeat() 方法重复输出一个字符串

这个解决方案比较新潮,您将使用 String.prototype.repeat() 方法:

repeat() 方法构造并返回一个新字符串,该字符串包含被链接在一块儿的指定数量的字符串的副本。 这个方法有一个参数 count 表示重复次数,介于0和正无穷大之间的整数 : [0, +∞) 。表示在新构造的字符串中重复了多少遍原字符串。重复次数不能为负数。重复次数必须小于 infinity,且长度不会大于最长的字符串。

这里是解决方案:

function repeatStringNumTimes(string, times) {
  //步骤1.若是 times 为正数,返回重复的字符串
  if (times > 0) { // (3 > 0) => true
    return string.repeat(times); // return "abc".repeat(3); => return "abcabcabc";
  }
  
  //Step 2. Else 若是times是负数,若是为true则返回一个空字符串
  else {
    return "";
  }
}
 
repeatStringNumTimes("abc", 3);

去掉注释后:

function repeatStringNumTimes(string, times) {
  if (times > 0)
    return string.repeat(times);
  else
    return "";
}
repeatStringNumTimes("abc", 3);

您可使用三元表达式做为 if/else 语句的快捷方式,以下所示:

function repeatStringNumTimes(string, times) {
  return times > 0 ? string.repeat(times) : "";
}
repeatStringNumTimes("abc", 3);

问题3: 变量提高(Hoisting)

执行如下代码的结果是什么?为何?

function test() {
   console.log(a);
   console.log(foo());
   
   var a = 1;
   function foo() {
      return 2;
   }
}
 
test();

答案

这段代码的执行结果是undefined 和 2。

这个结果的缘由是,变量和函数都被提高(hoisted) 到了函数体的顶部。所以,当打印变量a时,它虽存在于函数体(由于a已经被声明),但仍然是undefined。换言之,上面的代码等同于下面的代码:

function test() {
   var a;
   function foo() {
      return 2;
   }
 
   console.log(a);
   console.log(foo());
   
   a = 1;
}
 
test();

问题4: 在javascript中,this是如何工做的

如下代码的结果是什么?请解释你的答案。

var fullname = 'John Doe';
var obj = {
   fullname: 'Colin Ihrig',
   prop: {
      fullname: 'Aurelio De Rosa',
      getFullname: function() {
         return this.fullname;
      }
   }
};
 
console.log(obj.prop.getFullname());
 
var test = obj.prop.getFullname;
 
console.log(test());

答案

这段代码打印结果是:Aurelio De Rosa 和 John Doe 。缘由是,JavaScript中关键字this所引用的是函数上下文,取决于函数是如何调用的,而不是怎么被定义的。

在第一个console.log(),getFullname()是做为obj.prop对象的函数被调用。所以,当前的上下文指代后者,而且函数返回这个对象的fullname属性。相反,当getFullname()被赋值给test变量时,当前的上下文是全局对象window,这是由于test被隐式地做为全局对象的属性。基于这一点,函数返回window的fullname,在本例中即为第一行代码设置的。

问题5: call() 和 apply()

修复前一个问题,让最后一个console.log() 打印输出Aurelio De Rosa.

答案

这个问题能够经过运用call()或者apply()方法强制转换上下文环境。若是你不了解这两个方法及它们的区别,我建议你看看这篇文章 function.call和function.apply之间有和区别?。 下面的代码中,我用了call(),但apply()也能产生一样的结果:

console.log(test.call(obj.prop));

问题6: 闭包(Closures)

考虑下面的代码:

var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
   nodes[i].addEventListener('click', function() {
      console.log('You clicked element #' + i);
   });
}

请问,若是用户点击第一个和第四个按钮的时候,控制台分别打印的结果是什么?为何?

答案

上面的代码考察了一个很是重要的 JavaScript 概念:闭包(Closures)。对于每个JavaScript开发者来讲,若是你想在网页中编写5行以上的代码,那么准确理解和恰当使用闭包是很是重要的。若是你想开始学习或者只是想简单地温习一下闭包,那么我强烈建议你去阅读 Colin Ihrig 这个教程:JavaScript Closures Demystified

也就是说,代码打印两次You clicked element #NODES_LENGTH,其中NODES_LENGTH是nodes的结点个数。缘由是在for循环完成后,变量i的值等于节点列表的长度。此外,由于i在代码添加处理程序的做用域中,该变量属于处理程序的闭包。你会记得,闭包中的变量的值不是静态的,所以i的值不是添加处理程序时的值(对于列表来讲,第一个按钮为0,对于第二个按钮为1,依此类推)。在处理程序将被执行的时候,在控制台上将打印变量i的当前值,等于节点列表的长度。

问题7: 闭包(Closures)

修复上题的问题,使得点击第一个按钮时输出0,点击第二个按钮时输出1,依此类推。

答案

有多种办法能够解决这个问题,下面主要使用两种方法解决这个问题。

第一个解决方案使用当即执行函数表达式(IIFE)再建立一个闭包,从而获得所指望的i的值。实现此方法的代码以下:

var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
   nodes[i].addEventListener('click', (function(i) {
      return function() {
         console.log('You clicked element #' + i);
      }
   })(i));
}

另外一个解决方案不使用IIFE,而是将函数移到循环的外面。这种方法由下面的代码实现:

function handlerWrapper(i) {
   return function() {
      console.log('You clicked element #' + i);
   }
}
 
var nodes = document.getElementsByTagName('button');
for (var i = 0; i < nodes.length; i++) {
   nodes[i].addEventListener('click', handlerWrapper(i));
}

问题8:数据类型

考虑以下代码:

console.log(typeof null);
console.log(typeof {});
console.log(typeof []);
console.log(typeof undefined);

答案

前面的问题彷佛有点傻,但它考察 typeof 操做符的知识。不少JavaScript开发人员不知道typeof的一些特性。在此示例中,控制台将显示如下内容:

object
object
object
undefined

最使人惊讶的输出结果多是第三个。大多数开发人员认为typeof []会返回Array。若是你想测试一个变量是否为数组,您能够执行如下测试:

var myArray = [];
if (myArray instanceof Array) {
   // do something...
}

问题9:事件循环

下面代码运行结果是什么?请解释。

function printing() {
   console.log(1);
   setTimeout(function() { console.log(2); }, 1000);
   setTimeout(function() { console.log(3); }, 0);
   console.log(4);
}
printing();

答案

输出结果:

1
4
3
2

想知道为何输出顺序是这样的,你须要弄了解setTimeout()作了什么,以及浏览器的事件循环原理。浏览器有一个事件循环用于检查事件队列,处理延迟的事件。UI事件(例如,点击,滚动等),Ajax回调,以及提供给setTimeout()和setInterval()的回调都会依次被事件循环处理。所以,当调用setTimeout()函数时,即便延迟的时间被设置为0,提供的回调也会被排队。回调会呆在队列中,直到指定的时间用完后,引擎开始执行动做(若是它在当前不执行其余的动做)。所以,即便setTimeout()回调被延迟0毫秒,它仍然会被排队,而且直到函数中其余非延迟的语句被执行完了以后,才会执行。

有了这些认识,理解输出结果为“1”就容易了,由于它是函数的第一句而且没有使用setTimeout()函数来延迟。接着输出“4”,由于它是没有被延迟的数字,也没有进行排队。而后,剩下了“2”,“3”,二者都被排队,可是前者须要等待一秒,后者等待0秒(这意味着引擎完成前两个输出以后立刻进行)。这就解释了为何“3”在“2”以前。

问题10:算法

写一个isPrime()函数,当其为质数时返回true,不然返回false。

答案

我认为这是面试中最多见的问题之一。然而,尽管这个问题常常出现而且也很简单,可是从被面试人提供的答案中能很好地看出被面试人的数学和算法水平。

首先, 由于JavaScript不一样于C或者Java,所以你不能信任传递来的数据类型。若是面试官没有明确地告诉你,你应该询问他是否须要作输入检查,仍是不进行检查直接写函数。严格上说,应该对函数的输入进行检查。

第二点要记住:负数不是质数。一样的,1和0也不是,所以,首先测试这些数字。此外,2是质数中惟一的偶数。没有必要用一个循环来验证4,6,8。再则,若是一个数字不能被2整除,那么它不能被4,6,8等整除。所以,你的循环必须跳过这些数字。若是你测试输入偶数,你的算法将慢2倍(你测试双倍数字)。能够采起其余一些更明智的优化手段,我这里采用的是适用于大多数状况的。例如,若是一个数字不能被5整除,它也不会被5的倍数整除。因此,没有必要检测10,15,20等等。若是你深刻了解这个问题的解决方案,我建议你去看相关的Wikipedia介绍。

最后一点,你不须要检查比输入数字的开方还要大的数字。我感受人们会遗漏掉这一点,而且也不会由于此而得到消极的反馈。可是,展现出这一方面的知识会给你额外加分。

如今你具有了这个问题的背景知识,下面是总结以上全部考虑的解决方案:

function isPrime(number) {
   // If your browser doesn't support the method Number.isInteger of ECMAScript 6,
   // you can implement your own pretty easily
   if (typeof number !== 'number' || !Number.isInteger(number)) {
      // Alternatively you can throw an error.
      return false;
   }
   if (number < 2) {
      return false;
   }
 
   if (number === 2) {
      return true;
   } else if (number % 2 === 0) {
      return false;
   }
   var squareRoot = Math.sqrt(number);
   for(var i = 3; i <= squareRoot; i += 2) {
      if (number % i === 0) {
         return false;
      }
   }
   return true;
}

问题11:数据类型

var a = {n : 1};
var b = a;
a.x = a = {n : 2};
console.log(a.x);  
console.log(b.x);

解析:

var a = {n : 1};
var b = a;
// 此时b = {n:1};
//若是此时a.n=4,那么b.n也等于4
a.x = a = {n : 2};
// 从右往左赋值,a = {n:2}; 新对象
// b = {n:2},//此时笔者认为b应该仍是{n:1}待考证确认
// a.x 中的a是{n:1}; {n:1}.x = {n:2}; 旧对象
// 由于b和a是引用的关系因此b.x也等于 {n:2}
console.log(a.x); undefined
// 此时的a是新对象,新对象上没有a.x 因此是undefined
console.log(b.x); {n:2}
var i = 10;
i += i *= i;

// i*=i 100
// i+= 这里的i是 =10不是100
console.log(i);

问题12:

if (!("a" in window)) {
    var a = 1;
}

console.log(a);

解析:

在浏览器环境中,全局变量都是window的一个属性,即
var a = 1 等价于 window.a = 1。in操做符用来判断某个属性属于某个对象,能够是对象的直接属性,也能够是经过prototype继承的属性。

再看题目,在浏览器中,若是没有全局变量 a ,则声明一个全局变量 a (ES5没有块级做用域),而且赋值为1。不少人会认为打印的是1。非也,你们不要忘了变量声明会被前置!什么意思呢?题目也就等价于

var a;

if (!("a" in window)) {
    a = 1;
}

console.log(a);

因此其实已经声明了变量a,只不过if语句以前值是undefined,因此if语句压根不会执行。
最后答案就是 undefined

问题13:

var a = 1,
    b = function a(x) {
        x && a(--x);
    };
console.log(a);

解析:
这道题有几个须要注意的地方:

1.变量声明、函数声明会被前置,可是函数表达式并不会,准确说相似变量声明前置,举个栗子:

console.log('b', b); // b undefined
var b = function() {}
console.log('b', b); // b function () {}

2.具名的函数表达式的名字只能在该函数内部取到,举个例子(排除老的IE?):

var foo = function bar () {}

console.log('foo', foo); 
// foo function bar(){}

console.log('bar', bar);
// Uncaught ReferenceError: bar is not defined

综合这两点,再看题目,最后输出的内容就为 1

问题14:

function a(x) {
    return x * 2;
}
var a;
console.log(a);

解析:
函数声明会覆盖变量声明,但不会覆盖变量赋值,举个栗子简单粗暴:

function foo(){
    return 1;
}
var foo;
console.log(typeof foo);    // "function"

函数声明的优先级高于变量声明的优先级,但若是该变量foo赋值了,那结果就彻底不同了:

function foo(){
    return 1;
}
var foo = 1;
console.log(typeof foo);    // "number"

变量foo赋值之后,变量赋值初始化就覆盖了函数声明。这个须要注意
再看题目

function a(x) {
    return x * 2;
}
var a;
console.log(a); // function a(x) {...}

问题15:

function b(x, y, a) {
    arguments[2] = 10;
    console.log(a);
}
b(1, 2, 3);

解析:
这题考察 arguments 对象的用法(详看JavaScript中的arguments对象)
通常状况,arguments与函数参数是动态绑定关系(为何说是通常稍后会解释),因此很好理解,最后输出的是10

可是可是可是,咱们不要忘了一个特殊状况–严格模式,在严格模式中 arguments 与至关于函数参数的一个拷贝,并无动态绑定关系,举个栗子:

'use strict'
// 严格模式!!

function b(x, y, a) {
    arguments[2] = 10;
    console.log(a);
}
b(1, 2, 3); // 3

问题16:

function a() {
    console.log(this);
}
a.call(null);

解析:

function a() {
    console.log(this);
}
a.call(null);

关于 a.call(null); 根据ECMAScript262规范规定:
若是第一个参数传入的对象调用者是null或者undefined的话,call方法将把全局对象(浏览器上是window对象)做为this的值。因此,无论你何时传入null或者 undefined,其this都是全局对象window。因此,在浏览器上答案是输出 window 对象。

可是可是可是,咱们依旧不能忘记一个特殊状况–严格模式,在严格模式中,null 就是 null,undefined 就是 undefined ,举个栗子:

'use strict';
// 严格模式!!

function a() {
    console.log(this);
}
a.call(null); // null
a.call(undefined); // undefined

参考文章:
1.10道典型的JavaScript面试题
2.对匿名函数的深刻理解(完全版) 见评论区
3.你真的知道JS吗?

相关文章
相关标签/搜索