«
JavaScript基础-面试

时间:2022-6   


JavaScript基础

什么是 DOM 和 BOM?

1、DOM 指的是文档对象模型,它指的是把文档当做一个对象,这个对象主要定义了处理网页内容的方法和接口。
2、BOM 指的是浏览器对象模型,它指的是把浏览器当做一个对象来对待,这个对象主要定义了与浏览器进行交互的方法和接口。BOM 的核心是 window,而 window 对象具有双重角色,它既是通过 js 访问浏览器窗口的一个接口,又是一个 Global(全局)对象。这意味着在网页中定义的任何对象,变量和函数,都作为全局对象的一个属性或者方法存在。

(1)location 对象

(2)history 对象

(3)Navigator 对象

js 中 var 的变量和 function 的函数名重名时的执行结果

console.log(a);
var a = 100;
function a () {};
console.log(a);

console.log(a);
function a () {};
var a = 100;
console.log(a);

答案是两个场景输入都是一样的.结果都为:
ƒ a () {}
100

结论:函数声明会覆盖变量声明

JS 为什么放到后面,CSS 会阻塞渲染吗

为什么外链 css 为什么要放头部?

首先整个页面展示给用户会经过 html 的解析与渲染过程。
而外链 css 无论放在 html 的任何位置都不影响 html 的解析,但是影响 html 的渲染。
如果将 css 放在尾部,html 的内容可以第一时间显示出来,但是会阻塞 html 行内 css 的渲染。
浏览器的这个策略其实很明智的,想象一下,如果没有这个策略,页面首先会呈现出一个行内 css 样式,待 CSS 下载完之后又突然变了一个模样。用户体验可谓极差,而且渲染是有成本的。
如果将 css 放在头部,css 的下载解析是可以和 html 的解析同步进行的,放到尾部,要花费额外时间来解析 CSS,并且浏览器会先渲染出一个没有样式的页面,等 CSS 加载完后会再渲染成一个有样式的页面,页面会出现明显的闪动的现象。

为什么 script 要放在尾部?

因为当浏览器解析到 script 的时候,就会立即下载执行,中断 html 的解析过程,如果外部脚本加载时间很长(比如一直无法完成下载),就会造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。
具体的流程是这样的:

JavaScript 脚本延迟加载的方式有哪些?

一般有以下几种方式:

为什么函数的 arguments 参数是类数组而不是数组?如何遍历类数组?

arguments是一个对象,它的属性是从 0 开始依次递增的数字,还有callee和length等属性,与数组相似;但是它却没有数组常见的方法属性,如forEach, reduce等,所以叫它们类数组。
要遍历类数组,有三个方法:
(1)将数组的方法应用到类数组上,这时候就可以使用call和apply方法,如:

function foo(){ 
  Array.prototype.forEach.call(arguments, a => console.log(a))
}

(2)使用Array.from方法将类数组转化成数组:‌

function foo(){ 
  const arrArgs = Array.from(arguments) 
  arrArgs.forEach(a => console.log(a))
}

(3)使用展开运算符将类数组转化成数组

function foo(){ 
    const arrArgs = [...arguments] 
    arrArgs.forEach(a => console.log(a)) 
}

对象属性的循环遍历

set()和 map()区别

forEach和map方法有什么区别

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

map和Object的区别

- Map Object
意外的键 Map默认情况不包含任何键,只包含显式插入的键。 Object 有一个原型, 原型链上的键名有可能和自己在对象上的设置的键名产生冲突。
键的类型 Map的键可以是任意值,包括函数、对象或任意基本类型。 Object 的键必须是 String 或是Symbol。
键的顺序 Map 中的 key 是有序的。因此,当迭代的时候, Map 对象以插入的顺序返回键值。 Object 的键是无序的
Size Map 的键值对个数可以轻易地通过size 属性获取 Object 的键值对个数只能手动计算
迭代 Map 是 iterable 的,所以可以直接被迭代。 迭代Object需要以某种方式获取它的键然后才能迭代。
性能 在频繁增删键值对的场景下表现更好。 在频繁添加和删除键值对的场景下未作出优化。

(其他)JavaScript有哪些内置对象

全局的对象( global objects )或称标准内置对象,不要和 "全局对象(global object)" 混淆。这里说的全局的对象是说在 全局作用域里的对象。全局作用域中的其他对象可以由用户的脚本创建或由宿主程序提供。

标准内置对象的分类:

(1)值属性,这些全局属性返回一个简单值,这些值没有自己的属性和方法。例如 Infinity、NaN、undefined、null 字面量

(2)函数属性,全局函数可以直接调用,不需要在调用时指定所属对象,执行结束后会将结果直接返回给调用者。例如 eval()、parseFloat()、parseInt() 等

(3)基本对象,基本对象是定义或使用其他对象的基础。基本对象包括一般对象、函数对象和错误对象。例如 Object、Function、Boolean、Symbol、Error 等

(4)数字和日期对象,用来表示数字、日期和执行数学计算的对象。例如 Number、Math、Date

(5)字符串,用来表示和操作字符串的对象。例如 String、RegExp

(6)可索引的集合对象,这些对象表示按照索引值来排序的数据集合,包括数组和类型数组,以及类数组结构的对象。例如 Array

(7)使用键的集合对象,这些集合对象在存储数据时会使用到键,支持按照插入顺序来迭代元素。 例如 Map、Set、WeakMap、WeakSet

(8)矢量集合,SIMD 矢量集合中的数据会被组织为一个数据序列。 例如 SIMD 等

(9)结构化数据,这些对象用来表示和操作结构化的缓冲区数据,或使用 JSON 编码的数据。例如 JSON 等

(10)控制抽象对象 例如 Promise、Generator 等

(11)反射。例如 Reflect、Proxy

(12)国际化,为了支持多语言处理而加入 ECMAScript 的对象。例如 Intl、Intl.Collator 等

(13)WebAssembly

(14)其他。例如 arguments

总结: js 中的内置对象主要指的是在程序执行前存在全局作用域里的由 js 定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般经常用到的如全局变量值 NaN、undefined,全局函数如 parseInt()、parseFloat() 用来实例化对象的构造函数如 Date、Object 等,还有提供数学计算的单体内置对象如 Math 对象。

(其他)use strict是什么意思 ? 使用它区别是什么?

use strict 是一种 ECMAscript5 添加的(严格模式)运行模式,这种模式使得 Javascript 在更严格的条件下运行。设立严格模式的目的如下:

区别:

(其他)原型与原型链

对原型、原型链的理解

在JavaScript中是使用构造函数来新建一个对象的,每一个构造函数的内部都有一个 prototype 属性,它的属性值是一个对象,这个对象包含了可以由该构造函数的所有实例共享的属性和方法。当使用构造函数新建一个对象后,在这个对象的内部将包含一个指针,这个指针指向构造函数的 prototype 属性对应的值,在 ES5 中这个指针被称为对象的原型。一般来说不应该能够获取到这个值的,但是现在浏览器中都实现了 proto 属性来访问这个属性,但是最好不要使用这个属性,因为它不是规范中规定的。ES5 中新增了一个 Object.getPrototypeOf() 方法,可以通过这个方法来获取对象的原型。

当访问一个对象的属性时,如果这个对象内部不存在这个属性,那么它就会去它的原型对象里找这个属性,这个原型对象又会有自己的原型,于是就这样一直找下去,也就是原型链的概念。原型链的尽头一般来说都是 Object.prototype 所以这就是新建的对象为什么能够使用 toString() 等方法的原因。

特点: JavaScript 对象是通过引用来传递的,创建的每个新对象实体中并没有一份属于自己的原型副本。当修改原型时,与之相关的对象也会继承这一改变。

原型修改、重写

function Person(name) {
    this.name = name
}
// 修改原型
Person.prototype.getName = function() {}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype) // true
console.log(p.__proto__ === p.constructor.prototype) // true
// 重写原型
Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // false

可以看到修改原型的时候p的构造函数不是指向Person了,因为直接给Person的原型对象直接用对象赋值时,它的构造函数指向的了根构造函数Object,所以这时候p.constructor === Object ,而不是p.constructor === Person。要想成立,就要用constructor指回来:

Person.prototype = {
    getName: function() {}
}
var p = new Person('hello')
p.constructor = Person
console.log(p.__proto__ === Person.prototype)        // true
console.log(p.__proto__ === p.constructor.prototype) // true

原型链指向

p.__proto__  // Person.prototype
Person.prototype.__proto__  // Object.prototype
p.__proto__.__proto__ //Object.prototype
p.__proto__.constructor.prototype.__proto__ // Object.prototype
Person.prototype.constructor.prototype.__proto__ // Object.prototype
p1.__proto__.constructor // Person
Person.prototype.constructor  // Person

原型链的终点是什么

由于Object是构造函数,原型链终点是Object.prototype.__proto__,而Object.prototype.__proto__=== null // true,所以,原型链的终点是null。原型链上的所有原型都是对象,所有的对象最终都是由Object构造的,而Object.prototype的下一级是Object.prototype.__proto__

获得对象非原型链上的属性

使用后hasOwnProperty()方法来判断属性是否属于原型链的属性:

function iterate(obj){
   var res=[];
   for(var key in obj){
        if(obj.hasOwnProperty(key))
           res.push(key+': '+obj[key]);
   }
   return res;
} 

this关键字

第一种是函数调用模式,当一个函数不是一个对象的属性时,直接作为函数来调用时,this 指向全局对象。

console.log(window === this); // true
var a = 1;
this.b = 2;
window.c = 3;
console.log(a + b + c); // 6
function foo(){
  return this;
}
console.log(foo() === window); // true
function Person(name){
  this.name = name;
  this.say = () => {
    var name = "xb";
    return this.name;
  }
}
var person = new Person("axuebin");
console.log(person.say()); // axuebin

第二种是方法调用模式,如果一个函数作为一个对象的方法来调用时,this 指向这个对象。

var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
console.log(person.getName()); // axuebin

这里有一个需要注意的地方

var name = "xb";
var person = {
  name: "axuebin",
  getName: function(){
    return this.name;
  }
}
var getName = person.getName;
console.log(getName()); // xb

第三种是构造器调用模式,如果一个函数用 new 调用时,函数执行前会新创建一个对象,this 指向这个新创建的对象。

第四种是apply 、 call 和 bind 调用模式,这三个方法都可以显示的指定调用函数的 this 指向。其中 apply 方法接收两个参数:一个是 this 绑定的对象,一个是参数数组。call 方法接收的参数,第一个是 this 绑定的对象,后面的其余参数是传入函数执行的参数。也就是说,在使用 call() 方法时,传递给函数的参数必须逐个列举出来。bind 方法通过传入一个对象,返回一个 this 绑定了传入对象的新函数。这个函数的 this 指向除了使用 new 时会被改变,其他情况下都不会改变。

var person = {
                name: "axuebin",
                age: 25
            };
            function say(job) {
                console.log(this.name + ":" + this.age + " " + job);
            }
            say.call(person, "FE"); // axuebin:25 FE
            say.apply(person, ["FE"]); // axuebin:25 FE
            var sayPerson = say.bind(person, "FE");
            sayPerson(); // axuebin:25 FE

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

作用域链/闭包

闭包

闭包是指有权访问另一个函数作用域中变量的函数,创建闭包的最常见的方式就是在一个函数内创建另一个函数,创建的函数可以访问到当前函数的局部变量。

闭包有两个常用的用途:

比如,函数 A 内部有一个函数 B,函数 B 可以访问到函数 A 中的变量,那么函数 B 就是闭包。

function A() {
  let a = 1
  window.B = function () {
      console.log(a)
  }
}
A()
B() // 1

在 JS 中,闭包存在的意义就是让我们可以间接访问函数内部的变量。经典面试题:循环中使用闭包解决 var 定义函数的问题

for (var i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

首先因为 setTimeout 是个异步函数,所以会先把循环全部执行完毕,这时候 i 就是 6 了,所以会输出一堆 6。解决办法有三种:

for (var i = 1; i <= 5; i++) {  
;(function(j) {    
    setTimeout(function timer() {      
    console.log(j)    
        }, j * 1000)  
    })(i)
}

在上述代码中,首先使用了立即执行函数将 i 传入函数内部,这个时候值就被固定在了参数 j 上面不会改变,当下次执行 timer 这个闭包的时候,就可以使用外部函数的变量 j,从而达到目的。

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

执行上下文介绍

执行上下文的类型

执行栈

怎么创建执行上下文?

作用域链

数据类型

JavaScript有哪些数据类型

JavaScript共有八种数据类型,分别是 UndefinedNullBooleanNumberStringObjectSymbolBigInt
其中 Symbol 和 BigInt 是ES6 中新增的数据类型:

这些数据可以分为原始数据类型和引用数据类型:

两种类型的区别在于存储位置的不同:

堆和栈的概念存在于数据结构和操作系统内存中,在数据结构中:

在操作系统中,内存被分为栈区和堆区:

JavaScript 判断数组的五种方法

instanceof

const arr= []
instanceof arr === Array // true

Array.isArray

const arr = []
Array.isArray(arr) // true

const obj = {}
Array.isArray(obj) // false

Object.prototype.isPrototypeOf

const arr = []
Object.prototype.isPrototypeOf(arr, Array.prototype) // true

Object.getPrototypeOf

const arr = []
Object.getPrototypeOf(arr) === Array.prototype // true

Object.prototype.toString

const arr = []
Object.prototype.toString.call(arr) === '[object Array]' // true

const obj = {}
Object.prototype.toString.call(obj) // "[object Object]"

null 和 undefined 的差异

(其他)typeof NaN 的结果是什么?

NaN 指"不是一个数字”(not a number),NaN 是一个“警戒值”(sentinel value,有特殊用途的常规值),用于指出数字类型中的错误情况,即“执行数学运算没有成功,这是失败后返回的结果”。

typeof NaN; // "number"

NaN 是一个特殊值,它和自身不相等,是唯一一个非自反(自反,reflexive,即 x === x 不成立)的值。而 NaN !== NaN 为 true。

(其他)isNaN 和 Number.isNaN 函数的区别

JavaScript 中==、===和 Object.is()的区别

isNaN 和 Number.isNaN 函数的区别?

JavaScript 中的包装类型

在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:

const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"

在访问\'abc\'.length时,JavaScript 将'abc'在后台转换成String('abc'),然后再访问其length属性。
JavaScript也可以使用Object函数显式地将基本类型转换为包装类型:

var a = 'abc'
Object(a) // String {"abc"}

也可以使用valueOf方法将包装类型倒转成基本类型:

var a = 'abc'
var b = Object(a)
var c = b.valueOf() // 'abc'

看看如下代码会打印出什么:

var a = new Boolean( false );
if (!a) {
    console.log( "Oops" ); // never runs
}

答案是什么都不会打印,因为虽然包裹的基本类型是false,但是false被包裹成包装类型后就成了对象,所以其非值为false,所以循环体中的内容不会运行。

数组扁平化

flat(depth)

let a = [1,[2,3]];  
a.flat(); // [1,2,3]  
a.flat(1); //[1,2,3]

reduce

function flatten(arr){
  return arr.reduce(function(prev, cur){
    return prev.concat(Array.isArray(cur) ? flatten(cur) : cur)
  }, [])
}
const arr = [1, [2, [3, 4]]];
console.log(flatten(arr));

解构运算符 ...

function flatten(arr){
  while(arr.some(item => Array.isArray(item))){
    arr = [].concat(...arr);
  }
  return arr;
}

const arr = [1, [2, [3, 4]]];
console.log(flatten(arr));

数组去重

for 双重循环

function Array_dfor(data) {
  const newArray = [];
  let isRepeat;
  for (let i = 0; i < data.length; i++) {
    isRepeat = false;
    for (let j = 0; j < newArray.length; j++) {
      if (data[i] === newArray[j]) {
        isRepeat = true;
        break;
      }
    }
    if (!isRepeat) {
      newArray.push(data[i]);
    }
  }
  return newArray;
}

includes()

function Array_includes(data) {
  var arr = [];
  for (var i = 0; i < data.length; i++) {
    if (!arr.includes(data[i])) {
      arr.push(data[i])
    }
  }
  return arr;

indexOf()

function Array_indexOf(data) {
  var arr = [];
  for (var i = 0; i < data.length; i++) {
    if (arr.indexOf(data[i]) === -1){
      arr.push(data[i])
    }
      }
  return arr;
}

Map()

function Array_Map(data) {
  const newArr = [];
  const tmp = new Map();
  for (var i = 0; i < data.length; i++) {
    if (!tmp.has(data[i])){
      tmp.set(data[i],1)
      newArr.push(data[i])
    }
  }
  return newArr;
}

Set()

function Array_set(data) {
  return Array.from(new Set(data))
}

ES6

let、const、var的区别

(1)块级作用域: 块作用域由 { }包括,let和const具有块级作用域,var不存在块级作用域。块级作用域解决了ES5中的两个问题:

(2)变量提升: var存在变量提升,let和const不存在变量提升,即在变量只能在声明之后使用,否在会报错。
(3)给全局添加属性: 浏览器的全局对象是window,Node的全局对象是global。var声明的变量为全局变量,并且会将该变量添加为全局对象的属性,但是let和const不会。
(4)重复声明: var声明变量时,可以重复声明变量,后声明的同名变量会覆盖之前声明的遍历。const和let不允许重复声明变量。
(5)暂时性死区: 在使用let、const命令声明变量之前,该变量都是不可用的。这在语法上,称为暂时性死区。使用var声明的变量不存在暂时性死区。
(6)初始值设置: 在变量声明时,var 和 let 可以不用设置初始值。而const声明变量必须设置初始值。
(7)指针指向: let和const都是ES6新增的用于创建变量的语法。 let创建的变量是可以更改指针指向(可以重新赋值)。但const声明的变量是不允许改变指针的指向。

暂时性死区

箭头函数

iterator 迭代器

解构赋值

剩余/扩展运算符

扩展运算符

剩余运算符

剩余运算符和扩展运算符的区别就是,剩余运算符会收集这些集合,放到右边的数组中,扩展运算符是将右边的数组拆分成元素的集合,它们是相反的

对象属性/方法简写

需要注意的是 省略的是属性名而不是值 值必须是一个变量

for ... of 循环

Promise(常用)

(详细见异步编程部分介绍)

Module(常用)

函数默认值

(其他)ES6模块与CommonJS模块有什么异同?

ES6 Module和CommonJS模块的区别:

ES6 Module和CommonJS模块的共同点:

ES7 的特性

Array.prototype.includes()

指数操作符

console.log(2**10);// 输出 1024

ES8 的特性

async/await

它也是为了解决回调地狱的问题,它只是一种语法糖。从本质上讲,await 函数仍然是 promise,其原理跟 Promise 相似。不过比起 Promise 之后用 then 方法来执行相关异步操作,async/await 则把异步操作变得更像传统函数操作。

async function helloAsync(){
    return "helloAsync";
}
console.log(helloAsync());  // Promise {<resolved>: "helloAsync"}
helloAsync().then(v=>{
    console.log(v); // helloAsync
})

通过上面的代码可以得出结论,async 函数内 return 的值会被封装成一个 Promise 对象,由于 async 函数返回 Promise 对象,所以该函数可以按照 Promise 对象标准使用 then 方法进行后续异步操作。
(如果要把 async 函数方法跟 Promise 对象方法做对比的话,那么下面的 Promise 对象异步方法代码是完全相等于上面的 async 函数异步方法。)

var helloAsync = function(){
    return new Promise(function(resolve){
        resolve("helloAsync");
    })
}
console.log(helloAsync())  
helloAsync().then(v=>{
    console.log(v);         
})

async 函数运行的时候是同步运行的,Promise 对象本身内容也是同步运行,这一点两者也是一致的,只有在 then 方法的时候才会被放入异步队列。

await
await 操作符用于等待一个 Promise 对象,它只能在异步函数 async function 内部使用。
async 函数运行的时候是同步运行,但是当 async 函数内部存在 await 操作符的时候,则会把 await 操作符标示的内容同步执行,await 操作符标示的内容之后的代码则被放入异步队列等待。
(await 标识的代码表示该代码运行需要一定的时间,所以后续的代码得进异步队列等待)
下面放一段 await 标准用法:

function testAwait (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function helloAsync() {
  var x = await testAwait ("hello world");
  console.log(x); 
}
helloAsync ();

其实 await 多多少少对应了 Promise 对象异步方法里面的 then 方法,可以将上面代码改写成下面样式,结果也是一致的:

function testAwait (x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function helloAsync() {
  var x = testAwait ("hello world");//此处 x 是一个 Promise 对象
  x.then(function(value){
      console.log(value); 
  });
}
helloAsync ();
// hello world

上述方法把 await 去掉,使用 then 取代,能够起到同样的作用。两者都是把特定区域的代码放到异步队列中执行。

Object.values()

const obj = {a: 1, b: 2, c: 3};
const values=Object.values(obj);
console.log(values);//[1, 2, 3]

Object.entries

String padding

'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

异步编程

异步编程的实现方式

Promise

什么是 Promise

当把一件事情交给promise时,它的状态就是Pending,任务完成了状态就变成了Resolved、没有完成失败了就变成了Rejected。

Promise解决了什么问题

如题

let fs = require('fs')
fs.readFile('./a.txt','utf8',function(err,data){
  fs.readFile(data,'utf8',function(err,data){
    fs.readFile(data,'utf8',function(err,data){
      console.log(data)
    })
  })
})

上面的代码有如下缺点:

使用promise方法对代码进行改进

let fs = require('fs')
function read(url){
  return new Promise((resolve,reject)=>{
    fs.readFile(url,'utf8',function(error,data){
      error && reject(error)
      resolve(data)
    })
  })
}
read('./a.txt').then(data=>{
  return read(data) 
}).then(data=>{
  return read(data)  
}).then(data=>{
  console.log(data)
})

总结:promise可以解决了地狱回调的问题。

Promise方法

then()

let promise = new Promise((resolve,reject)=>{
    ajax('first').success(function(res){
        resolve(res);
    })
})
promise.then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{
    return new Promise((resovle,reject)=>{
        ajax('second').success(function(res){
            resolve(res)
        })
    })
}).then(res=>{

})

catch()

p.then((data) => {
     console.log('resolved',data);
},(err) => {
     console.log('rejected',err);
     }
); 
p.then((data) => {
    console.log('resolved',data);
}).catch((err) => {
    console.log('rejected',err);
});

all()

javascript
let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(1);
    },2000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(2);
    },1000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(3);
    },3000)
});
Promise.all([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果为:[1,2,3] 
})

race()

let promise1 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       reject(1);
    },2000)
});
let promise2 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(2);
    },1000)
});
let promise3 = new Promise((resolve,reject)=>{
    setTimeout(()=>{
       resolve(3);
    },3000)
});
Promise.race([promise1,promise2,promise3]).then(res=>{
    console.log(res);
    //结果:2
},rej=>{
    console.log(rej)};
)

finally()

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

上面代码中,不管promise最后的状态,在执行完then或catch指定的回调函数以后,都会执行finally方法指定的回调函数。

async/await对比Promise的优势

面向对象程序设计

创建对象

一般使用字面量的形式直接创建对象,但是这种创建方式对于创建大量相似对象的时候,会产生大量的重复代码。但 js和一般的面向对象的语言不同,在 ES6 之前它没有类的概念。但是可以使用函数来进行模拟,从而产生出可复用的对象创建方式,常见的有以下几种:

工厂模式

构造函数模式

原型模式

组合使用构造函数模式和原型模式

动态原型模式

寄生构造函数模式

继承

原型链继承

function Parent() {
   this.isShow = true
   this.info = {
       name: "yhd",
       age: 18,
   };
}

Parent.prototype.getInfo = function() {
   console.log(this.info);
   console.log(this.isShow); // true
}

function Child() {};
Child.prototype = new Parent();

let Child1 = new Child();
Child1.info.gender = "男";
Child1.getInfo();  // {name: "yhd", age: 18, gender: "男"}

let child2 = new Child();
child2.getInfo();  // {name: "yhd", age: 18, gender: "男"}
child2.isShow = false

console.log(child2.isShow); // false

借用构造函数

function Parent() {
  this.info = {
    name: "yhd",
    age: 19,
  }
}

function Child() {
    Parent.call(this)
}

let child1 = new Child();
child1.info.gender = "男";
console.log(child1.info); // {name: "yhd", age: 19, gender: "男"};

let child2 = new Child();
console.log(child2.info); // {name: "yhd", age: 19}

通过使用call()或apply()方法,Parent构造函数在为Child的实例创建的新对象的上下文执行了,就相当于新的Child实例对象上运行了Parent()函数中的所有初始化代码,结果就是每个实例都有自己的info属性。
1、传递参数
相比于原型链继承,盗用构造函数的一个优点在于可以在子类构造函数中像父类构造函数传递参数。

function Parent(name) {
    this.info = { name: name };
}
function Child(name) {
    //继承自Parent,并传参
    Parent.call(this, name);

     //实例属性
    this.age = 18
}

let child1 = new Child("yhd");
console.log(child1.info.name); // "yhd"
console.log(child1.age); // 18

let child2 = new Child("wxb");
console.log(child2.info.name); // "wxb"
console.log(child2.age); // 18

在上面例子中,Parent构造函数接收一个name参数,并将他赋值给一个属性,在Child构造函数中调用Parent构造函数时传入这个参数, 实际上会在Child实例上定义name属性。为确保Parent构造函数不会覆盖Child定义的属性,可以在调用父类构造函数之后再给子类实例添加额外的属性。

组合继承

function Parent(name) {
   this.name = name
   this.colors = ["red", "blue", "yellow"]
}
Parent.prototype.sayName = function () {
   console.log(this.name);
}

function Child(name, age) {
   // 继承父类属性
   Parent.call(this, name)
   this.age = age;
}
// 继承父类方法
Child.prototype = new Parent();

Child.prototype.sayAge = function () {
   console.log(this.age);
}

let child1 = new Child("yhd", 19);
child1.colors.push("pink");
console.log(child1.colors); // ["red", "blue", "yellow", "pink"]
child1.sayAge(); // 19
child1.sayName(); // "yhd"

let child2 = new Child("wxb", 30);
console.log(child2.colors);  // ["red", "blue", "yellow"]
child2.sayAge(); // 30
child2.sayName(); // "wxb"

上面例子中,Parent构造函数定义了name,colors两个属性,接着又在他的原型上添加了个sayName()方法。Child构造函数内部调用了Parent构造函数,同时传入了name参数,同时Child.prototype也被赋值为Parent实例,然后又在他的原型上添加了个sayAge()方法。这样就可以创建 child1,child2两个实例,让这两个实例都有自己的属性,包括colors,同时还共享了父类的sayName方法。

原型式继承

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun()
}

let person = {
  name: "yhd",
  age: 18,
  friends: ["jack", "tom", "rose"],
  sayName:function() {
    console.log(this.name);
  }
}

let person1 = objectCopy(person);
person1.name = "wxb";
person1.friends.push("lily");
person1.sayName(); // wxb

let person2 = objectCopy(person);
person2.name = "gsr";
person2.friends.push("kobe");
person2.sayName(); // "gsr"

console.log(person.friends); // ["jack", "tom", "rose", "lily", "kobe"]

寄生式继承

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}

function createAnother(original) {
  let clone = objectCopy(original);
  clone.getName = function () {
    console.log(this.name);
  };
  return clone;
}

let person = {
     name: "yhd",
     friends: ["rose", "tom", "jack"]
}

let person1 = createAnother(person);
person1.friends.push("lily");
console.log(person1.friends);
person1.getName(); // yhd

let person2 = createAnother(person);
console.log(person2.friends); // ["rose", "tom", "jack", "lily"]

寄生式组合继承

function objectCopy(obj) {
  function Fun() { };
  Fun.prototype = obj;
  return new Fun();
}

function inheritPrototype(child, parent) {
  let prototype = objectCopy(parent.prototype); // 创建对象
  prototype.constructor = child; // 增强对象
  Child.prototype = prototype; // 赋值对象
}

function Parent(name) {
  this.name = name;
  this.friends = ["rose", "lily", "tom"]
}

Parent.prototype.sayName = function () {
  console.log(this.name);
}

function Child(name, age) {
  Parent.call(this, name);
  this.age = age;
}

inheritPrototype(Child, Parent);
Child.prototype.sayAge = function () {
  console.log(this.age);
}

let child1 = new Child("yhd", 23);
child1.sayAge(); // 23
child1.sayName(); // yhd
child1.friends.push("jack");
console.log(child1.friends); // ["rose", "lily", "tom", "jack"]

let child2 = new Child("yl", 22)
child2.sayAge(); // 22
child2.sayName(); // yl
console.log(child2.friends); // ["rose", "lily", "tom"]

垃圾回收机制

标记清除

引用计数

function fun() {
    let obj1 = {};
    let obj2 = {};
    obj1.a = obj2; // obj1 引用 obj2
    obj2.a = obj1; // obj2 引用 obj1
}

这种情况下,就要手动释放变量占用的内存:

obj1.a =  null
obj2.a =  null

哪些情况会导致内存泄漏

以下四种情况会造成内存的泄漏:

事件流

传播过程

捕获阶段

目标阶段

冒泡阶段

说一下图片的懒加载和预加载

mouseover 和 mouseenter 的区别

事件委托

事件委托的作用

为什么要事件委托

事件委派应用

给 ul 注册点击事件,然后利用事件对象的 target 来找到当前点击的 li,因为点击 li,事件会冒泡到 ul 上,ul 有注册事件,就会触发事件监听器,这里只操作了一次 DOM ,提高了程序的性能。

<body>
    <ul>
        <li>1</li>
        <li>2</li>
        <li>3</li>
        <li>4</li>
        <li>5</li>
    </ul>
    <script>
        // 事件委托的核心原理:给父节点添加侦听器, 利用事件冒泡影响每一个子节点
        var ul = document.querySelector('ul');
        ul.addEventListener('click', function(e) {
            // alert('点我应有弹框!');
            // e.target 这个可以得到我们点击的对象
            e.target.style.backgroundColor = 'pink';
        })
    </script>
</body>

这里面试官可能会继续出题,如果通过按钮新增一个新的li标签,如何监听事件

事件循环机制/Event Loop

MacroTask(宏任务)

MicroTask(微任务)

浏览器中的 Event Loop

我们总结一下,每一次循环都是一个这样的过程:

node 中的事件循环的顺序

总结:浏览器环境下,microtask 的任务队列是每个 macrotask 执行完之后执行。而在 Node.js 中,microtask 会在事件循环的各个阶段之间执行,也就是一个阶段执行完毕,就会去执行 microtask 队列的任务。

如题

setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function () {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function () {
    console.log('promise2')
  })
}, 0)

微任务和宏任务的区别

手撕代码

防抖和节流

防抖实现

所谓防抖,就是指触发事件后 n 秒后才执行函数,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

function debounce(fn, delay) {
  var timeout = null;
  return function (e) {
    clearTimeout(timeout);
    timeout = setTimeout(() => {
      fn.apply(this, arguments);
    }, delay)
  }
}

function handle() {
  console.log('防抖', Math.random())
}

window.addEventListener('scroll', debounce(handle, 50))

节流实现

所谓节流,就是指连续触发事件但是在 n 秒中只执行一次函数。 节流会稀释函数的执行频率。

function throttle(fn, delay) {
  let canRun = true;
  return function () {
    if (!canRun) return;
    canRun = false;
    setTimeout(()=>{
      fn.apply(this,arguments)
      canRun = true
    },delay)
  }
}
function sayHi(e) {
  console.log('节流:', e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi,500));

应用场景

1、search 搜索联想,用户在不断输入值时,用防抖来节约请求资源。
2、window 触发 resize 的时候,不断的调整浏览器窗口大小会不断的触发这个事件,用防抖来让其只触发一次

1、鼠标不断点击触发,mousedown(单位时间内只触发一次)
2、监听滚动事件,比如是否滑到底部自动加载更多,用 throttle 来判断

深浅拷贝

浅拷贝

function shallowCopy(obj) {
  var data={};
  for (var i in obj){
    // for in  循环,也会循环原型链上的属性,所以这里需要判断一下确定某个对象是否具有带指定名称的属性
    if (obj.hasOwnProperty(i)){
        data[i] = obj[i]
    }
  }
  return data

}

深拷贝

function deepCopy(obj) {
  if (typeof obj !== 'object') return;
  var newObj = obj instanceof Array ? [] : {};
  for (var key in obj) {
    if (obj.hasOwnProperty(key)) {
      newObj[key] = typeof obj[key] === 'object' ? deepCopy(obj[key]) : obj[key]
    }
  }
  return newObj;
}

手动封装Ajax

const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
  if (this.readyState !== 4) return;
  // 当请求成功时
  if (this.status === 200) {
    handle(this.response);
  } else {
    console.error(this.statusText);
  }
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
  console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);

使用Promise封装AJAX:

// promise 封装实现:
function getJSON(url) {
  // 创建一个 promise 对象
  let promise = new Promise(function(resolve, reject) {
    let xhr = new XMLHttpRequest();
    // 新建一个 http 请求
    xhr.open("GET", url, true);
    // 设置状态的监听函数
    xhr.onreadystatechange = function() {
      if (this.readyState !== 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;
}

手写 instanceof方法

//方法一
function new_instanceof(left, right) {
  let _left = left.__proto__
  while (_left !== null) {
    if (_left === right.prototype) {
      return true
    }
    _left = _left.__proto__
  }
  return false
}
//方法二
function myInstanceof(left, right) {
  // 获取对象的原型
  let proto = Object.getPrototypeOf(left)
  // 获取构造函数的 prototype 对象
  let prototype = right.prototype; 

  // 判断构造函数的 prototype 对象是否在对象的原型链上
  while (true) {
    if (!proto) return false;
    if (proto === prototype) return true;
    // 如果没有找到,就继续从其原型上找,Object.getPrototypeOf方法用来获取指定对象的原型
    proto = Object.getPrototypeOf(proto);
  }
}

call()/apply()/bind()函数

手写 call()

Function.prototype.myCall = function(context) {
  // 判断调用对象
  if (typeof this !== "function") {
    console.error("type error");
  }
  // 获取参数
  let args = [...arguments].slice(1),
    result = null;
  // 判断 context 是否传入,如果未传入则设置为 window
  context = context || window;
  // 将调用函数设为对象的方法
  context.fn = this;
  // 调用函数
  result = context.fn(...args);
  // 将属性删除
  delete context.fn;
  return result;
};

手写 apply()

Function.prototype.myApply=function(context){
  // 获取调用`myApply`的函数本身,用 this 获取,如果 context 不存在,则为 window
  var context = context || window;
  var fn = Symbol();
  context[fn] = this;
  //获取传入的数组参数
  var args = arguments[1];
  if (args == undefined) { //没有传入参数直接执行
    // 执行这个函数
    context[fn]()
  } else {
    // 执行这个函数
    context[fn](...args);
  }
  // 从上下文中删除函数引用
  delete context.fn;
}

手写bind()

Function.prototype.myBind = function(context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new TypeError("Error");
  }
  // 获取参数
  var args = [...arguments].slice(1),
    fn = this;
  return function Fn() {
    // 根据调用方式,传入不同绑定值
    return fn.apply(
      this instanceof Fn ? this : context,
      args.concat(...arguments)
    );
  };
};

手写 promise.all()

function PromiseAll(promises) {
  //返回一个 Promise 对象
  return new Promise((resolve, reject) => {
    //判断传入的参数是否为数组
    if (!Array.isArray(promises)) {
      return reject(new Error("传入的参数不是数组"))
    }
    const res = [];
    //设置一个计时器
    let count = 0;
    for (let i = 0; i < promises.length; i++) {
      Promise.resolve(promises[i]).then(value => {
        res[i] = value;
        if (++count === promises.length) {
          resolve(res)
        }
      }).catch(e => reject(e))
    }
  })
}
PromiseAll([1, 2, 3]).then(o => console.log(o))
PromiseAll([1,Promise.resolve(3)]).then(o=>console.log(o))
PromiseAll([1,Promise.reject(3).then(o=>console.log(o))])

new操作符的实现

new操作符的执行过程:

(1)首先创建了一个新的空对象

(2)设置原型,将对象的原型设置为函数的 prototype 对象。

(3)让函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)

(4)判断函数的返回值类型,如果是值类型,返回创建的对象。如果是引用类型,就返回这个引用类型的对象。

具体实现:

function objectFactory() {
  let newObject = null;
  let constructor = Array.prototype.shift.call(arguments);
  let result = null;
  // 判断参数是否是一个函数
  if (typeof constructor !== "function") {
    console.error("type error");
    return;
  }
  // 新建一个空对象,对象的原型为构造函数的 prototype 对象
  newObject = Object.create(constructor.prototype);
  // 将 this 指向新建对象,并执行函数
  result = constructor.apply(newObject, arguments);
  // 判断返回对象
  let flag = result && (typeof result === "object" || typeof result === "function");
  // 判断返回结果
  return flag ? result : newObject;
}
// 使用方法
objectFactory(构造函数, 初始化参数);

JavaScript 两个变量交换值

异或运算

var a = 1, // 二进制:0001
    b = 2;  // 二进制:0010

a = a ^ b; // 计算结果:a = 0011, b = 0010
b = a ^ b; // 计算结果:a = 0011, b = 0001
a = a ^ b; // 计算结果:a = 0010, b = 0001

ES6 的解构

let a = 1,
    b = 2;

[a, b] = [b, a];

利用数组特性进行交换

var a = 1,
    b = 2;

a = [a, b];
b = a[0];
a = a[1];

斐波那契数列

function fibonacci(n) {
  /*
    斐波那契:由0和1开始,之后的斐波那契数列每一项都等于前两项之和。
    斐波那契数列前两项都是1,所以判断n是否等于1或者2,如果是则直接返回1
    斐波那契数列示例:1、1、2、3、5、8、13、21、34
  */
  n = n && parseInt(n);
  if (n == 1 || n == 2) {
    return 1;
  }
  // 使用arguments.callee实现递归
  return arguments.callee(n - 2) + arguments.callee(n - 1);
}

let sum = fibonacci(8)
console.log(sum) // 21

斐波那契数列的第n项

// 要求输入一个整数n,请你输出斐波那契数列的第n项(从0开始,第0项为0,第1项是1)。
function Fibonacci(n) {
  let arr = [0, 1];
  for (let i = 2; i <= n; i++) {
    arr.push(arr[i - 1] + arr[i - 2])
  }
  return arr[n]
}

用setTimeout实现setInterval

function mySetInterval(fn, millisec) {
  function interval() {
    setTimeout(interval, millisec);
    fn();
  }
  setTimeout(interval, millisec)
}

实现函数柯里化

function curry(fn) {
  let judge = (...args) => {
    if (args.length == fn.length) return fn(...args)
    return (...arg) => judge(...args, ...arg)
  }
  return judge
}

let curryTest=curry((a,b,c,d)=>a+b+c+d)

console.log(curryTest(1,2,3)(4))
console.log(curryTest(1,2)(4)(3))
console.log(curryTest(1,2)(3,4))
console.log(curryTest(1)(2)(3)(4))

选择排序

function selectSort(data) {
  for (var i = 0; i < data.length; i++) {
    var minIndex = i;
    var temp;
    for (var j = i + 1; j < data.length; j++) {
      if (data[j] < data[minIndex]) {
        minIndex = j;
      }
    }
    temp = data[i];
    data[i] = data[minIndex];
    data[minIndex] = temp;
  }
  return data;
}

插入排序

function insertSort(data) {
  var preIndex, current;
  for (var i = 1; i < data.length; i++) {
    preIndex = i - 1;
    current = data[i];
    while (preIndex >= 0 && current < data[preIndex]) {
      data[preIndex + 1] = data[preIndex];
      preIndex--;
    }
    data[preIndex+1] = current;
  }
  return data;
}

快速排序

function quickSort(data) {
  if (data.length <= 1) {
    return data;
  }
  var pivot, pivotIndex, left, right;
  left = [];
  right = [];
  pivotIndex = Math.floor(data.length / 2);
  pivot = data.splice(pivotIndex, 1)[0];
  for (var i = 0; i < data.length; i++) {
    if (data[i] < pivot) {
      left.push(data[i])
    } else {
      right.push(data[i])
    }
  }
  return quickSort(left).concat([pivot], quickSort(right))
}

冒泡排序

function bubbleSort(data) {
  var temp;
  for (var i = 0; i < data.length - 1; i++) {
    for (var j = 0; j < data.length - 1 - i; j++) {
      if (data[j] > data[j + 1]) {
        temp = data[j];
        data[j] = data[j + 1];
        data[j + 1] = temp;
      }
    }
  }
  return data;
}

第一秒打印1的问题

// 写一个函数,第一秒打印 1,第二秒打印 2
function f1() {
  for (let i = 0; i < 5; i++) {
    setTimeout(() => {
      console.log(i)
    }, 1000 * i)
  }
}

// f1();
function f2() {
  for (var i = 0; i < 5; i++) {
    (function f(i) {
      setTimeout(() => {
        console.log(i)
      }, 1000 * i)
    })(i)
  }
}

f2();

(牛客)获取 url 中的参数

// 获取 url 中的参数
// 1. 指定参数名称,返回该参数的值 或者 空字符串
// 2. 不指定参数名称,返回全部的参数对象 或者 {}
// 3. 如果存在多个同名参数,则返回数组
// 4. 不支持URLSearchParams方法
// 输入:
// http://www.nowcoder.com?key=1&key=2&key=3&test=4#hehe key
// 输出:
// [1, 2, 3]
function getUrlParam(sUrl, sKey) {

  var param = sUrl.split('#')[0].split('?')[1];
  if (sKey){//指定参数名称
    var strs = param.split('&');
    var arrs = new Array();//如果存在多个同名参数,则返回数组
    for(var i = 0, len = strs.length; i < len; i++){
      var tmp = strs[i].split('=');
      if(tmp[0] == sKey){
        arrs.push(tmp[1]);
      }
    }
    if (arrs.length == 1){//返回该参数的值或者空字符串
      return arrs[0];
    } else if (arrs.length == 0){
      return "";
    } else {
      return arrs;
    }
  } else {//不指定参数名称,返回全部的参数对象 或者 {}
    if(param == undefined || param == ""){
      return {};
    } else {
      var strs = param.split('&');
      var arrObj = new Object();
      for(var i = 0, len = strs.length; i < len; i++){
        var tmp = strs[i].split('=');
        if (!(tmp[0] in arrObj)) {
          arrObj[tmp[0]] = [];
        }
        arrObj[tmp[0]].push(tmp[1]);
      }
      return arrObj;
    }
  }
}

青蛙跳台阶

// 一只青蛙一次可以跳上1级台阶,也可以跳上2级。求该青蛙跳上一个n级的台阶总共有多少种跳法(先后次序不同算不同的结果)。
function jumpFloor(number) {
  // write code here
  let res = []
  res[0] = 0;
  res[1] = 1;
  res[2] = 2;
  for (let i = 3; i <= number; i++) {
    res[i] = res[i - 1] + res[i - 2]
  }
  return res[number]
}

module.exports = {
  jumpFloor: jumpFloor
};

青蛙跳台阶扩展问题

// 一只青蛙一次可以跳上1级台阶,也可以跳上2级……它也可以跳上n级。求该青蛙跳上一个n级的台阶(n为正整数)总共有多少种跳法。
function jumpFloorII(number) {
  // write code here
  if (number === 0) return 0
  return Math.pow(2, number - 1)
}

module.exports = {
  jumpFloorII: jumpFloorII
};