前端面试高频出现的八个基础JS概念

数据类型

Javascript 世界里将数据类型分为了两种:原始数据类型引用数据类型前端

共有如下八种数据类型:NumberStringBooleanNullUndefinedObjectSymbol(ES6)BigInt(ES10)面试

原始数据类型:StringNumberBooleanNullUndefinedSymbolBigInt
引用数据类型:Object数组

不一样类型的存储方式:
原始数据类型:原始数据类型的值在内存中占据固定大小,保存在栈内存中。
引用数据类型:引用数据类型的值是对象,在栈内存中只是保存对象的变量标识符以及对象在堆内存中的储存地址,其内容是保存中堆内存中的。微信

varletconst 有什么区别

  1. var 具备变量提高性质,letconst 没有。

所谓 变量提高 指的是 JS 在预编译阶段,函数和以 var 声明的变量会被 JS 引擎提高至当前做用域顶端。markdown

// 编译前
function sayName() {
    console.log(name);
    var name = '橙某人';
}
// 编译后
function sayName() {
    var name;
    console.log(name);
    name = '橙某人';
}
复制代码
  1. var 不具块级做用域,letconst 具备块级做用域性质。
// 例子一
{
    var a = 1;
    let b = 2;
    const c = 3;
}
console.log(a); // 1
console.log(b); // 报错
console.log(c); // 报错
// 例子二
function fn() {
    if(true) {
        var a = 1;
        let b = 2;
        const c = 3;
    }
    console.log(a); // 1
    console.log(c); // 报错
    console.log(c); // 报错
}
fn()
复制代码

暂时性死区:JS 引擎在预编译代码的时候,若是遇到 var 声明会将它提高至当前做用域顶端,若是遇到 letconst 会将它们放入暂时性死区(Temporal Dead Zone),简称 TDZ, 它的性质是会造成一个封闭的做用域,任何访问 TDZ 中的变量就会报错。只有在执行过变量的声明语句后,变量才会从 TDZ 中移除,才能进行正常的变量访问。闭包

  1. var 能重复声明,letconst 重复声明会报错。
  2. var 全局声明变量会挂载在 windowletconst 不会。
var a = a;
console.log(window.a); // 1

let b = 2;
console.log(window.b); // undefined

const c = 3;
console.log(window.c); // undefined
复制代码

了解一下 JS 的工做过程,有利于更好的理解问题哦:app

JS 引擎的执行过程分为三个阶段:语法分析阶段、预编译阶段、执行阶段。

语法分析阶段:检查书写的 JS 语法有没有错误,如是否少写个'{'等。
预编译阶段:分为全局预编译、局部预编译。函数

全局预编译:post

  1. 建立GO对象。
  2. 找变量声明,将变量声明做为GO对象的属性名,并赋值undefined。
  3. 找全局里的函数声明,将函数名做为GO对象的属性名,值赋予函数体。

局部预编译:学习

  1. 建立一个AO对象。
  2. 找形参和变量声明,将形参和变量声明做为AO对象的属性名,值为undefined。
  3. 将实参和形参统一。
  4. 在函数体里找函数声明,将函数名做为AO对象的属性名,值赋予函数体。

执行阶段:从上到下,逐行执行,变量赋值阶段也在此完成。

判断数据类型的四种方式

  1. typeof,区分不了细致的 Object 类型,如 ArrayDateRegExp都只是返回 object
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof true); // boolean
console.log(typeof null); // object, null 在 typeof 下被标记为 object, 这是JS一个历史bug了
console.log(typeof undefined); // undefined
console.log(typeof {}); // object
console.log(typeof Symbol()); // symbol
console.log(typeof 1n); // bigint
复制代码

建立一个 BigInt 类型的方式有两种:在一个整数字面量后面加 n 或者调用 BigInt 函数。
const a = BigInt(1);
const b = 1n;

  1. Object.prototype.toString.call(),基本能知足对各类数据类型的判断。
console.log(Object.prototype.toString.call(1)); // [object Number]
console.log(Object.prototype.toString.call('1')); // [object String]
console.log(Object.prototype.toString.call(true)); // [object Boolean]
console.log(Object.prototype.toString.call(null)); // [object Null]
console.log(Object.prototype.toString.call(undefined)); // [object Undefined]
console.log(Object.prototype.toString.call({})); // [object Object]
console.log(Object.prototype.toString.call(Symbol())); // [object Symbol]
console.log(Object.prototype.toString.call(1n)); // [object BigInt]
复制代码

它对于其余一些内置引用类型的判断也很适合。

console.log(Object.prototype.toString.call([])); // [object Array]
console.log(Object.prototype.toString.call(new Date())); // [object Date]
console.log(Object.prototype.toString.call(/a/g)); // [object RegExp]
console.log(Object.prototype.toString.call(function(){})); // [object Function]
console.log(Object.prototype.toString.call(new Error())); // [object Error]
console.log(Object.prototype.toString.call(Math)); // [object Math]
console.log(Object.prototype.toString.call(JSON)); // [object JSON]
function fn() {
    console.log(Object.prototype.toString.call(arguments)); // [object Arguments]
}
fn();
复制代码
  1. constructor,是根据原型链原来来判断的,由于每个实例对象均可以经过 constructor 来访问它的构造函数。
console.log((1).constructor === Number);
console.log('1'.constructor === String);
console.log(true.constructor === Boolean);
// console.log(null.constructor);
// console.log(undefined.constructor);
console.log({}.constructor === Object);
console.log(Symbol().constructor === Symbol);
console.log(1n.constructor === BigInt);
复制代码

因为 undefinednull 是无效的对象,并不具有 constructor 并且 null 是做为原型链的末端结尾。

  1. instanceof,内部机制是经过检查构造函数的原型对象(prototype)是否出如今被检测对象的原型链上来判断的。
console.log(1 instanceof Number); // false
console.log('1' instanceof String); // false
console.log(true instanceof Boolean); // false
console.log({} instanceof Object); // true
console.log(function(){} instanceof Function); // true
复制代码

instanceof 强调的是对拥有原型链的引用类型对象的判断,因此对于原生数据类型就一筹莫展(字面量方式建立的不能检测)。咱们对于它更多的是在这种场景中使用:

function Person(){};
function Student(){};
var p = new Person();
console.log(p instanceof Person); // true
console.log(p instanceof Student); // false
复制代码

如何快速判断一个 window 类型

这是小编在一次面试中面试官问个人题目,当时很懵,由于这个问题是创建在我刚答完上面说过的 “判断数据类型的四种方式” 的时候,当时我随口就说了分别能利用 Object.prototype.toString.call()constructorinstanceof 三种方式来判断。

console.log(Object.prototype.toString.call(window)); // [object Window]
console.log(window.constructor === Window); // true
console.log(window instanceof Window); // true
复制代码

面试官一脸严肃的表情问我“除了这三种,还有吗?”

我......心想...还有吗???这还不够吗?脑壳想不出来东西了,我傻笑着抓着脑壳说:“暂时我没想到其余方式了,这还有其余快的方式吗?求指教”。

面试官:“很简单,window 它有一个 window属性指向自身,能够利用这个特性来判断。”

console.log(window.window === window); // true
复制代码

好家伙,细,真的细。

image.png

闭包

一个万年常考题,MDN 对闭包的定义为:

闭包是指那些可以访问自由变量的函数。

??? 懵逼?会不会有小伙伴学了好久的闭包,也常用,但就是死活说不清楚它是啥,能干什么?在网上关于它的文章,那是数不胜数,基本一搜索就一大把,各篇文章说得五花八门。

但看了那么多文章学习,你又是否学会了呢?在我这,我以为学会一个知识点,就是你能用大白话把它说出来,而不是背它的概念,只有这样才能说明本身消化了,剩下的就是不断去应用增强印象就能够了,慢慢就记牢了。

此次不对闭包深究了,没意思,就简单说说几个问答题。

面试官:说说你对闭包的理解?
这是一个客观性很是强的问法,咱们直追本质,面试官想知道什么呢?他其实只是想听听你对闭包是否有本身的我的理解,对于概念的他天天不知道听过多少次了。

答:咱们知道函数内部能直接读取函数外部定义的变量,可是函数外部没法读取函数内部的变量,闭包在我看来,就是提供一种访问函数内部变量的桥梁。

面试官:说说闭包的好处和坏处?

答:使用闭包的好处是提供了访问函数内部变量的方式与避免形成全局变量污染。使用闭包的坏处是会形成内存消耗大,滥用闭包会致使内存泄露,形成页面奔溃。

面试官:你能说说为何使用闭包会形成内存泄露吗?

答:函数的做用域链是在函数定义的时候建立的,在函数运行完成,销毁的时候消亡,这时它内部的变量就应该被销毁,内存被回收,可是闭包能让其继续延续下去,不被垃圾回收机制回收。因为变量都是维护在内存中的,这些变量数据就会一直占用着内存,最后超载使用内存,形成内存泄露。

面试官:手写一个闭包的例子吧?
手写例子通常也是必不可少的,闭包的例子很是很是的多,咱们只要记住一些简单、容易记住的小例子防身,我以为就能够了。
一道经典的闭包例子:

var inputs = document.getElementsByTagName('input');
for(var i = 0;i<inputs.length;i++) {
    (function(i) {
        inputs[i].onclick = function() {
            console.log(i)
        }
    })(i)
}
复制代码

防抖节流

这也是一个老题目了,就在我写这篇文章的前两天,恰好就被问过。(T_T) 这是两个容易记混淆的概念,这里我想了两个例子,你且看看妥不稳当。

相信各位对王者荣耀(赶忙上号,峡谷见!!!)不陌生了,咱们直接来看:

  • 防抖:有点像英雄在回城的时候,每次点击回城要必定时间才能传送,在这个时间内若是移动英雄,则要从新开始。
  • 节流:有点像射手英雄,无论你按得多快,只要攻速没增长,也只会一下一下射出攻击而已。

嘿嘿,这两个例子如何?有没有帮你记住了他俩呢?(  ̄ ▽ ̄)

防抖(debounce)

在触发事件后 N 秒后才执行函数,若是在 N 秒内又触发了函数,则从新进行计时。

  • 应用场景:
    输入框进行输入实时搜索、页面触发resize事件的时候。

  • 手写:

    function debounce(fn, wait) {
        var timer = null
        return () => {
            clearTimeout(timer);
            timer = setTimeout(fn, wait)
        }
    }
    复制代码

节流(throttle)

在规定的一个单位时间内,只触发一次函数,若是单位时间内触发屡次函数,只有一次生效。

  • 应用场景:
    页面滚动事件。

  • 手写:

    function throttle(fn, wait) {
        var timer = null;
        return () => {
            if(!timer) {
                timer = setTimeout(() => {
                    timer = null;
                    fn()
                }, wait)
            }
        }
    }
    复制代码

上面列举了两个的简单手写过程,如今去面试,基本都有手写代码题了,这都成为一个潮流了,这致使了不少时候咱们要记住不少代码的书写过程,这真的让人难受想哭(︶︿︶)。
我本身的方法是,我会记住最简单的代码结构,剔除那些非主流的功能设计,只留一个能实现主要功能的代码构架就行,剩下的若是真遇到手写代码的时候,再本身慢慢推导出来就行。(虽然不少时候可能也推导不出来,哈哈哈)

原型与原型链

  • 原型

什么是原型呢?它是一个对象,咱们也称它为原型对象,代码中用 prototype 来表示。

  • 原型链

那什么又是原型链呢?原型与原型层层相连接的过程即为原型链。

原型与原型链 在前端是一个很是基础的 JS 概念了,相信你也或多或少会有所听过了,咱们且来看看下面这张图你是否看得懂:

image.png

要理解好 原型与原型链 咱们要记住五个很重要的东西,这是咱们每次回顾它们的时候都要想起来的:

  • JS 把对象(除null)分为普通对象与函数对象,无论是什么对象都会有一个 __proto__ 属性。
  • 函数对象还会有一个 prototype 属性,也就是说函数默认拥有 __proto__ 属性与 prototype 属性。
  • 普通对象的 __proto__ 属性与函数对象的 prototype 属性都会指向它们对应的原型对象。
  • 函数对象另外一个 __proto__ 属性会指向 Function.prototype 原型,原型链的末端为 null
  • 原型对象 它会拥有一个 constructor 属性指向它的构造函数。

通常面试聊到 原型与原型链 能讲清楚这五个点基本也就及格了,那么如何记住这些东西?

答案:画图,按着本身的理解,本身画两天你就印象深入,不骗你,略略略。

如何推导出这个图,能够看看小编前面写过这篇文章:看完,你会发现,原型、原型链原来如此简单!

call与apply与bind

call/apply/bind 的面试题无非逃不过的就是手写代码实现了(︶︿︶),咱们就不聊它的应用了,简单聊聊它会涉及的问题不是。

面试官:它们三者有什么做用?

答:它们三者的主要做用都是为了改变函数的 this 指向,目前大部分关于它们的应用都是围绕这一点来进行的。

面试官:那它们之间有什么区别?

答: 有三点不一样:

  1. 参数不一样,callbind 参数是经过一个一个传递的, apply 只接收一个参数而且是以数组的形式传递,后续参数是无效的。
  2. 执行时机不一样,callapply 当即调用当即执行,bind 调用后会返回一个函数,调用该函数才执行。
  3. bind 返回的函数能做为构造函数使用,传递的参数依然有效,但绑定的 this 将失效。

(记忆apply 传递的参数为数组,能够根据开头 a 等同于 Array 记忆哦(-^〇^-))

有时候适当的总结会让面试官很舒心,你上面哔哩啪啦讲一大堆,讲完本身也忘了,对比你直接说: “有xx点不一样,第一点是...第二点是...” ,相信后者的方式更能博得面试官的好感。

面试官:手写实现bind()方法吧?
只是能说出它们三者的做用区别,并不能让咱们脱颖而出,只有理解够深咱们才能卷得过别人,手写必不可少!特别是 bind的,大厂的面试中基本是高频出现。

关于 callapply 的实现你能够看小编以前看的文章了解一下。 Call与Apply函数的分析及手写实现

bind 的实现以下:

Function.prototype.myBind = function(context) {
    // 保存原函数
    var _fn = this;
    // 获取第一次传递的参数
    var params = Array.prototype.slice.call(arguments, 1);

    var Controller = function() {}; 

    // 返回的函数, 可能会被看成 new调用
    var bound = function() {  
        // 获取第二次传递的参数
        var secondParams = Array.prototype.slice.call(arguments);
        /** 考虑返回的bound函数是被当成 普通的调用 仍是 new调用:
         *  new调用: 绑定的 this 失效, bound函数中的this指向自身
         *  普通的调用: 正常改变执行函数的 this 指向, 把它指向 context
         */
        var targetThis = this instanceof Controller ? this : context;
        return _fn.apply(targetThis, params.concat(secondParams));
    }
    /**
     * 1. 返回的函数应该具备原函数的原型。
     * 2. 修改返回函数的原型不能影响原函数的原型。
     */ 
    Controller.prototype = _fn.prototype;
    bound.prototype = new Controller();
    return bound;
}
复制代码

测试代码:

function fn(val1, val2, val3) {console.log(this, val1, val2, val3)}
var res = fn.myBind(obj, 1, 2)
res(3); // {name: "橙某人"} 1 2 3
复制代码

深浅拷贝

若是你学过一些前端知识,知道栈空间与堆空间,那么我相信你对于理解这个概念必定没啥问题了,涉及这个知识点的面试题通常可能会是代码层面上,如:实现一个浅拷贝函数?或者递归实现一下深拷贝函数等。

浅拷贝

浅拷贝操做会建立一个新对象,这个对象有着原始对象属性值的一份精确拷贝,若是属性是基本类型,拷贝的就是基本数据类型的值,若是属性是引用类型,拷贝的就是内存地址。

  • 手写
    function copy(original) {
        var o = {};
        for(var key in original) {
            o[key] = original[key];
        }
        return o;
    }
    复制代码

深拷贝

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

  • 手写
    function deepCopy(original) {
        if(typeof original !== 'object') return;
        var o = {}
        for(let key in original) {
            o[key] = typeof original[key] === 'object' ? deepCopy(original[key]) : original[key]
        }
        return o;
    }
    复制代码

若是你了解深浅拷贝的基本概念,相信上面的代码对你没什么难度的。(^▽^)

面试官:你以为赋值与浅拷贝有什么区别? (这是在网上冲浪的时候偶然看到的一题)

  1. 把一个对象赋值给另外一个新变量时,赋值的是该对象在栈中的地址,两个对象指向的是同一个堆空间。
  2. 浅拷贝是从新在堆空间中建立一块空间,拷贝后的基本数据类型不相互影响,拷贝后的对象引用类型会相互影响。

(说白了就是:是否在堆内存中建立新空间。)

New关键字

关于 new 关键字相信用法你已经很是熟了。

function Person() {};
var p = new Person();
复制代码

可是,我须要你记住它干了三件事件:

  1. 新建了一个空对象并返回。
  2. 新对象的原型(__proto__)指向构造函数的原型(prototype)。
  3. 构造函数的 this 指向新对象。

记住了这三件事情基本也就不用担忧和它相关的面试题了,即便是让你来模拟它的实现,也是很是简单的。

function myNew(constructor) {
    // 1. 建立新对象
    const newObject = new Object();
    // 2. 改变新对象的原型指向
    newObject.__proto__ = constructor.prototype;
    // 3. 构造函数的 this 指向新对象
    constructor.apply(newObject, Array.prototype.slice.call(arguments, 1))

    return newObject;
}
复制代码

是否是彻底没有难度? 更多详情

微信图片_20210112181033.jpg

柯里化函数

在数学和计算机科学中,柯里化是一种将使用多个参数的一个函数转换成一系列使用一个参数的函数的技术。

这概念很懵吧?没错,我也懵。不过没有关系,咱们记住它的例子就行,它的例子颇有特色,看完,你再来回头想一想可能就能懂了噢。

咱们先来看一个很简单的三个数累加示例:

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

这是一个很简单的操做,但有时咱们但愿 add 函数可以灵活一点,像是这样子的形式也能实现:

console.log(add(1, 2)(3)); // 6
console.log(add(1)(2, 3)); // 6 
console.log(add(1)(2)(3)); // 6
console.log(add(4, 5, 6)); // 15
复制代码

这个时候就会用到柯里化的概念了,下面咱们来直接看它的代码,带很详细的解释,实际的代码没有多少行的,彻底没有负担的,哈哈。

/**
 * 柯里化函数: 延迟接收参数, 延迟执行, 返回一个函数继续接收剩余的参数
 * call: 收集参数
 * apply: 注入参数, 参数变成了数组, 借用apply能依次注入参数
 */ 
function curry(fn) {
    // 获取原函数的参数长度
    var argsLength = fn.length;

    return function curried() {
        // 获取调用 curried 函数的参数
        var args1 = Array.prototype.slice.call(arguments);
        // 判断收集的参数是否知足原函数的参数长度: 知足-调用原函数返回结果  不知足-继续柯里化(递归)
        if(args1.length >= argsLength) {
            // 调用原函数返回结果
            return fn.apply(this, args1);
        }else {
            // 不知足继续返回一个函数收集参数
            return function() {
                var args2 = Array.prototype.slice.call(arguments);
                // 继续柯里化
                return curried.apply(this, args1.concat(args2));
            }
        }
    }
}
复制代码

测试代码

function add(a, b, c) {
    return a + b + c;
}
var newAdd = curry(add);
console.log(newAdd(1, 2, 3)); // 6
console.log(newAdd(1, 2)(3)); // 6
console.log(newAdd(1)(2, 3)); // 6
console.log(newAdd(1)(2)(3)); // 6
console.log(newAdd(4, 5, 6)); // 15
复制代码

柯里化函数在我看来就是,延迟接收参数,延迟执行,返回一个函数继续接收剩余的参数,当函数参数接收知足条件的时候,就执行原函数返回结果。

至此,本篇文章就写完啦,撒花撒花。

image.png

但愿本文对你有所帮助,若有任何疑问,期待你的留言哦。 老样子,点赞+评论=你会了,收藏=你精通了。

相关文章
相关标签/搜索