🔥【前端面试题】🍕 JavaScript篇

96 阅读25分钟

数据类型

在JavaScript中,我们可以分成两种类型:

  • 基本类型
  • 复杂类型

基本类型主要为以下6种:

  • Number
  • String
  • Boolean
  • Undefined
  • null
  • BigInt 可以表示任意大小的整数。
  • Symbol 代表独一无二的值,最大的用法是用来定义对象的唯一属性名。

Symbol (符号)是原始值,且符号实例是唯一、不可变的。符号的用途是确保对象属性使用唯一标识符,不会发生属性冲突的危险

let genericSymbol = Symbol();
let otherGenericSymbol = Symbol();
console.log(genericSymbol == otherGenericSymbol); // false

let fooSymbol = Symbol('foo');
let otherFooSymbol = Symbol('foo');
console.log(fooSymbol == otherFooSymbol); // false

复杂类型统称为Object,主要讲述下面三种:

  • Object
  • Array
  • Function

基本数据类型和引用数据类型存储在内存中的位置不同:

  • 基本数据类型存储在栈中
let a = 10;
let b = a; // 赋值操作
b = 20;
console.log(a); // 10值
  • 引用类型的对象存储于堆中
var obj1 = {}
var obj2 = obj1;
obj2.name = "Xxx";
console.log(obj1.name); // xxx

数据类型的判断

  • typeof:能判断所有值类型,函数。不可对 null、对象、数组进行精确判断,因为都返回 object 。
console.log(typeof undefined); // undefined
console.log(typeof 2); // number
console.log(typeof true); // boolean
console.log(typeof "str"); // string
console.log(typeof Symbol("foo")); // symbol
console.log(typeof 2172141653n); // bigint
console.log(typeof function () {}); // function
// 不能判别
console.log(typeof []); // object
console.log(typeof {}); // object
console.log(typeof null); // object
  • instanceof:能判断对象类型,不能判断基本数据类型,其内部运行机制是判断在其原型链中能否找到该类型的原型。比如考虑以下代码:
class People {}
class Student extends People {}

const vortesnail = new Student();

console.log(vortesnail instanceof People); // true
console.log(vortesnail instanceof Student); // true

其实现就是顺着原型链去找,如果能找到对应的 Xxxxx.prototype 即为 true 。比如这里的 vortesnail 作为实例,顺着原型链能找到 Student.prototype 及 People.prototype ,所以都为 true 。

  • Object.prototype.toString.call():所有原始数据类型都是能判断的,还有 Error 对象,Date 对象等。
Object.prototype.toString.call(2); // "[object Number]"
Object.prototype.toString.call(""); // "[object String]"
Object.prototype.toString.call(true); // "[object Boolean]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(Math); // "[object Math]"
Object.prototype.toString.call({}); // "[object Object]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(function () {}); // "[object Function]"

实现一个全局通用的数据类型判断方法

function getType(obj){
  let type  = typeof obj;
  if (type !== "object") {    // 先进行typeof判断,如果是基础数据类型,直接返回
    return type;
  }
  // 对于typeof返回结果是object的,再进行如下的判断,正则返回结果
  return Object.prototype.toString.call(obj).replace(/^\[object (\S+)\]$/, '$1'); 
}

浅拷贝和深拷贝

浅拷贝

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

在JavaScript中,存在浅拷贝的现象有:

  • Object.assign
  • Array.prototype.slice(), Array.prototype.concat()
  • 使用拓展运算符实现的复制

手写浅拷贝

function clone(target) {
    let cloneTarget = {};
    for (const key in target) {
        cloneTarget[key] = target[key];
    }
    return cloneTarget;
};

深拷贝

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

常见的深拷贝方式有:

  • _.cloneDeep()
  • jQuery.extend()
  • JSON.stringify()

手写深拷贝

/**
 * 深拷贝
 * @param {Object} obj 要拷贝的对象
 * @param {Map} map 用于存储循环引用对象的地址
 */

function deepClone(obj = {}, map = new Map()) {
  // 可能是对象或者普通的值  如果是函数的话是不需要深拷贝
  if (typeof obj !== "object") {
    return obj;
  }
   // 是对象的话就要进行深拷贝
  if (map.get(obj)) {
    return map.get(obj);
  }

  let result = {};
  // 初始化返回结果
  if (
    obj instanceof Array ||
    // 加 || 的原因是为了防止 Array 的 prototype 被重写,Array.isArray 也是如此
    Object.prototype.toString(obj) === "[object Array]"
  ) {
    result = [];
  }
  // 防止循环引用
  map.set(obj, result);
  for (const key in obj) {
    // 保证 key 不是原型属性
    if (obj.hasOwnProperty(key)) {
      // 递归调用
      result[key] = deepClone(obj[key], map);
    }
  }

  // 返回结果
  return result;
}

数组的常用方法

下面前三种是对原数组产生影响的增添方法,第四种则不会对原数组产生影响

  • push():将参数添加到数组末尾,返回数组的最新长度
  • unshift():在数组开头添加任意多个值,然后返回新的数组长度
  • splice():传入三个参数,分别是开始位置、0(要删除的元素数量)、插入的元素,返回空数组
  • concat():首先会创建一个当前数组的副本,然后再把它的参数添加到副本末尾

下面三种都会影响原数组,最后一项不影响原数组:

  • pop():用于删除数组的最后一项,返回被删除的项
  • shift():删除数组的第一项,返回被删除的项
  • splice():传入两个参数,开始位置,删除元素的数量,返回包含删除元素的数组
  • slice():用于创建一个包含原有数组中一个或多个元素的新数组,不会影响原始数组
let colors = ["red", "green", "blue", "yellow", "purple"];
let colors2 = colors.slice(1);
let colors3 = colors.slice(1, 4);
console.log(colors)   // red,green,blue,yellow,purple
concole.log(colors2); // green,blue,yellow,purple
concole.log(colors3); // green,blue,yellow

splice():传入三个参数,开始位置,删除元素的数量,要插入的任意个元素,返回删除元素的数组,对原数组产生影响

let colors = ["red", "green", "blue"];
let removed = colors.splice(1, 1, "red", "purple"); // 插入两个值,删除一个元素
console.log(colors); // red,red,purple,blue
console.log(removed); // green,只有一个元素的数组

即查找元素,返回元素坐标或者元素值

  • indexOf():返回要查找的元素在数组中的位置,如果没找到则返回 -1
  • includes():用于检查数组是否包含某个特定的值,找到返回true,否则false
  • find():返回第一个匹配的元素

排序方法

  • reverse():将数组元素方向反转
  • sort():接受一个比较函数,用于判断哪个值应该排在前面

转换方法 join() 方法接收一个参数,即字符串分隔符,返回包含所有项的字符串

let colors = ["red", "green", "blue"];
alert(colors.join(",")); // red,green,blue
alert(colors.join("||")); // red||green||blue

迭代方法 常用来迭代数组的方法(都不改变原数组)有如下:

  • some():对数组每一项都运行传入的测试函数,如果至少有1个元素返回 true ,则这个方法返回 true
  • every():对数组每一项都运行传入的测试函数,如果所有元素都返回 true ,则这个方法返回 true
  • forEach():对数组每一项都运行传入的函数,没有返回值
  • filter():对数组每一项都运行传入的函数,函数返回 true 的项会组成数组之后返回
  • map():对数组每一项都运行传入的函数,返回由每次函数调用的结果构成的数组

字符串的常用方法

除了常用+以及${}进行字符串拼接之外,还可通过concat

let result = stringValue.concat("world");

  • slice()
  • substr()
  • substring()
let stringValue = "hello world";
console.log(stringValue.slice(3)); // "lo world"
console.log(stringValue.substring(3)); // "lo world"
console.log(stringValue.substr(3)); // "lo world"
console.log(stringValue.slice(3, 7)); // "lo w"
console.log(stringValue.substring(3,7)); // "lo w"
console.log(stringValue.substr(3, 7)); // "lo worl"

  • trim()、trimLeft()、trimRight():删除前、后或前后所有空格符,再返回新的字符串
  • repeat():接收一个整数参数,表示要将字符串复制多少次,然后返回拼接所有副本后的结果
  • padStart()、padEnd():复制字符串,若小于指定长度,则在相应一边填充字符,直至满足长度条件
  • toLowerCase()、 toUpperCase():大小写转化

  • chatAt():返回给定索引位置的字符,由传给方法的整数参数指定
  • indexOf():从字符串开头去搜索传入的字符串,并返回位置(如果没找到,则返回 -1 )
  • startWith()、includes():从字符串中搜索传入的字符串,并返回一个布尔值

转换方法 spilt:把字符串按照指定的分割符,拆分成数组中的每一项

let str = "12+23+34"
let arr = str.split("+") // [12,23,34]

模板匹配方法

  • match()
  • search()
  • replace():接收两个参数,第一个参数为匹配的内容,第二个参数为替换的元素(可用函数)
let text = "cat, bat, sat, fat";
let result = text.replace("at", "ond");
console.log(result); // "cond, bat, sat, fat"

作用域、作用域链

作用域,即变量(变量作用域又称上下文)和函数生效(能被访问)的区域或集合 换句话说,作用域决定了代码区块中变量和其他资源的可见性 我们一般将作用域分成:

  • 全局作用域

任何不在函数中或是大括号中声明的变量,都是在全局作用域下,全局作用域下声明的变量可以在程序的任意位置访问

  • 函数作用域

函数作用域也叫局部作用域,如果一个变量是在函数内部声明的它就在一个函数作用域下面。这些变量 只能在函数内部访问,不能在函数以外去访问

  • 块级作用域

在大括号中使用let和const声明的变量存在于块级作用域中。在大括号之外不能访问这些变量

词法作用域: 词法作用域,又叫静态作用域,变量被创建时就确定好了,而非执行阶段确定的。也就是说我们写好代码时它的作用域就确定了,JavaScript 遵循的就是词法作用域

var a = 2;
function foo(){
  console.log(a)
}
function bar(){
  var a = 3;
  foo();
}
bar()
//遵循词法作用域,相同层级的 foo 和 bar 就没有办法访问到彼此块作用域中的变量,所以输出2

作用域链: 当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止

执行上下文、执行上下文栈

当 JavaScript 代码执行一段可执行代码时,会创建对应的执行上下文。对于每个执行上下文,都有三个重要属性:

  • 变量对象(Variable object,VO);
  • 作用域链(Scope chain);
  • this

执行上下文的类型分为三种:

  • 全局执行上下文:只有一个,浏览器中的全局对象就是 window对象,this 指向这个全局对象
  • 函数执行上下文:存在无数个,只有在函数被调用的时候才会被创建,每次调用函数都会创建一个新的执行上下文
  • Eval 函数执行上下文: 指的是运行在 eval 函数中的代码,很少用而且不建议使用

执行上下文栈: 当 JavaScript 开始要解释执行代码的时候,最先遇到的就是全局代码,所以初始化的时候首先就会向执行上下文栈压入一个全局执行上下文。当执行一个函数的时候,就会创建一个执行上下文,并且压入执行上下文栈,当函数执行完毕的时候,就会将函数的执行上下文从栈中弹出。

函数执行上下文中作用域链和变量对象的创建过程:

var scope = "global scope";
function checkscope(){
    var scope2 = 'local scope';
    return scope2;
}
checkscope();

执行过程如下: 1.checkscope 函数被创建,保存作用域链到 内部属性[[scope]]

checkscope.[[scope]] = [
    globalContext.VO
];

2.执行 checkscope 函数,创建 checkscope 函数执行上下文,checkscope 函数执行上下文被压入执行上下文栈

ECStack = [
    checkscopeContext,
    globalContext
];

3.checkscope 函数并不立刻执行,开始做准备工作,第一步:复制函数[[scope]]属性创建作用域链

checkscopeContext = {
    Scope: checkscope.[[scope]],
}

4.第二步:用 arguments 创建活动对象,随后初始化活动对象,加入形参、函数声明、变量声明

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: checkscope.[[scope]],
}

5.第三步:将活动对象压入 checkscope 作用域链顶端

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: undefined
    },
    Scope: [AO, [[Scope]]]
}

6.准备工作做完,开始执行函数,随着函数的执行,修改 AO 的属性值

checkscopeContext = {
    AO: {
        arguments: {
            length: 0
        },
        scope2: 'local scope'
    },
    Scope: [AO, [[Scope]]]
}

7.查找到 scope2 的值,返回后函数执行完毕,函数上下文从执行上下文栈中弹出

ECStack = [
    globalContext
];

闭包

闭包的定义如下:

在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。可以在一个内层函数中访问到其外层函数的作用域。

也可以这样说:

闭包是指那些能够访问自由变量的函数。 自由变量是指在函数中使用的,但既不是函数参数也不是函数的局部变量变量。 闭包 = 函数 + 函数能够访问的自由变量。

在某个内部函数的执行上下文创建时,会将父级函数的活动对象加到内部函数的 [[scope]] 中,形成作用域链,所以即使父级函数的执行上下文销毁(即执行上下文栈弹出父级函数的执行上下文),但是因为其活动对象还是实际存储在内存中可被内部函数访问到的,从而实现了闭包。

this指向问题

默认Window

  • this 指向默认是指向的 window

new绑定

  • 当使用 new 关键字调用函数时,函数中的 this 一定是 JS 创建的新对象。
function test() {
 this.x = 1;
}

var obj = new test();
obj.x // 1

箭头函数

  • 它的 this 不会被改变,里面的 this 其实是根据他的上级来定的,也就是他的 this 指向等于他的上级。
const obj = {
  sayThis: () => {
    console.log(this);
  }
};

obj.sayThis(); // window 因为 JavaScript 没有块作用域,所以在定义 sayThis 的时候,里面的 this 就绑到 window 上去了

隐式绑定

  • 函数还可以作为某个对象的方法调用,这时this就指这个上级对象
function test() {
  console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;

obj.m(); // 1
//这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

var o = {
    a:10,
    b:{
        fn:function(){
            console.log(this.a); //undefined
        }
    }
}
o.b.fn();
//上述代码中,this的上一级对象为b,b内部并没有a变量的定义,所以输出undefined

显式绑定

  • apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数
var x = 0;
function test() {
 console.log(this.x);
}

var obj = {};
obj.x = 1;
obj.m = test;
obj.m.apply(obj) // 1

优先级

  • new绑定优先级 > 显示绑定优先级 > 隐式绑定优先级 > 默认绑定优先级

bind、call、apply

call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向 call、apply

  • apply() 和 call() 的第一个参数都是 this,区别在于通过 apply 调用时实参是放到数组中的,而通过 call 调用时实参是逗号分隔的。都只是临时改变this指向一次,改变this指向后原函数会立即执行
function fn(...args){
    console.log(this,args);
}
let obj = {
    myname:"张三"
}

fn.apply(obj,[1,2]); // this会变成传入的obj,传入的参数必须是一个数组;
fn.call(obj,1,2); 

//当第一个参数为null、undefined的时候,默认指向window(在浏览器中)
fn.apply(null,[1,2]); // this指向window
fn.apply(undefined,[1,2]); // this指向window
fn.call(null,[1,2]); // this指向window
fn.call(undefined,[1,2]); // this指向window

bind

  • bind 方法与 call / apply 最大的不同就是前者返回一个绑定上下文的函数,而后两者是直接执行了函数。
  • 改变this指向后不会立即执行,而是返回一个永久改变this指向的函数
  • 第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
const bindFn = fn.bind(obj); // this 也会变成传入的obj ,bind不是立即执行需要执行一次
bindFn(1,2) // this指向obj

//简写
// 方式一:只在bind中传递函数参数
fn.bind(obj,1,2)()

// 方式二:在bind中传递函数参数,也在返回函数中传递参数
fn.bind(obj,1)(2)

手写bind

Function.prototype.myBind = function (context) {
    // 判断调用对象是否为函数
    if (typeof this !== "function") {
        throw new TypeError("Error");
    }

    // 获取参数
    const args = [...arguments].slice(1),
          fn = this;

    return function Fn() {

        // 根据调用方式,传入不同绑定值
        return fn.apply(this instanceof Fn ? new fn(...arguments) : context, args.concat(...arguments)); 
    }
}

事件流、事件委托

事件流都会经历三个阶段:

  • 事件捕获阶段(capture phase)
  • 处于目标阶段(target phase)
  • 事件冒泡阶段(bubbling phase)

image.png

事件冒泡是一种从下往上的传播方式,由最具体的元素(触发节点)然后逐渐向上传播到最不具体的那个节点,也就是DOM中最高层的父节点

<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Event Bubbling</title>
    </head>
    <body>
        <button id="clickMe">Click Me</button>
    </body>
</html>
//然后,我们给button和它的父元素,加入点击事件
var button = document.getElementById('clickMe');

button.onclick = function() {
  console.log('1.Button');
};
document.body.onclick = function() {
  console.log('2.body');
};
document.onclick = function() {
  console.log('3.document');
};
window.onclick = function() {
  console.log('4.window');
};

//
1.button
2.body
3.document
4.window
//点击事件首先在button元素上发生,然后逐级向上传播

事件捕获与事件冒泡相反 事件委托

  • 事件委托就是在冒泡阶段完成,事件委托,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,而不是目标元素
<ul id="list">
  <li>item 1</li>
  ......
  <li>item n</li>
</ul>

// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
    // 兼容性处理
    var event = e || window.event;
    var target = event.target || event.srcElement;
    // 判断是否匹配目标元素
    if (target.nodeName.toLocaleLowerCase === 'li') {
        console.log('the content is: ', target.innerHTML);
    }
});

new关键字

new操作符用于创建一个给定构造函数的实例对象

  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数中的属性
  • new 通过构造函数 Person 创建出来的实例可以访问到构造函数原型链中的属性(即实例与构造函数通过原型链连接了起来)
function Person(name, age){
    this.name = name;
    this.age = age;
}
Person.prototype.sayName = function () {
    console.log(this.name)
}
const person1 = new Person('Tom', 20)
console.log(person1)  // Person {name: "Tom", age: 20}
t.sayName() // 'Tom'
  • 构造函数中返回一个原始值,这个返回值并没有作用
  • 构造函数如果返回值为一个对象,那么这个返回值会被正常使用
function Test(name) {
  this.name = name
  console.log(this) // Test { name: 'xxx' }
  return { age: 26 }
}
const t = new Test('xxx')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'

流程

  • 创建一个新的对象obj
  • 将对象与构建函数通过原型链连接起来
  • 将构建函数中的this绑定到新建的对象obj上
  • 根据构建函数返回类型作判断,如果是原始值则被忽略,如果是返回对象,需要正常处理

手写

function mynew(Func, ...args) {
    // 1.创建一个新对象
    const obj = {}
    // 2.新对象原型指向构造函数原型对象
    obj.__proto__ = Func.prototype
    // 3.将构建函数的this指向新对象
    let result = Func.apply(obj, args)
    // 4.根据返回值判断
    return result instanceof Object ? result : obj
}

AJAX

AJAX全称(Async Javascript and XML) 即异步的JavaScript 和XML,是一种创建交互式网页应用的网页开发技术,可以在不重新加载整个网页的情况下,与服务器交换数据,并且更新部分网页 Ajax的原理简单来说通过XmlHttpRequest对象来向服务器发异步请求,从服务器获得数据,然后用JavaScript来操作DOM而更新页面 封装

//封装一个ajax请求
function ajax(options) {
    //创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest()


    //初始化参数的内容
    options = options || {}
    options.type = (options.type || 'GET').toUpperCase()
    options.dataType = options.dataType || 'json'
    const params = options.data

    //发送请求
    if (options.type === 'GET') {
        xhr.open('GET', options.url + '?' + params, true)
        xhr.send(null)
    } else if (options.type === 'POST') {
        xhr.open('POST', options.url, true)
        xhr.send(params)

    //接收请求
    xhr.onreadystatechange = function () {
        if (xhr.readyState === 4) {
            let status = xhr.status
            if (status >= 200 && status < 300) {
                options.success && options.success(xhr.responseText, xhr.responseXML)
            } else {
                options.fail && options.fail(status)
            }
        }
    }
}

使用

ajax({
  type: 'post',
  dataType: 'json',
  data: {},
  url: 'https://xxxx',
  success: function(text,xml){//请求成功后的回调函数
    console.log(text)
  },
  fail: function(status){////请求失败后的回调函数
    console.log(status)
  }
})

Promise.all

  • Promise.all 的返回值是一个新的 Promise 实例。
  • Promise.all 接受一个可遍历的数据容器,容器中每个元素都应是 Promise 实例。咱就是说,假设这个容器就是数组。
  • 数组中每个 Promise 实例都成功时(由pendding状态转化为fulfilled状态),Promise.all 才成功。这些 Promise 实例所有的 resolve 结果会按照原来的顺序集合在一个数组中作为 Promise.all 的 resolve 的结果。
  • 数组中只要有一个 Promise 实例失败(由pendding状态转化为rejected状态),Promise.all 就失败。Promise.all 的 .catch() 会捕获到这个 reject。

手写

Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    // 参数可以不是数组,但必须具有 Iterator 接口,判断 promises 对象是否具有可迭代的能力
    if (typeof promises[Symbol.iterator] !== "function") {
      reject("Type error");
    }
    if (promises.length === 0) {
      resolve([]);
    } else {
      const res = [];
     // 创建一个计数器,每有一个 Promise 实例成功,计数器加一
      let count = 0;
      const len = promises.length;
      for (let i = 0; i < len; i++) {
        //Promise 实例是不能直接调用 resolve 方法的,得在 .then() 中去收集结果
        Promise.resolve(promises[i])
          .then((data) => {
            res[i] = data;
            if (++count === len) {
              resolve(res);
            }//处理失败的情况
          },reject)
      }
    }
  });
};

垃圾回收机制

内存泄漏

  • 内存泄漏 是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

大多数语言提供自动内存管理,减轻程序员的负担,这被称为"垃圾回收机制" Javascript 具有自动垃圾回收机制(GC:Garbage Collecation),也就是说,执行环境会负责管理代码执行过程中使用的内存 原理:垃圾收集器会定期(周期性)找出那些不在继续使用的变量,然后释放其内存 两种实现方式:

  • 标记清除:标记阶段即为所有活动对象做上标记,清除阶段则把没有标记(也就是非活动对象)销毁。
  • 引用计数:它把对象是否不再需要简化定义为对象有没有其他对象引用到它。如果没有引用指向该对象(引用计数为 0),对象将被垃圾回收机制回收。

标记清除的缺点:

  • 内存碎片化,空闲内存块是不连续的,容易出现很多空闲内存块,还可能会出现分配所需内存过大的对象时找不到合适的块。
  • 分配速度慢,因为即便是使用 First-fit 策略,其操作仍是一个 O(n) 的操作,最坏情况是每次都要遍历到最后,同时因为碎片化,大对象的分配效率会更慢。

解决以上的缺点可以使用 标记整理(Mark-Compact)算法,标记结束后,标记整理算法会将活着的对象(即不需要清理的对象)向内存的一端移动,最后清理掉边界的内存(如下图)image.png 引用计数的缺点:

  • 需要一个计数器,所占内存空间大,因为我们也不知道被引用数量的上限。
  • 解决不了循环引用导致的无法回收问题。

V8 的垃圾回收机制也是基于标记清除算法,不过对其做了一些优化。

  • 针对新生区采用并行回收。
  • 针对老生区采用增量标记与惰性回收。

本地存储

关于cookie、sessionStorage、localStorage三者的区别主要如下:

  • 存储大小:cookie数据大小不能超过4k,sessionStorage和localStorage虽然也有存储大小的限制,但比cookie大得多,可以达到5M或更大
  • 有效时间:localStorage存储持久数据,浏览器关闭后数据不丢失除非主动删除数据; sessionStorage数据在当前浏览器窗口关闭后自动删除;cookie设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭
  • 数据与服务器之间的交互方式,cookie的数据会自动的传递到服务器,服务器端也可以写cookie到客户端; sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存

柯里化

柯里化是把一个多参数函数转化成一个嵌套的一元函数的过程 其中具有多个参数的函数被分解为多个函数,当被串联调用时,将一次一个地累积所有需要的参数。这种技术帮助编写函数式风格的代码,使代码更易读、紧凑。

//一个二元函数如下:
let fn = (x,y)=>x+y;

//转化成柯里化函数如下:
const curry = function(fn){
    return function(x){
        return function(y){
            return fn(x,y);
        }
    }
}
let myfn = curry(fn);
console.log( myfn(1)(2) );

数字精度丢失

0.1 + 0.2 === 0.3 // false

//在javascript语言中,0.1 和 0.2 都转化成二进制后再进行运算
// 0.1 和 0.2 都转化成二进制后再进行运算
0.00011001100110011001100110011001100110011001100110011010 +
0.0011001100110011001100110011001100110011001100110011010 =
0.0100110011001100110011001100110011001100110011001100111

// 转成十进制正好是 0.30000000000000004
//所以输出false

原因总结:

  • 进制转换 :js 在做数字计算的时候,0.1 和 0.2 都会被转成二进制后无限循环 ,但是 js 采用的 IEEE 754 二进制浮点运算,最大可以存储 53 位有效数字,于是大于 53 位后面的会全部截掉,将导致精度丢失。
  • 对阶运算 :由于指数位数不相同,运算时需要对阶运算,阶小的尾数要根据阶差来右移(0舍1入),尾数位移时可能会发生数丢失的情况,影响精度。

解决方法: 把小数转成整数后再运算

/**
 * 精确加法
 */
function add(num1, num2) {
  const num1Digits = (num1.toString().split('.')[1] || '').length;
  const num2Digits = (num2.toString().split('.')[1] || '').length;
  const baseNum = Math.pow(10, Math.max(num1Digits, num2Digits));
  return (num1 * baseNum + num2 * baseNum) / baseNum;
}

防抖节流

有些时候,我们并不能或者不想频繁触发事件,这时候就应该用到函数防抖和函数节流。

  • 节流: n 秒内只运行一次,若在 n 秒内重复触发,只有一次生效(平a)
function throttled(fn, delay) {
    let timer = null
    let starttime = Date.now()
    return function () {
        let curTime = Date.now() // 当前时间
        let remaining = delay - (curTime - starttime)  // 从上一次到现在,还剩下多少多余时间
        let context = this
        let args = arguments
        clearTimeout(timer)
        if (remaining <= 0) {
            fn.apply(context, args)
            starttime = Date.now()
        } else {
            timer = setTimeout(fn, remaining);
        }
    }
}
  • 防抖: n 秒后在执行该事件,若在 n 秒内被重复触发,则重新计时(回城)
function debounce(func, wait, immediate) {

    let timeout;

    return function () {
        let context = this;
        let args = arguments;

        if (timeout) clearTimeout(timeout); // timeout 不为null
        if (immediate) {
            let callNow = !timeout; // 第一次会立即执行,以后只有事件执行后才会再次触发
            timeout = setTimeout(function () {
                timeout = null;
            }, wait)
            if (callNow) {
                func.apply(context, args)
            }
        }
        else {
            timeout = setTimeout(function () {
                func.apply(context, args)
            }, wait);
        }
    }
}

变量提升

变量提升(hoisting)是用于解释代码中变量声明行为的术语。使用 var 关键字声明或初始化的变量,会将声明语句“提升”到当前作用域的顶部。 但是,只有声明才会触发提升,赋值语句(如果有的话)将保持原样。

// 用 var 声明得到提升
console.log(foo); // undefined
var foo = 1;
console.log(foo); // 1

// 用 let/const 声明不会提升
console.log(bar); // ReferenceError: bar is not defined
let bar = 2;
console.log(bar); // 2

函数声明会使函数体提升,但函数表达式(以声明变量的形式书写)只有变量声明会被提升。

// 函数声明
console.log(foo); // [Function: foo]
foo(); // 'FOOOOO'
function foo() {
  console.log('FOOOOO');
}
console.log(foo); // [Function: foo]

// 函数表达式
console.log(bar); // undefined
bar(); // Uncaught TypeError: bar is not a function
var bar = function () {
  console.log('BARRRR');
};
console.log(bar); // [Function: bar]

Generator

Generator 是 ES6 引入的新语法,它是一种可以暂停和继续执行的函数。通过使用yield关键字,我们可以在函数中暂停执行并返回一个值。 简单的用法,可以当做一个Iterator可迭代器来用,用于进行遍历操作。复杂一些的用法是在内部保存一些状态,成为一个状态机。 Generator 基本语法包含两部分:函数名前要加一个星号;函数内部用 yield 关键字返回值。

  • yield,表达式本身没有返回值,或者说总是返回 undefined。
  • next,方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
function * foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}
var b = foo(5); 
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false } 
b.next(13) // { value:42, done:true }

Generator 的优点在于它提供了一种更灵活的控制流程的方式。我们可以使用yield来控制函数的执行,根据需要决定是否继续执行下一步。这种能力可以用于实现异步操作的顺序控制,使得代码更具可读性。 然而,Generator的语法相对复杂,使用时需要手动迭代生成器并调用next方法。它也无法直接处理异步操作,需要结合其他机制(如回调函数)来实现。

Set、Map

Set是一种叫做集合的数据结构,Map是一种叫做字典的数据结构 什么是集合?什么又是字典?

  • 集合 是由一堆无序的、相关联的,且不重复的内存结构【数学中称为元素】组成的组合
  • 字典 是一些元素的集合。每个元素有一个称作key 的域,不同元素的key 各不相同

区别?

  • 共同点:集合、字典都可以存储不重复的值
  • 不同点:集合是以[值,值]的形式存储元素,字典是以[键,值]的形式存储

image.png

WeakSet、WeakMap

WeakSet弱引用集合

  • 没有 clear() 和 size
  • 不再是可迭代对象,不能使用 keys、values、entries、forEach
  • 只能存储对象值的集合
  • 弱引用:对象在外部的引用消失,在WeakSet中也会消失,被被当成垃圾回收掉

WeakMap弱引用映射表。

  • 没有 clear() 和 size
  • 不再是可迭代对象,不能使用 keys、values、entries、forEach
  • 必须是对象可以是任意类型
  • 弱引用:对象在外部的引用消失,在WeakSet中也会消失,被被当成垃圾回收掉