高级js前端开发那些事儿

JS 面试题汇总

2020-12-29  本文已影响0人  为光pig

说一下JS 中的数据类型有哪些

JS 数据类型包括 基本 / 引用 / 特殊 数据类型:

1.基本数据类型:String、Number、Boolean
2.引用数据类型:Object、Array、Function
3.特殊数据类型:Null、Undefined
4.原始数据类型 Symbol (ES6)
5.独一无二的值,即 Symbol('1’) != Symbol('1’)

追问:判断 JS 数据类型有几种方法

常用的有 typeof、instanceof,
不常用的有 constructor、 prototype / toString

1.typeof 是个一元运算,放在任意类型的运算数之前,返回一个 字符串 说明运算数的类型。
可检测出的类型有:
'number'、'string'、'boolean'、'object'
'undefined','function'、'symbol'
其中对象"object" 包括:Object、Array、new RegExp()、new Date() 和 Null 特殊类型
缺点:判断普通类型没有问题,但不能准确判断 引用数据类型

2.instanceof 运算符用来检测一个对象在其原型链中是否存在一个构造函数的 prototype 属性
通俗讲 instanceof 检测的是原型,检测左边的对象是否是右边类的实例

 [] instanceof Array ==> true

注意:instanceof 能够判断出 [] 是 Array 的实例,也是 Object 的实例
因为 [].proto 指向 Array.prototype,而 Array.prototype.proto 又指向了 Object.prototype,最终 Object.prototype.proto 指向了 null 原型链结束。
类似的还有 new Date(),new Error() 和 new 自定义类()
归纳:所有对象都是 Object 的实例 或 Object是一切对象的父对象

3.根据对象的 constructor 判断
原理:每个构造函数都有一个 constructor 属,指回它本身

[].coconstructor === Array ==> true

判断 数字、字符串、函数 和 日期时,必须得用关键字 new 创建才行
因为只有构造函数才有 constructor 属性,还有两点需要注意:

null 和 undefined 是无效的对象,因此不会有 constructor 存在,
函数的 constructor 是不稳定的,当重写 prototype 后,
原有的 constructor 引用会丢失,constructor 会默认为 Object

4.使用 toString 判断
toString() 是 Object 的原型方法,该方法默认返回当前对象的 [[Class]] 。
这是一个内部属性,其格式为 [object Xxx] ,其中 Xxx 就是对象的类型。
对于 Object 对象,直接调用 toString() 就能返回 [object Object] 。
而对于其他对象,则需要通过 call / apply 来调用才能返回正确的类型信息。

Object.prototype.toString.call(undefined) ===  '[object Undefined]'
Object.prototype.toString.call(null) ===  '[object Null]'
Object.prototype.toString.call(123) === '[object Number]'

5.JQuery 提供的 jquery.type()
返回说明操作数的字符串

jQuery.type(123) === "number"
jQuery.type(undefined) === "undefined"
jQuery.type(null ) === "null "
Query.type(new Date()) === "date"
jQuery.type(new Error()) === "error"

追问:null 和 undefined 有啥区别?

null:是 Null类型,表示一个 空对象指针 或 尚未存在的对象
即该处不应该有值,使用typeof运算得到 object ,是个特殊对象值,转为数值为 0
也可以理解是表示程序级的、正常的或在意料之中的值的空缺

  1. 作为函数的参数,表示该函数的参数不是对象
  2. 作为对象原型链的终点
    注意:null 不是一个对象,但 typeof null === object 原因是不同的对象在底层都会表示为二进制,在 JS 中如果二进制的前三位都为 0,就会被判断为object类型,null 的二进制全为 0,自然前三位也是 0,所以 typeof null === 'objcet'

undefined:是Undefined 类型,表示一个 的原始值 或 缺少值,
即此处应该有一个值,但还没有定义,使用 typeof undefined === 'undefined',转为数值为 NaN
它是在 ECMAScript 第三版引入的预定义全局变量,为了区分空指针对象未初始化的变量
也可以理解是表示系统级的出乎意料的类似错误的值的空缺
1.变量被声但没有赋值时
2.调用函数时,应该提供的参数没有提供时
3.对象没有赋值的属性时,属性值为 undefined
4.函数没有返回值时,默认返回值为 undefined

追问: JS 有哪些内置对象

数据封装类对象:Object、Array、Boolean、Number、String
其他对象:Function、Arguments、Math、Date、RegExp、Error

追问:说说你对原型和原型链的理解

原型: 每一个构造函数都会自动带一个 prototype 属性,是个指针,指向一个对象,就是 原型对象
原型对象 上默认有一个属性constructor ,也是个指针,指向构造函数本身

原型链: 每个实例对象都有一个原型__proto__,这个原型还可以有它自己的原型,以此类推,形成一个链式结构即原型链
每个构造函数都有一个原型对象prototype,原型对象上包含一个指向构造函数的指针 constructor
而每个实例都包含着一个指向原型对象的内部指针 __proto__
可以通过内部指针 __proto__访问到原型对象,原型对象通过 constructor 找到构造函数。
如果 A对象 在 B 对象的原型链上,可以说它们是 B对象继承了 A对象。
原型链作用:如果试图访问对象的某个属性,会首先在 对象内部 寻找该属性,直至找不到,然后才在该对象的原型里去找这个属性,以此类推。

new 关键字创建一个实例都做了什么?

1.像普通对象一样,形成自己的私有作用域( 形参赋值,变量提升 )
2.创建一个新对象,将 this 指向这个新对象( 构造函数的作用域赋给新对象 )
3.执行构造函数中的代码,为这个新对象添加属性、方法
4.返回这个新对象( 新对象为构造函数的实例 )
手写一个 new 原理如下:

function myNew(fn, ...arg){
    // 创建一个对象,让它的原型链指向 fn.prototype
    
    // 普通方法
    // let obj = {};
    // obj.__proto__ = fn.prototype;
    
    // 使用 Object.create([A对象]):创建一个空对象 obj,并让 obj.__proto__ 等于 A对象
    let obj = Object.create(fn.prototype);

    fn.call(obj, ...arg);
    return obj;
}

可以用 instanceof 测试构造函数的prototype属性是否出现在实例对象的原型链中
也可以用 obj.hasOwnProperty(prop)测试对象自身属性中是否具有指定的属性

追问:call / apply / bind 有啥区别

都是替换函数中不想要的this
callapply 是临时的且立即执行,
bind 是永久绑定不立即执行,返回一个新函数,需要时再去执行这个新函数。

call: call( thisObj, obj1, obj2... )
要求传入函数的参数必须单独传入

apply: apply(t hisObj, [argArray] )
要求传入函数的参数必须放入数组中整体传入
apply会将数组打散为单个参数值分别传入

bind: 永久绑定函数中的 this,作用如下:

1.创建一个和原函数功能完全一样的新函数.
2.将新函数中的 this 永久绑定为指定对象
3.将新函数中的部分固定参数提前永久绑定

说说 ES6、ES7、ES8 的新特性

ES6的特性:

1.类(class)
2.模块化(Module)导出(export)导入(import)
3.箭头(Arrow)函数
4.函数参数默认值
5.模板字符串
6.延展操作符(Spread operator) 和 剩余运算符(rest operator)
7.ES6中允许我们在设置一个对象的属性的时候不指定属性名
8.Promise 异步编程的解决方案
9.支持 let 与 const 块级作用域

ES7的特性

1.includes() 函数用来判断一个数组是否包含一个指定的值,返回true / false
2.指数操作符在ES7中引入了指数运算符,具有与Math.pow(..)等效的计算结果

ES8的特性

1.加入了对 async/await 的支持,也就我们所说的异步函数
2.Object.values() 是一个与 Object.keys()类似的新函数,但返回的是 Object 自身属性的所有值,不包括继承的值
3.Object.entries() 函数返回一个给定对象自身可枚举属性的键值对的数组
4.String.padStart(targetLength,[padString])String.padEnd(targetLength,padString])
5.Object.getOwnPropertyDescriptors() 函数用来获取一个对象的所有自身属性的描述符,如果没有任何自身属性,则返回空对象。

require 和 import 区别

import 和 require都是被模块化所使用。

1.遵循规范

2.调用时间

3.本质

4.性能

追问:Es6 Module 和 Common.js 的区别

CommonJS
ES6 Module 模块

ES6 模块中的值属于动态只读引用。

综上:

事件委托是什么,原理是什么

事件委托: 利用事件冒泡,只指定一个事件处理程序,就可以管理某一类型的所有事件。
原理:利用事件的 冒泡原理
事件冒泡:就是事件从最深的节点开始,然后逐步向上传播事件。

作用:
如何 阻止冒泡 和 默认事件
停止冒泡: 
window.event ? window.event.cancelBubble = true : e.stopPropagation();
阻止默认事件: 
window.event ? window.event.returnValue = false : e.preventDefault();

追问:说前端中的事件流

事件发生时在元素节点之间按照特定的顺序传播的过程叫做DOM事件流
共分为三大阶段:

捕获阶段(事件从 Document 节点 自上而下 向目标节点传播的阶段)
目标阶段(真正的目标节点正在处理事件的阶段)
冒泡阶段(事件从目标节点 自下而上 向 Document 节点传播的阶段)

事件冒泡:从事件源逐级向上传播到 DOM 最顶层节点的过程。
事件捕获:从 DOM 最顶层节点逐级向下传播到事件源的过程。

追问:说说事件队列

JavaScript语言的一大特点就是 单线程,同一个时间只能做一件事。

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是 单线程,否则会带来很复杂的同步问题。比如 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?

为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript单线程 的本质。

任务队列的本质

主线程(执行栈)和 任务队列 先进先出 的通信称为 事件循环( Event Loop )
主要分为:
宏任务(macro-task):DOM事件绑定,定时器,Ajax回调
微任务(micro-task):Promise,MutationObserver (html5新特性)
事件循环机制:主线程 =>所有微任务 ->宏任务
先进先执行,如果里面有微任务,则下一步先执行微任务,否则继续执行宏任务

setTimeout()
将事件插入到了事件队列,必须等到当前代码(执行栈)执行完,主线程才会去执行它指定的回调函数。
当主线程时间执行过长,无法保证回调会在事件指定的时间执行。
浏览器端每次setTimeout 会有 4ms 的延迟,当连续执行多个 setTimeout,有可能会阻塞进程,造成性能问题。
setImmediate()
事件插入到事件队列尾部,主线程和事件队列的函数执行完成之后立即执行。和 setTimeout(fn,0) 的效果差不多。

追问:说说堆栈

栈内存 一般储存 基础数据类型,遵循 先进后出后进先出 的原则,大小固定并由系统自动分配内存空间,运行效率高有序存储
中的 DOM render,ajax,setTimeout,setInterval会依次进入到队列中,当栈中代码执行完毕后,再将队列中的事件放到执行栈中依次执行

内存 一般储存 引用数据类型,JavaScript 不允许直接访问 堆内存 中的位置,需要从 栈中 获取该对象的地址引用/指针,再从 堆内存 中获取数据。存储值大小不定,可动态调整,主要用来存放对象。空间大,但是运行效率相对较低无序存储可根据引用直接获取

说下代码执行结果

let obj = {}, a = 0, b = '0';
obj[a] = 123;
obj[b] = 456;
console.log(obj); // {0: 456} 

对象存在 堆 中,数字属性 和 字符串属性相等
let obj = {}, a = Symbol(1), b = Symbol(1);
obj[a] = 123;
obj[b] = 456;
console.log(obj); // {Symbol(1): 123, Symbol(1): 456}

Symbol 表示独一无二的值,即 Symbol(1) != Symbol(1)
let obj = {}, a = {name: '张三'}, b = {name: '李四'};
obj[a] = 123;
obj[b] = 456;
console.log(obj); // {[object Object]: 456}

把对象作为另一个对象的属性时,会 调用 toString 转换为字符串

追问:对象和数组有啥区别

对象:是包含已命名的值的无序集合,也被称为关联数组
数组:是包含已编码的值的有序集合

追问:数组常用的操作方法有哪些

操作数组:push,splice,join,concat
遍历数组:map,forEach,reduce
筛选数组:filter,some,find,findIndex

追问:如何快速合并两个数组?

(a). arrA.concat(arrB)
(b). Array.prototype.push.apply(arrA,arrB);
(c). Array.prototype.concat.apply(arrA,arrB);
(d). Array.prototype.concat.call(arrA,arrB);
(e). 数组转成字符串拼接在切割成数组, 或者是循环其中一个数组等...
性能自测对比:
Array.prototype.concat.call > Array.prototype.concat.apply > concat > Array.prototype.push.apply

追问:map 和 forEach 有何区别

相同点:

如只是单纯的遍历可用 forEach()
如操作原数组得到新数组可用 map()

追问:什么是数组扁平化,实现扁平化的方法有哪些?

数组扁平化:一个多维数组变为一维数组,方法如下:
1.flat( ES 6)
flat() 方法会按照一个可指定的深度递归遍历数组,并将所有元素与遍历到的子数组中的元素合并为一个新数组返回。

let newArray = arr.flat([depth]);
depth值可选: 指定要提取嵌套数组的结构深度,默认值为 1,不确定层级也可写 `Infinity`。

2.reduce

function flatten(arr) {  
    return arr.reduce((result, item)=> {
        return result.concat(Array.isArray(item) ? flatten(item) : item);
    }, []);
}

3.String & split

function flatten(arr) {
    return arr.toString().split(',').map(function(item) {
        return Number(item);
    })
} 

4.join & split

function flatten(arr) {
    return arr.join(',').split(',').map(function(item) {
        return parseInt(item);
    })
}

5.扩展运算符

[].concat(...[1, 2, 3, [4, 5]]);  // [1, 2, 3, 4, 5]

也可以做一个遍历,若 arr 中含有数组则使用一次扩展运算符,直至没有为止,如下:
扩展运算符每次只能展开一层数组

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

6.递归

function flatten(arr) { 
  var res = [];
  arr.map(item => {
    if(Array.isArray(item)) {
      res = res.concat(flatten(item));
    } else {
      res.push(item);
    }
  }); 
  return res;
}

追问:说说缓存 SessionStorage,LocalStorage,Cookie

sessionStorage 是会话级别存储,只要会话结束关闭窗口,sessionStorage 立即被销毁。
localStorage 是持久化的本地存储,除非主动删除数据,否则数据是永远不会过期的。
sessionStroagelocalStroage 存储大小可以达到 5M,不能和服务器做交互。
cookie 的数据会始终在同源http请求中携带,在浏览器和服务器之间来回传递。单个cookie 不能超过4K,只在设置的 cookie 过期时间之前有效,即使窗口关闭或浏览器关闭 。很多浏览器都限制一个站点最多保存20个Cookie

说说深拷贝 和 浅拷贝

浅拷贝:只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。如果其中一个对象改变了这个地址,就会影响到另一个对象。

深拷贝:将一个对象从内存中完整的拷贝一份出来,从堆内存中开辟一个新的区域存放新对象,且修改新对象不会影响原对象。

// 递归算法实现深克隆
function deepClone(obj){
  if(obj === null) return null;
  if(typeof obj !=='object') return obj;
  if(obj instanceof RegExp) return new RegExp(obj);
  if(obj instanceof Date) return new Date(obj);

  // 克隆的结果和之前保持相同的所属类
  let newObj = new obj.constructor;
  for(let key in obj){
    if(obj.hasOwnProperty(key)){
        newObj[key] = deepFn(obj[key]);
    }
  }
  return newObj
}

.说说 DOM 和 BOM

ECMAScript (核心) : 描述了 JS 的语法 和 基本对象。
文档对象模型 (DOM): 处理 网页内容 的方法和接口。
W3C 的标准( 所有浏览器公共遵守的标准 )
浏览器对象模型 (BOM): 与 浏览器交互 的方法和接口。
各个浏览器厂商根据 DOM 在各自浏览器上的实现( 不同厂商之间实现存在差异 )

DOM 的 API :

节点创建型 API:

document.createElement(),document.createTextNode(),parent.cloneNode(true)
document.createDocumentFragment() 创建文档片段,解决大量添加节点造成的回流问题

页面修改型 API:

parent.appendChild(child),parent.removeChild(child)
parent.replcaeChild(newChild,oldChild)
parent.insertBefore(newNode, referenceNode)

节点查询型 API:

document.getElementById()
document.getElementsByTagName() 返回即时的 HTMLCollection 类型
document.getElementsByName() 根据指定的 name 属性获取元素,返回即时的 NodeList
document.getElementsByClassName() 返回即时的 HTMLCollection
document.querySelector() 获取匹配到的第一个元素,采用的是深度优先搜索
docuemnt.querySelectorAll() 返回非即时的 NodeList,也就是说结果不会随着文档树的变化而变化

节点关系型 API:

父关系型:
node.parentNode()
兄弟关系型:
node.previouSibling() 返回节点的前一个节点(包括元素节点,文本节点,注释节点)
node.previousElementSibling() 返回前一个元素节点
node.nextSibling() 返回下一个节点
node.nextElementSibling() 返回下一个元素节点

子关系型

parent.childNodes() 返回一个即时的NodeList,包括了文本节点和注释节点
parent.children() 一个即时的HTMLCollection,子节点都是Element
parent.firsrtNode(),parent.lastNode(),hasChildNodes()

元素属性型 API:

element.setAttribute(“name”,“value”) 为元素添加属性
element.getAtrribute(“name”) 获取元素的属性

元素样式型 API:

window.getComputedStyle(element) 返回一个CSSStyleDeclaration,可以从中访问元素的任意样式属性。
element.getBoundingClientRect() 返回一个DOMRect对象,里面** 包括了元素相对于可视区的位置 top,left**,以及元素的大小,单位为纯数字。可用于判断某元素是否出现在了可视区域

BOM的 API :

window对象方法:

.js 延迟加载的方式有哪些?

.说说跨域

跨域:指一个域下的文档或脚本试图去请求另一个域下的资源,由于浏览器同源策略限制而产生。
同源策略: 同协议+同端口+同域名。即便两个不同的域名指向同一个ip地址,也非同源。
如果缺少了同源策略,浏览器很容易受到XSS、CSFR 等攻击。

解决方案:

.for in 和 for of 的区别

-for in遍历的是数组的索引,在for in
(1).for inindex 索引为字符串型数字,不能直接进行几何运算
(2). for in 遍历顺序有可能不是按照实际数组的内部顺序
(3). 因为for in是遍历可枚举的属性,也包括原型上的属性( 如不想遍历原型上的属性,可通过 hasOwnProperty 判断某个属性是属于原型 还是 实例上 )。

一般是使用for in 来遍历对象,for of 遍历数组

.instanceof 的原理是什么?

function myInstanceof(left, right) {
  let prototype = right.prototype
  left = left.__proto__
  while (true) {
    if (left === null || left === undefined)
      return false
    if (prototype === left)
      return true
    left = left.__proto__
  }
}
思路:
首先获取类型的原型
然后获得对象的原型
然后一直循环判断对象的原型是否等于类型的原型,直到对象原型为 null,因为原型链最终为 null

. setInterval 存在的问题

定时器的代码执行部分不断的被调入任务队列中,如果定时器的执行时间比间隔时间长,最终可能导致定时器堆叠在一起执行。

js 引擎为了解决这个问题,采用的方式是若任务队列中存在这个定期器,则不会将新的定时器放入任务队列,这样做的弊端是可能导致某些间隔被跳过。

解决方法:循环调用setTimeout来实现setInterval:(即用setTimeout来实现setInterval

 setTimeout(function fn(){
    ...
    setTimeout(fn,delay)
},delay)

列举几条 JS 的基本代码规范

.什么是作用域链(scope chain)

作用域链: 由各级作用域对象连续引用,形成的链式结构

函数的声明周期:

作用

上一篇 下一篇

猜你喜欢

热点阅读