前端JavaScript常见面试题(一)

数据类型

JavaScript有哪些数据类型

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

  • Symbol 代表创建后独一无二且不可变的数据类型,它主要是为了解决可能出现的全局变量冲突的问题。
  • BigInt 是一种数字类型的数据,它可以表示任意精度格式的整数,使用 BigInt 可以安全地存储和操作大整数,即使这个数已经超出了 Number 能够表示的安全整数范围。

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

  • 栈:原始数据类型(Undefined、Null、Boolean、Number、String)
  • 堆:引用数据类型(对象、数组和函数)

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

  • 原始数据类型直接存储在栈(stack)中的简单数据段,占据空间小、大小固定,属于被频繁使用数据,所以放入栈中存储;
  • 引用数据类型存储在堆(heap)中的对象,占据空间大、大小不固定。如果存储在栈中,将会影响程序运行的性能;引用数据类型在栈中存储了指针,该指针指向堆中该实体的起始地址。当解释器寻找引用值时,会首先检索其在栈中的地址,取得地址后从堆中获得实体。

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

  • 在数据结构中,栈中数据的存取方式为先进后出。
  • 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。

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

  • 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区内存一般由开发着分配释放,若开发者不释放,程序结束时可能由垃圾回收机制回收。

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

  • 使用 Object 的原型方法 isPrototypeOf,判断两个对象的原型是否一样, 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]"
  • 为什么不直接用 obj.toString()呢?
    这是因为 toString 为 Object 的原型方法,而 Array,function 等类型作为 Object 的实例,都重写了 toString 方法。不同的对象类型调用 toString 方法时,根据原型链的知识,调用的是对应的重写之后的 toString 方法,而不会去调用 Object 上原型 toString 方法(返回对象的具体类型),所以采用 obj.toString()不能得到其对象类型,只能将 obj 转换为字符串类型;因此,在想要得到对象的具体类型时,应该调用 Object 上原型 toString 方法。

null 和 undefined 的差异

  • null 转为数字类型值为 0,而 undefined 转为数字类型为 NaN(Not a Number)
  • undefined 是代表调用一个值而该值却没有赋值,这时候默认则为 undefined
  • null 是一个很特殊的对象,最为常见的一个用法就是作为参数传入(说明该参数不是对象)
  • 设置为 null 的变量或者对象会被内存收集器回收

手写 instanceof方法

  • 我们要判断 A 是不是属于 B 这个类型,只需要当 A 的原型链上存在 B 即可,即 A 顺着proto向上查找,一旦能访问到 B 的原型对象 B.prototype,表明 A 属于 B 类型,否则的话 A 顺着proto最终会指向 null。
//方法一
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);
  }
}

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

  • ==:等同,比较运算符,两边值类型不同的时候,先进行类型转换,再比较;
  • ===:恒等,严格比较运算符,不做类型转换,类型不同就是不等;
  • Object.is()是 ES6 新增的用来比较两个值是否严格相等的方法,与===的行为基本一致。
  • 先说===,这个比较简单,只需要利用下面的规则来判断两个值是否恒等就行了:
    • 如果类型不同,就不相等
    • 如果两个都是数值,并且是同一个值,那么相等;
    • 值得注意的是,如果两个值中至少一个是 NaN,那么不相等(判断一个值是否是 NaN,可以用 isNaN()或 Object.is()来判断)。
    • 如果两个都是字符串,每个位置的字符都一样,那么相等;否则不相等。
    • 如果两个值都是同样的 Boolean 值,那么相等。
    • 如果两个值都引用同一个对象或函数,那么相等,即两个对象的物理地址也必须保持一致;否则不相等。
    • 如果两个值都是 null,或者都是 undefined,那么相等。
  • 再说 Object.is(),其行为与===基本一致,不过有两处不同:
    • +0 不等于-0。
    • NaN 等于自身。

isNaN 和 Number.isNaN 函数的区别?

  • 函数 isNaN 接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的的值都会返回 true,因此非数字值传入也会返回 true ,会影响 NaN 的判断。
  • 函数 Number.isNaN 会首先判断传入参数是否为数字,如果是数字再继续判断是否为 NaN ,不会进行数据类型的转换,这种方法对于 NaN 的判断更为准确。

什么是 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

  • ES6知识推荐大家看B站《尚硅谷Web前端ES6教程,涵盖ES6-ES11》课程,在秋招时有通过这个课程进行复习,效果不错,同时我把课程的代码和笔记放到GitHub供大家下载吧,里面讲的内容比较详细,GitHub地址见文章末尾。

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声明的变量是不允许改变指针的指向。
图片说明

暂时性死区

  • ES6 新增的 let、const 关键字声明的变量会产生块级作用域,如果变量在当前作用域中被创建之前被创建出来,由于此时还未完成语法绑定,如果我们访问或使用该变量,就会产生暂时性死区的问题,由此我们可以得知,从变量的创建到语法绑定之间这一段空间,我们就可以理解为‘暂时性死区’

箭头函数

  • 箭头函数没有 arguments
  • 箭头函数没有 prototype 属性,不能用作构造函数
  • 箭头函数没有自己 this,它的 this 是词法的,引用的是上下文的 this,即在你写这行代码的时候就箭头函数的 this 就已经和外层执行上下文的 this 绑定了(这里个人认为并不代表完全是静态的,因为外层的上下文仍是动态的可以使用 call,apply,bind 修改,这里只是说明了箭头函数的 this 始终等于它上层上下文中的 this)
  • 箭头函数的 this 指向即使使用 call,apply,bind 也无法改变(这里也验证了为什么 ECMAScript 规定不能使用箭头函数作为构造函数,因为它的 this 已经确定好了无法改变)

iterator 迭代器

  • 可迭代的数据结构会有一个[Symbol.iterator]方法
  • [Symbol.iterator]执行后返回一个 iterator 对象
  • iterator 对象有一个 next 方法
  • 执行一次 next 方法(消耗一次迭代器)会返回一个有 value,done 属性的对象

解构赋值

  • 解构赋值可以直接使用对象的某个属性,而不需要通过属性访问的形式使用,对象解构原理个人认为是通过寻找相同的属性名,然后原对象的这个属性名的值赋值给新对象对应的属性

剩余/扩展运算符

扩展运算符

  • 以数组为例,使用扩展运算符使得可以"展开"这个数组,可以这么理解,数组是存放元素集合的一个容器,而使用扩展运算符可以将这个容器拆开,这样就只剩下元素集合,你可以把这些元素集合放到另外一个数组里面。

图片说明

剩余运算符

  • 剩余运算符最重要的一个特点就是替代了以前的 arguments

图片说明

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

对象属性/方法简写

  • 对象属性简写
  • es6 允许当对象的属性和值相同时,省略属性名

图片说明

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

  • 方法简写

图片说明

for ... of 循环

  • for ... of 是作为 ES6 新增的遍历方式,允许遍历一个含有 iterator 接口的数据结构并且返回各项的值
  • ES3 中的 for ... in 的区别如下 1、for ... of 只能用在可迭代对象上,获取的是迭代器返回的 value 值,for ... in 可以获取所有对象的键名 2、for ... in 会遍历对象的整个原型链,性能非常差不推荐使用,而 for ... of 只遍历当前对象不会遍历它的原型链 3、对于数组的遍历,for ... in 会返回数组中所有可枚举的属性(包括原型链上可枚举的属性),for ... of 只返回数组的下标对应的属性值

Promise(常用)

(后面讲到异步再详细介绍)

Module(常用)

  • ES6 Module 使用 import 关键字导入模块,export 关键字导出模块,它还有以下特点
  • ES6 Module 是静态的,也就是说它是在编译阶段运行,和 var 以及 function 一样具有提升效果(这个特点使得它支持 tree shaking)
  • 自动采用严格模式(顶层的 this 返回 undefined)
  • ES6 Module 支持使用 export {<变量>}导出具名的接口,或者 export default 导出匿名的接口

函数默认值

  • ES6 允许在函数的参数中设置默认值
    ES5 写法:
    图片说明
    ES6写法:
    图片说明

ES7 的特性

Array.prototype.includes()

  • includes() 函数用来判断一个数组是否包含一个指定的值,如果包含则返回 true,否则返回 false。

  • includes 函数与 indexOf 函数很相似,下面两个表达式是等价的:
    arr.includes(x)arr.indexOf(x) >= 0

  • 使用 indexOf()验证数组中是否存在某个元素,这时需要根据返回值是否为-1 来判断:

    let arr = ['react', 'angular', 'vue'];
    if (arr.indexOf('react') !== -1){    
      console.log('react 存在');
    }

指数操作符

  • 在 ES7 中引入了指数运算符,具有与 Math.pow(..)等效的计算结果。
console.log(2**10);// 输出 1024

ES8 的特性

  • async/await
  • Object.values()
  • Object.entries()
  • String padding
  • 函数参数列表结尾允许逗号
  • Object.getOwnPropertyDescriptors()

async/await

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

  • async 用于声明一个异步函数,该函数执行完之后返回一个 Promise 对象,可以使用 then 方法添加回调函数。
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()

  • Object.values()是一个与 Object.keys()类似的新函数,但返回的是 Object 自身属性的所有值,不包括继承的值。
const obj = {a: 1, b: 2, c: 3};
const values=Object.values(obj);
console.log(values);//[1, 2, 3]

Object.entries

  • Object.entries()函数返回一个给定对象自身可枚举属性的键值对的数组。

String padding

  • 在 ES8 中 String 新增了两个实例函数 String.prototype.padStart 和 String.prototype.padEnd,允许将空字符串或其他字符串添加到原始字符串的开头或结尾。
'x'.padStart(5, 'ab') // 'ababx'
'x'.padStart(4, 'ab') // 'abax'
'x'.padEnd(5, 'ab') // 'xabab'
'x'.padEnd(4, 'ab') // 'xaba'

JavaScript基础

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

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

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

为什么 script 要放在尾部?

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

  • 浏览器一边下载 HTML 网页,一边开始解析。
  • 解析过程中,发现 script 标签
  • 暂停解析,网页渲染的控制权转交给 JavaScript 引擎
  • 如果 script 标签引用了外部脚本,就下载该脚本,否则就直接执行
  • 执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页

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

  • 延迟加载就是等页面加载完成之后再加载 JavaScript 文件。js 延迟加载有助于提高页面加载速度。

一般有以下几种方式:

  • defer 属性: 给 js 脚本添加 defer 属性,这个属性会让脚本的加载与文档的解析同步解析,然后在文档解析完成后再执行这个脚本文件,这样的话就能使页面的渲染不被阻塞。多个设置了 defer 属性的脚本按规范来说最后是顺序执行的,但是在一些浏览器中可能不是这样。
  • async 属性: 给 js 脚本添加 async 属性,这个属性会使脚本异步加载,不会阻塞页面的解析过程,但是当脚本加载完成后立即执行 js 脚本,这个时候如果文档没有解析完成的话同样会阻塞。多个 async 属性的脚本的执行顺序是不可预测的,一般不会按照代码的顺序依次执行。
  • 动态创建 DOM 方式: 动态创建 DOM 标签的方式,可以对文档的加载事件进行监听,当文档加载完成后再动态的创建 script 标签来引入 js 脚本。
  • 使用 setTimeout 延迟方法: 设置一个定时器来延迟加载 js 脚本文件
  • 让 JS 最后加载: 将 js 脚本放在文档的底部,来使 js 脚本尽可能的在最后来加载执行。

防抖和节流

防抖实现

所谓防抖,就是指触发事件后 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));

应用场景

  • debounce

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

  • throttle

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

为什么函数的 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)) 
}

什么是 DOM 和 BOM?

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

(1)location 对象

(2)history 对象

  • history.go() -- 前进或后退指定的页面数 history.go(num);
  • history.back() -- 后退一页
  • history.forward() -- 前进一页

(3)Navigator 对象

  • navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
  • navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie

深浅拷贝

  • 浅拷贝:仅仅是复制了引用,彼此之间的操作会互相影响
  • 深拷贝:在堆中重新分配内存,不同的地址,相同的值,互不影响

浅拷贝

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;
}

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

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

对象属性的循环遍历

  • for ... in

    • for in 循环会遍历对象自身和继承的可枚举属性,不包含 Symbol 属性。

    • 如果只需要对象自身的属性,可以通过 Object.prototype.hasOwnProperty() 进行过滤。

  • Object.keys(), Object.values(), Object.entries()

    • ES5 引入了 Object.keys 方法,返回一个数组,成员是参数对象自身的(不含继承的)所有可遍历(enumerable)属性(不含 Symbol 属性)的键名。
  • Object.getOwnPropertyNames()

    • Object.getOwnPropertyNames 返回一个数组,包含对象自身的所有属性(不含 Symbol 属性,但是包括不可枚举属性)的键名。
  • Object.getOwnPropertySymbols()

    • Object.getOwnPropertySymbols 返回一个数组,包含对象自身的所有 Symbol 属性的键名。
  • Reflect.ownKeys()

    • Reflect.ownKeys 返回一个数组,包含对象自身的(不含继承的)所有键名,不管键名是 Symbol 或字符串,也不管是否可枚举。

set()和 map()区别

  • Map 对象保存键值对。任何值(对象或者原始值) 都可以作为一个键或一个值。构造函数 Map 可以接受一个数组作为参数。
  • Set 对象允许你存储任何类型的值,无论是原始值或者是对象引用。它类似于数组,但是成员的值都是唯一的,没有重复的值。

forEach和map方法有什么区别

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

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

作用域链/闭包

闭包

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

闭包有两个常用的用途:

  • 闭包的第一个用途是使我们在函数外部能够访问到函数内部的变量。通过使用闭包,可以通过在外部调用闭包函数,从而在外部访问到函数内部的变量,可以使用这种方法来创建私有变量。
  • 闭包的另一个用途是使已经运行结束的函数上下文中的变量对象继续留在内存中,因为闭包函数保留了这个变量对象的引用,所以这个变量对象不会被回收。

比如,函数 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,从而达到目的。

  • 第二种就是使用 setTimeout 的第三个参数,这个参数会被当成 timer 函数的参数传入。
for (var i = 1; i <= 5; i++) {
  setTimeout(
    function timer(j) {
      console.log(j)
    },
    i * 1000,
    i
  )
}
  • 第三种就是使用 let 定义 i 了来解决问题了,这个也是最为推荐的方式
for (let i = 1; i <= 5; i++) {
  setTimeout(function timer() {
    console.log(i)
  }, i * 1000)
}

执行上下文介绍

  • 简而言之,执行上下文是评估和执行 JavaScript 代码的环境的抽象概念。每当 Javascript 代码在运行的时候,它都是在执行上下文中运行。

执行上下文的类型

  • 全局执行上下文 — 这是默认或者说基础的上下文,任何不在函数内部的代码都在全局上下文中。它会执行两件事:创建一个全局的 window 对象(浏览器的情况下),并且设置 this 的值等于这个全局对象。一个程序中只会有一个全局执行上下文。
  • 函数执行上下文 — 每当一个函数被调用时, 都会为该函数创建一个新的上下文。每个函数都有它自己的执行上下文,不过是在函数被调用时创建的。函数上下文可以有任意多个。每当一个新的执行上下文被创建,它会按定义的顺序(将在后文讨论)执行一系列步骤。
  • Eval 函数执行上下文 — 执行在 eval 函数内部的代码也会有它属于自己的执行上下文,但由于 JavaScript 开发者并不经常使用 eval,所以在这里我不会讨论它。

执行栈

  • 执行栈,也就是在其它编程语言中所说的“调用栈”,是一种拥有 LIFO(后进先出)数据结构的栈,被用来存储代码运行时创建的所有执行上下文。

怎么创建执行上下文?

  • 创建执行上下文有两个阶段:1) 创建阶段 和 2) 执行阶段。

  • 在创建阶段会发生三件事:

    • this 值的决定,即我们所熟知的 This 绑定。
    • 创建词法环境组件。
    • 创建变量环境组件。
  • 执行阶段

    • 在此阶段,完成对所有这些变量的分配,最后执行代码。

作用域链

  • 当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级(词法层面上的父级)执行上下文的变量对象中查找,一直找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表就叫做作用域链。

分割线

补充

ES6知识点笔记——GitHub地址

https://github.com/AprilEcho/ES6.git

内容持续更新中~

#互联网求职##面经##秋招##前端##JavaScript#
全部评论
太牛了哇,大佬
点赞 回复
分享
发布于 2021-11-26 01:09
mark
点赞 回复
分享
发布于 2021-12-25 10:34
滴滴
校招火热招聘中
官网直投
内容已整理到牛客博客,且附PDF和markdown文档网盘链接供大家下载
点赞 回复
分享
发布于 2022-01-13 09:35

相关推荐

22 95 评论
分享
牛客网
牛客企业服务