review【JS】

285 阅读15分钟

前言

网上这些东西都写烂了为什么还要写?

  • 勤能生巧,好记性不如烂笔头
  • 知识归纳,虽然有很多东西只是表现得那样没有深入研究,但是却能够让你有了知识轮廓

JS基础

JS数据类型

  • 基本类型:Number、Boolean、String、null、undefined、symbol(ES6 新增的),BigInt(ES2020)
  • 引用类型:Object,对象子类型(Array,Function)

数据类型检测

  • typeof
typeof "" // "string"
typeof 1 // "number"
typeof NaN // "number"
typeof true // "boolean"
typeof null // "object"
typeof undefined // "undefined"
typeof [] // "object"
typeof function(){} // "function"
typeof {} // "object"
// typeof可以用于检测基本类型,但碰到引用类型均返回为object
  • instanceof
"" instanceof String // false
1 instanceof Number // false
true instanceof Boolean // false
[] instanceof Array // true
(function(){}) instanceof Function // true
({}) instanceof Object // true
// instanceof可以用于引用类型的检测,但对于基本类型是不生效的,另外,不能用于检测null和undefined
  • constructor
('').constructor === String // true
(1).constructor === Number // true
(true).constructor === Boolean // true
([]).constructor === Array // true
(function() {}).constructor === Function // true
({}).constructor === Object // true

// 不能用于检测null和undefined
// 引用类型当原型改变后就变成了新的原型的类型
function fun() {};
fun.prototype = new Array();
let f = new fun();
console.log(f.constructor===fun); // false
console.log(f.constructor===Array); // true
  • Object.prototype.toString.call()
Object.prototype.toString.call('') // "[object String]"
Object.prototype.toString.call(1) // "[object Number]"
Object.prototype.toString.call(true) // "[object Boolean]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(function() {}) // "[object Function]"
Object.prototype.toString.call({}) // "[object Object]"

原型以及原型链

参考 轻松理解JS 原型原型链

四个概念

  1. JS分函数对象和普通对象,每个对象都有__proto__属性,但是只有函数对象才有prototype属性
  2. Object、Function都是JS内置的函数, 类似的还有我们常用到的Array、RegExp、Date、Boolean、Number、String
  3. 属性__proto__是一个对象,它有两个属性,constructor和__proto__
  4. 函数的prototype属性指向的就是原型对象(或者说Fn.prototype就是原型对象),它有一个默认的constructor(构造函数)属性,这个属性是一个指向prototype属性所在函数的指针

两个准则

  1. 函数的原型对象的constructor指向函数本身
  2. 实例的__proto__和原型对象指向同一个地方
function Person(name) {
    this.name = name;
}
Person.prototype.sayHi = function () {
    console.log("hi");
};
let a = new Person("a");
// 准则一
console.log(Person.prototype.constructor === Person);
// 准则二
console.log(a.__proto__ === Person.prototype); // true
console.log((Person.prototype.__proto__ = Object.prototype)); // true
console.log((Object.prototype.__proto__ = null)); // null 原型链到此停止

// 衍生思考 任何对象都有一个constructor属性,指向它的构造函数
console.log(a.constructor === Person); // true
console.log(Person.__proto__ === Person.constructor.prototype); // true
console.log(Person.__proto__ === Person.constructor.__proto__); // true
console.log(Person.prototype === Person.constructor.prototype); // false

console.log(Person.__proto__ === Object.__proto__); // true
console.log(Person.constructor === Object.constructor); // true
console.log(Person.prototype === Object.prototype); // false
// 由上可以看出Function和Object的区别在于原型上面的功能不同

什么是原型链?

当对象查找一个属性的时候,如果没有在自身找到,那么就会查找自身的原型,如果原型还没有找到,那么会继续查找原型的原型,直到找到 Object.prototype 的原型时,此时原型为 null,查找停止。 这种通过原型链接的逐级向上的查找链被称为原型链

继承

原型链继承

一个对象可以使用另外一个对象的属性或者方法,就称之为继承。具体是通过将这个对象的原型设置为另外一个对象,这样根据原型链的规则,如果查找一个对象属性且在自身不存在时,就会查找另外一个对象,相当于一个对象可以使用另外一个对象的属性和方法了。

function Animal() {
    this.colors = ['black', 'white']
}
Animal.prototype.getColor = function() {
    return this.colors
}
function Dog() {}
Dog.prototype =  new Animal()

let dog1 = new Dog()
dog1.colors.push('brown')
let dog2 = new Dog()
console.log(dog2.colors)  // ['black', 'white', 'brown']

原型链继承存在的问题:

  1. 原型中包含的引用类型属性将被所有实例共享
  2. 子类在实例化的时候不能给父类构造函数传参

借用构造函数实现继承

function Animal(name) {
    this.name = name
    this.getName = function() {
        return this.name
    }
}
function Dog(name) {
    Animal.call(this, name)
}
Dog.prototype =  new Animal()

借用构造函数实现继承解决了原型链继承的 2 个问题:引用类型共享问题以及传参问题。但是由于方法必须定义在构造函数中,所以会导致每次创建子类实例都会创建一遍方法。

组合式继承

组合继承结合了原型链和盗用构造函数,将两者的优点集中了起来。基本的思路是使用原型链继承原型上的属性和方法,而通过盗用构造函数继承实例属性。这样既可以把方法定义在原型上以实现重用,又可以让每个实例都有自己的属性。

function Animal(name) {
    this.name = name
    this.colors = ['black', 'white']
}
Animal.prototype.getName = function() {
    return this.name
}
function Dog(name, age) {
    Animal.call(this, name)
    this.age = age
}
Dog.prototype =  new Animal()
Dog.prototype.constructor = Dog

寄生式组合继承

组合继承已经相对完善了,但还是存在问题,它的问题就是调用了 2 次父类构造函数,第一次是在 new Animal(),第二次是在 Animal.call() 这里。 所以解决方案就是不直接调用父类构造函数给子类原型赋值,而是通过创建空函数F获取父类原型的副本。

function Animal(name) {
    this.name = name;
    this.colors = ["black", "white"];
}
Animal.prototype.getName = function () {
    return this.name;
};
function Dog(name, age) {
    Animal.call(this, name);
    this.age = age;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;

es6 Class继承

class Animal {
    constructor(name) {
        this.name = name
    } 
    getName() {
        return this.name
    }
}
class Dog extends Animal {
    constructor(name, age) {
        super(name) // 关键
        this.age = age
    }
}

作用域

参考 深入理解JavaScript作用域和作用域链

什么是作用域?

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。 es5只有全局作用域和函数作用域,es新增块级作用域由const、let生成。

什么是作用域链?

当访问一个变量时,编译器在执行这段代码时,会首先从当前的作用域中查找是否有这个标识符,如果没有找到,就会去父作用域查找,如果父作用域还没找到继续向上查找,直到全局作用域为止,而作用域链,就是有当前作用域与上层作用域的一系列变量对象组成,它保证了当前执行的作用域对符合访问权限的变量和函数的有序访问。

闭包

什么是闭包

闭包是指有权访问另外一个函数作用域中的变量的函数

闭包产生的本质

当前环境中存在指向父级作用域的引用

一般如何产生闭包

  • 返回函数
  • 函数当做参数传递

变量提升

浅谈JS变量提升

  • 声明会提升到作用域顶端
console.log(v1);
var v1 = 100;
function foo() {
    console.log(v1);
    var v1 = 200;
    console.log(v1);
}
foo();
console.log(v1);
// 打印顺序为 undefined undefined 200 100
  • 函数提升是整个代码块提升到它所在的作用域的最开始执行
foo(); // 1
var foo;
function foo() {
    console.log(1);
}
foo = function () {
    console.log(2);
};
foo() // 2
foo(); // Uncaught TypeError: foo is not a function
var a = true;
if (a) {
    function foo() {
        console.log(1);
    }
} else {
    function foo() {
        console.log(2);
    }
}
// 不要在块内部声明函数

const、let和var的区别

  • 块级作用域
  • 不存在变量提升
  • 暂时性死区
  • 不可重复声明
  • let、const声明的全局变量不会挂在顶层对象下面

const命令两个注意点:

  • const 声明之后必须马上赋值,否则会报错
  • const 简单类型一旦声明就不能再更改,复杂类型(数组、对象等)指针指向的地址不能更改,内部数据可以更改。

this指向

  • this 永远指向最后调用它的那个对象
  • 箭头函数的 this 始终指向函数定义时的 this,而非执行时
  • 匿名函数的 this 永远指向 window
  • 作为方法调用:a.fn() -> this 永远指向最后调用它的那个对象
  • 作为函数调用:fn() -> 没有挂载在任何对象上,所以对于没有挂载在任何对象上的函数,在非严格模式下 this 就是指向 window 的 参考

juejin.cn/post/684490…

立即执行函数

参考 「每日一题」什么是立即执行函数?有什么作用?

什么是立即执行函数?

  • 声明一个匿名函数
  • 马上调用这个匿名函数
(function (tip) {
    alert(tip);
})("我是匿名函数");

立即执行函数有什么用?

  • 创建一个独立的作用域,这个作用域里面的变量,外面访问不到,避免命名冲突(即避免「变量污染」)
  • 常用于模块化开发 示例
var liList = ul.getElementsByTagName('li')
for(var i = 0; i < 6; i++){
    // i 是贯穿整个作用域的
    liList[i].onclick = function() {
        alert(i) // 为什么 alert 出来的总是 6,而不是 0、1、2、3、4、5
    }
    // 解决办法?用立即执行函数给每个 li 创造一个独立作用域
    !(function(i){
        liList[i].onclick = function(){
        alert(i) // 0、1、2、3、4、5
    })(i)
}

instanceof原理

参考 浅谈 instanceof 和 typeof 的实现原理

instanceof 就是判断右边变量的 prototype 在左边变量的原型链上即可

function instanceOf(left, right) {
    let leftProto = left.__proto__
    let rightProto = right.prototype
    while (true) {
        if (leftProto === null) {
            return false
        }
        if (leftProto === rightProto) {
            return true
        }
        leftProto = leftProto.__proto__
    }
}

v8垃圾回收机制

参考
简单了解JavaScript垃圾回收机制

前端面试常考题:JS垃圾回收机制

常用的几种GC(垃圾回收)算法

  • 引用计数法
  • 标记清除算法
  • 复制算法

浮点数精度 0.1 + 0.2 不等于 0.3

参考 0.1 + 0.2不等于0.3?为什么JavaScript有这种“骚”操作?

进制转换

在两数相加时,会先转换成二进制,0.1和0.2转换成二进制后会无限循环,然后进行对阶运算,JS 引擎对二进制进行截断,所以造成精度丢失。

0.1 -> 0.0001100110011001...(无限循环)
0.2 -> 0.0011001100110011...(无限循环)

JS最大安全数是 Number.MAX_SAFE_INTEGER == Math.pow(2,53) - 1
精度损失可能出现在进制转化对阶运算过程中

事件循环机制(Event Loop)

参考 这一次,彻底弄懂 JavaScript 执行机制

事件循环机制从整体上告诉了我们 JavaScript 代码的执行顺序。指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。

一个事件循环

  1. 整体script作为第一个宏任务进入主线程执行script
  2. 遇到微任务就把微任务放入微任务队列,遇到宏任务就把宏任务放入宏任务队列
  3. 执行完script后先执行微任务队列
  4. 第一轮事件循环结束开始宏任务队列,开始下一轮循环

宏任务:Script/setTimeout/setInterval/setImmediate/ I/O / UI Rendering
微任务:process.nextTick()/Promise  

上诉的 setTimeout 和 setInterval 等都是任务源,真正进入任务队列的是他们分发的任务。 优先级

  • setTimeout = setInterval
  • setTimeout > setImmediate 
  • process.nextTick > Promise

事件流

参考
你真的理解 事件冒泡 和 事件捕获 吗?
JavaScript事件三部曲之事件机制的原理

事件冒泡和事件捕获

事件流是网页元素接收事件的顺序,"DOM2级事件"规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
首先发生的事件捕获,为截获事件提供机会。然后是实际的目标接受事件。最后一个阶段是事件冒泡阶段,可以在这个阶段对事件做出响应。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>事件冒泡</title>
  </head>
  <body>
    <div>
      <p id="parEle">我是父元素 <span id="sonEle">我是子元素</span></p>
    </div>
  </body>
</html>
<script type="text/javascript">
  const sonEle = document.getElementById("sonEle");
  const parEle = document.getElementById("parEle");
  // addEventListener第三个参数 true/false 分別代表 捕获/冒泡 机制
  parEle.addEventListener(
    "click",
    function () {
      console.log("父级 冒泡");
    },
    false
  );
  parEle.addEventListener(
    "click",
    function () {
      console.log("父级 捕获");
    },
    true
  );
  sonEle.addEventListener(
    "click",
    function () {
      console.log("子级冒泡");
    },
    false
  );
  sonEle.addEventListener(
    "click",
    function () {
      console.log("子级捕获");
    },
    true
  );
</script>

点击子元素时依次触发:父级捕获 -> 子级捕获 -> 子级冒泡 -> 父级冒泡

事件代理(事件委托)

事件代理是利用事件的冒泡原理来实现的,比如我们平时在给ul中的li添加事件的时候,我们都是通过for循环一个个添加,如果li很多个的话,其实就有点占内存了,这个时候可以用事件代理来优化性能。

事件是如何实现的?

基于发布订阅模式,就是在浏览器加载的时候会读取事件相关的代码,但是只有实际等到具体的事件触发的时候才会执行。

比如点击按钮,这是个事件(Event),而负责处理事件的代码段通常被称为事件处理程序(Event Handler),也就是「启动对话框的显示」这个动作。

在 Web 端,我们常见的就是 DOM 事件:

  • DOM0 级事件,直接在 html 元素上绑定 on-event,比如 onclick,取消的话,dom.onclick = null,同一个事件只能有一个处理程序,后面的会覆盖前面的。
  • DOM2 级事件,通过 addEventListener 注册事件,通过 removeEventListener 来删除事件,一个事件可以有多个事件处理程序,按顺序执行,捕获事件和冒泡事件
  • DOM3级事件,增加了事件类型,比如 UI 事件,焦点事件,鼠标事件

手写JS

new

function _new(fn, ...args) {
    // 创造一个新对象
    const obj = {};
    // 新对象原型链接到构造函数的原型对象
    obj.__proto__ = fn.prototype;
    // 改变this指向,为实例添加方法和属性
    let ret = fn.apply(obj, args);
    // 确保返回的是一个对象(万一fn不是构造函数)
    return typeof res === "object" ? ret : obj;
}
// 优化
function _new(fn, ...args) {
    const obj = Object.create(fn.prototype);
    const ret = fn.apply(obj, args);
    // 如果函数返回非空并是对象 返回result 否则 返回newObj
    return result instanceof Object ? result : newObj;
}

function Foo(count) {
    this.count = count;
}
Foo.prototype.add = function () {
    this.count++;
    console.log(this.count);
};

let foo = _new(Foo, 18);
foo.add(); // 19

call、apply、bind

// call
Function.prototype._call = function (context, ...args) {
  context["fn"] = this;
  context["fn"](...args);
  delete context["fn"];
}

// apply

Function.prototype._apply = function (context, argsArr) {
  context["fn"] = this;
  context["fn"](argsArr);
  delete context["fn"];
}

// bind
Function.prototype._bind = function (context, ...args) {
  context["fn"] = this;
  
  return function (..._args) {
    args = args.concat(_args);
    
    context["fn"](...args);
    delete context["fn"];   
  }
}

// 测试
function bar(...args) {
    console.log(this.age, args);
}
let a = {
    age: 18,
};
bar._call(a, 1, 2); // 18 [1, 2]
bar._apply(a, [1, 2]); // 18 [1, 2]
bar._bind(a, 1, 2)(); // 18 [1, 2]

函数防抖,节流

节流旨在时间段内控制触发的频率,防抖旨在时间段内只触发最后一次。

函数防抖

function debounce(fn, delay) {
  let timer = null;
  return function(...args) {
     // 清除已存在的定时器
     timer && clearTimeout(timer)
     timer = setTimeout(function() {
        fn.call(this, ...args)
     }, delay)
  }
}

function click(v) {
  console.log(1, v);
}

const debounceClick = debounce(click, 500);

document.getElementById("btn").addEventListener("click", function () {
  debounceClick(123);
});

函数节流

function throttle(fn, delay) {
  // 记录上次触发的时间戳
  let lastTime = new Date().getTime();
  return function(...args) {
     // 记录当前触发的时间戳
     let nowTime = new Date().getTime();
     // 如果当前触发与上次触发的时间差值 大于 设置的周期则允许执行
     if (nowTime - lastTime > delay) {
        fn.call(this, ...args);
        // 更新时间戳
        lastTime = nowTime;
     }
  }
}

function click(v) {
  console.log(1, v);
}

const throttleClick = throttle(click, 1000);

document.getElementById("btn").addEventListener("click", function () {
  throttleClick(123);
});

防抖应用场景

  • 每个调整大小/滚动都会触发统计事件
  • 验证文本输入(在连续文本输入后,发送Ajax请求进行验证)
  • 监视滚动scroll事件(在添加去抖动后滚动,只有在用户停止滚动后才会确定它是否已到达页面底部) 节流应用场景
  • 实现DOM元素的拖放功能mousemove
  • 搜索关联keyup
  • 计算鼠标移动距离mousemove
  • 画布模拟草图功能mousemove
  • 射击游戏中的 mousedown/keydown事件(每单位时间只能发射一颗子弹)
  • 监视滚动scroll事件(添加节流后,只要滚动页面,就会每隔一段时间才会计算)

深浅拷贝

// 浅拷贝
function shallowClone(obj) {
  let ret = {};
  for (let key in obj) {
    result[key] = obj[key];
  }
  return ret;
}

// 深拷贝
function deepCopy(obj) {
 if (typeof obj === "object") {
    let ret = obj.constructor === Array ? [] : {};
    for (let key in obj) {
      ret[key] = typeof obj[key] === "object" ? deepCopy(obj[key]) : obj[key];
    }
    return ret;
  }
  return obj;
}

promise

参考 BAT前端经典面试问题:史上最最最详细的手写Promise教程

函数柯里化

接收一部分参数,返回一个函数接收剩余参数,接收足够参数后,执行原函数。

function curry(fn, ...args) {
    // 通过函数的 length 属性,获取函数的形参个数,形参的个数就是所需的参数个数
    const needArgLen = fn.length;
    return function (..._args) {
        args = [...args, ..._args];
        if (args.length < needArgLen) {
            return curry.call(this, fn, args);
        }
        return fn.apply(this, args);
    };
}
// 测试
let add = curry(function (a, b, c, d, e) {
    console.log(a + b + c + d + e);
});
add(1, 2, 3, 4, 5); // 不会输出任何东西,因为参数不够
// add(1, 2, 3, 4, 5); // 15
// add(1)(2)(3, 4, 5); // 15
// add(1, 2)(3, 4)(5); // 15
// add(1)(2)(3)(4)(5); // 15

冒泡排序

像水里的泡泡一样, 越往上越大

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

快排

function quickSort(arr) {
  // 结束状态
  if (arr.length <= 1) return arr;

  // 获取中间值下标
  let middleIndex = Math.floor(arr.length / 2);
  // 获取中间值
  let middle = arr.splice(middleIndex, 1)[0];

  // 小于中间值放左边
  let left = [];
  // 大于中间值放右边
  let right = [];

  for (let i = 0; i < arr.length; i++) {
    if (arr[i] < middle) {
      left.push(arr[i]);
    } else {
      right.push(arr[i]);
    }
  }
  
  // 递归 直到 left,right 长度为一
  return [...quickSort(left), middle, ...quickSort(right)];
}

其他排序参考 手撕前端面试之经典排序算法

数组扁平化

function myFlat(arr) {
    let ret = [];
    for (let item of arr) { // of返回值 in返回key
        if (Array.isArray(item)) {
            ret.push(...myFlat(item));
            // 或者
            // ret = [...ret, ...myFlat(item)];
        } else {
            ret.push(item);
        }
    }
    return ret;
}

ajax

function ajax(options}) {
    // 创建XMLHttpRequest对象
    const xhr = new XMLHttpRequest();

    // 发送请求
    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(xhr.responseText, xhr.responseXML);
            } else {
                options.fail(status);
            }
        }
    };
}

// 使用方法
ajax({
    type: "get",
    dataType: "json",
    data: {},
    url: "https://xxxx",
    success: function (text, xml) {
        //请求成功后的回调函数
    },
    fail: function (status) {
        //请求失败后的回调函数
    },
});

字符串方法

trim

String.prototype._trim = function(){
    return this.replace(/(^\s*)|(\s*$)/g, "");
}

// 测试
let str = " str ";
console.log("hi" + str._trim() + "hi");

数组方法

map

Array.prototype._map = function (fn) {
    // this 即是数组本身
    let arr = this;
    let ret = [];
    for (let i = 0; i < arr.length; i++) {
        ret.push(fn(arr[i], i));
    }
    return ret;
};

// 测试 
let arr = [1, 2, 3, 4, 5];
arr._map((item, index) => {
    console.log(item, index);
    return item * 2;
});

reduce

// 此处只考虑了 initialValue 为数字的情况, 如要把initialValue设置为对象、数组、字符串、布尔值。。请依次做判断
Array.prototype._reduce = function (fn, initialValue) {
    let arr = this;
    let ret = typeof initialValue === "undefined" ? arr[0] : initialValue;
    let i = typeof initialValue === "undefined" ? 1 : 0;
    for (i; i < arr.length; i++) {
        ret = fn(ret, arr[i], i, arr);
    }
    return ret;
};

// 测试
const arr = [2, 3, 4, 5];
arr._reduce((total, cur, index, arr) => {
    return total + cur;
}, 0);

filter

 Array.prototype._filter = function (fn) {
    let arr = this;
    let ret = [];
    for (let i = 0; i < arr.length; i++) {
        if (fn(arr[i], i, arr)) {
            ret.push(arr[i]);
        }
    }
    return ret;
};
    
// 测试
let arr = [1, 2, 3, 4, 5, 6, 7];
let ret = arr._filter((item, index, arr) => {
    return item % 2;
});
console.log(ret); // [1, 3, 5, 7]

参考优秀博文
字节跳动最爱考的前端面试题:JavaScript 基础
死磕 36 个 JS 手写题
由浅入深,66条JavaScript面试知识点
总结了17年初到18年初百场前端面试的面试经验(含答案)