面试题集锦:JavaScript

124 阅读24分钟

js 中基础数据类型有哪几种?了解包装对象么?

基础数据类型有6种:string、number、boolean、null、undefined、symbol。

基础数据类型都是值,所以没有方法提供调用,例如:undefined.split('')

那为什么比如 "abc"split('') 类似这种调用可以被允许?原因是 js 中存在包装对象,会把字符串先包装成对象,然后再调用对象下的一些方法,方法调用完成之后再销毁对象。这样就完成了基础数据类型的函数调用功能。

JavaScript 允许访问字符串,数字,布尔值和 symbol 的方法和属性。为了使它们起作用,创建了提供额外功能的特殊  “对象包装器”,使用后即被销毁

详情查看:juejin.cn/post/698893…

基本类型和引用类型数据存储方式?(栈内存,堆内存)

在JS中,主要通过这两种数据结构存储数据。用户可以直接操作栈,但不允许直接操作堆

  • :比较小,存储变量和基本数据类型
  • :比较大,存储引用数据类型

如何判断一个数据是 NaN

NaN 非数字 但是用 typeof 检测是 number 类型。

  • 利用 isNaN(NaN) => true
  • 利用 NaN != NaN
  • 利用ES6 中提供的 Object.is()方法(判断两个值是否相等) Object.is(NaN,NaN) === true

Js 中 null 与 undefined 区别

相同点∶用 if 判断时,两者都会被转换成 false

不同点∶Number(null)为0;Number(undefined)为 NaN

Null表示一个值被定义了,但是这个值是空值

Undefined 变量声明但未赋值

Script标签中的async和defer属性有什么区别呢?

  • async:表示脚本会在“后台”下载,并在加载就绪时执行(加载优先顺序)。多个async不能保存执行顺序。

    • 不会阻塞 DOMContentLoaded
  • defer:表示脚本会在“后台”下载,然后等 DOM 构建完成后,脚本才会执行。。有多个 defer,按出现的顺序执行

    • 在 DOMContentLoaded 事件之前执行

DOMContentLoaded 与 load 的区别

  • DOMContentLoaded 浏览器已完全加载 HTML,并构建了 DOM 树,但像 <img> 和样式表之类的外部资源可能尚未加载完成
document.addEventListener("DOMContentLoaded", ready);
  • load 浏览器不仅加载完成了 HTML,还加载完成了所有外部资源:图片,样式等。

  • beforeunload  用户正在离开:我们可以检查用户是否保存了更改,并询问他是否真的要离开

  • unload 用户几乎已经离开了,但是我们仍然可以启动一些操作(只能执行不涉及延迟或询问用户的简单操作),例如发送统计数据

    • 我们可以使用 navigator.sendBeacon 来发送网络请求(数据大小限制在 64kb)。它会在后台发送数据,转换到另外一个页面不会有延迟:浏览器离开页面,但仍然在执行 sendBeacon

ES6新特性

const 与 let,结构赋值,for...of,对象字面量,模板字面量,展开运算符,箭头函数,默认函数参数,class类。

var let const

  • var重复声明声明时挂在 window 对象上函数作用域;会变量提升
  • let 不允许重复声明;不会通过 window 访问;块级作用域不会变量提升;声明前使用会造成“暂时性死区”;
  • const 不允许重复声明一旦声明就不允许更改(注意下对象,内层可以被更改); 不会通过 window 访问;块级作用域不会变量提升;

数组遍历查找

  • arr.forEach() 对数组中的每一个元素,执行一次提供的函数,返回 undefined不能中断循环

  • arr.find() 对数组中的每一个元素,执行一次提供的函数,查找 第一个 具有特定条件的元素找到返回该元素(结束查找),否则返回 undefined

    • 如果 function() 返回 true,则搜索停止,并返回 item。如果没有搜索到,则返回 undefined
  • arr.findIndex()arr.find() 基本上是一样的,但它返回找到元素的索引,而不是元素本身。并且在未找到任何内容时返回 -1

  • arr.filter() 对数组中的每一个元素,执行一次提供的函数,查找符合条件的所有元素,并作为一个新数组返回。

  • arr.map() 对数组中的每一个元素,执行一次提供的函数,返回执行结果组成的新数组

  • arr.some() 测试数组中是否 至少有一个元素 通过了指定函数的测试,结果返回布尔值

  • arr.every() 测试数组中是否 所有元素通过了指定函数的测试,结果返回布尔值

类数组和数组有什么区别?类数组转数组的方法有什么?

类数组与数组的区别:

  • 相同点:都可用下标访问每个元素,都有length属性

  • 不同点:数组对象的类型是Array,类数组对象的类型是object类数组不具有数组所具有的方法数组遍历可以用 for in和for循环,类数组只能用for循环遍历

类数组转数组:

  • Array.prototype.slice.call(arrayLike, start);
  • Array.from(arrayLike);
  • 扩展运算符

介绍下 Set、Map、WeakSet 和 WeakMap 的区别?

Set:“值的集合”(没有键),它的每一个值只能出现一次

WeakSet:成员都是对象成员都是弱引用,可以被垃圾回收机制回收,可以用来保存 DOM 节点,不容易造成内存泄漏;

Map:本质上是键值对的集合,就像一个 Object 一样,但是它们最大的差别是 Map 允许任何类型的键(key),而 Object 的键只能是字符串

WeakMap:只接受对象最为键名(null 除外),不接受其他类型的值作为键名;键名是弱引用,键值可以是任意的,键名所指向的对象可以被垃圾回收,此时键名是无效的不能遍历

MapSet 中迭代总是按照值插入的顺序进行的,所以我们不能说这些集合是无序的,但是我们不能对元素进行重新排序,也不能直接按其编号来获取元素

WeakMap 和 WeakSet 最明显的局限性就是不能迭代,并且无法获取所有当前内容

new 实现原理

  1. 创建一个空对象 obj
  2. 将该对象 obj 的原型链 __proto__ 指向构造函数的原型 prototype,并且在原型链 __proto__ 上设置 构造函数 constructor 为要实例化的 Fn
  3. 传入参数,并让 构造函数 Fn 改变指向到 obj,并执行
  4. 最后返回 obj

箭头函数与普通函数的区别

箭头函数是匿名函数,不能作为构造函数,不能使用 new

箭头函数不能绑定 arguments,要用 rest 参数解决

箭头函数没有原型属性

箭头函数不能绑定 this,会捕获其所在的上下文的 this值,作为自己的this值

如何判断 this ?箭头函数的 this 是什么?

可以分成三种场景来描述

1、函数中直接调用,默认情况下 this 指向 window

function fn () {
    console.log(this)
}
fn() // this 指向 window

image.png

默认情况下 this 指向 window。但在 严格模式<script type="module"> 下,thisundefined

2、在对象里调用,this 指向调用的对象

let obj = {
    name: 'una',
    fn: function () {
        console.log(this)
    }
}

obj.fn() // this 指向调用的对象

image.png

3、在构造函数及类中,this 指向实例化的对象

function Person () {
    this.name = 'una'
}
Person.prototype.fn = function () {
    console.log(this)
}

let una = new Person()
una.fn() // this 指向实例化的对象 una

image.png

箭头函数没有自己的 this,会继承外层的 this

function Person () {
    this.name = 'una'
}
Person.prototype.fn = () => {
    console.log(this)
}

let una = new Person()
una.fn() // this 指向实例化的对象 una

image.png

.call() .apply() .bind() 的区别和作用

作用:改变函数执行时的上下文。简而言之就是改变函数运行时的 this 指向(默认指向 window)。

区别:调用方式和参数传递不同

  • apply 和 call

    • 第一个参数都是要改变指向的对象
    • 第二个参数apply 是数组,而call 则是 arg1,arg2... 这种形式
  • bind

    • 返回一个新的函数,这个函数不会马上执行,需要手动执行。参数可以放在执行时传递,也可以放在绑定 this 时传入
function fn(...args){
    console.log(this, args)
}

fn(1, 2) // 默认指向 `window`

let obj = { name: 'una' }
fn.call(obj, 1, 2) 
fn.apply(obj, [1, 2]) // 传入参数必须是数组
fn.bind(obj)(1, 2) // bind 不是立即执行,需要手动执行。参数可以放在执行时传递,也可以放在绑定 this 时传入。
fn.bind(obj, 1)(2)

image.png

扩展: bind 实现

// context 是第一个参数,就是我们要绑定的 this 指向
Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError('It's not function');
    }
    
    let args = [...arguments].slice(1); // 获取参数(第一个是要绑定的this指向,要剔除掉)
    let fn = this;
    
    // bind 要手动执行,显而易见它应该返回一个函数
    return function () {
        // bind 参数可以放在执行时传递,也可以放在绑定 this 时传入。所以要把参数进行合并
        return fn.apply(context, args.concat(...arguments));
    }
}

此时执行 fn.myBind(obj, 1)(2) 结果与之前的一样。

但是如果我们换一种执行方法,通过 new 去执行函数:

let foo = fn.bind(obj)
let res = new foo(1, 2);

image.png

可以看到原生的 bind 此时返回的 this 是实例化的 fn 函数。但是我们自己实现的 bind 就不行了:

let foo1 = fn.myBind(obj)
let res1 = new foo(1, 2);

此时返回的 thisobj,与原生的效果不符。让我们稍微修改一下:

...
return function Fn() {
    // 如果是用 Fn 函数构造的就返回它的实例
    return fn.apply(this instanceof Fn ? new fn : context, args.concat(...arguments));
}

此时再执行就没有问题了。

完整版代码:

// context 是第一个参数,就是我们要绑定的 this 指向
Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError('It's not function');
    }
    
    let args = [...arguments].slice(1); // 获取参数(第一个是要绑定的this指向,要剔除掉)
    let fn = this;
    
    // bind 要手动执行,显而易见它应该返回一个函数
    return function () {
        // bind 参数可以放在执行时传递,也可以放在绑定 this 时传入。所以要把参数进行合并
        // 如果是用 Fn 函数构造的就返回它的实例
        return fn.apply(this instanceof Fn ? new fn : context, args.concat(...arguments));
    }
}

什么是回调?回调使用中存在什么问题?

答案:回调即是函数指针的调用,即是一个通过函数指针调用的函数。如下代码:

function foo (callback){
    callback && callback();
}
  
foo(() => {
    console.log('我是回调函数')
})

使用回调函数有一个很大缺点就是造成回调地狱,回调地狱是为了实现某些逻辑出现函数的层层嵌套,类似如下代码:

move(ele, 300, "1eft", functlon () {
    move(ele, 300, "top", functlon () {
        move(ele, 0, "1eft", functlon () {
            move(ele, 0, "top", functlon () {
                console.log("所有运动完毕")
            })
        })
    })
)

回调地狱会造成代码可读性及可维护性变差。同样每个嵌套函数耦合性强,一层变动会引起其他的结果变动。同样回调地狱如果出现错误不好处理错误。

解决回调地狱问题可以通过观察者横式promiseasync/await 来处理。

async await

promise

Promise.allSettled 是什么?如何实现?

Promise.allSettled 是 ES2020 新特性,可以执行多个 Promise 对象,获取多个 Promise 对象状态,无论成功或失败

Promise.all 执行多个 Promise 对象,成功执行则返回对象状态数组。但必须是 resolve 状态,否则会报错

let p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        // resolve(1)
        reject('error')
    }, 1000)
})
let p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve(2)
    }, 1000)
})

Promise.all([p1, p2]).then(res => {
    console.log(res)
})
// [1, 2]

Promise.allSettled([p1, p2]).then(res => {
    console.log(res)
})

Promise.all 若将其中一个 Promise 对象状态改为 reject,则会报错。

image.png

Promise.allSettled 不会报错。

image.png

扩展:Promise.allSettled 实现

function MyAllSettled (list) {
    let resArr = new Array(list.length) // 收集结果,数组长度为待执行的 Promise 个数
    let num = 0 // 执行次数
    
    // Promise.allSettled 返回的结果可以 .then,说明需要返回一个 Promise
    return new Promise(resolve => {
        list.forEach((item, index) => {
            // 每个 Promise 去获得结果
            item.then(res => {
                // resolve
                let obj = {
                    status: 'fulfilled',
                    value: res
                }
                resArr[index] = obj
                num++
                if(num === list.length){
                    resolve(resArr)
                }
            }, err => {
                // reject
                let obj = {
                    status: 'rejected',
                    reason: err
                }
                resArr[index] = obj
                num++
                if(num === list.length){
                    resolve(resArr)
                }
            })
        })
    })
}

async await对比promise的优缺点

setTimeout、Promise、Async/Await 的区别

blog.csdn.net/yun_hou/art…

事件循环

zhuanlan.zhihu.com/p/87684858

问题:什么是同步?什么是异步?

同步和异步是一种消息通知机制

  • 同步阻塞:A 调用 B,B 处理获得结果,才返回给 A。A 在这个过程中,一直等待 B 的处理结果,没有拿到结果之前,需要 A (调用者)一直等待和确认调用结果是否返回,拿到结果,然后继续往下执行。

    做一件事,没有拿到结果之前,就一直在这等着,一直等到有结果了,再去做下边的事。

  • 异步非阻塞:A 调用 B,无需等待 B 的结果,B 通过状态、通知等,来通知 A 或回调函数来处理

8344d70d6c3b16206283aa09e92bb30.jpg

image.png

先执行完同步任务,才会去执行异步任务异步任务还会分为宏任务和微任务

什么是宏任务?什么是微任务?

  • 微任务:一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前

  • 宏任务:宏任务的时间粒度比较大,执行的时间间隔是不能精确控制的,对一些高实时性的需求就不太符合。

ca016aac8b52a02d8c9c4287214f9fb.jpg

常见微任务:

  1. Promise.then
  2. MutaionObserver
  3. Object.observe (已废弃)
  4. process.nextTick ( Node.js )

常见宏任务:

  1. <script> (可以理解为外层同步代码)
  2. setTimeout / setInterval
  3. UI rendering / UI 事件
  4. postMessage,NessageChannel
  5. setImediate ( Node.js )、I/O

事件循环面试题

console.log(1)
let p = new Promise(resolve => {
    console.log(2)
    resolve(3)
})
p.then((res) => {
    console.log(res)
})
setTimeout(() => {
    console.log(4)
}, 0)
console.log(5)
// 1->2->5->3->4

解析:

  • 首先,事件循环从宏任务队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务;
  • 遇到了console.log,输出 1;
  • 接着往下走,遇到 promise,new promise 中的代码立即执行,输出 2,然后执行 resolve ;
  • 将其 then 分发到微任务队列中去,记为 then1;
  • 遇到 setTimeout ,将其分发到任务队列中去,记为 timemout1;
  • 接着遇到 console.log 代码,直接输出 5;至此,当前宏任务执行完毕
  • 接着检查微任务队列,发现有个 then1 微任务,执行,输出 3;
  • 再检查微任务队列,发现已经清空,则开始检查宏任务队列
  • 执行 timeout1,输出 4;
  • 至此,所有的队列都已清空,执行完毕。 其输出的顺序依次是:1->2->5->3->4。

扩展:Promise 平行链

Promise.resolve().then(() => {
    console.log(0);
    return Promise.resolve(4);
}).then((res) => {
    console.log(res)
})

Promise.resolve().then(() => {
    console.log(1);
}).then(() => {
    console.log(2);
}).then(() => {
    console.log(3);
}).then(() => {
    console.log(5);
}).then(() =>{
    console.log(6);
})
// 0->1->2->3->4->5->6

js里的所有对象都有原型吗?

__proto__prototype 之间有什么关系

__proto__ 实例化对象,prototype (原型属性)构造函数/类

答案:所有对象都有 __proto__ 属性,函数这个特殊对象除了具有 __proto__ 属性,还有特有的原型属性 prototypeprototype 对象默认有两个属性,constructor 属性和 __proto__ 属性。 prototype 属性可以给函数和对象添加可共享(继承)的方法、属性,而 __proto__ 是查找某函数或对象的原型链方式。 constructor,这个属性包含了一个指针,指回原构造函数。

image.png

节流和防抖分别是什么?在什么场景下使用?请分别实现一个节流函数和一个防抖函数?

答案:防抖( debounce )就是在事件触发后的 n 秒之后,再去执行真正需要执行的函数,如果在这 n 秒之内事件又被触发,则重新开始计时(不再触发,才去执行)。防抖函数实现如下:

const debounce = (fn , delay = 500) => {
    let timer ;
    return function (...args){
        clearTimeout(timer) // 清空定时器
        timer = setTimeout( function () (
            fn.apply(this, args); // 改变 this 指向
        ), delay ) // 延迟一定时间执行
    }
}

节流( throttle )就是规定好一个单位时间,触发函数一次。如果在这个单位时间内触发多次函数的话(高频率),只有一次是可被执行的。想执行多次的话,只能等到下一个周期里。实现代码如下:

// 定时器版本,事件触发延后
const trottle1 = (fn, delay = 500) => {
    let timer
    return (...args) => {
        if (!timer){
            timer = setTimeout(() => {
                clearTimeout(timer)
                timer = null;
                fn.apply(this, args);
            }, delay );
        }
    }
}

// 时间戳版本,立即执行
const trottle2 = (fn, delay = 500) => {
    let oldTime = Date.now(); 
    return (...args) => {
        let nowTime = Date.now();
        if (nowTime - oldTime >= delay){
            oldTime = Date.now(); 
            fn.apply(this, args);
        }
    }
}

实际运用中比如,按键高频率重复触发,拖拽场景、表单验证场景 resize, scroll 、 onmousemove 等等触发事件。

事件流

什么是事件流∶事件流描述的是从页面中接收事件的顺序

事件发生时会在元素节点与根节点之间按照特定的顺序传播,路径所经过的所有节点都会收到该事件,这个传播过程即DOM事件流

事件传播的顺序对应浏览器的两种事件流模型:捕获型事件流和冒泡型事件流。

  • 冒泡型事件流:事件的传播是从最特定事件目标到最不特定的事件目标。即从DOM树的叶子到根。
  • 捕获型事件流:事件的传播是从最不特定事件目标到最特定的事件目标。即从DOM树的根到叶子。

事件冒泡

一个事件触发后,会在子元素和父元素之间传播,这种传播分为三个阶段:

  • 捕获阶段(从 window 对象传导到目标节点(从外到里),这个阶段不会响应任何事件)
  • 目标阶段,(在目标节点上触发)
  • 冒泡阶段(从目标节点传导回 window 对象(从里到外))

事件委托是什么?

事件委托就是利用事件冒泡,只制定一个时间处理程序,就可以管理某一类型的所有事件

将原本需要绑定在子元素的响应事件委托给父元素,让父元素担当事件监听的职务。

如何确定事件源? Event.target 谁调用谁就是事件源

什么是纯函数?使用纯函数有什么好处?

答案:纯函数是对给定的输入返还相同输出的函数。纯函数在函数式编程中被大量使用,在前端如 reactjs 、 redux 等库中大量被使用。 例如 let double = value => value * 2 函数只依赖外部变量 value 来影响 double 的值,每次给定的输入会有相同的输出。假如:

let num = 2
let double = value => num * 2;

如上函数就不是一个纯函数, double 值会受到外部 num 值的影响,导致 double 函数出现副作用(副作用:一个函数执行过程产生了外部可观察的变化)。

纯函数好处:

  1. 可以产生可测试的代码。如下:
test('double(2)等于4', () => { 
    expect(double(2)).toBe(4);
})
  1. 可读性更强。

  2. 可缓存,通过缓存 缓存执行结果,如下:

function memoize (fn) {
    let cache = []; 
    return function ({
        cache.push(fn.apply(this, arguments)); 
        return cache;
    }
}

let addFn = memoize (function (a, b) {
    return a + b;
})

console.log(addFn(1, 2)); // [3]
console.log(addFn(2, 2)); // [3, 4]

闭包

外部参数在内部 return 方法中引用,捆绑在一起形成闭包

本质官方解答:函数在执行的时候会放在栈上执行,当函数执行完毕了,就从栈中移除,但如果外部函数的参数在内部方法进行引用,则无法释放,因此形成了闭包

function mackPower(power){
    return function (x) {
        return power * x
    }
}
let power2 = mackPower(2)
let power3 = mackPower(3)
console.log( power2(2) )   // 4
console.log( power3(3) )   // 9

特点∶

  • 函数嵌套函数
  • 函数内部可以引用外部的参数和变量
  • 参数和变量不会被垃圾回收机制回收

优点∶

  • 变量长期驻在内存中;
  • 避免全局变量的污染;
  • 私有化变量;
  • 模拟块级作用域

缺点∶

  • 会导致函数的变量一直保存在内存中,过多的闭包可能会导致内存泄露

高阶函数

函数作为参数/返回值的函数就叫高阶函数。

函数柯里化 实现 add(1)(2)(3)

答案:考察函数柯里化,柯里化( Currying )是把接受多个参数的函数转变为接受一个单一参数的函数,并且返回接受余下的参数且返回结果新函数的技术。

function add (a){
    return function (b){ 
        return function (c){
            return a + b + c ;
        }
    }
}
console.log(add(1)(2)(3)); // 6

当然也会有多参数的柯里化

function add = function (a, b, c){
    return a + b + c
}

const curry = function (fn){
    return function curriedFn(...args){
        if(args.length < fn.length ){
            return function (){
                return curry.apply(null, args.concat(Array.from(arguments))) // arguments 是伪数组,需要转化
                // return curry(...args.concat(Array.from(arguments)))
            }
        }
        return fn.apply(this, args);
        // return fn(...args);
    }
}

const myadd = curry(add)
let res = myAdd(1)(2)(3)
console.log(res) // 6

柯里化作用:1、参数复用。2、延迟执行。3、某些语言及特定环境下只能接受一个参数。

函数式编程中的 compose (*)

答案:compose 是函数式编程中一个非常重要的函数, compose 的函数作用就是组合函数的,将函数串联起来执行。将多个函数组合起来,一个函数的输出结果是另一个函数的输入参数(从右至左),一旦第一个函数开始执行,就会像多米诺骨牌一样推导执行了。

// reverse 函数倒置执行 reduce 累加
const compose = (...fns) => {
    return (arg) => {
        return fns.reverse().reduce((acc, fn) => {
            return fn(acc) // acc 函数运行结果累加值 fn 当前函数 arg 初始值
        }, arg);
    }
}
// 简写
// const compose = (...fns) => arg => fns.reverse().reduce((acc, fn) => fn(acc), arg);

let aFn = a => a * 2
let bFn = b => b + 3

let myFn = compose(aFn, bFn)
let res = myFn(2)
console.log(res) // 10

按要求实现 go 函数

示例:

go('1') // 'go1'
go()('1') // 'goo1'
go()()('1') // 'gooo1'

答案:这里主要是高阶函数和闭包及递归的运用,具体代码如下:

function go (...args) {
    let o = 'o'
    const fn = (...args) => {
        if (typeof args[0] === 'undefined') {
            return (...args) => {
                o += 'o'
                return fn(...args)
            }
        } else {
            return 'g' + o + args[0]
        }
    }
    return fn(...args)
}

let res = go()('1')
console.log(res) // 'goo1'

实现一个 myMap 函数,实现类似 map 功能?

答案:这个是通过声明式编程(结果)方式抽象循环逻辑,代码如下:

命令式编程(过程)

Array.prototype.myMap = function (fn) {
    let resultArr = []
    for(let i = 0; i < this.length; i++){
        resultArr.push(fn(this[i], i, this))
    }
    return resultArr
}

let arr = ['张三', '李四', '王五']
let res = arr.myMap((item, key, arr) => {
    console.log(item, key, arr)
})
console.log(res) // ['张三', '李四', '王五']

数组去重(*)

function uniqueArr(arr) {
  return [...new Set(arr)];
}

数组扁平化(*)

数组扁平化:使多维数组变成一维数组。

  • 递归版:
function flatter(arr) {
  if (!arr.length) return;
  return arr.reduce(
    (pre, cur) =>
      Array.isArray(cur) ? [...pre, ...flatter(cur)] : [...pre, cur],
    []
  );
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));
  • 迭代版:
function flatter(arr) {
  if (!arr.length) return;
  while (arr.some((item) => Array.isArray(item))) {
    arr = [].concat(...arr);
  }
  return arr;
}
// console.log(flatter([1, 2, [1, [2, 3, [4, 5, [6]]]]]));

深拷贝(*)

// 深拷贝
function deepCopy(obj) {
    let newObj = Array.isArray(obj) ? [] : {}; // 可能有数组
    for(let key in obj){ // 遍历对象。for...in 不仅会循环对象自身的属性和方法,还会循环对象的原型以及原型链上的属性和方法。
        if(obj.hasOwnProperty(key)){ // 判断是否是对象的自身属性和方法
            if(typeof obj[key] === "object"){ // 判断是否是对象
                newObj[key] = deepCopy(obj[key]) ; // 再次循环
            }else{ // 非对象,简单数据类型
                newObj[key] = obj[key];
            }
        }
    }
    return newObj;
}

Js中常见的内存泄漏

  • 意外的全局变量;
  • 被遗忘的计时器或回调函数;
  • 脱离 DOM的引用;
  • 闭包;

内存泄漏是指一块被分配的内存既不能使用又不能回收,直到浏览器进程结束。

释放内存的方法∶赋值为 null;

JS 监听对象属性的改变

  • 在 ES5 中可以通过Object.defineProperty 来实现已有属性的监听:
Object.defineProperty(user,'name',{
    set: function(key,value){}
})

缺点∶如果id 不在user 对象中,则不能监听 id 的变化。

  • 在ES6 中可以通过 Proxy 来实现:
Var user = new Proxy({},{
    set: function(target,key,value,receiver){}
})

  这样即使有属性在user 中不存在,通过 user.id 来定义也同样可以这样监听这个属性的变化哦。

Commonjs ES6模块区别?

common模块是拷贝,可以修改值,es6 模块是引用,只读状态,不能修改值;

commonjs 模块是运行时加载,es6 模块是编译时输出接口;

JS 的各种位置,比 clientHeight,scrollHeight,offsetHeight,以及scrollTop, offsetTop,clientTop 的区别?

clientHeight∶表示的是可视区域的高度,不包含 border 和滚动条;

offsetHeight∶表示可视区域的高度,包含了 border 和滚动条;

scrollHeight∶表示了所有区域的高度,包含了因为滚动被隐藏的部分;

clientTop∶表示边框 border 的厚度,在未指定的情况下一般为0;

scrollTop∶滚动后被隐藏的高度,获取对象相对于由 offsetParent 属性指定的父坐标(css定位的元素或 body 元素距离顶端的高度。

JS 拖拽功能的实现

首先是三个事件,分别是mousedown,mousemove,mouseup当鼠标点击按下的时候,需要一个 tag 标识此时已经按下,可以执行mousemove 里面的具体方法。clientX,clientY 标识的是鼠标的坐标,分别标识横坐标和纵坐标,并且我们用 offsetX 和 offsetY 来表示元素的元素的初始坐标,移动的举例应该是∶鼠标移动时候的坐标-鼠标按下去时候的坐标。也就是说定位信息为; 鼠标移动时候的A标-鼠标按下去时候的坐标+元素初始情况下的 offetLef.还有一点也是原理性的东西,也就是拖拽的同时是绝对定位,我们改变的是绝对定位条件下的 left 以及 top 等等值。

补充∶也可以通过 html5 的拖放(Drag 和 drop)来实现

JS 中的垃圾回收机制

必要性∶由于字符串、对象和数组没有固定大小,所以当他们的大小已知时,才能对他们进行动态的存储分配。JavaScript 程序每次创建字符串、数组或对象时,解释器都必须分配内存来存储那个实体。只要像这样动态地分配了内存,最终都要释放这些内存以便他们能够被再用,否则,JavaScript 的解释器将会消耗完系统中所有可用的内存,造成系统崩溃

这段话解释了为什么需要系统需要垃圾回收,JS 不像C/C++,他有自己的一套垃圾回收机制(Garbage Collection)。JavaScript 的解释器可以检测到何时程序不再使用一个对象了,当他确定了一个对象是无用的时候,他就知道不再需要这个对象,可以把它所占用的内存释放掉了。

例如∶

var a="hello world"; 
var b="world";
Var a=b; // 这时,会释放掉"hello world"

垃圾回收的方法∶标记清除、计数引用。

  • 标记清除

这是最常见的垃圾回收方式,当变量进入环境时,就标记己这个变量为"进入环境",从逻辑上讲,永远不能释放进入环境的变量所占的内存,永远不能释放进入环境变量所占用的内存,只要执行流程进入相应的环境,就可能用到他们。当离开环境时,就标记为离开环境。

垃圾回收器在运行的时候会给存储在内存中的变量都加上标记(所有都加),然后去掉环境变量中的变量,以及被环境变量中的变量所引用的变量(条件性去除标记),删除所有被标记的变量,删除的变量无法在环境变量中被访问所以会被删除,最后垃圾回收器,完成了内存的清除工作,并回收他们所占用的内存。

  • 引用计数法

引用计数法的意思就是每个值没引用的次数,当声明了一个变量,并用一个引用类型的值赋值给改变量,则这个值的引用次数为 1;相反的,如果包含了对这个值引用的变量又取得了另外一个值,则原先的引用值引用次数就减 1,当这个值的引用次数为 0 的时候,说明没有办法再访问这个值了,因此就把所占的内存给回收进来,这样垃圾收集器再次运行的时候,就会释放引用次数为 0 的这些值。

用引用计数法会存在内存泄露,下面来看原因∶

function problem() {
    var objA= new Object();
    var objB= new Object();
    objA.someOtherObject = objB;
    objB.anotherObject =objA;
}

在这个例子里面,objA 和 objB 通过各自的属性相互引用,这样的话,两个对象的引用次数都为 2,在采用引用计数的策略中,由于函数执行之后,这两个对象都离开了作用域,函数执行完成之后,因为计数不为 0,这样的相互引用如果大量存在就会导致内存泄露。

特别是在 DOM 对象中,也容易存在这种问题∶

var element=document.getElementByld('.);
var myObj=new Object();
myObj.element=element;
element.someObject=myObj;

这样就不会有垃圾回收的过程。

发布/订阅模式和观察者模式的区别是什么

观察者模式中主体和观察者是互相感知的,发布-订阅模式是借助第三方来实现调度的,发布者和订阅者之间互不感知