• home > webfront > ECMAS > emphasis >

    JS易错笔试题(一):作用域|变量提升|this指向|正则|异步队列

    Author:zhoulujun Date:

    前端JS基础笔试题集:js易错笔试题收集,收集了js面试题笔试题中的各种,分类型讲解,深入浅出,举一反三。刷笔试题不是为了应付笔试,而是为了实打实地提高JavaScript内功。

    变量及变量提升

    var name = 'World!';
    (function () {
        if (typeof name === 'undefined') {
            var name = 'Jack';
            console.log('Goodbye ' + name);
        } else {
            console.log('Hello ' + name);
        }
    })();

    运行结果为什么是“Goodbye Jack”,即name是undefined而

    变量声明提升的作用在一个函数体内声明的变量,JS解析器都会将其移动到函数体的顶部

    var name = 'World!';
    (function () {
        console.log(name);
        if(2!==2){
            var name;
        }
    })()

    执行的时候有个变量查找的过程,如果在当前函数体内没找到,就会到定义的函数体的外层函数中去寻找,一直向上到全局对象中寻找,还是找不到就会报TypeError错误

    隐式的全局变量

    var a = 1
    function func() {
        a = b = 2
    }
    func()
    console.log(a)
    console.log(b) // ?

    JS中不用var声明的变量默认是全局变量,而这里的连等使的情况更加隐蔽。这里的b是全局的,因此func外可以访问。

    变量声明早于代码运行(Scoping and Hoisting

    var uname = 'jack'
    function change() {
        console.log(uname) // ?
        var uname = 'lily'
        console.log(uname)
    }
    change()

    这里容易犯迷糊的是第一个alert,如果认为函数change外已经声明赋值了,此时应该是jack,实际函数内又var了一次(虽然var在其后),预解析时仍然会将其置undefined。这也是为什么书里都建议变量声明都放在代码块的最前面。 

    函数声明早于变量声明

    function change() {
        console.log(typeof fn) // ?
        function fn() {
            console.log('hello')
        }
        var fn;
        console.log( fn)// ?
    }
    change()

    change内先alert出fn,后函数声明,再变量声明。如果fn没有函数声明而仅是变量声明,那么结果与5一样是undefined。但这里却是function。即同一个作用域内,函数声明放在代码块后面和前面都没有关系,函数可以正常使用。而变量声明则需先置前,先使用则是undefined。

    类是题目:

    console.log(typeof fn);
    function fn() {};
    var fn

    动态作用域和静态作用域的区别:

    上面题目改造下:

    var uname = 'jack'
    function change() {
      console.log(uname)
    }
    
    (function () {
      console.log(uname) // ?
      var uname = 'lily'
      change();
    })()

    静态作用域又称之为词法作用域:即词法作用域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,它会根据函数定义的环境中查询。

    JS 的变量是遵循静态作用域的,在上述代码中会打印出jack  而非 lily,因为 static 函数在作用域创建的时候,记录的 foo 是 jack,如果是动态作用域的话,那么它应该打印出 lily

    • 静态作用域是产生闭包的关键,即它在代码写好之后就被静态决定它的作用域了。

    • 动态域的函数中遇到既不是形参也不是函数内部定义的局部变量的变量时,到函数调用的环境去查询在

     JS 中,关于 this 的执行是基于动态域查询的,下面这段代码打印出 1,如果按静态作用域的话应该会打印出 2

    var foo = 1;
    
    var obj = {
        foo: 2,
        bar: function() {
            console.log(this.foo);
        }
    };
    
    var bar = obj.bar;
    bar();


    变量作用域

    在 JavaScript 中, 作用域为可访问变量,对象,函数的集合。JavaScript 函数作用域: 作用域在函数内修改。

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

    这一题的陷阱是,在函数表达式中有两个赋值,但a是用关键字var 来声明的,这意味着a是局部变量,而b则被赋予为全局变量。

    另一个陷阱是,它并没有使用严格模式(use strict)。在函数里面,如果启用了严格模式,


    在循环内使用闭包

    变量声明提升的作用在一个函

    const arr = [10, 12, 15, 21];
    for (var i = 0; i < arr.length; i++) {
     setTimeout(function() {
     console.log('The index of this number is: ' + i);
     }, 0);
    }

    如果运行上面代码,3秒延迟后你会看到,实际上每次打印输出是4,而不是期望的0,1,2,3 。

    为了正确理解为什么会发生这种情况,在JavaScript中很有用,这正是面试官真正的意图。

    其原因是因为 setTimeout 函数创建了一个可以访问其外部作用域的函数(也就是我们经常说的闭包),每个循环都包含了索引i。

    3秒后,该函数被执行并且打印出i的值,其在循环结束时为4,因为它的循环周期经历了0,1,2,3,4,并且循环最终在4时停止。

    第一个,闭包,访问全局作用域

    const arr = [10, 12, 15, 21];
    for (var i = 0; i < arr.length; i++) {
     // 通过传递变量 i
     // 在每个函数中都可以获取到正确的索引
     setTimeout(function(i_local) {
     return function() {
     console.log('The index of this number is: ' + i_local);
     }
     }(i), 0);
    }

    第二个,匿名函数,访问函数内变量

    const arr = [10, 12, 15, 21];
    for (var i = 0; i < arr.length; i++) {
        // 通过传递变量 i
        // 在每个函数中都可以获取到正确的索引
        (function (i_local) {
            setTimeout(function() {
                console.log('The index of this number is: ' + i_local);
            }, 30);
        })(i)
    }

    第三,用let

    const arr = [10, 12, 15, 21];
    for (let i = 0; i < arr.length; i++) {
     // 使用ES6的let语法,它会创建一个新的绑定
     // 每个方法都是被单独调用的
     // 更多详细信息请阅读: http://exploringjs.com/es6/ch_variables.html#sec_let-const-loop-heads
     setTimeout(function() {
     console.log('The index of this number is: ' + i);
     }, 0);
    }

    第四,数组

    const arr = [10, 12, 15, 21];
    for (var i = 0; i < arr.length; i++) {
     let arr2 = Object.assign([],arr)
     setTimeout(function() {
     console.log('The index of this number is: ' + arr2.shift());
     }, 0);
    }

    类似的题目还有,dom元素查找,数组dom的index值,如:

    var lis = document.links;
    for(var i = 0, length = lis.length; i < length; i++) {
        lis[i].index = i;
        lis[i].onclick = function() {
            alert(this.index);
        };
    }

    这里还涉及一个问题,就是JS队列执行问题

    JS闭包应用

    js实现类似于add(1)(2)(3)调用方式的方法
    群里有人说实现类似add(1)(2)(3)调用方式的方法,结果马上有人回答:

    var add = function(a){
      return function(b){
        return function(c){
          return a+b+c;
        };
      };
    };
    add(1)(2)(3); //6

    没错!那要是add(1)(2)(3)(4) 这样4个调用呢,那这个肯定不适用了。

    这种就是类似于执行一个函数返回函数自身值:

    function add(x) {
      var sum = x;
      var tmp = function (y) {
        sum = sum + y;
        return tmp;
      };
      tmp.toString = function () {
        return sum;
      };
      return tmp;
    }

    如果参数自由组合add(1)(2,3)(4,5,6)呢?

    function add() {
      let args=[].slice.call(arguments);
      let reFun=function () {
        let add=function () {
          [].push.apply(args,[].slice.call(arguments));
          return add;
        };
        add.toString=function () {
          return args.reduce(function (a,b) {
            return a+b;
    
          })
        };
        return add;
    
      };
      return reFun.apply(null,args);
    }
    console.log(+sum(2,3)(3,2));

    首先要一个数记住每次的计算值,所以使用了闭包,在tmp中记住了x的值,第一次调用add(),初始化了tmp,并将x保存在tmp的作用链中,然后返回tmp保证了第二次调用的是tmp函数,后面的计算都是在调用tmp, 因为tmp也是返回的自己,保证了第二次之后的调用也是调用tmp,而在tmp中将传入的参数与保存在作用链中x相加并付给sum,这样就保证了计算;

    但是在计算完成后还是返回了tmp这个函数,这样就获取不到计算的结果了,我们需要的结果是一个计算的数字那么怎么办呢,首先要知道JavaScript中,打印和相加计算,会分别调用toString或valueOf函数,所以我们重写tmp的toString和valueOf方法,返回sum的值;

    js执行队列

    console.time('timer');
    function f1() {
        console.log('fn1');
    }
    
    function f2() {
        console.log('fn1');
    }
    
    setTimeout(f1,100);
    setTimeout(f2,200);
    function  wait(timer) {
        let now=+new Date();
        while(new Date()-now<timer){
        }
    }
    wait(500);
    console.timeEnd('timer');

    timer: 500.308ms  fn1  fn1

    全部执行完后,才会执行异步函数

    JS原型与构造函数

    构造函数不需要显式声明返回值

    function foo(){ 
      return foo; 
    }
    
    console.log(new foo() instanceof foo);

    因为new 的规则是,如果函数返回对象则就是这个对象,否则是函数中的this

    所以 new foo() 其实就是 foo,所以foo instanceof foo 是false

    new foo() === foo 会是 true

    当代码new f()执行时,下面事情将会发生:

     1.一个新对象被创建。它继承自f.prototype

     2.构造函数f被执行。执行的时候,相应的传参会被传入,同时上下文(this)会被指定为这个新实例。new f等同于new f(),只能用在不传递任何参数的情况。

     3.如果构造函数返回了一个“对象”,那么这个对象会取代整个new出来的结果。如果构造函数没有返回对象,那么new出来的结果为步骤1创建的对象, 

    ps:一般情况下构造函数不返回任何值,不过用户如果想覆盖这个返回值,可以自己选择返回一个普通对象来覆盖。当然,返回数组也会覆盖,因为数组也是对象。 

    于是,我们这里的new f()返回的仍然是函数f本身,而并非他的实例 

    换段代码

    function foo(){ 
      return this; 
    }
    
    let f = new foo();
    console.log(f instanceof foo);

    如何实现一个 new

    function _new (fn, ...arg) {
        var obj = Object.create(fn.prototype)
        const result = fn.apply(obj, ...arg)
        return Object.prototype.toString.call(result) == '[object Object]' ? result : obj
    }

    首先创建一个空的对象,空对象的 ___proto____属性指向构造函数的原型对象

    把上面创建的空对象赋值构造函数内部的this,用构造函数内部的方法修改空对象

    如果构造函数返回一个非基本类型的值,则返回这个值,否则上面创建的对象

    关于js原型

    function A(){
    }
    function B(a){
        this.a = a;
    }
    function C(a){
        if(a){
            this.a = a;
        }
    }
    A.prototype.a = 1;
    B.prototype.a = 1;
    C.prototype.a = 1;
    
    console.log(new A().a);
    console.log(new B().a);
    console.log(new C(2).a);

    分析:

    console.log(new A().a);  //new A()为构造函数创建的对象,本身没有a属性,所以向它的原型去找,发现原型的a属性的属性值为1,故该输出值为1;

    console.log(new B().a);  //new B()为构造函数创建的对象,该构造函数有参数a,但该对象没有传参,故该输出值为undefined;

    console.log(new C(2).a);  //new C()为构造函数创建的对象,该构造函数有参数a,且传的实参为2,执行函数内部,发现if为真,执行this.a = 2,故属性a的值为2;

    故这三个的输出值分别为:1、undefined、2.  

    再来一道复杂的:

    function Foo() {
      getName = function () {
         console.log(1);
      };
      return this;
    }
    Foo.getName = function () {
       console.log(2);
    };
    Foo.prototype.getName = function () {
       console.log(3);
    };
    var getName = function () {
      console.log(4);
    };
    function getName() {
       console.log(5);
    }
    //请写出以下输出结果:
    Foo.getName();
    getName();
    Foo().getName();
    getName();
    new Foo.getName();
    new Foo().getName();
    new new Foo().getName();

    答案是:

    1. Foo.getName();//2

    2. getName();//4

    3. Foo().getName();//1

      Foo函数的第一句 getName = function () { alert (1); }; 是一句函数赋值语句,注意它没有var声明,所以先向当前Foo函数作用域内寻找getName变量,没有。再向当前函数作用域上层,即外层作用域内寻找是否含有getName变量,找到了,也就是第二问中的alert(4)函数,将此变量的值赋值为 function(){alert(1)}。

    4. getName();//1

    5. new Foo.getName();//2 等价于:new (Foo.getName)(); 

    6. new Foo().getName();//3 等价于:(new Foo()).getName() 

      Foo此时作为构造函数却有返回值,所以这里需要说明下js中的构造函数返回值问题。若有返回值是非引用类型则与无返回值相同,实际返回其实例化对象。若返回值是引用类型,则实际返回值为这个引用类型。而this在构造函数中本来就代表当前实例化对象,遂最终Foo函数返回实例化对象。之后调用实例化对象的getName函数,因为在Foo构造函数中没有为实例化对象添加任何属性,遂到当前对象的原型对象(prototype)中寻找getName

    7. new new Foo().getName();//3 等价于:new ((new Foo()).getName)();

    详细解说请查看:一道常被人轻视的web前端常见面试题(JS)——http://www.jb51.net/article/79461.htm

    这里还会牵涉符号优先级,可以如下出题

    符号优先级 对象赋值 深入理解“连等赋值”问题

    var foo = { n: 1 };
    var bar = foo;
    foo.x = foo = { n: 2 };
    console.log(foo.x); // undefined


    考察this

    var length = 10
    function fn(){
        console.log(this.length)
    }
    var obj = {
        length: 5,
        method: function(fn) {
            fn() // ?
            arguments[0]() // ?
        }
    }
    obj.method(fn)

    输出为:undefined  1

    这里的坑主要是arguments,我们知道取对象属于除了点操作符还可以用中括号,这里fn的scope是arguments,即fn内的this===arguments,调用时仅传了一个参数fn,因此length为1。可以简化为

    var length=10
    function fn(){
        console.log(this.length)
    }
    function test(fn){
        arguments[0]()
    }
    test(fn)

    输出1

    this 的指向

    var name = "windowsName";
    function a() {
        var name = "Cherry";
        console.log(this.name);   // windowsName
        console.log("inner:" + this); // inner: Window
    }
    a();
    console.log("outer:" + this)   // outer: Window

    这个相信大家都知道为什么 log 的是 windowsName,因为根据刚刚的那句话“this 永远指向最后调用它的那个对象”,我们看最后调用 a 的地方 a();,前面没有调用的对象那么就是全局对象 window,这就相当于是 window.a();注意,这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined。

    函数表达式具名(函数声明同时赋值给另一个变量)或函数声明立即执行时,名仅在该函数内可访问

    ~function() {
        console.log(typeof next) // ?
        ~function next() {
            console.log(typeof next) // ?
        }()
    }()

    外层匿名函数自执行,打印next,接着内层具名函数自执行。会发现具名的next仅在其自身函数体内可访问,即输出为function。外面是不可见的,typeof就为undefined了。(注:此题IE6/7/8中输出为function function, 标准浏览器为undefined function)

    同样的情况也发生在将具名函数赋值给一个变量时,如下

    var func = function a() {
        console.log(typeof a)
    }
    func() // ?
    console.log(typeof a) // ?

    这条规则是标准中(ES3/ES5)都已明确指出,但IE6、7、8没有严格遵从。可参见w3help的分析或李松峰老师的翻译《命名函数表达式探秘

     类是题目:

    if('a' in window) {
      var a = 10;
    }
     
    console.log(a);//10

    切记只有function(){}内新声明的才能是局部变量,while{…}、if{…}、for(..) 之内的都是全局变量(除非本身包含在function内)。

    给基本类型数据添加属性,不报错,但取值时是undefined

    a = 3
    a.prop = 4
    alert(a + a.prop) // ?

    变量a为数字3,给其添加prop属性,值为4(奇怪吧在JS中这是允许的,且不会有语法错误)。然后alert出a+a.prop的结果。结果是NaN。a.prop为undefined,3+undefined为NAN。

    举一反三,给字符串添加属性,字符串也允许。结果呢?


    理解稀疏数组

    var ary = [0,1,2];
    ary[10] = 10;
    ary=ary.filter(function(x) { return x === undefined;});
    console.log(ary)

    答案是[]

    [译]JavaScript中的稀疏数组与密集数组

    ==

    []==[]

    == 是万恶之源, 看上图

    答案是 false

    我们先来考虑这个问题,console.log([] == false)会打印什么呢?

    答案是true。为什么呢?

    首先,因为当"=="号两边其中一个是布尔值的话,先把它转换为数字(ECMAScript的规范)。于是就变成了求[] == 0。

    [] == [] 这个好理解. 当两个值都是对象 (引用值) 时, 比较的是两个引用值在内存中是否是同一个对象. 因为此 [] 非彼 [], 虽然同为空数组, 确是两个互不相关的空数组, 自然 == 为 false.

    [] == ![] 这个要牵涉到 JavaScript 中不同类型 == 比较的规则, 具体是由相关标准定义的. ![] 的值是 false, 此时表达式变为 [] == false, 参照标准, 该比较变成了 [] == ToNumber(false), 即 [] == 0. 这个时候又变成了 ToPrimitive([]) == 0, 即 '' == 0, 接下来就是比较 ToNumber('') == 0, 也就是 0 == 0, 最终结果为 true.[] == {} 同第一个.

    js arguments

    function sidEffecting(ary) {
      ary[0] = ary[2];
    }
    function bar(a,b,c=3) {
      c = 10
      sidEffecting(arguments);
      return a + b + c;
    }
    console.log(bar(1,1,1))

    答案是12

    即使修改如下

    function bar(a,b,c=3) {
        arguments[0]=10
        arguments[1]=10
        arguments[2]=10
        return a + b + c;
    }
    console.log(bar(1,1,1))

    修改arguments无效!

    如果是其中的参数没有赋予默认值

    function sidEffecting(ary) {
      ary[0] = ary[2];
    }
    function bar(a,b,c) {
      c = 10
      sidEffecting(arguments);
      return a + b + c;
    }
    console.log(bar(1,1,1))

    答案就是21

    这是一个大坑, 尤其是涉及到 ES6语法的时候


    知识点: Functions/arguments

    首先 The arguments object is an Array-like object corresponding to the arguments passed to a function.

    也就是说 arguments 是一个 object, c 就是 arguments[2], 所以对于 c 的修改就是对 arguments[2] 的修改.

    所以答案是 21.

    然而!!!!!!


    当函数参数涉及到 any rest parameters, any default parameters or any destructured parameters 的时候, 这个 arguments 就不在是一个 mapped arguments object 了.....

    var x = [].reverse;
    x();

    函数参数没有赋默认值,arguments 是一个 object,修改arguments ,函数参数跟着修改。如果参数赋默认值了,这个 arguments 就不在是一个 mapped arguments object 了.....

    js 正则表达式

    查找字符串中出现最多的字符和个数

    hashTable实现

    hash table方式:
    var s = 'aaabbbcccaaabbbaaa';
    var obj = {};
    var maxn = -1;
    var letter;
    for(var i = 0; i  maxn) {
          maxn = obj[s[i]];
          letter = s[i];
        }
      } else {
        obj[s[i]] = 1;
        if(obj[s[i]] > maxn) {
          maxn = obj[s[i]];
          letter = s[i];
        }
      }
    }
    alert(letter + ': ' + maxn);

    正则表达式实现

    var str="sssfgtdfssddfsssfssss";
    var num=0;
    var value=null;
    function max(){
        var new_str=str.split("").sort().join("");
        var re=/(\w)\1+/g;                 
        //没有\1,re就是一整个排好序的字符串,有了\1就是出现过的有重复的取出来,\1表示跟前面第一个子项是相同的
        new_str.replace(re,function($0,$1){    
        //$0代表取出来重复的一个个整体,如[s,s...],[f,f..],[d,d....]  $1代表这个整体里的字符
            if(num<$0.length){
                num=$0.length;
                value=$1
            }
        });
        alert(value+":"+num)
    };
    max(str);

    简化如下

    function getMaxRepeatChartInString (string) {
        if (!string || string.length < 2) {
            return string
        }
        let arr = string.split('').sort().join('').match(/(\w+)\1/g).sort(function (a, b) {
            return a.length-b.length
        })
        return arr.pop()[0]
    }
    
    let testStr = 'aa,c2,cc,33,66,446sddfsdasdfasddddds'
    console.log(getMaxRepeatChartInString(testStr))


    将url的查询参数解析成字典对象

    这个题目不约而同的出现在了多家公司的面试题中,当然也是因为太过于典型,解决方案无非就是拆字符或者用正则匹配来解决,我个人强烈建议用正则匹配,因为url允许用户随意输入,如果用拆字符的方式,有任何一处没有考虑到容错,就会导致整个js都报错。而正则就没有这个问题,他只匹配出正确的配对,非法的全部过滤掉,简单,方便。


    function getQueryObject(url) {
        var obj = {};
        url||(url=window.location.search)
        url.replace(/([^?&=]+)=([^?&=]*)/g, function ($0, $1, $2) {
            console.log($0,$1,$2)
            obj[decodeURIComponent($1)] = String(decodeURIComponent($2));
        });
        return obj;
    }


    参考文章:

    https://segmentfault.com/a/1190000004224719

    https://segmentfault.com/a/1190000002965140

    https://zhuanlan.zhihu.com/p/25351196




    转载本站文章《JS易错笔试题(一):作用域|变量提升|this指向|正则|异步队列》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/js6/2017_0712_8031.html