自用js面试题

194 阅读27分钟

1、js数据类型

  • 基本数据类型

    Number、String、Boolean、Null、Undefined、Symbol(表示独一无二的值,由Symbol函数生成)、bigInt

  • 引用数据类型

    object、Array、Date、Function、RegExp

  • 存放位置不同

    基本类型的变量会保存在栈内存中,如果在一个函数中声明一个基本类型的变量,这个变量在函数执行结束后会自动销毁。 而引用类型的变量名会保存在栈内存中,但是变量值会存储在堆内存中,引用类型的变量不会自动销毁,当没有引用变量引用它时,系统的垃圾回收机制会回收它

  • 赋值不同

    基本类型的赋值相当于深拷贝,赋值后相当于又开辟了一个内存空间

    引用类型的赋值是浅拷贝,当我们对对象进行操作时,其实操作的只是对象的引用

  • 如何实现一个对象的深拷贝呢?

    最简单的方式就是使用 JSON.stringifyJSON.parse 这组 API

    JSON.stringify() 方法用于将 JavaScript 值转换为 JSON 字符串;JSON.parse() 方法将 JSON 字符串转化为 JavaScript 对象

    let obj = {
        name:'小明'
    }
    let obj2 JSON.parse(JSON.stringify(obj))
    //但是这种方式有些问题,当 obj 里面有函数或者 undefined 就会失效
    

    递归实现深拷贝

    function deep(obj) {
        let oo = {}
        for (const key in obj) {
            if (typeof obj[key] === 'object') {
                oo[key] = deep(obj[key])
    		} else {
                oo[key] = obj[key]
            }
        }
        return oo
    }
    

2、js变量和函数声明的提升

在js中变量和函数的声明会提升到最顶部执行

函数的提升高于变量的提升

函数内部如果用 var 声明了相同名称的外部变量,函数将不再向上寻找。

匿名函数不会提升。

3、闭包

闭包就是能够读取其他函数内部变量的函数

闭包基本上就是一个函数内部返回一个函数

好处

可以读取函数内部的变量

将变量始终保持在内存中

可以封装对象的私有属性和私有方法

坏处

比较耗费内存、使用不当会造成内存溢出的问题


几乎所有编程语言最基本的功能之一,就是能够储存变量当中的值,并且能在之后对这个值进行访问或修改。事实上,正是这种储存和访问变量的值的能力将状态带给了程序。若没有了状态这个概念,程序虽然也能够执行一些简单的任务,但它会受到高度限制,做不到非常有趣。 但是将变量引入程序会引起几个很有意思的问题,也正是我们将要讨论的:这些变量住在哪里?换句话说,它们储存在哪里?最重要的是,程序需要时如何找到它们?

这些问题说明需要一套设计良好的规则来存储变量,并且之后可以方便地找到这些变量, 这套规则被称为作用域。

es6之前只有函数作用域,没有块级作用域。

什么是闭包呢?在 JavaScript 中,根据词法作用域的规则,内部函数总是可以访问其外部函数中声明的变量,当通过调用一个外部函数返回一个内部函数后,即使该外部函数已经执行结束了,但是内部函数引用外部函数的变量依然保存在内存中,我们就把这些变量的集合称为闭包。

function demo() {
	let a = '张三'
    return function () {
        return a
    }
}
const d = demo()
console.log(d()) // 张三
//我们通过在 demo 函数中返回一个函数,在返回的函数中再返回这个变量,然后当我们在外部去调用这个返回出来的函数时就可以得到这个变量的值。也就是说 d 函数 保存了对 a 的引用,这就形成了闭包。

实例

for (var i = 0; i < 10; i++) {
  setTimeout(function () {
    console.log(i);
  }, 1000);
}// 打印10个10
1、代码执行 for 循环,i 依次从 0 加到 9,循环十次。
2、代码等待定时器 1 秒钟时间到,执行定时的里面的内容。
3、执行打印 i 语句,因为定时器函数中没有声明 i 变量,所以代码只能去定时器函数外的作用域去查找。
4、在外部找到了 i 此时 i 已经变成了 10,所以打印 1010
for (let i = 0; i < 10; i++) {
    function(i) {
        setTimeout(function() { 
            console.log(i)
        },1000)
    }
}

4、== 和 ===的区别

==是非严格意义上的相等

值相等就相等

===是严格意义上的相等,会比较两边的数据类型和值大小

值和引用地址都相等才相等

5、this

this总是指向函数的直接调用者

如果有new关键字,this指向new出来的对象

在事件中,this指向触发这个事件的对象

6、js数组和对象的遍历方式

for in

for

forEach

for-of

7、map与forEach的区别

forEach 方法,是最基本的方法,就是遍历与循环,默认有 3 个传参:分别是遍历的数组内

容 item、数组索引 index、和当前遍历数组 Array

map 方法,基本用法与 forEach 一致,但是不同的,它会返回一个新的数组,所以 callback

需要有 return 值,如果没有,会返回 undefined

8、箭头函数与普通函数的区别?

(1)箭头函数比普通函数更加简洁 如果没有参数,就直接写一个空括号即可 如果只有一个参数,可以省去参数括号 如果有多个参数,用逗号分割 如果函数体的返回值只有一句,可以省略大括号 如果函数体不需要返回值,且只有一句话,可以给这个语句前面加一个void关键字。最常用的就是调用一个函数: let fn = () => void doesNotReturn() (2) 箭头函数没有自己的this 箭头函数不会创建自己的this,所以它没有自己的this,它只会在自己作用域的上一层继承this。所以箭头函数中的this的指向在它在定义时一家确定了,之后不会改变。

(3)箭头函数继承来的this指向永远不会改变

(4) call()、apply()、bind()等方法不能改变箭头函数中的this指向

(5) 箭头函数不能作为构造函数使用

(6) 箭头函数没有自己的arguments

(7) 箭头函数没有prototype

(8) 箭头函数不能用作Generator函数,不能使用yeild关键字

9、同源策略

同源指的是域名、协议、端口号相同

10、如何解决跨域

jsonp跨域

document.domain + iframe 跨域

nodejs中间件代理跨域

后端在头部信息里面设置安全域名 crows

11、严格模式的限制

变量必须声明后再使用 函数的参数不能有同名属性,否则报错 不能使用 with 语句 禁止 this 指向全局对象

12、es6新增

新增模板字符串 箭头函数 for-of(用来遍历数据—例如数组中的值。) ES6 将 Promise 对象纳入规范,提供了原生的 Promise 对象。 增加了 let 和 const 命令,用来声明变量。 还有就是引入 module 模块的概念

函数可以设置默认值,函数如果设置了默认值,length属性会失效

Set和Map数据解构

Set - 它类似于数组,但是成员的值都是唯一的,没有重复的值

const arr = new Set([1, 2, 3])
arr.add(4); // 向arr中添加元素
arr.delete(1); // 删除数据为1的元素
arr.size; // 返回arr长度
arr.has(2); // 判断arr中是否有2这个元素
arr.clear(); // 清除所有元素

下面我们使用 Set 来实现数组去重:

const arr = [1, 2, 3, 4, 1, 2, 3]
const arr2 = [...new Set(arr)]
console.log(arr2) // [1,2,3,4]

Map 的使用很像对象 Object,也是通过健值的方式储存数据,不同的是 Object 中的键只能是字符串,而 Map 中的健可以是任意数据类型

const m = new Map()
const k = {
    name:'张三'
}
m.set(k,18)
m.get(k) // 18

Map 的 api 和 Set 的基本一致,如下所示:

const m = new Map()
m.set("name", "张三"); // 设置元素
m.get("name"); // 张三
m.has("name"); // 判断有没有这个元素
m.size; // 获取map的长度

Promise

Promise 是异步编程的一种解决方案,在没有 Promise 之前,我们只能通过回调的方式实现异步编程,如下所示:

function fn(name, fn) {
    const nameVal = '我是' + name
    setTimeout(function() [
        fn(nameVal)
    ],1000)
}
fn('张三',function(val) {
    console.log(val) // 我是张三
})

但是如果回调函数比较多的话,就会陷入回调地狱,大大降低了代码的可读性。而 Promise 就是来解决这个问题的,具体用法如下所示:

const promise = new Promise(function(resolve,reject){
    if(/*异步程序成功*/){
        resolve(res)
    }else{
        reject(error)
    }
})
promise.then(function(res){

},function(error){

})

Promise有几个比较重要的方法

1、Promise.prototype.then() Promise实例添加状态改变时的回调函数

2、Promise.prototype.catch() 发生错误时的回调函数

3、Promise.all()Promise.all可以将多个Promise实例包装成一个新的Promise实例。同事成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值

4、Promise.race()可以将多个Promise实例包装成一个新的Promise实例,哪个结果获得的快,就返回哪个结果,不管结果本身是成功状态还是失败状态。

13、attribute 和 property 的区别是什么?

attribute 是 dom 元素在文档中作为 html 标签拥有的属性 property 就是 dom 元素在 js 中作为对象拥有的属性。 对于 html 的标准属性来说,attribute 和 property 是同步的,是会自动更新的 但是对于自定义的属性来说,他们是不同步的

14、let和const 的区别是什么?

let 命令不存在变量提升,如果在 let 前使用,会导致报错 如果块区中存在 let 和 const 命令,就会形成封闭作用域 不允许重复声明 const定义的是常量,不能修改,但是如果定义的是对象,可以修改对象内部的数据,也即const 声明的简单类型的变量不可以修改

15、内存泄漏

定义:程序中己动态分配的堆内存由于某种原因程序未释放或无法释放引发的各种问题。 js中可能出现的内存泄漏情况:结果:变慢,崩溃,延迟大等 js中可能出现的内存泄漏原因 全局变量 dom 清空时,还存在引用 定时器未清除 子元素存在引起的内存泄露

16、数组(array)方法

map : 遍历数组,返回回调返回值组成的新数组 forEach : 无法 break ,可以用 try/catch 中 throw new Error 来停止 filter : 过滤 some : 有一项返回 true ,则整体为 true every : 有一项返回 false ,则整体为 false join : 通过指定连接符生成字符串 push / pop : 末尾推入和弹出,改变原数组, 返回推入/弹出项 unshift / shift : 头部推入和弹出,改变原数组,返回操作项 sort(fn) / reverse : 排序与反转,改变原数组 concat : 连接数组,不影响原数组, 浅拷贝 slice(start, end) : 返回截断后的新数组,不改变原数组 splice(start,number,value…): 返回删除元素组成的数组,value 为插入项,改变原数组 indexOf / lastIndexOf(value, fromIndex) : 查找数组项,返回对应的下标 reduce / reduceRight(fn(prev, cur) ,defaultPrev) : 两两执行,prev 为上次化简函数的return 值,cur 为当前值(从第二项开始)

17、说说异步编程的实现方式?

回调函数 优点:简单、容易理解 缺点:不利于维护、代码耦合高

事件监听 优点:容易理解,可以绑定多个事件,每个事件可以指定多个回调函数 缺点:事件驱动型,流程不够清晰

发布/订阅(观察者模式) 类似于事件监听,但是可以通过‘消息中心’,了解现在有多少发布者,多少订阅者

Promise 对象 优点:可以利用 then 方法,进行链式写法;可以书写错误时的回调函数 缺点:编写和理解,相对比较难

Generator 函数 优点:函数体内外的数据交换、错误处理机制 缺点:流程管理不方便

async 函数 优点:内置执行器、更好的语义、更广的适用性、返回的是 Promise、结构清晰 缺点:错误处理机制

18、说说面向对象编程思想?

基本思想是使用对象,类,继承,封装等基本概念来进行程序设计 优点 易维护 易扩展 开发工作的重用性、继承性高,降低重复工作量。 缩短了开发周期

19、项目性能优化

减少 HTTP 请求数 减少 DNS 查询 使用 CDN 避免重定向 图片懒加载 减少 DOM 元素数量 减少 DOM 操作 使用外部 JavaScript 和 CSS 压缩 JavaScript、CSS、字体、图片等 优化 CSS Sprite 使用 iconfont 多域名分发划分内容到不同域名 尽量减少 iframe 使用 避免图片 src 为空 把样式表放在 link 中 把 JavaScript 放在页面底部

20、什么是单线程,和异步的关系?

单线程 :只有一个线程,只能做一件事 原因 : 避免 DOM 渲染的冲突 浏览器需要渲染 DOM JS 可以修改 DOM 结构 JS 执行的时候,浏览器 DOM 渲染会暂停 两段 JS 也不能同时执行(都修改 DOM 就冲突了) webworker 支持多线程,但是不能访问 DOM 解决方案 :异步

21、说说负载均衡?

单台服务器共同协作,不让其中某一台或几台超额工作,发挥服务器的最大作用 http 重定向负载均衡:调度者根据策略选择服务器以 302 响应请求,缺点只有第一次有效果,后续操作维持在该服务器 dns 负载均衡:解析域名时,访问多个 ip 服务器中的一个(可监控性较弱)原因 - 避免 DOM 渲染的冲突 反向代理负载均衡:访问统一的服务器,由服务器进行调度访问实际的某个服务器,对统一的服务器要求大,性能受到 服务器群的数量

22、作用域链?

作用域链可以理解为一组对象列表,包含 父级和自身的变量对象,因此我们便能通过作用域链访问到父级里声明的变量或者函数


如果在当前作用域中没有发现此变量的声明,程序就会去他父作用域去查找,直到找到为止,在浏览器中最外层的作用域是 window,如果最后在 window 上都没有找到的话,就会返回 xxx is not defined 查找结束。

23、什么是原型、原型链、继承?

所有的函数都有prototype属性(原型) 所有的对象都有__proto__属性 在Javascript中,每个函数都有一个原型属性prototype指向自身的原型,而由这个函数创建的对象也有一个__proto__属性指向这个原型,而函数的原型是一个对象,所以这个对象也会有一个__proto__指向自己的原型,这样逐层深入直到Object对象的原型,这样就形成了原型链。


`__proto__`和`[[prototype]]`是同一个东西,都是原型

每个函数都有 prototype 属性,每一个对象都有 __proto__ 属性(这个属性称之为原型[[prototype]]),在我们执行 new 的时候,对象的 __proto__ 指向这个构造函数的 prototype

首先,每个函数都有个 prototype 属性,这个属性指向函数的原型对象,同时 prototype 里面有个 constructor 属性回指到该函数。

function Demo() {}

Demo.prototype.constructor === Demo; // true

现在我们使用 new 操作符来创建一个实例对象,如下所示:

当使用 new 操作符后,Demo 就变成了构造函数

function Demo() {}
const d = new Demo()

d 既然是对象,自然有 __proto__,同时指向构造函数 Demo 的 prototype,如下所示:

function Demo() {}
const d = new Demo()
d.__proto__ === Demo.prototype // true

当我们访问一个对象的属性时,程序会先去这个对象里面查找,如果没有找到会去这个对象的原型上查找,如下所示:

function Demo() {
  this.name = "蓝桥";
}
Demo.prototype.say = function () {
  console.log("我是", this.name);
};

const d = new Demo();

// 虽然 Demo 上没有 say 方法,但是因为Demo的prototype上有此方法,所以下面的调用可以正常打印。

d.say(); // 我是蓝桥
d.say() 
	↓
d中查找方法
	↓
发现d中没有
	↓
去d.__proto__中查找
	↓
d.__proto__ === Demo.prototype
	↓
等价于去Demo.prototype中查找
	↓
在Demo.prototype中找到say
	↓
调用say()

继承

1、原型继承 - 创建一个父类直接赋值给子类的prototype,缺点是父类的属性会被所示的子类共享,且不能给父类传参

2、构造函数继承,在子类中通过改变this指向来调用父类,缺点是不能继承父类的prototype

3、组合继承,结合原型继承和构造函数继承的优点,缺点是子类继承了两份父类,重复继承

4、寄生组合继承,通过Object.create方式来优化组合继承,缺点是实现复杂

5、ES6 extends实现继承,缺点是兼容性较差


所有对象都有__proto__,所有的函数都有prototype,prototype中存的是一个对象,这个对象同样拥有__proto__

所有函数和对象的__proto__都是由显式原型赋值而来的。

所有对象的__proto__都由构造函数的prototoye赋值而来

24、JS垃圾回收机制是怎样的?

1.概述

js的垃圾回收机制是为了防止内存泄漏(已经不需要的某一块内存还一直存在着),垃圾回收机制就是不停歇的寻找这些不再使用的变量,并且释放掉它所指向的内存。 在JS中,JS的执行环境会负责管理代码执行过程中使用的内存。

2.变量的生命周期

当一个变量的生命周期结束之后,它所指向的内存就会被释放。js有两种变量,局部变量和全局变量,局部变量是在他当前的函数中产生作用,当该函数结束之后,该变量内存会被释放,全局变量的话会一直存在,直到浏览器关闭为止。

3.js垃圾回收方式 有两种方式: 标记清除、引用计数

标记清除:大部分浏览器使用这种垃圾回收,当变量进入执行环境(声明变量)的时候,垃圾回收器将该变量进行了标记,当该变量离开环境的时候,将其再度标记,随之进行删除。

引用计数:这种方式常常会引起内存的泄露,主要存在于低版本的浏览器。它的机制就是跟踪某一个值得引用次数,当声明一个变量并且将一个引用类型赋值给变量得时候引用次数加1,当这个变量指向其他一个时引用次数减1,当为0时出发回收机制进行回收。

25、逐进增强和优雅降级

逐进增强 针对低版本浏览器进行构建页面,保证最基本的功能,然后再针对高版本浏览器进行效果、交互等改进和追加功能达到更好的用户体验。 优雅降级 一开始就构建完整的功能,然后再针对低版本浏览器进行兼容

26、JavaScript new一个对象的过程

  1. 首先创一个新的空对象。
  2. 根据原型链,设置空对象的 __proto__ 为构造函数的 prototype
  3. 构造函数的 this 指向这个对象,执行构造函数的代码(为这个新对象添加属性)。
  4. 判断函数的返回值类型,如果是引用类型,就返回这个引用类型的对象。
function myNew(context) {
  const obj = new Object();
  obj.__proto__ = context.prototype;
  const res = context.apply(obj, [...arguments].slice(1));
  return typeof res === "object" ? res : obj;
}

27、解构及扩展运算符

解构运算符多用于接口返回数据的处理,把自己想要的数据解构出来,并设置默认值

const data = await getInfo()
const {name = 'xxx',sex = 0} = data

扩展运算符(...)可以理解成把对象里面的全部内容解构出来,使用如下所示:

const obj1 = {
  name: "张三",
};
const obj2 = {
  age: 18,
};
const user = { ...obj1, ...obj2 }; // { name:'张三',age:18 }

28、手写call

call 函数可以用来改变函数的 this 指向

const obj = { name:'张三' }
const userInfo = { 
    name:'李四',
    say:function(age) {
        console.log('我叫' + this.name,'年龄'+ age)
    }
}
userInfo.say.call(obj.18) // 输出我叫张三,年龄18
//原本 say 函数里面的 this 指向的是 userInfo 本身,经过 call 调用后改变了 this 指向,从而指向了 obj。
Function.prototype.myCall = function(obj, ...arg) {
    // ...arg代表后面的所有参数
    const fnName = Symbol()
    obj[fnName] = this
    const res = obj[fnName](...arg)
    delete obj.fnName
    return res
}
//实现这个函数的原理就是把要改变 this 指向的函数作为对象的一个属性,这样的话在实现这个函数的时候,this 自然就会指向这个对象了。

//如果看不懂上面的函数那也没关系,因为这不是我们的重点。我们需要关心的是 Symbol 的使用,在这里我们使用 Symbol 创建出了一个唯一的函数名称,这样就确保不会跟 obj 原有的名称冲突。

29、浏览器事件循环机制

首先需要明确,浏览器之所以有事件循环机制,最本质的原因就是因为 JavaScript 是单线程的问题。那 Javascript 为什么是单线程呢?

作为浏览器脚本语言,JavaScript 的主要用途是与用户互动,以及操作 DOM。这决定了它只能是单线程,否则会带来很复杂的同步问题。比如,假定 JavaScript 同时有两个线程,一个线程在某个 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准呢?这不就乱套了嘛。虽然 JavaScript 是单线程的,但是在实际开发中我们确实需要处理一些异步的问题,那就要求 JavaScript 的运行环境来提供一套方案让我们更好的处理一些异步问题,在前端层面,浏览器是 JavaScript 唯一的运行环境,这就有了浏览器的事件循环机制。

先来学习一下什么是宏任务和微任务

  • 宏任务

    JavaScript 是单线程,但浏览器是多线程的,JavaScript 执行在浏览器中,在 V8 里跑着的一直是一个一个的宏任务,就相当于排队打饭一样,一个人相当于一个宏任务。具体执行流程图如下:

    宏任务1
    ↓
    宏任务2
    
    let name = '张三'
    setTimeout(function () {
        console.log(name)// 李四
    }, 0)
    name = '李四'
    console.log(name)// 李四
    

    <

    浏览器在执行上面代码时会先执行主线程代码(宏任务 1)然后再执行setTimeout 里面的代码。虽然 setTimeout 的定时时间为 0,但是浏览器在处理的时候会把它当做下一个宏任务进行处理,定时器也是宏任务的典型代表。

  • 微任务

    当浏览器执行完一个宏任务后,就会看看有没有微任务执行,如果有微任务执行就会先把当前的微任务执行完,再去执行下一个宏任务。微任务的代表有 ajax、回调函数、和 Promise。 执行流程如下:

    主线程从 "任务队列" 中读取事件,这个过程是循环不断的,所以整个过程的这种运行机制又称为 Event Loop(事件循环)。为了方便理解,我们来看下面一个案例:

    console.log(1);
    setTimeout(() => {
      console.log(2);
    });
    
    new Promise((res, req) => {
      console.log(3);
      res();
    }).then(() => {
      console.log(4);
    });
    
    console.log(5);
    
    // 以上代码的执行结果是? 13542
    
    代码从上到下执行
    ⬇
    打印 1
    ⬇
    遇到 setTimeout 是下一个宏任务,目前先不处理
    ⬇
    遇到 Promise 打印出 3 then 回调函数是微任务,先不处理
    ⬇
    打印 5 且第一个宏任务执行完毕
    ⬇
    开始执行微任务队列
    ⬇
    打印 4 微任务队列执行完毕
    ⬇
    开始执行下一个宏任务
    ⬇
    打印 2
    ⬇
    程序结束
    

    所以以上代码打印结果为 1 3 5 4 2,这里需要注意的是 只有 Promise then 或者 catch 里面的方法是微任务,Promise 里面的回调是当作主程序的宏任务进行处理的。

微任务是 ES6 语法规定的(被压入 micro task queue)。

宏任务是由浏览器规定的(通过 Web APIs 压入 Callback queue)。

宏任务执行时间一般比较长。

每一次宏任务开始之前一定是伴随着一次 event loop 结束的,而微任务是在一次 event loop 结束前执行的。

30、柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数且返回结果的新函数的技术。

foo(a,b,c) -> foo(a)(b)(c) 的过程就叫柯里化

foo(a,b,c){
    return a+b+c
}

foo(a) {
    return fo1(b) {
        return fo2(c) {
            console.log(a+b+c)
        }
    }
}
const foo = a=>b=>c=> console.log(a+b+c)
foo(a)(b)(c)

31、判断一个值是否是数组

Array.isArray(arr); // true
arr.__proto__ === Array.prototype; // true
arr instanceof Array; // true
Object.prototype.toString.call(arr); // "[object Array]"

32、浅拷贝和深拷贝

浅拷贝

​ 创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值,如果属性是引用类型,拷贝的就是内存地址 ,所以如果其中一个对象改变了这个地址,就会影响到另一个对象。

// Object.assign()方法可以把任意多个的源对象自身的可枚举属性拷贝给目标对象,然后返回目标对象。
let obj1 = { person: {name: "kobe", age: 41},sports:'basketball' };
let obj2 = Object.assign({}, obj1);
obj2.person.name = "wade";
obj2.sports = 'football'
console.log(obj1); // { person: { name: 'wade', age: 41 }, sports: 'basketball' }

// 展开运算符
let obj1 = { name: 'Kobe', address:{x:100,y:100}}
let obj2= {... obj1}
obj1.address.x = 200;
obj1.name = 'wade'
console.log('obj2',obj2) // obj2 { name: 'Kobe', address: { x: 200, y: 100 } }

// 直接赋值
for (let key in obj){
    newObj[key] = obj[new]
}

// 函数库lodash,该函数库也有提供_.clone用来做 Shallow Copy,后面我们会再介绍利用这个库实现深拷贝。
var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.clone(obj1);
console.log(obj1.b.f === obj2.b.f);// true

// Array.prototype.concat()
let arr = [1, 3, {
    username: 'kobe'
    }];
let arr2 = arr.concat();    
arr2[2].username = 'wade';
console.log(arr); //[ 1, 3, { username: 'wade' } ]

// Array.prototype.slice()
let arr = [1, 3, {
    username: ' kobe'
    }];
let arr3 = arr.slice();
arr3[2].username = 'wade'
console.log(arr); // [ 1, 3, { username: 'wade' } ]

深拷贝

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

乞丐版

这种方法虽然可以实现数组或对象深拷贝,但不能处理函数和正则,因为这两者基于JSON.stringify和JSON.parse处理后,得到的正则就不再是正则(变为空对象),得到的函数就不再是函数(变为null)了。

JSON.parse(JSON.stringify())

lodash.cloneDeep()

var _ = require('lodash');
var obj1 = {
    a: 1,
    b: { f: { g: 1 } },
    c: [1, 2, 3]
};
var obj2 = _.cloneDeep(obj1);
console.log(obj1.b.f === obj2.b.f);// false

手写深拷贝

遍历对象、数组直到里边都是基本数据类型,然后再去复制,就是深度拷贝

function deepClone(obj, hash = new WeakMap()) {
  if (obj === null) return obj; // 如果是null或者undefined我就不进行拷贝操作
  if (obj instanceof Date) return new Date(obj);
  if (obj instanceof RegExp) return new RegExp(obj);
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") return obj;
  // 是对象的话就要进行深拷贝
  if (hash.get(obj)) return hash.get(obj);
  let cloneObj = new obj.constructor();
  // 找到的是所属类原型上的constructor,而原型上的 constructor指向的是当前类本身
  hash.set(obj, cloneObj);
  for (let key in obj) {
    if (obj.hasOwnProperty(key)) {
      // 实现一个递归拷贝
      cloneObj[key] = deepClone(obj[key], hash);
    }
  }
  return cloneObj;
}
let obj = { name: 1, address: { x: 100 } };
obj.o = obj; // 对象存在循环引用的情况
let d = deepClone(obj);
obj.address.x = 200;
console.log(d);

手写深拷贝简单版

function deepClone(newObj,obj) {
    // 遍历
    // 如果遇到obj[key]是复杂数据类型,再进行一次遍历操作
    for (let key in obj) {
        if (obj[key] instanceof Array) {
            // 保证newObj[key]是数组
            newObj[key] = []
            // 递归进行遍历
            deepClone(newObj[key],obj[key])
        } else if (obj[key] instanceof Object) {
            newObj[key] = {}
            // 递归进行遍历
            deepClone(newObj[key],obj[key])
        } else {
            newObj[key] = obj[key]
        }
    }
}

// 解决循环引用问题

33、0.1+0.2 !== 0.3 的原因及解决办法

小数被转换成二进制后会变成无限循环,而JavaScript采用的是IEEE754的64位双精度版本,由:

1为数符:标记正负,0为正,1为负

11位阶码:数字的整数部分

52位尾数:数字的小数部分

在JavaScript中只能存储52位小数,那么0.1的小数位在第52位时就需要判读进位(第53位为1就+1,为0则不进位)

console.log(parseFloat((0.1 + 0.2).toFixed(10)) === 0.3); // true
parseFloat(0.1+0.2).toFixed(10)
parseFloat((0.1+0.2).toFixed(10))
console.log(parseFloat((0.1 + 0.2).toFixed(10))); // 0.3
想办法规避掉这类小数计算时的精度问题就好了,那么最常用的方法就是将浮点数转化成整数计算。因为整数都是可以精确表示的。
(0.1*10+0.2*10)/10 -> 0.3

34、call()、bind()、apply()

call() 方法在使用一个指定的 this 值和若干个指定的参数值的前提下调用某个函数或方法。

Function.prototype.myCall = function (context) {
  // 判断调用对象
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 首先获取参数
  let args = [...arguments].slice(1);
  let result = null;
  // 判断 context 是否传入,如果没有传就设置为 window
  context = context || window;
  // 将被调用的方法设置为 context 的属性
  // this 即为我们要调用的方法
  context.fn = this;
  // 执行要被调用的方法
  result = context.fn(...args);
  // 删除手动增加的属性方法
  delete context.fn;
  // 将执行结果返回
  return result;
};

apply()我们会了 call 的实现之后,apply 就变得很简单了,他们没有任何区别,除了传参方式。

Function.prototype.myApply = function (context) {
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  let result = null;
  context = context || window;
  // 与上面代码相比,我们使用 Symbol 来保证属性唯一
  // 也就是保证不会重写用户自己原来定义在 context 中的同名属性
  const fnSymbol = Symbol();
  context[fnSymbol] = this;
  // 执行要被调用的方法
  if (arguments[1]) {
    result = context[fnSymbol](...arguments[1]);
  } else {
    result = context[fnSymbol]();
  }
  delete context[fnSymbol];
  return result;
}

bind 返回的是一个函数

Function.prototype.myBind = function (context) {
  // 判断调用对象是否为函数
  if (typeof this !== "function") {
    throw new Error("Type error");
  }
  // 获取参数
  const args = [...arguments].slice(1),
  const fn = this;
  return function Fn() {
    return fn.apply(
      this instanceof Fn ? this : context,
      // 当前的这个 arguments 是指 Fn 的参数
      args.concat(...arguments)
    );
  };
};

35、hash和history

  • hash

    hash 模式是一种把前端路由的路径用井号 # 拼接在真实 url 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发 onhashchange 事件。

    hash变化会触发网页跳转,即浏览器的前进和后退。

    hash可以改变url,但是不会触发页面重新加载(hash的改变是记录在 window.history 中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次 http 请求,所以这种模式不利于 SEO 优化。hash 只能修改 # 后面的部分,所以只能跳转到与当前 url 同文档的 url

    hash 通过 window.onhashchange 的方式,来监听 hash 的改变,借此实现无刷新跳转的功能。

    hash 永远不会提交到 server 端(可以理解为只在前端自生自灭)。

  • history

    history APIH5 提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL 地址而不重新发起请求

    对于 history 来说,主要有以下特点:

    • 新的 url 可以是与当前 url 同源的任意 url ,也可以是与当前 url 一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中。
    • 通过 history.state ,添加任意类型的数据到记录中。
    • 可以额外设置 title 属性,以便后续使用。
    • 通过 pushStatereplaceState 来实现无刷新跳转的功能。

36、set和map的区别

1、Map是键值对,Set是值得集合,当然键和值可以是任何得值 2、Map可以通过get方法获取值,而set不能因为它只有值 3、都能通过迭代器进行for...of 遍历 4、Set的值是唯一的可以做数组去重,而Map由于没有格式限制,可以做数据存储

37、map和foreach有什么区别

foreach()方法会针对每一个元素执行提供的函数,该方法没有返回值,是否会改变原数组取决与数组元素的类型是基本类型还是引用类型 map()方法不会改变原数组的值,返回一个新数组,新数组中的值为原数组调用函数处理之后的值:

38、localStorage sessionStorage cookies 有什么区别?

localStorage:以键值对的方式存储 储存时间没有限制 永不生效 除非自己删除记录
sessionStorage:当页面关闭后被清理与其他相比不能同源窗口共享 是会话级别的存储方式
cookies 数据不能超过4k 同时因为每次http请求都会携带cookie 所有cookie只适合保存很小的数据 如会话标识

39、ES6 模块与 CommonJS 模块差异

CommonJS

require()
module.exports = {}

ES6

import ...
export 
export default

CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。

CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。

CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。

​ CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。

CommonJS 模块的require()是同步加载模块,ES6 模块的import命令是异步加载,有一个独立的模块依赖的解析阶段。

40、数组去重

简单版

let array = [1, 1, '1', '1']
let arr2 = [...new Set(array)]

filter

indexOf()方法返回在数组中可以找到一个给定元素的第一个索引,当第一个和当前的index相等的时候,说明这是第一个找到的元素,当返回的索引和index不相等的时候,说明当前存在至少两个相同的元素。
function unique(arr) {
  return arr.filter((item, index, array) => {
    return array.indexOf(item) === index;
  });
}

对象的键名是一致的,无法区分Number和String

let newArr = []
let obj = {}
for (let i = 0;i<arr.length;i++) {
    if (!obj[arr[i]]){
        obj[arr[i]] = 1
        newArr.push(arr[i])
    } else {
        obj[arr[i]]++
    }
}

41、http

1.1 状态码分类

  • 1xx - 服务器收到请求。
  • 2xx - 请求成功,如 200。
  • 3xx - 重定向,如 302。
  • 4xx - 客户端错误,如 404。
  • 5xx - 服务端错误,如 500。

1.2 常见状态码

  • 200 - 成功。
  • 301 - 永久重定向(配合 location,浏览器自动处理)。
  • 302 - 临时重定向(配合 location,浏览器自动处理)。
  • 304 - 资源未被修改。
  • 403 - 没权限。
  • 404 - 资源未找到。
  • 500 - 服务器错误。
  • 504 - 网关超时。

42、性能优化

代码层面

  • 防抖和节流(resize,scroll,input)。

  • 减少回流(重排)和重绘。

  • 事件委托。

  • css 放 ,js 脚本放 最底部。

  • 减少 DOM 操作。

  • 按需加载,比如 React 中使用 React.lazyReact.Suspense ,通常需要与 webpack 中的 splitChunks 配合。

构建方面

  • 压缩代码
  • 常用的第三方库使用CDN服务

43、冒泡排序

这里外圈决定要循环多少轮,而内圈指定双指针

function bobbleSort(arr) {
    let temp = 0
    for (let i = 0; i < arr.length -1; i++) {
        for (let j = 0; j< arr.length -1 -i;j++) {
            if (arr[j] > arr[j+1]) {
                temp = arr[j+1]
                arr[j+1] = arr[j]
                arr[j] = temp
            }
        }
    }
}

let arr = [1,5,8,7,9,4,6,10,2,3]
bobbleSort(arr)
console.log(arr);

44、防抖节流

深拷贝、数组去重、vue2响应式、ajax、手写数组排序和求中位数