高级JS试题

momo707577045

本文是JS高级入门的配套试题,题目解析的部分内容将引用JS高级入门

题目一(this指针)

    function logName(){
        console.log(this.name);
    }
    function doFun1(fn){
        fn();
    }
    function doFun2(o){
        o.logName();
    }
    var obj = {
        name: "LiLei",
        logName: logName
    };
    var name = "HanMeiMei";
    doFun1(obj.logName);
    doFun2(obj);

解析

题目首先定义了三个函数:
【】logName:打印调用者(即谁去调用它,通常是一个对象)的name属性。
【】doFun1:接收一个函数,并直接运行这个函数。
【】doFun2:接收一个对象,并由这个对象去调用logName函数。
而后定义了两个变量:
【】obj是一个对象,里面定义了name(假设另命名为_name)的字符串变量。定义了logName(假设另起名字为_logName),指向外部的logName函数。
【】name是一个字符串变量。
最后分别对两个函数进行调用,
【】doFun1(obj.logName)。

  • 向函数传递了obj对象内部的_logName,而_logName是指向logName的。所以实际上doFun1接收的是指向logName函数的变量。即等价于doFun1(logName)。

  • 而在doFun1内部是直接执行logName的。没有明确的调用者。则这时等价于由window对象去调用,等价于window.logName。

  • 而再由于所有在全局作用域(注意仅仅是全局作用域)中定义的变量都是window对象下的变量。都可以通过window对象进行访问。
    所以这时访问到的就是name。

  • 所以输出的是HanMeiMei。

【】doFun2(obj)。

  • 传递给doFun2的是obj的地址值,即doFun2中的o指向的就是obj,等价于obj。

  • o.logName是由o去调用logName。相当于obj.logName。

  • 所以找到的是obj内部的name(即我们假定的_name)。

  • 所以打印出的是LiLei。

答案

HanMeiMei
LiLei

题目二(this指针的修改apply)

    function fun(somthing) {
        console.log(this.name, somthing);
    }
    function bindFun(fn, obj) {
        return function () {
            return fn.apply(obj, arguments);
        }
    }
    var obj = {
        name: "LiLei"
    };
    var bar = bindFun(fun, obj);
    var b = bar("HanMeiMei");
    console.log(b);

解析

本题的主要考点是this的相关函数apply。
首先定义了三个函数:
【】fun函数:接收变量,并且打印这个函数的调用者的name值,以及形参something。
【】bindFun函数:接收两个参数,分别是函数以及对象。bindFun没有逻辑操作。只是返回了一个匿名函数(我们假定为_fun)
【】_fun函数:同样没有操作,直接返回一个已经通过apply修改了this的函数的执行结果。(即直接返回一个函数执行结果值,而这个函数的this的值是已经被修改过的)
而后定义了三个变量:
【】obj是一个对象,里面只定义了name字符串变量。
【】bar是一个变量,这个变量的值为bindFun函数执行的结果值。
【】b是一个变量,值为bar函数执行的结果值。
整段代码中,除了定义,一共只执行了3句逻辑语句:
【】在bindFun函数的执行,传递了两个函数,分别是fun函数,obj对象。bindFun函数执行结果是返回一个函数_fun。所以bar指向_fun函数。
【】bar函数的调用,即相当于_fun函数的调用。

  • _fun函数的返回fn.apply(obj, arguments)的运行值;

  • 其中这里的fn是bindFun函数接受的第一个参数fun,即返回的是fun函数。

  • 而这个fun函数进行了apply的函数调用,修改了函数中this的值。

  • this指向为bindFun函数的第二个参数值,即外部的obj变量。

  • apply函数还接受了第二个参数arguments,即_fun函数的参数数组。即bar函数的参数数组【'HanMeiMei'】。作为fun函数的参数。

  • apply函数的作用是马上执行调用者函数,即马上执行bar(即_fun,即fun)函数。

  • fun函数执行是打印this.name,和参数something。this指向obj,所以this.name为LiLei。而something值为【'HanMeiMei'】

  • 所以打印了,LiLeiHanMeiMei。并且没有返回值。

【】b是bar函数的返回值,然而bar函数并没有返回东西,所以是undefined。所以console.log(b)打印的值是undefined。

绕了一圈,整个过程相当于执行。fun.call(obj,'HanMeiMei');

答案

LiLeiHanMeiMei
undefined

题目三(自执行函数)

function logName() {
    console.log(this);
    console.log(this.name);
}
var name = "XiaoMing";
var LiLei = { name: "LiLei", logName: logName };
var HanMeiMei = { name: "HanMeiMei" };
(HanMeiMei.logName = LiLei.logName)();

解析

这题相对简单一些。只考到自执行函数的定义。并没有借助他的特性做代码隔离。只是想考一下是否已经掌握了自执行函数的定义。
首先定义了一个函数:
【】logName,打印出调用者(即this指针的指向),已经调用者的name属性值
而后定义了三个变量:
【】name:字符串变量,由于全局的,所以相当于window对象的属性值。
【】LiLei:定义了对象,该对象包含name值,以及logName(我们假定LiLei的logName重新命名为_logName)。指向外部的logName函数
【】HanMeiMei:定义了对象,该对象只包含name值。
最终只执行了一条语句,两个步骤。
分别是赋值语句:

  • 为HanMeiMei对象添加一个参数logName,赋值为LiLei的_logName。即指向外部的logName。这不是重点,重点是赋值语句的结果是什么?
    是所赋的值,即赋值语句的结果是LiLei.logName,即HanMeiMei.logName = LiLei.logName终止得到的是logName。

  • 然后对这个函数进行调用。即logName(),这时this没有发生改变,还是window,所以输出是打印window对象。已经window.name即XiaoMing。

答案

XiaoMing

题目四(声明提前)

    test();
    var test = 0;
    function test() {
        console.log(1);
    }
    test();
    test = function () {
        console.log(2);
    };
    test();

解析

声明提前的题,把背后的代码执行顺序理顺就好。

首先将声明都放到代码的最上面:

  • var test;//定义变量

  • function test(){console.log(1)}//定义函数

然后执行的操作:

  • test();//函数调用操作

  • test = 0;//赋值操作

  • test();//函数调用操作

  • test = function(){console.log(2)}//赋值操作,将test赋值为函数

  • test();//函数调用操作

所以上述代码等价于(声明提前,先定义,后执行):

  • var test;

  • function test(){console.log(1)}

  • test();//调用函数,输出1

  • test = 0;

  • test();//此时test为0,不是函数,将报错test is not a function

  • test = function(){console.log(2)}//由于JS报错,后面的代码将不被运行

  • test();//由于JS报错,后面的代码将不被运行

综合来说,这里设置了三个考点:

  • 声明提前。

  • 函数的定义与函数赋值的区别,function xx为函数定义,将整体上移。

  • JS报错后,后面的代码将不被运行。

答案

1
Uncaught TypeError: test is not a function

题目五(基本类型与引用类型)

    var name = "LiLei";
    var people = {name: "HanMeiMei"};
    function test1(name) {
        name = "LaoWang";
    }
    function test2(obj) {
        obj.name = "LaoWang";
    }
    test1(name);
    test2(people);
    console.log("name    " + name);
    console.log("name    " + people.name);

解析

题目首先定义了两个函数:
【】test1:接受一个参数,并修改该参数的值。
【】test2:接收一个对象,并修改该对象的属性的值。
而后定义了两个变量:
【】name是一个字符串参数,值为'LiLei'
【】people是一个对象,包含一个name属性。值为'HanMeiMei'
然后分别对两个函数进行调用:
test1(name):即将name作为参数,调用test1函数。

  • 这时函数的内部将产生一个新的参数name(记作_name),它的值等于外部的name的值(LiLei);

  • test1函数将_name的值修改为'LaoWang',但是由于_name和name是两份独立的变量。所以name的值不受改变。

test2(people):将people作为参数,调用test1函数。

  • 这时函数的内部将产生一个新的参数obj,它的值等于外部的people的值;

  • 而people是引用类型,其值为对象{name:"HanMeiMei"}的地址值。所以obj也为对象{name:"HanMeiMei"}的地址值。

  • test2对对象{name:"HanMeiMei"}的name属性进行修改,改为'LaoWang'
    所以obj和people的name值都发生了改变。

这里涉及到两个知识点

  • 基本数据类型是按值访问的,即该变量就存在了实际值。而引用数据类型保存的是则是对实际值的引用(即指向实际值的指针)。

  • 函数形参(即在函数中实际使用的值,如test函数里面的name)和参数的实参(即往调用函数时调用的参数,如test(name)中的name)的值相同,但并不是"同一个值"(在内存中的地址是不同的,相当于var a = b =0;)。

  • 在函数参数的传递,是通过按值传递的。

答案

LiLei
LaoWang

题目六(JS线程闭包)

    执行的结果是什么,分别在什么时间输出
    for (var i = 1; i < 5; i++) {
        setTimeout(function () {
            console.log(i);
        }, i * 1000);
     }

解析

JS线程的规则:程序将先把主逻辑的内容做完,再去读取消息列表,调用消息列表中的回调函数

  • 在这里主逻辑为一个for循环,从i为1到i为4循环执行4次for循环的内容。

  • for循环内,调用setTimeout函数,并设置第二个参数的值为i。注意这里是对setTimeout函数直接进行调用。所以参数中i的值是随for循环改变的。所以相当于执行

    • setTimeout(function () {doSomething}, 1000);

    • setTimeout(function () {doSomething}, 2000);

    • setTimeout(function () {doSomething}, 3000);

    • setTimeout(function () {doSomething}, 4000);

  • 而setTimeout的作用是往消息队列中存放一个回调函数,并在特定时间间隔后执行它。所以该回调函数会在for循环之后完成。

  • for循环执行完时,i的值为5。(因为5<5不成立,结束循环)。所以调用回调函数function(){console.log(i)}时,i的值为5。所以输出为5。

  • 闭包是因为回调函数引用到了for循环的i,回调函数没执行完,i不能被回收。所以还是能访问到。

答案

输出4次,每一秒输出一个5

题目七(作用域陷阱)

    function logName(){
        console.log(name);
    }
    function test () {
        var name = 3;
        logName();
    }
    var name = 2;
    test();

解析

一句话可以解释完:作用域的层级关系与函数定义时所处的层级关系相同
注意是,函数定义时的层级关系,而不是调用时的层级关系。
在这里,logName函数,test函数以及外部的name变量(值为2)处于同一个层级。
所以调用logName时,找到的是外部的name变量。
所以打印出2

答案

2

题目八(隐式闭包)

    function logNum(num) {
        console.log("num   " + num);
    }
    for (var i = 0; i < 2; i++) {
        var fun = logNum.bind(null, i);
        setTimeout(fun, 100);
    }

解析

这道题和第六题很像,但是运行结果并不一样。
原因是bind函数产生了隐式闭包。
为什么bind函数能修改this指针?底层实现笔者不知道,但是我们可以利用闭包的特性通过自执行函数来模拟bind函数。
bind(obj,elem0,elem1,elem2...)函数相当于

(function(_obj,_elem0,_elem1,_elem2...){
    return function(){//这个函数就是调用bind函数的函数
        doSomething;
        //将this.替换成_obj.
        //这里将可能使用到_elem0,_elem1,_elem2参数。
    }
})(obj,elem0,elem1,elem2...)

通过自执行函数返回一个函数,形成一个闭包,内部函数调用的参数是自执行函数的参数,而不是外部的元素。

根据前面第五题的解析函数形参(即在函数中实际使用的值,如test函数里面的name)和参数的实参(即往调用函数时调用的参数,如test(name)中的name)的值相同,但并不是"同一个值"(在内存中的地址是不同的,相当于var a = b =0;)。
doSomething中使用到的参数是自执行函数的形参(_elemX),而不是外部的实参elem。
所以外部的elem的变化对doSomething没有影响。

根据解析,对题目进行改造,相当于

function logNum(num) {
    console.log("num   " + num);
}
for (var i = 0; i < 2; i++) {
    var fun = (function(_i){
        return function(){
            console.log(_i);
        }
    })(i);
    setTimeout(fun, 100);
}

而自执行函数是定义完马上执行的。所以拿到的值_i也是随着for循环改变的。
所以输出0 1

利用该特性还可以解决常见的面试题:通过原生JS返回所点击元素索引值。

    var domList = document.getElementsByTagName('div');
    for (var i = 0, length = domList.length; i < length; i++) {
        var item = domList[ i ];
        (function(_item, _i) {
            _item.addEventListener('click', function() {
                console.log(_i);
            })
        })(item, i)
    }

答案

0
1
阅读 2.8k

原创小文章
专注中小团队开发。前端技术,效率工具分享与探讨。
0 条评论
你知道吗?

宣传栏