JavaScript面试题(作用域、闭包、this指向、综合面试题)

一、作用域

1、什么是作用域

​ 几乎所有编程语言最基本的功能之一,就是能够存储变量当中的值,并且能够对这些值进行访问或者修改。

那这些变量储存在哪里呢?最重要的是程序需要时,如何找到这些变量

这些问题说明程序就要设计一套规则来存储变量,并且方便的找到这些变量,这套规则就是就成为作用域

作用域:其实就是一套规则,用于确定在何处以及如何查找变量。

2、js编译原理

通常将JS归类为动态语言、解释执行语言但事实上是一个编译语言,js在执行代码之前都要有一个预编译过程,编译过程也叫预解析

// 我们将 var a = 2;分解,看看js编译器是怎么样处理的

遇到var a; 编译器会询问作用域是否已经有一个该名称的变量存在于同一个作用域的集合中,如果有,编译器会忽略该声明,继续进行编译;否则它会要求作用域在当前作用域的集合中声明一个新的变量,并命名为 a

然后编译器会为引擎生成运行时想要的代码(也就是编译完成后的代码),引擎运行时首先询问作用域,在当前的作用域集合中,是否存在一个叫a的变量,如果有,那么引擎就会使用这个变量a,就将2 赋值 给它,如果没有,引擎就会继续查找该变量,如果没有找到,引擎就抛出异常!

3、作用域嵌套

// 当一个块或者函数嵌套在另一块或者函数中,就发生了作用域的嵌套,因此,在当前的作用域中无法找到某个变量时,引擎就会在最外层嵌套的作用域中继续查找,直到找到该变量,或者是找到全局作用域为止。

//  案例
function foo(a){
    
    
            console.log(a+b);
        }
        var b = 10;
        foo(2); // 12

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GYFvKFuq-1637154774849)(\images\作用域比喻建筑.png)]

4、词法作用域

词法作用域就是在写代码时将变量和块作用域写在哪里来决定,也就是代码在编写过程中体现出来的作用范围,代码一旦写好, 不用执行,作用范围就已经确定好了,这个就是词法作用域。

在 js 中词法作用域规则:

  1. 函数允许访问函数外的数据.
  2. 整个代码结构中只有函数可以限定作用域.
  3. 作用规则首先使用提升规则分析
  4. 如果当前作用域中有一个变量, 就不考虑外面的同名的变量
//案例
 function foo(a){
    
    
        var b = a * 2;
        function bar(c){
    
    
            console.log(a,b,c);
        }
        bar(b * 3);
      }
      foo(2); // 2,4,12

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HgRgqNnU-1637154774853)(\images\词法作用域.png)]

5、函数作用域和块作用域

// 由函数创建的作用域就是函数作用域,也就是说属于这个函数的全部变量都可以在整个函数的范围内使用及复用。
// 标识符(变量、函数)的定义
var a = 2;
     function foo(){
    
    
        var a = 3;
        console.log(a); // 3
     }
     foo();
     console.log(a) // 2
     
 // 块作用域
// 除了Javascript(ES5之前版本)外很多编程语言都支持块作用域
     
      for(var i=0;i<5;i++){
    
    
       
      	var a = 10;
        console.log(a);
    } 
     console.log(a);
	 console.log(i);
// 使用Es6语法 let声明变量
 for(var i=0;i<5;i++){
    
    
       
        let a = 10;  // let 用来任意代码块中声明变量
        console.log(a);
    }
   console.log(a); // 报错

二、变量声明和函数声明提升

​ 先声明,再赋值

// 变量声明提升

// 案例
var a = 10;
console.log(a) // 10

// 改动一下
console.log(a) // undefined
var a = 10;

// 总结 只有声明本省会提升,而赋值或者其他运行逻辑会留在原来的位置

// 函数声明提升
//函数声明和变量声明都会提升,但是函数会首先被提升,然后才是变量

//案例1
 	var a = 1;
     function a(){
    
    

     }
     console.log(a); //1

//案例2
 		console.log(a); //function a(){}
        var a = 1;
        console.log(a); //1
        function a(){
    
    }
	// 解析
 function a(){
    
    } // 函数声明提升,同时默认声明了 var a;
        var a; // var a; 是重复声明,因此会被忽略掉。
        console.log(a); // function a(){}
        a = 1;
        console.log(a); //1

// 函数声明会提升,函数表达式不会被提升

// 案例
 foo(); // 1
      var foo;
    function foo(){
    
    
        console.log(1);
    }
    foo = function(){
    
    
        console.log(2);
    }

      // 代码解析
    function foo(){
    
    
        console.log(1);
    }
      foo(); // 1
      var foo;  
    foo = function(){
    
    
        console.log(2);
    }
// 尽管 var foo; 出现在 function foo(){console.log(1);} 之前,但它是重复声明所以被忽略,因为函数声明会提升到普通变量之前。
    
     // 代码变动
     function foo(){
    
    
        console.log(1);
    }
    foo = function(){
    
    
        console.log(2);
    }
      foo(); // 2

      var foo;  

三、闭包

1、什么是闭包?

说闭包之前,必须理解Javascript特殊的变量作用域,内部函数能够访问外部函数的变量,这个是根据作用域查找规则而来的。

闭包:函数在定义的词法作用域以外的地方被调用了,闭包可以让函数继续访问之前定义时的词法作用域。

​ 总结:闭包就是能够读取其他函数内部变量的函数,闭包就是将函数内部和函数外部连接起来的一座桥梁

// 在函数外是访问不了函数内部的 var 的 变量

/*function fn(){
	var num = 10;
}

fn();
console.log(num); // num is not defined*/


/*function fn(){

	var num = 10;

	function foo() {

		console.log(num); // 10
	}

	foo();
}

fn();*/

 // 我们可以 如果foo函数中没有num 变量会向上一级作用域中查找,直到找到或者查找到全局作用域为止,但是不能从fn函数向内部查找变量。

// 既然foo函数可以访问fn函数中的变量,那么只要把foo函数作为返回值,我们不就可以在fn函数外部读取它的内部变量了吗

// 创建闭包的常见方式就是在一个函数内部创建另一个函数,作为返回值或参数传递到函数外部
/*function fn(){

	var num = 10;

	return function foo() {

		console.log(num); // 10
	}
	
}

var result = fn();

result();*/

// 被返回的 foo 函数就是闭包,注意:并不是fn函数是闭包,

// foo函数执行的位置和定义的位置是不相同的,foo是在函数fn中定义的,但执行却是在全局环境中,虽然在全局环境中执行的,但函数仍然可以访问之前定义时的词法作用域。


function foo(){
    
    
        var a = 2;
        function bar(){
    
    
            console.log(a) //2
        }
        bar();
    }
    foo();
// bar()能够访问外部作用域中的变量a,这是闭包吗?
// 确切的说不是,这只是词法作用域查找规则,这个规则只是闭包的一部分

//改动代码

  function foo(){
    
    
        var a = 2;
        function bar(){
    
    
            console.log(a) //2
        }
       return bar;
    }
   var fn = foo();
   console.log(fn);
   fn()  // 这个就是闭包的效果
   
   // 案例
   function foo(){
    
    
        var a = 2;
        function baz(){
    
    
            console.log(a); // 2
        }
        bar(baz);
    }
    function bar(fn){
    
    
        fn(); // 这也是闭包效果
    }
    foo();

    // 把内部函数baz传递给bar当调用这个内部的函数是时(也就是fn),它涵盖的foo()内部的作用域的闭包就可以观察到了,因为能够访问a

// 当然传递函数也可以是间接的
//案例
	 var fn;
    function foo(){
    
    
        var a = 2;
        function baz(){
    
    
            console.log(a); // 2
        }
       fn = baz; // 讲baz分配到全局变量中
    }
    function bar(){
    
    
        fn(); // 这还是闭包效果
    }
    foo();
    bar();

// 闭包就是可以在外部访问其他函数内部变量的函数(这个函数就是foo函数)

闭包的用途

它的最大用处有两个,

1、是前面提到的可以读取函数内部的变量,

2、就是让这些变量的值始终保持在内存中。

/* function father(){
 	  var n = 999;

 		add = function(){
 			n++;
 		};

 	  return function son(){
 			console.log(n);
 	  };

   }
   var result=father();
  result(); // 999
	add();
  result(); // 1000*/
// 	result实际上就是闭包son函数。它一共运行了两次,第一次的值是999,第二次的值是1000。
// 这证明了,函数father中的局部变量n一直保存在内存中,并没有在father调用后被自动清除。

// 原因就在于father是son的父函数,而son被赋给了一个全局变量,这导致son始终在内存中,而son的存在依赖于father,因此father也始终在内存中,不会在调用结束后被垃圾回收机制(garbage collection)回收

// 大白话:在爷爷的环境中执行了爸爸,爸爸返回了孙子,本来爸爸执行了,爸爸的环境应该被清除掉,但是孙子引用(依赖)了爸爸的环境,导致爸爸释放不了内存了。

/*var fun1,fun2,fun3;
function test() {

    var a = 10;

    fun1 = function () {
    	console.log(a);
    };

    fun2 = function () {
    	a++;
    };

    fun3 = function (x){
    	a = x;
    };
}


test();
fun1();   //10
fun2();
fun1();   //11
fun3(5);
fun1();   //5
var fun4 = fun1;
test();
fun1();   //10
fun4();   //5*/


// 我们第二次调用test函数时一个新的闭包被创建,它能访问到的变量也是重新创建的,跟前面的没有关系,因此再调用fun1时输出10。我们要记住这句话: 如果你在一个函数内部声明了另一个函数,那么这个外部函数每次被调用都会产生一个闭包,创建崭新的执行上下文环境。
// 那么调用fun4怎么又输出了5呢,这是因为var fun4=fun1是在第一次调用test发生的,那么fun4可以访问的变量也是第一次调用test时创建的变量对象,即使在别的地方被调用,它的作用域链也就是可访问的变量是不变的。我们要记住这句话: 一个函数可以访问的变量对象要到创建这个函数的执行环境中去找而不是调用这个函数的执行环境


// JavaScript具有自动垃圾回收机制,垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存,不再使用的变量也就是生命周期结束的变量,当然只可能是局部变量,全局变量的生命周期直至浏览器卸载页面才会结束。局部变量只在函数的执行过程中存在,而在这个过程中会为局部变量在栈或堆上分配相应的空间,以存储它们的值,然后在函数中使用这些变量,直至函数结束,闭包中由于内部函数的原因

使用闭包的注意点

1)由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是,在退出函数之前,将不使用的局部变量全部删除。

四、this

1、先排除误解

1> this指向自身

2> this指向函数的作用域

2、this到底是什么?

this是在运行时进行绑定的,不是在编写的时候的绑定的,this的绑定和函数的声明的位置没有任何的关系,因为this是在调用时被绑定的,完全取决于函数的调用位置(也就是函数的调用方法(方式)),函数的调用位置就是函数在代码中被调用的位置,而不是声明的位置。

当一个函数被调用时,会创建一个执行上下文(环境context,跟BFC上下文类似),这个上下文包含函数在哪里被调用、函数的调用方式、传入的参数信息等,this就是这个上下文中的一个属性,会在函数执行的时候用到。

this的指向,是在函数被调用的时候确定的。也就是执行上下文被创建时确定的。因此我们可以很容易就能理解到,一个函数中的this指向,可以是非常灵活的。

什么是执行上下文

执行上下文可以看做当前代码的运行环境或者作用域。执行上下文其中包括了全局以及函数级别的执行上下文:
在这里插入图片描述
在这里插入图片描述

3、函数的调用方式(函数的调用位置)

// 案例
	var obj = {
    
    
            foo: function() {
    
    
                console.log(this)
            }
        }

        var bar = obj.foo
        obj.foo() // 打印出的 this 是 obj
        bar() // 打印出的 this 是 window
        
        

// 1、方法调用模式
// 当一个函数被保存为对象的一个属性时, 我们称它为一个方法, 当一个方法被调用时, this指向该对象
function foo(){
    
    
    console.log(this.a);
}
var obj = {
    
    
    a: 2,
    foo:foo
};
obj.foo();

// 以上代码可以改成为
/* var obj = {
    a: 2,
    foo:function (){
    console.log(this.a);
}
};
obj.foo();
*/

// 当foo()被调用时,它的前面确实加上了对象obj 的引用,当函数引用有上下文对象时,隐式绑定规则会把函数调用中的this绑定到这个上下文对象上,因此,当foo()被调用时this绑定到了obj,所以,this.a和obj.a是一样的

// 2、函数调用模式(默认调用模式)
//当一个函数并非一个对象的属性时, 它被当作一个函数来调用, 此时的this指向全局对象
function foo(){
    
    
    console.log(this.a);
}
var a = 2;
foo();


// 以上代码可以改为
var foo = function (){
    
    
  console.log(this.a);
}
var a = 2;
foo();

// 当foo()调用时 this.a 被解析成了全局变量a 因为函数调用应用了this的默认绑定,因此this指向全局


// 3、构造器调用模式
// 结合new前缀调用的函数被称为构造器函数, 此时的this指向该构造器函数的实例对象

function show(val) {
    
    
	this.value = val;
};
show.prototype.getVal = function() {
    
    
	console.log(this.value);
};
var fun = new show(1);
fun.getVal(); // 输出1
console.log(fun.value) // 输出1

// 从上面的结果, 可以看出, 此时的this指向了func对象.

// new绑定
//我们已经知道,this,是在函数调用过程中确定,因此,搞明白new的过程中到底发生了什么就变得十分重要。

//通过new操作符调用构造函数,会经历以下4个阶段。

//创建一个新的对象;
//将构造函数的this指向这个新对象;
//指向构造函数的代码,为这个对象添加属性,方法等(这个新对象会执行原型链连接);
//如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

// 案例
function Person(name, age) {
    
    
 
    // 这里的this指向了谁?
    this.name = name;
    this.age = age;  
    console.log(this); // 
    console.log(this.name);
    console.log(this.age);
}
  // Person('xiaoming', 20); // 执行 Person函数,this 指向是 window
Person.prototype.getName = function() {
    
    
    console.log(this);
    console.log(this.name);
    // 这里的this又指向了谁?
    return this.name;
}
 
// 上面的2个this,是同一个吗,他们是否指向了原型对象?
 
var p1 = new Person('xiaoming', 20); // this 指向 p1 对象
p1.getName(); // this 指向 p1 对象

// 因此,当new操作符调用构造函数时,this其实指向的是这个新创建的对象,最后又将新的对象返回出来,被实例对象p1接收。因此,我们可以说,这个时候,构造函数的this,指向了新的实例对象,p1。

// 而原型方法上的this就好理解多了,根据上边对函数中this的定义,
// p1.getName()中的getName为调用者,他被p1所拥有,因此getName中的this,也是指向了p1。


// 4、call/apply调用模式
//JavaScript内部提供了一种机制,让我们可以自行手动设置this的指向。它们就是call与apply。所有的函数都具有着两个方法。它们除了参数略有不同,其功能完全一样。它们的第一个参数都为this将要指向的对象。

// 如下例子所示。fn并非属于对象obj的方法,但是通过call,我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。这就是call/apply的用法。

function fn() {
    
    
    console.log(this.a);
}
var obj = {
    
    
    a: 20
}
 
fn.call(obj);// 20

// 而call与applay后面的参数,都是向将要执行的函数传递参数。其中call以一个一个的形式传递,apply以数组的形式传递。这是他们唯一的不同。

function fn(num1, num2) {
    
    
    console.log(this.a + num1 + num2);
}
var obj = {
    
    
    a: 20
}
 
fn.call(obj, 100, 10); // 130
fn.apply(obj, [20, 10]); // 50

// call与apply方法是如何工作的,它们的第一参数是一个对象,是给this准备的,接着在调用函数时将其绑定到this,因为可以直接指定this的绑定对象,因此我们成为显示绑定

// fn并非属于对象obj的方法,但是通过apply(),我们将fn内部的this绑定为obj,因此就可以使用this.a访问obj的a属性了。

五、综合面试题

 // 构造函数Foo
        // function Foo() {
    
    
        //     getName = function () {
    
    
        //         console.log(1);
        //     };
        //     return this;
        // } 
        // // 构造函数自身添加一个getName属性
        // Foo.getName = function () {
    
    
        //     console.log(2);
        // }; 
        // // 原型上添加一个getNaeme属性
        // Foo.prototype.getName = function () {
    
    
        //     console.log(3);
        // }; 
        // // 一个getName变量
        // var getName = function () {
    
    
        //     console.log(4);
        // }; 
        // // 一个getName函数
        // function getName() {
    
    
        //     console.log(5);
        // }

        // 按照js的与解析规则,以上代码可以写成如下代码
         // 构造函数Foo
        // function Foo() {
    
    
        //     getName = function () {
    
    
        //         console.log(1);
        //     };
        //     return this;
        // } 
        // // 一个getName函数
        // function getName() {
    
    
        //     console.log(5);
        // }
        // // 构造函数自身添加一个getName属性
        // Foo.getName = function () {
    
    
        //     console.log(2);
        // }; 
        // // 原型上添加一个getNaeme属性
        // Foo.prototype.getName = function () {
    
    
        //     console.log(3);
        // }; 
        //  // 一个getName变量
        // var getName; // 此处的是重复声明,所以会被忽略(也可以理解为这段代码理论上是没有的)。
        // getName = function () {
    
    
        //     console.log(4);
        // }; 


        // 因为最后的getName = function () {console.log(4);}; 给覆盖掉,所以实际上可以简化成以下写法


      
         // 构造函数Foo
        function Foo() {
    
    
        	// 隐士全局变量
            getName = function () {
    
    
                console.log(1);
            };
            return this;
        } 
        // 构造函数自身添加一个getName属性
        Foo.getName = function () {
    
    
            console.log(2);
        }; 
        // 原型上添加一个getNaeme属性
        Foo.prototype.getName = function () {
    
    
            console.log(3);
        }; 
         // 一个getName变量
        var getName;
        getName = function () {
    
    
            console.log(4);
        }; 


        Foo.getName(); // 打印2
        getName(); // 打印4

        // 打印1 --> Foo()执行时覆写了全局下的getName,
        // 然后返回window对象,window.getName执行修改后的新函数,最终打印1
        Foo().getName(); 

        getName(); // 因为全局的getName已经被修改了,最终打印1
        new Foo.getName(); // new了Foo的getName属性,所以打印2
        new Foo().getName(); 
        // new Foo(),得到一个Foo的实例,实例继续调用getName方法,最终打印原型上getName为3
        
        // 这个语法可以拆分为两部: var result = new Foo(); new result.getName();
        // 最终第一个new关键字,new的是Foo.prototype.getName,所以最终打印3
        new new Foo().getName(); 


学IT,上博学谷

猜你喜欢

转载自blog.csdn.net/it_cgq/article/details/121387345