JavaScript面试题

140 阅读7分钟

一、基本数据类型和引用数据类型

Number、String、Boolean、Null、 Undefined、Symbol(ES6)

基本数据类型:按值存栈中,简单数据段,数据大小确定,内存空间大小可以分配

引用数据类型:存堆中,每个空间大小不一样,根据情况进行特定的配置。

引用类型数据,在栈中,存的实际上是对象在堆内存中的引用地址。

obj1赋值给obj2,实际上这个 堆内存对象 在栈中的引用地址,复制了一份给了obj2。

他们指向了同一个堆内存对象,所以修改obj2其实就是修改那个对象,所以通过obj1访问也能访问的到。

二.、闭包

1、闭包

闭包就是有权访问另一个函数作用域中的变量 的函数 

闭包是种现象。chrome里,闭包指向被访问的

**在本质上,闭包是将函数内部和函数外部连接起来的桥梁。**

`我们在返回函数的时候,并不是单纯的返回了一个函数,我们把该函数连同他的AO链一起返回了。`
  1. **js垃圾回收机制:**js 中的变量和函数不再用后,会被自动js垃圾回收机制回收。
  2. 形成闭包的条件:有函数/作用域的嵌套;内部函数引用外部函数的变量/参数。
  3. **闭包的结果:**内部函数 用 外部函数的 那些变量和参数仍然会保存,用return返回了此内部函数,上面的变量和参数不会被回收。
  4. 闭包的原因:**返回的函数并非孤立的函数,而是连同周围的环境(AO)打了一个包,成了一个封闭的环境包,共同返回出来 ---->闭包。**
  5. 函数的作用域,取决于声明时而不取决于调用时。
  6. 变量存储function(){}{}[]存储的是一个地址。

2、闭包的4种表现形式

  • 返回一个函数
  • 作为函数参数传递
  • 回调函数
  • 非典型闭包IIFE(立即执行函数表达式)

a.返回一个函数:这种形式的闭包在JS中非常非常常见。

var a  = 1;
function foo(){
  var a = 2;
  // 这就是闭包
  return function(){
    console.log(a);
  }
}
var bar = foo();
// 输出2,而不是1
bar();
复制代码

b.作为函数参数传递:无论通过何种手段将内部函数传递到它所在词法作用域之外,它都会持有对原始作用域的引用,无论在何处执行这个函数,都会产生闭包。

var a=1;
function foo(){
    var a=2;
    function baz(){
        console.log(a);
    }
    bar(baz);
}
function bar(fn){
    //这就是闭包
    fn();
}
//输出2,而不是1
foo();
复制代码

c.回调函数:在定时器、事件监听、Ajax请求、跨窗口通信、Web Workers或者任何异步中,只要使用了回调函数,实际上就是在使用闭包

//定时器
setTimeout(function timeHandler(){
    console.log('timer');
},100)

//事件监听
$('#container').click(function(){
    console.log('DOM Listener');
})
复制代码

d.IIFE:IIFE(立即执行函数表达式)并不是一个典型的闭包,但它确实创建了一个闭包。

var a = 2;
(function IIFE(){
  // 输出2
  console.log(a);
})();

3、解决循环输出问题

for(var i = 1; i <= 5; i ++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}
//为什么会全部输出6?如何改进,让它输出1,2,3,4,5?(方法越多越好)
复制代码

代码分析

for循环创建了5个定时器,并且定时器是在循环结束后才开始执行

for循环结束后,用var i定义的变量i此时等于6

依次执行五个定时器,都打印变量i,所以结果是打印5次6

**第1种改进:利用IIFE(立即执行函数表达式)**当每次for循环时,把此时的i变量传递到定时器中

for(var i=1;i<=5;i++){
  (function(j){
    setTimeout(function timer(){
      console.log(j)
    },0)
  })(i)
}
复制代码

**第2种改进:**setTimeout函数的第三个参数,可以作为定时器执行时的变量进行使用

for(var i=1;i<=5;i++){
  setTimeout(function timer(j){
    console.log(j)
  }, 0, i)
}
复制代码

第3种改进(推荐):在循环中let i代替****var i

for(let i=1;i<=5;i++){
  setTimeout(function timer(){
    console.log(i)
  }, 0)
}

三、构造函数的成员

非静态成员是指,每个实例对象中都保存属于该实例自身的同名变量/方法。通过实例对象操作它们的时候,不会印象其它实例里的相应成员。

静态成员是指,类的成员变量/方法,属于类本身,而非某个对象实例。

                        也就是说,各个实例对象用某个静态成员变量/方法用的是同一个副本

私有是指,实例对象的成员变量/方法,只能通过特定公共接口,供外部访问。无法通过操作对象本身获取。公有成员无此限制。

非静态+公有:this.xxx

静态+公有:构造函数.prototype.xxxx的变量/方法。

                   (牵一发而动全身,哪怕你是通过实例1.xxx修改了,实例2.xxx也跟着变)

非静态+私有:构造函数+闭包

静态+私有:外部私有作用域+闭包

构造函数:

  • 实例成员:通过this创建的属性和方法,this.xxx写的,只能通过 实例化对象 访问
  • 静态成员:在构造函数本身上直接添加的 只能通过 构造函数本身 访问

                        Star.sex = '',Foo.a = 1; 调用的时候直接写Star.sex,Foo.a,不然取不到

当new实例的时候,Foo 函数内的属性、方法已初始化,覆盖了同名的静态方法

方法,是函数是复杂数据类型。

new多个实例对象,浪费了多个内存空间存放同一个函数。

prototype: 让多个对象 共享同一个函数

四.原型链

  1. 每个对象都可以有一个原型__proto__,这个原型还可以有它自己的原型,以此类推,形成一个原型链
  2. 查找特定属性,我们先去这个对象里找,没有的话就去它的原型对象里,还没有的话去向原型对象的原型对象里去寻找

a.原型对象 prototype

  1. 每个函数都有一个prototype属性。定义构造函数时自动创建,相当于个指针,从一个函数指向一个对象
  2. 它总是被__proto__所指,也是(构造)函数所创建的实例的原型对象prototype。
  3. 包含所有实例共享的属性和方法

      (所有实例化对象都去同一个地址找某个方法,节省空间)

b.原型__proto__

  • 对象独有(函数也有因为万物皆对象),为对象的查找机制 提供一个方向(路线)

  • __proto__总是指向prototype,应用原型链查询

c. '构造函数'constructor:

  • 所有prototype****都有constructor属性,指向构造函数。相当于constructor属性是原始构造函数的引用

  • 所有的实例对象都可以访问constructor属性,记录了实例对象到底引用哪个构造函数

                                                  (因为实例有__proto指向prototype__)

总结

  • 所有对象都有__proto__

  • 函数这个特殊对象,有__proto__属性,还有prototype

    • prototype对象默认有,constructor属性和__proto__属性。

    • prototype可以给函数和对象添加可共享(继承)的方法、属性,

    • __proto__是查找某函数或对象的原型链方式。

    • constructor这个属性包含了一个指针,指回原构造函数。

d.实例化new的过程

var obj  ={};                    // 创建一个空对象
obj.__proto__ = Star.prototype;  // 让obj的proto ——→ 构造函数的prototype
Star.call(obj);    		 // 调用构造函数,并让this指向obj
return obj;                      // 返回这个对象

五.call apply bind

1.立即调用并改变this指向:

  • fn.call(obj,2,4)

  • fn.apply(obj,[2,4])

应用:

// push()一次一个元素,apply能让数组1添加数组2
Array.prototype.push.apply(array1, array2);     

// 数组Array没有直接找最大值的方法,但Math下有
maxInNumbers = Math.max.apply(Math, arr);

//用Object下的toString验证任意 的类型 是不是数组
functionisArray(obj){ 
    return Object.prototype.toString.call(obj) === '[object Array]' ;
}

//定义一个log方法,代理console.log,并前缀(app)
function log(){                                        传入参数不确定,用arguments
  var args = Array.prototype.slice.call(arguments);    伪数组 转换为 正常数组
  args.unshift('(app)');
  console.log.apply(console, args);
};

2.不调用但改变this指向

  • fn.bind(obj,2,5) "绑定"函数 bind的时候创建了一个新函数,传入的第一个参数作为this

  • 多次bind() 无效。

更深层次的原因, bind() 的实现,相当于所用函数在内部包了一个 call / apply ,第二次 bind() 相当于再包住第一次 bind() ,故第二次以后的 bind 是无法生效的。