JS相关面试题

238 阅读6分钟

数据类型

1、javascript有哪些数据类型?有什么区别?

  • ES5中分为基础数据类型(Undefined、Null、Boolean、String、Number)和引用数据类型(Object、Array、Function),ES6新增了Symbol、BigInt的数据类型。

    其中,Symbol、BigInt的含义:

    • Symbol创建之后代表独一无二且不可变的数据类型,本质上是一种唯一标识符,可作为对象的唯一属性名,主要是为了解决全局变量冲突的问题。
    • BigInt是一种数字类型的数据,比Number数据类型支持的范围更大的整数值,使用BigInt,整数溢出不再是问题,另外可以更加安全的使用时间戳,大整数。
  • 区别,存储上的不同:

    • 基本数据类型直接存储在栈中,占据 空间小、大小固定,属于被频繁使用数据。存取的方式是先进后出。
    • 引用数据类型是存储在堆中的对象,占据空间大、大小不固定,引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。堆是一个优先队列,先进先出,堆中的内存一般由开发者分配释放,若开发者不释放,程序结束由垃圾回收机制回收。

2、数据类型检测的方式有哪些?

  • typeof 有6种数据结果、其中数组、对象、null都是object,其他判断正确
 typeof 1   // number
 typeof true  // boolean
 typeof 'test' // string
 typeof function(){}  // function
 typeof undefined  // undefined
 typeof [] // object
 typeof null // object
 typeof {} // object
  • instanceof 只能判断引用数据类型,内部机制就是判断在原型链中能否找到该类型的原型。
  1 instanceof Number // falsee
  [] instanceof Array // true
  function(){} instanceof Function // true
  {} instanceof Object // true
  • constructor
  1. 判断数据的类型
    (1).constructor === Number // true
    (true).constructor === boolean; // true
    ('str').constructor === String; // true
    ([]).constructor === Array; // true
    (function(){}).constructor === Function; // true
    ({}).constructor === Object // true

2.对象实例通过constructor对象访问它的构造函数

    function Fn(){}
    Fn.prototype = new Array();
    var f = new Fn();
    console.log(f.constructor === Fn); // false
    console.log(f.constructor === Array); // true
  • Object.prototype.toString.call()
  console.info(Object.prototype.toString.call(null) === '[object Null]') 
  console.info(Object.prototype.toString.call(undefined) === '[object Undefined]') 
  console.info(Object.prototype.toString.call('123') === '[object String]') 
  console.info(Object.prototype.toString.call(123) === '[object Number]') 
  console.info(Object.prototype.toString.call(true) === '[object Boolean]') //引用类型 

  function fn(){
     console.log('test');
  }

  console.info(Object.prototype.toString.call(fn) === '[object Function]'); 
  var date = new Date(); 
  console.info(Object.prototype.toString.call(date) === '[object Date]') 
  var arr = [1,2,3]; 
  console.info(Object.prototype.toString.call(arr) === '[object Array]')

3、null、undefined的区别?

undefined代表未定义,null代表空对象,一般变量声明了还没有定义的时候会返回undefined,赋给可能返回对象的变量,作为初始化。

typeof null  // undefined
null == undefined // true
null === undefined // false

4、Object.is()与比较操作符“==”、“===”的区别?

  • 双等号(==)进行相等判断时,如果两边的类型不一致,则会进行强制类型转化后再进行比较。
  • 三等号(===)进行相等判断时,如果两边的类型不一致时,不会做强制类型准换,直接返回 false
  • 使用 Object.is 来进行相等判断时,一般情况下和三等号的判断相同,它处理了一些特殊的情况,比如 -0 和 +0 不再相等,两个 NaN 是相等的。

JS基础

1、判断数组的方式

 const arr = [1,2,3];
 Array.isArray(arr);
 Object.protorype.toString.call() === 'Array';
 arr instanceof Array
 Array.prototype.isPrototypeOf(arr);

2、深拷贝、浅拷贝以及各自的方法?

概念:深浅拷贝只针对引用数据类型,浅拷贝只复制指向某个对象的指针,而不是复制对象本身,新旧对象共享一个内存;深拷贝是创造一个一摸一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。

方法:

  • 浅拷贝
  1. 直接赋值
      var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
      var obj = fruits;
      obj[2] = "water";
      obj[3][0] = "water";
      console.log("fruits", fruits); // ['Banana', 'Orange', 'water', ['water''blue']]
      console.log("obj", obj); // ['Banana', 'Orange', 'water', ['water''blue']]

2.Object.assign

var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
var obj = Object.assign([], fruits);
obj[2] = "water";
obj[3][0] = "water";
console.log("fruits", fruits); // ['Banana', 'Orange', 'Apple', ['water''blue']]
console.log("obj", obj); //  ['Banana', 'Orange', 'water', ['water''blue']]

Object.assign()方法是把源对象自身的任意多个的可枚举属性拷贝给目标对象,然后返回给目标对象,然后返回给目标对象,但因为拷贝的是对象的属性的引用,不是对象本身,属于浅拷贝

  1. ...
      var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
      var obj = {...fruits};
      obj[2] = "water";
      obj[3][0] = "water";
      console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple',    ['water''blue']]
      console.log("obj", obj); //  ['Banana', 'Orange', 'water', ['water''blue']]
  1. concat()
      var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
      var obj = fruits.concat();
      obj[2] = "water";
      obj[3][0] = "water";
      console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple', ['water''blue']]
      console.log("obj", obj);//  ['Banana', 'Orange', 'water', ['water''blue']]
  1. slice()
      var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
      var obj = fruits.slice();
      obj[2] = "water";
      obj[3][0] = "water";
      console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple', ['water''blue']]
      console.log("obj", obj);//  ['Banana', 'Orange', 'water', ['water''blue']]

注: 2、3、4、5方法都是对第一层属性依次拷贝,如果第一层的属性是基本数据类型,就拷贝值;如果是引用数据类型,就拷贝内存地址。

  • 深拷贝 1.JSON.stringify() 和 JSON.parse()
     var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
     var obj = JSON.parse(JSON.stringify(fruits))
     obj[2] = "water";
     obj[3][0] = "water";
     console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple', ['Mango''blue']]
     console.log("obj", obj);//  ['Banana', 'Orange', 'water', ['water''blue']]

  1. jquery .clone(true)/.clone(true) / .extend(true)
      var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
      var obj = $.extend(true,{},fruits);
      obj[2] = "water";
      obj[3][0] = "water";
      console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple', ['Mango''blue']]
      console.log("obj", obj);//  ['Banana', 'Orange', 'water', ['water''blue']]
  1. 递归进行拷贝
    function deepClone(obj){
        var temp = Array.isArray(obj)?[]:{};
        for(var key in obj){
        temp[key] = typeof obj[key] === 'object' ? deepClone(obj[key]):obj[key];
      }
    return temp;
    }
  var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
  var obj = deepClone(fruits);
  obj[2] = "water";
  obj[3][0] = "water";
  console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple', ['Mango''blue']]
  console.log("obj", obj);// ['Banana', 'Orange', 'water', ['water''blue']]
  1. 推荐
 function  deepClone(source){
    if(!source) return;
    var target;
    if(JSON){
        target = JSON.parse(JSON.stringify(source));
    } else if(typeof(source)==='object'){
       target = source instanceof(Array) ? [] : {};
       for(var key in source){
         temp[key] = typeof obj[key] === 'object' ? deepClone(obj[key]):obj[key];
       }
    } else {
     target = source;
    }
    return target;
 }
 var fruits = ["Banana", "Orange", "Apple", ["Mango", "blue"]];
  var obj = deepClone(fruits);
  obj[2] = "water";
  obj[3][0] = "water";
  console.log("fruits", fruits);// ['Banana', 'Orange', 'Apple', ['Mango', 'blue']]
  console.log("obj", obj);// ['Banana', 'Orange', 'water', ['water', 'blue']]

3、数组有哪些原生的方法?

详情查看原生方法

4、数组的遍历方法有哪些?

方法是否改变原数组特点
forEach()数组方法,不改变原数组,没有返回值,使用foreach遍历,break不能中断循环,使用return不能返回到外层函数
map()数组方法,不改变原数组,有返回值,可链式调用
filter()数组方法,过滤数组,返回符合条件的元素的数组,可链式调用
some()、every()数组方法,some()只要有一个是true,返回true;every()有一个是false,便返回false
find()、findIndex()数组方法,find()返回第一个符合条件的值;findIndex()返回第一个符合条件的值的索引值
reduce()、reduceRight()数组方法,reduce()对数组进行正序操作;reduceRight()对数组逆序操作
for...offor...of遍历具有迭代器(iterator) 对象的属性,返回的是数组的元素、对象的属性值,不能遍历普通的obj对象

5、for...in和for...of的区别?如何用for...of遍历对象?

详情查看

6、为什么函数的arguments参数是类数组而不是数组?如何遍历类数组?两者的区别

arguments是一个对象,它的属性是从0开始依次递增的数字,还有callee和length等属性,与数组相似;但是没有数组常见的属性和方法,比如forEach、reduce等,所以叫类数组。
遍历类数组:

     // 方法一
     Array.prototype.forEach.call(arguments,a=>console.log(a))

     // 方法二
      Array.from(arguments).forEach((a)=>{
         console.log(a);
      })
     // 方法三
     [...arguments].forEach(a=>{
        console.log(a);
     })
     // 方法四
      Array.prototype.splice.call(arrayLike, 0).forEach(a=>{
        console.log(a);
     })
     // 方法五
     Array.prototype.slice.call(arrLike).forEach(a=>{
        console.log(a);
     })

7、javascript有哪些内置对象?

  • 值属性,没有自己的属性和方法,null、NaN、undefined等
  • 函数属性,parseInt()、parseFloat()、eval()等
  • 基本对象 Object、Function、Boolean、Error等
  • 数字和日期对象,Number、Math、Date
  • 字符串 String、RegExp
  • 可索引的集合对象 Array
  • 使用键的集合对象 Map、Set
  • 数学计算的单体内置对象 Math

8、什么是DOM、BOM?

js是有三个部分构成的,ECMAScript,DOM和BOM,根据宿主(浏览器)的不同,具体的表现形式也不同,ie和其他的浏览器风格迥异。

  1. DOM是W3C的标准;[所有浏览器公共遵守的标准]
  2. BOM是各个浏览器厂商根据DOM在各自浏览器上的实现
  3. window是BOM对象,而非js对象

DOM(文档对象模型)是 HTML 和 XML 的应用程序接口(API)。DOM将把整个页面规划成由节点层级构成的文档。HTML 或 XML 页面的每个部分都是一个节点的衍生物。

BOM(浏览器对象模型)是对浏览器窗口进行访问和操作,把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口,通常浏览器特定的 JavaScript 扩展都被看做 BOM 的一部分。

扩展包含:

  1. 弹出新的浏览器窗口
  2. 移动、关闭浏览器窗口以及调整窗口大小
  3. 提供 Web 浏览器详细信息的定位对象
  4. 提供用户屏幕分辨率详细信息的屏幕对象
  5. 对 cookie 的支持
  6. IE 扩展了 BOM,加入了 ActiveXObject 类,可以通过 JavaScript 实例化 ActiveX 对象

BOM的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。window 对象含有 location 对象、navigator 对象、screen 对象等子对象,并且 DOM 的最根本的对象 document 对象也是 BOM 的 window 对象的子对象。

9、 ES6模块和CommonJS模块有什么不同?

  • CommonJS
  1. 对于基本数据类型,属于复制,即会被模块缓存。
    2.对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改会影响另一个模块。
    3.当使用require命令加载某个模块时,就会运行整个模块的代码。
    4.当使用require命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果。
    5.循环加载时,属于加载时执行。即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
  • ES6模块 1.ES6模块中的值属于动态只读引用。
    2.对于只读来说,即不允许修改引入变量的值,import的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到import命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的模块去取值。
    3.对于动态来说,原始值发生变化,import加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
    4.循环加载时,ES6模块是动态引用,只要模块之间存在引用,代码就能够执行。

  • CommonJS

    // b.js
let count = 1
let plusCount = () => {
  count++
}
setTimeout(() => {
  console.log('b.js-1', count)
}, 1000)
module.exports = {
  count,
  plusCount
}

// a.js
let mod = require('./b.js')
console.log('a.js-1', mod.count)
mod.plusCount()
console.log('a.js-2', mod.count)
setTimeout(() => {
    mod.count = 3
    console.log('a.js-3', mod.count)
}, 2000)

node a.js
a.js-1 1
a.js-2 1
b.js-1 2  // 1秒后
a.js-3 3  // 2秒后
  • ES6
// b.js
import {foo} from './a.js';
export function bar() {
  console.log('bar');
  if (Math.random() > 0.5) {
    foo();
  }
}

// a.js
import {bar} from './b.js';
export function foo() {
  console.log('foo');
  bar();
  console.log('执行完毕');
}
foo();

babel-node a.js
foo
bar
执行完毕

// 执行结果也有可能是
foo
bar
foo
bar
执行完毕
执行完毕

10、常见的DOM操作有哪些?

  • DOM 节点的获取
getElementById // id查询
getElementByTagName // 按照标签名查询
getElementByClassName // 按照类名查询
querySelectorAll // 按照 css 选择器查询

var idDom = document.getElementById('idDom');  
var pList = document.getElementByTagName('p'); // 查询标签为p的集合
console.info(pList.length);
console.info(pList[0]);
var moocDom = document.getElementByClassName('mooc'); // 查询类名为mooc的
var moocList = document.querySelectorAll('.mooc'); // 查询类名为 mooc 的集合

  • DOM 节点的创建 创建一个新节点,并把它添加到 指定节点的后面。已知的HTML结构如下:
    <html>
        <head>
            <title>DEMO<title>
        </head>
        <body>
            <div id="contaioner">
                <h1 id="title">我是标题</h1>
            </div>
        </body>
    </html>

要求添加一个有内容的span节点到id为title的节点后面

    var container = document.getElementById('container');
    var targetSpan = document.createElement('span');
    // 设置 span 节点的内容 
    targetSpan.innerHTML = 'hello world' 
    // 把新创建的元素塞进父节点里去
    container.appendChild(targetSpan)
  • DOM 节点的删除结构 删除指定的DOM节点。已知的HTML结构如下:
    <html>
        <head>
            <title>DEMO<title>
        </head>
        <body>
            <div id="contaioner">
                <h1 id="title">我是标题</h1>
            </div>
        </body>
    </html>

要求添加一个有内容的span节点到id为title的节点后面

// 获取目标元素的父元素
   var container = document.getElementById('container')
   // 获取目标元素
  var targetNode = document.getElementById('title')
  // 删除目标元素 
  container.removeChild(targetNode)
// 获取目标元素的父元素
var container = document.getElementById('container')
// 获取目标元素
var targetNode = container.childNodes[1]
// 删除目标元素
container.removeChild(targetNode)

  • DOM 节点的修改 修改 DOM 元素这个动作可以分很多维度,比如说移动 DOM 元素的位置,修改 DOM 元素的属性等.

将指定的两个 DOM 元素交换位置, 已知的 HTML 结构如下:

<html>
  <head>
    <title>DEMO</title>
  </head>
  <body>
    <div id="container"> 
      <h1 id="title">我是标题</h1>
      <p id="content">我是内容</p>
    </div>   
  </body>
</html>

需要调换 title 和 content 的位置,可以考虑 insertBefore 或者 appendChild

// 获取父元素
var container = document.getElementById('container')   
 
// 获取两个需要被交换的元素
var title = document.getElementById('title')
var content = document.getElementById('content')
// 交换两个元素,把 content 置于 title 前面
container.insertBefore(content, title)

11、常用的正则表达式有哪些

// (1)匹配 16 进制颜色值
var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;

// (2)匹配日期,如 yyyy-mm-dd 格式
var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;

// (3)匹配 qq 号
var regex = /^[1-9][0-9]{4,10}$/g;

// (4)手机号码正则
var regex = /^1[34578]\d{9}$/g;

// (5)用户名正则
var regex = /^[a-zA-Z\$][a-zA-Z0-9_\$]{4,16}$/;

12、use strict是什么意思?使用它区别是什么?

use strict 是es5添加的运行模式,使js在严格的条件下运行,目的在于:
1.消除js语法的不合理、不严谨之处,减少怪异行为;
2.消除代码的不安全之处,保证代码运行的安全;
3.提高编译器效率,增加运行速度;
4.为未来新版本的js做好铺垫
区别:
1.禁止使用with语句 2.禁止this关键字指向全局对象 3.对象不能有重名的属性

13、如何判断一个对象是否属于某个类?

  • instanceof
var b = new Date();
b instanceof Array // false
b instanceof Date // true

function Demo(name,age){
    this.name = name; 
    this.age = age;
} 
var demo_1 = new Demo("1023",25);
console.log(demo_1 instanceof Demo); //true
console.log(demo_1 instanceof Object);//true

instanceof不但可以判断出是直接类的实例(通过new的方式),还可以判断是否是父类的实例

  • constructor
function Demo(name,age){
    this.name = name; 
    this.age = age;
} 
var demo_1 = new Demo("1023",25);

console.log(demo_1.constructor == Demo);//true 
console.log(demo_1.constructor == Object);//false

constructor属性只可以判断出是否是直接类的实例,不可以判断父类的实例

  • Object.prototype.toString.call
function Demo(name,age){
    this.name = name; 
    this.age = age;
} 
var demo_1 = new Demo("1023",25);
Object.prototype.toString.call(demo_1) === '[object Object]' // true

14、 forEach 和 map方法的区别?

这些方法是用来遍历数组的,两者的区别如下:

  • forEach方法会针对每一个元素执行提供的函数,对数据的操作会改变原数组,该方法没有返回值;
  • map方法不会改变数组的值,返回一个新数组,数组中的值为原数组调用函数处理之后的值;

15、 escape、encodeURI、encodeURIComponent 的区别?

  • encodeURI 是对整个 URI 进行转义,将 URI 中的非法字符转换为合法字符,所以对于一些在 URI 中有特殊意义的字符不会进行转义。
  • encodeURIComponent 是对 URI 的组成部分进行转义,所以一些特殊字符也会得到转义。
  • escape 和 encodeURI 的作用相同,不过它们对于 unicode 编码为 0xff 之外字符的时候会有区别,escape 是直接在字符的 unicode 编码前加上 %u,而 encodeURI 首先会将字符转换为 UTF-8 的格式,再在每个字节前加上 %。

ES6

1、let、const、var的区别

(1)块级作用域: 块作用域由{}包括,let和const具有块级作用域,var不存在块级作用域。
块级作用域解决ES5两个问题:
内层变量可能覆盖外层变量;
用来计数的循环变量泄露为全局变量;
(2)变量提升: var存在变量提升,let和const不存在变量提升,即变量只能在声明之后使用,否则会报错。
(3)全局添加属性:浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会 将该变量添加为全局对象的属性,const和let不允许。
(4)重复声明:var可以重复声明变量,后声明的同名变量会覆盖之前声明的变量,const和let不允许重复声明变量。
(5) 暂时性死区:在let、const声明变量之前,该变量都是不可用的,我们称之为暂时性死区,var不会存在。
(6) 初始值的设置:const声明变量必须设置初始值,let、var不用。 (7) 指针指向:const不允许改变指针的指向,let和const都可以更改(重新赋值)。

2、const对象的属性是否可以修改?

const保证的不是变量的值不动,而是变量指向的那个内存地址不能改动。对于基本数据类型,就是常量,不能改动。对于引用数据类型来说,变量指向的是内存的数据地址,保存的只是一个指针(栈中),只要指针不变,它指向的数据(存在堆中)结构变不变不受控制。

3、箭头函数和普通函数的区别?new一个箭头函数可以吗?

(1)箭头函数更简洁
(2)箭头函数没有自己的this
(3)call、apply、bind不能改变箭头函数this的指向
(4)箭头函数不能被new调用,不能作为构造函数使用
(5)箭头函数没有自己的arguments
(6)箭头函数没有prototype
注:箭头函数没有自己的this,它所谓的this是捕获其所在上下文的this值,并没有自己的this,不能被new调用,也不会被改变。

4、对对象和数组的解构的理解?

解构是ES6提供的一种新的提取数据的模式,这种模式能够从对象或数组里 有针对性地拿到想要的数值。

  • 数组的解构
 const [a,b,c] = [1,2,3]  // a 1 b 2 c 3
 const [a,,c] = [1,2,3] // a 1 c 3
  • 对象的解构
    const stu = { name: 'Bob', age: 24 }
    const { name, age } = stu; // 'Bob' 24
    // 对象解构严格以属性名作为定位依据,所以就算调换了 name 和 age 的位置,结果也是一样的
    const { age, name } = stu; // 24 'Bob'

原型、原型链

1、对原型、原型链的理解?

详情见 原型、原型链、继承

闭包、作用域、执行上下文

1、对闭包的理解?

  • 什么是闭包?

函数执行后返回的结果是一个内部函数,并被外部变量所引用,如果内部函数持有被执行函数作用域的变量,就形成了闭包。也就是说可以在内部函数访问到外部函数的作用域。创建闭包的常见方式是,在一个函数内部创建另一个函数。
使用闭包,一可以读取函数中的变量,二可以将函数中的变量存储在内存中,保护变量不受污染。

典型的例子

function createAction(){
    var message = "封闭环境内的变量";
}
  • 闭包的原理 函数的执行分为两个阶段(预编译阶段和执行阶段),在编译阶段,如果发现内部函数使用了外部函数的变量,则会在内存中创建一个“闭包”对象并保存对应的变量值,如果已存在"闭包",则需要增加对应属性值即可。执行完成之后,函数执行上下文会被销毁,函数对闭包对象的引用也会被销毁,但是其内部的函数还在用该“闭包”的引用,所以内部函数可以继续使用“外部函数”中的变量。

  • 闭包的优点 1.可以从内部函数访问外部函数的作用域中的变量,切访问的变量长期扎在内存中,可供之后使用 2.避免变量污染全局 3.把变量存在独立的作用域,作为私有成员存在。

  • 闭包的缺点 1.因为内部函数保存了对外部变量的引用,所以使用不当会导致内存泄漏。 2.对处理速度影响。闭包的层级决定了引用的外部变量在查找时经过的作用域长度。

  • 闭包的使用场景

  1. 模块封装,在各模块规范出现之前,都是用这种方式防止变量污染全局。
    var Person = (function(){
        var age = 10;
        function Person(){}
        Person.prototype.bar = function bar(){
           return age;
        }
         return Person;
    }())

2.在循环中创建闭包,防止取得意外的值

    for(var i=0;i<3;i++){
        document.getElementById('id'+i).onfocus = function(){
            alert(i);
        }
    }
    
    // 闭包解决
    function makeCallback(num){
        return function(){
            alert(num);
        }
    }
    
    for(var i=0;i<3;i++){
        document.getElementById('id'+i).onfocus = makeCallback(i);
    }

2、对作用域和作用域链的理解?

一、作用域

在运行代码中的某些特定部分中变量、函数、对象的可访问性。也就是说,作用域决定了代码区块中变量和其他资源的可见性。

function testFun(){
  var testVariable = '变量测试';
}
testFun(); // 要先执行这个函数
console.log(testVariable) // Uncaught ReferenceError: testVariable is not defined

作用域就是一个独立的地盘,让变量不会暴露出去。js中分三种作用域,全局作用域、函数作用域、块级作用域。

  • 全局作用域

1.任何不在函数中或者大括号中声明的变量,都在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问。

// 全局变量 
var greeting = 'Hello World!';
function greet() { console.log(greeting); }
// 打印 'Hello World!'
greet();
  • 函数作用域 2.函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量只能在函数内部访问,不能在函数以外去访问
function greet() { 
    var greeting = 'Hello World!';
    console.log(greeting);
} 
greet(); // 打印 'Hello World!' 
console.log(greeting); // 报错: Uncaught ReferenceError: greeting is not defined
  • 块级作用域 ES6引入了letconst关键字,和var关键字不同,在大括号中使用letconst声明的变量存在于块级作用域中
{
  // 块级作用域中的变量
  let greeting = 'Hello World!';
  var lang = 'English';
  console.log(greeting); // Prints 'Hello World!'
}

console.log(lang); // 变量 'English'
console.log(greeting);// 报错:Uncaught ReferenceError: greeting is not defined

二、作用域链

  • 自由变量 当前作用域没有定义的变量,这成为自由变量。自由变量的值是向父级作用域寻找,确切的说道创建这个函数的那个作用域去取值。

  • 作用域链 如果父级没有,再一层层向上寻找,直到找到全局作用域还没找到,就放弃,这种一层一层的关系,就是作用域链。

3、对执行上下文的理解?

简单来说,执行上下文就是评估和执行js代码的环境的抽象概念。每当js代码在运行的时候,它都是在执行上下文中运行。

  • 类型
  1. 全局执行上下文 -- 默认上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事情:创建一个全局的window对象,设置this的值等于这个全局对象。一个程序 中只会有一个全局执行上下文。

  2. 函数执行上下文 -- 每次调用函数时,都会为该函数创建一个新的执行上下文。每个函数都有自己的执行上下文,但是只有在函数被调用时才创建。一个程序中可以存在任意数量的执行上下文。

  3. 执行eval函数内部的代码也会有它属于自己的执行上下文,但是不经常用到eval。

  • 执行上下文栈
  1. js用执行上下文栈来管理执行上下文
  2. 当js执行代码时,首先遇到全局代码,会创建一个全局执行上下文并且压入执行栈中,每当遇到一个函数调用,就会为该函数创建一个新的执行上下文并压入栈顶,引擎会执行上下文栈顶的函数,当函数执行完成之后,执行上下文从栈中弹出,继续执行下一个上下文。当所有的代码都执行完毕之后,从栈中弹出全局执行上下文。
 let a = 'Hello World';
 function first(){
     console.log('first function');
     second();
     console.log('Again first function');
 }
 function second(){
    console.log('second function');
 }
 first(); // 执行顺序  先执行second(),再执行first()
  • 执行上下文的生命周期 执行上下文的生命周期包括创建阶段和执行阶段。
    在js代码执行之前,执行上下文将经历创建阶段,在创建阶段会发生三件事情:
    1.this值的绑定。
    在全局执行上下文中,this 的值指向全局对象。在函数执行上下文中,this 的值取决于该函数是如何被调用的。如果它被一个引用对象调用,那么 this 会被设置成那个对象,否则 this 的值被设置为全局对象或者 undefined
    2.创建词法环境组件。
    3.创建变量环境组件。

简单来说执行上下文就是指:

在执行一点JS代码之前,需要先解析代码。解析的时候会先创建一个全局执行上下文环境,先把代码中即将执行的变量、函数声明都拿出来,变量先赋值为undefined,函数先声明好可使用。这一步执行完了,才开始正式的执行程序。

在一个函数执行之前,也会创建一个函数执行上下文环境,跟全局执行上下文类似,不过函数执行上下文会多出this、arguments和函数的参数。

  • 全局上下文:变量定义,函数声明
  • 函数上下文:变量定义,函数声明,thisarguments

this、call、bind、apply

1、对this对象的理解?

this是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。在实际的开发中,this的指向通过以下几种调用模式来判断。

  • 函数调用模式 在全局作用域下,直接调用函数,this指向window
    function fn(){
        console.log(this); // this指向的是window对象
    }
    fn();
    
  • 方法调用模式 函数作为一个对象的方法来调用时,this指向这个对象。
var o = {
    user:"人生代码",
    fn:function(){
        console.log(this.user);  // 人生代码
    }
}
o.fn();
  • 构造器调用模式 如果一个函数用new调用时,函数执行前会新创建一个对象, this指向这个新创建的对象
function Fn(){
  this.user = "人生代码";
}
var a = new Fn();
console.log(a.user); // 人生代码
  • app、call、bind模式

三个方法都是显示的指定调用函数的this指向。其中apply方法接受两个参数:一个是this绑定的对象,一个是参数数组。call方法接收的参数,第一个是this绑定的对象,后面的参数是传入函数执行的参数。bind方法通过传入一个参数,返回一个this绑定了传入对象的新函数。这个函数的this指向除了使用new时会被改变,其他情况都不会被改变。

var FnOne=function(){
     this.name="张三";
}
var FnTwo=function(){
     console.log(this.name);//输出 张三
}
var fnOne=new FnOne();
FnTwo.call(fnOne);//改变fnTwo方法的this指向fnOne对象

使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式

2、call()、apply()、bind()区别?

call、apply、bind都是改变this指向的方法。

  • apply 接受两个参数,第一个参数指定了函数体内 this 对象的指向,第二个参数为一个带下标的集合,这个集合可以为数组,也可以为类数组,apply 方法把这个集合中的元素作为参数传递给被调用的函数。
fn.apply(obj, [1, 2])
  • call 传入的参数数量不固定,跟 apply 相同的是,第一个参数也是代表函数体内的 this 指向,从第二个参数开始往后,每个参数被依次传入函数
fn.call(obj, 1, 2);
  • 语法和call一模一样,区别在于立即执行还是等待执行,bind不兼容IE6~8
fn.bind(obj, 1, 2); // 改变fn中的this,fn并不执行

3、实现call、apply、bind函数?

  • call
 Function.prototype.myCall = function(context) {
     // 判断调用对象
     if(typeof this !== 'function'){
         console.log('type error')
     }
     // 获取参数
     let args = [...arguments].slice(1);
     let result = null;
     // 判断contetx是否传入
     context = context || window;
     // 将调用函数设为对象的方法
     context.fn = this;
     // 调用函数
     result = context.fn(...args);
     // 将属性删除
     delete context.fn;
     return result;
 }
  • apply
Function.prototype.myApply = function(context) {
    // 判断调用对象
    if(typeof this !== 'function'){
        console.log('type error')
    }
    let result = null;
    // 判断contetx是否传入
    context = context || window;
    // 将调用函数设为对象的方法
    context.fn = this;
    // 调用函数
    if(arguments[1]){
        result = context.fn(...arguments[1]);
    } else {
        result = context.fn();
    }
    // 将属性删除
    delete context.fn;
    return result;
}

  • bind
   Function.prototype.myBind = function(context) {
      // 判断调用对象
      if(typeof this !== 'function'){
          console.log('type error')
      }
     // 获取参数
      let args = [...arguments].slice(1);
      let fn = this;
      return function Fn(){
          return fn.apply(
              this instanceof Fn ? this : context, args.concat(...arguments)
          )
      }}

异步编程

1、 异步编程实现的方式?

  • 回调函数 使用回调函数的方式有一个缺点,多个回调函数嵌套的时候会造成回调函数地狱,上下两层的回调函数的代码耦合度太高,不利于代码的可维护性。
// 嵌套地狱
请求1(function(请求结果1){
    请求2(function(请求结果2){
        请求3(function(请求结果3){
            请求4(function(请求结果4){
                请求5(function(请求结果5){
                    请求6(function(请求结果3){
                        ...
                    })
                })
            })
        })
    })
})
  • Promise方式,使用Promise的方式可以将嵌套的回调函数作为链式调用,但是使用这种方法,有时会造成多个then的链式调用,可能会造成代码的语义不够明确。
new Promise(请求1)
    .then(请求2(请求结果1))
    .then(请求3(请求结果2))
    .then(请求4(请求结果3))
    .then(请求5(请求结果4))
    .catch(处理异常(异常信息))
  • generator的方式 通过yield关键字,把函数的执行流挂起,通过next()方法可以切换到下一个状态,为改变执行流程提供了可能,从而为异步编程提供解决方法。generator最大的特点就是可以控制函数的执行。
function* myGenerator() {
  yield '1'
  yield '2'
  return '3'
}

const gen = myGenerator();  // 获取迭代器
gen.next()  //{value: "1", done: false}
gen.next()  //{value: "2", done: false}
gen.next()  //{value: "3", done: true}
  • async/await 函数方式,async函数是generator和promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部执行到一个await语句的时候,如果语句返回一个promise对象,那么函数将会等待promise对象的状态变成resolve后再继续向下执行。因此可以将异步逻辑,转化为同步的顺序来书写,并且这个函数可以自动执行。
async function async1() {
  return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}

2、 对AJAX的理解,实现一个AJAX请求?

AJAX是Asynchrronous JavaScript and XML的缩写,通过Javascript的异步通信,从服务器获取XML文档从中提取数据,再更新当前网页的对应部分,而不是刷新整个网页。

// promise封装实现
const getJSON = (url) => {
 let promise = new Promise((resolve,reject)=>{
     let xhr = new XMLHttpRequest();
     xhr.open('GET',url,true);
     xhr.onreadystatechange = function(){
          if(this.rreadyState !== 4) return;
          // 当请求成功或失败时,改变Promise的状态
          if(this.status === 200){
              resolve(this.response);
          } else {
             reject(new Error(this.statusText))
          }
     };
     // 设置错误监听函数
     xhr.onerror = function() {
      reject(new Error(this.statusText));
     };
    // 设置响应的数据类型
    xhr.responseType = "json";
    // 设置请求头信息
    xhr.setRequestHeader("Accept", "application/json");
    // 发送 http 请求
    xhr.send(null);
 });
 return promise;
}

3、xhr、ajax、axios、fetch的区别

  • XMLHttpRequest对象 现代浏览器,最开始和服务器交换数据,都是使用的XMLHttpRequest对象,可以使用JSON、XML、HTML和text文本等格式发送和接收数据。
    if (window.XMLHttpRequest) { // model browser
      xhr = new XMLHttpRequest()
    } else if (window.ActiveXObject) { // IE 6 and older
      xhr = new ActiveXObject('Microsoft.XMLHTTP')
    }
    xhr.open('POST', url, true)
    xhr.send(data)
    xhr.onreadystatechange = function () {
      try {
        // TODO 处理响应
        if (xhr.readyState === XMLHttpRequest.DONE) {
          // XMLHttpRequest.DONE 对应值是 4
          // Everything is good, the response was received.
          if (xhr.status === 200) {
            // Perfect!
          } else {
            // There was a problem with the request.
            // For example, the response may hava a 404 (Not Found)
            // or 500 (Internal Server Error) response code.
          }
        } else {
          // Not ready yet
        }
      } catch (e) {
        // 通信错误的事件中(例如服务器宕机)
        alert('Caught Exception: ' + e.description)
      }
    }

优点:
1.不重新加载页面的情况下更新网页
2.页面已加载后从服务器请求/接收数据。
缺点: 使用繁琐,对于早期的IE浏览器有自己的实现,需要写兼容性代码

  • jQuery ajax 为了方便的操作DOM,并且规避一些浏览器兼容的问题,产生jQuery。里面的AJAX请求也兼容各浏览器,其实是对XMLHTTPRequest对象的封装。
    $.ajax({
      type: 'POST',
      url: url, 
      data: data,
      dataType: dataType,
      success: function () {},
      error: function () {}
    })

优点:
1.对原生XML的封装,做兼容处理,简化使用
2.增加对JSONP的支持。
缺点: 1.多个请求,有依赖关系,会形成回调地狱。
2.本身是针对MVC的编程,不符合现在前端MVVM的浪潮。

  • axios Axios是基于Promise的Http库,可以用在浏览器和node.js中,本质上还是对XML的封装,只不过是Promise的封装。
axios({
    method: 'post',
    url: '/user/12345',
    data: {
      firstName: 'liu',
      lastName: 'weiqin'
    }
  })
  .then(res => console.log(res))
  .catch(err => console.log(err))

优点:
1.从浏览器中创建XMLHttpRequests
2. 从 node.js 创建 http 请求
3.支持 Promise API
4.拦截请求和响应
5.转换请求数据和响应数据
6.取消请求
7.自动转换 JSON 数据
8.客户端支持防御 XSRF
缺点: 1.只支持现代浏览器。

  • fetch Fetch API提供了一个JAvascript接口,用于访问和操作HTTP管道的部分。它还提供了一个全局的fetch()方法,该方法提供了一种简单、合理的方式在跨网络异步获取资源。
    fetch是低层次的API,代替XHR,可以轻松处理各种格式,非文本化格式。
fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

优点:

  1. 语法简洁,更加语义化
  2. 基于标准 Promise 实现,支持 async/await
  3. 同构方便
  4. 更加底层,提供的API丰富(request, response)
  5. 脱离了XHR,是ES规范里新的实现方式
  6. 符合关注分离,没有将输入、输出和用事件来跟踪的状态混杂在一个对象里

缺点:

  1. fetch只对网络请求报错,对400500都当做成功的请求,需要封装去处理。(即使HTTP响应的状态码是 404 或 500,从 fetch()返回的 Promise 不会被标记为 reject。只有当网络故障时或请求被阻止时,才会标记为 reject
  2. fetch默认不会带cookie,需要添加配置项。(如果站点依赖于用户 session,则会导致未经认证的请求(要发送 cookies,必须设置 credentials 选项))
  3. fetch不支持abort,不支持超时控制,使用setTimeoutPromise.reject的实现超时控制并不能阻止请求过程继续在后台运行,造成了流量的浪费。
  4. fetch没有办法原生监测请求的进度,而XHR可以。

4、setTimeout、Promise、Async/Await的区别,宏任务、微任务?

5.对Promise的理解

6.Promise的基本用法

7.Promise解决了什么问题

8.Promise.all和Promise.race的区别?

持续更新~~