您的位置:  首页 > 技术 > 前端 > 正文

金三银四,那些烧脑的JS面试题及原理

2022-02-09 14:23 https://my.oschina.net/jill1231/blog/5436438 花木喔 次阅读 条评论
  • Q: JS代码是按顺序执行的吗?
  • A: JS代码执行过程中,需要先做变量提升,而之所以需要实现变量提升是因为JS代码在执行之前需要先编译

1、变量提升

变量和函数声明会被存放到变量环境中,变量的默认值会被设置为undefined

var scope = 'global scope'
function a(){
  // 3、顶层变量环境声明了scope初始化为undefined
  function b(){ 
    // 2、b函数的上层作用域是a,向上找scope
    console.log(scope)
  }
  return b;
  // 1、虽然声明在return语句后面,依然会提升到a函数作用域的顶层
  var scope = 'local scope'
}
a()() // undefined

1.1、同名处理

  • 同名函数,选择最后声明的
  • 变量和函数同名,选择函数
var a = 1
var getNum = function() {
  a = 2
}
function getNum() {
  a = 3
}
getNum()
console.log(a) // 2
// 变量和函数同名选择提升函数,函数提升包含初始化和赋值,接着执行函数,var声明的getNum被赋值为一个函数执行完成更改变量a为2

1.2、提升阶段

创建初始化赋值
let提升xx
var提升提升x
function提升提升提升

在块作用域内,let声明的变量仅在创建时被提升,在初始化之前使用变量,就会形成一个暂时性死区

var name = 'World'
;(function () {
  if (typeof name === 'undefined') {
    var name = "HuaMu"; 
    console.info('Goodbye ' + name)
  } else {
    console.info('Hello ' + name)
  }
})()
// if分支内的name会被提升到外层,且同全局变量同名,则访问不到外层的name,var仅创建和初始化,并未赋值,则值为undefined,满足if条件
// Goodbye HuaMu

2、调用栈

在执行上下文创建完成后,JS引擎会将执行上下文压入栈中,通常把这种用来管理执行上下文的栈称为执行上下文,又称调用栈

2.1、函数调用

  • JS引擎会为函数创建执行上下文,并将其压入调用栈
  • JS引擎执行函数代码
  • 执行完毕后,JS引擎将该函数的执行上下文弹出栈

2.2、栈溢出

当分配的调用栈空间被占满时,会引发“栈溢出”问题。即超过了最大栈容量或最大调用深度

2.2.1、场景

<!-- todo -->

3、作用域

作用域就是变量与函数的可访问范围,即作用域控制着变量和函数的可见性和生命周期。主要有全局作用域、函数作用域以及块级作用域

  • 当前作用域与上层作用域有同名变量时,无法访问和影响上层变量
let a = 1
function b(a) {
  a = 2
  console.log(a)
}
b(a) // 2
console.log(a) // 1

4、作用域链

通过作用域查找变量的链条称为作用域链,而作用域链是通过词法作用域来确定的。词法作用域由函数声明的位置来决定,是静态的,即在代码阶段就决定好了,和函数是怎么调用的没有关系

// 连等操作是从右向左执行的,相当于b = 10、let a = b,相当于隐式创建为一个全局变量b 
let a = b = 10; 
;(function(){ 
  // 跟着作用域链查找到全局变量b,并修改为20
  // 由于重新声明了,a变量只是局部变量,不影响全局变量a
  let a = b = 20 
})()
console.log(a) // 10
console.log(b) // 20

函数只会在第一次执行的时候被编译,因此编译时变量环境和词法环境最顶层数据已确定

var i = 1
function b() {
  console.log(i)
}
function a() {
  var i = 2
  b()
}
// 由于a函数在全局作用域被定义,即便b函数在a函数内执行,它也只能访问到全局的作用域
a() // 1

5、闭包

一个作用域引用着一个本该被销毁的作用域,称之为闭包。即一个函数引用着父作用域的变量,在父函数执行结束后依然进行调用

6、this

this是函数执行上下文对象,是动态变化的值,它没有作用域的限制,嵌套函数中的this不会从外层函数中继承,可通过箭头函数、self处理

6.1、类型

  • 全局执行上下文中的this: window
  • 函数执行上下文中的this: 严格模式 ? undefined: window
var v_out = 'v_out';
let c_out = 'c_out';
var inner = {
  v_out: 'v_in',
  c_out: 'c_in',
  v_func: function () {
    return this.v_out
  },
  c_func: function () {
    return this.c_out
  },
  func:()=>{
    return this.v_out
  }
};
// 获取对象作用域内的函数,在全局环境下调用 this 指向 window
const v_method = inner.v_func;
const c_method = inner.c_func; 
// 顶层 v_out 变量会提升挂载到 window 
v_method(); // 'v_out'
// 在块作用域内,const声明的变量不会挂载到 window,且父作用域不能访问子作用域
c_method(); // undefined

// 赋值表达式和逗号表达式会返回最后一个值本身,即inner.v_func函数本身,调用位置是全局环境
(inner.v_func, inner.v_func)();  // 'v_out'
(inner.v_func = inner.v_func)(); // 'v_out'

// 对象的方法调用,this指向该对象
inner.v_func()   // 'v_in'
(inner.v_func)() // 'v_in'

// 箭头函数没有自己的执行上下文,它继承调用函数的this,在这里是window
inner.func() // 'v_out'

6.2、更改this指向

绑定优先级为:new > 显示绑定(call、apply、bind) > 隐式绑定(调用函数对象) > 默认绑定(window)

6.2.1、通过函数的call、apply、bind方法设置

c_method.call(inner)
c_method.apply(inner)
c_method.bind(inner)()
简单实现:call、apply
  • 将当前函数链接到指定的上下文中,即将函数设置为对象属性
  • 当前函数在context上下文中执行
  • 移除context中已执行的当前函数
/**
 * 简单实现apply
 * @param {Function} fn 当前运行的函数
 * @param {Object} context 指定的上下文
 * @param {Array} args 参数集合
 * @returns 
 */
const apply = (fn,context=window,args=[])=>{
  // Symbol是es6增加的第六个基本类型,对于对象属性就是uuid
  const id = Symbol();
  // 将当前函数链接到指定的上下文中
  context[id] = fn;
  // 当前函数在context上下文中执行
  const res = context[id](...args)
  // 移除context中已执行的当前函数
  delete context[id]
  return res;
}

// -------test------- //
const context = {
  value:1
}

function fn (name,isGirl){
  console.log("🚀 ~ ", name,isGirl,this.value) 
}

apply(fn,context,['huamu',true]) // 🚀 ~  huamu true 1
简单实现:bind
  • call、apply不一样的点是bind返回一个新函数
function bind (fn, context=window, ...args) {
  return (...args2)=> apply(fn,context,[...args,...args2])
}

6.2.2、通过调用函数对象:指向对象本身

this的绑定是函数真正执行的位置

6.2.3、通过构造函数

function Foo() {
  getName = function () { console.log(1) }
  return this
}
Foo.getName = function () { console.log(2) }
Foo.prototype.getName = function () { console.log(3) }
var getName = function () { console.log(4) }
function getName() { console.log(5) }

// 执行Foo函数的静态方法
Foo.getName() // 2
// 函数getName提升并赋值,执行getName函数表达式
getName() // 4
// 在全局环境下执行Foo函数,this指向window,执行函数内的getName方法,覆盖了全局环境下的getName
Foo().getName() // 1
getName() // 1
// new用于调用函数,即 new Foo.getName() 相当于 new (Foo.getName)(),执行了Foo函数的静态方法
new Foo.getName() // 2
// new 和 . 的优先级一样高,从左往右执行,相当于 (new Foo()).getName(),new会创建一个新对象,执行新对象的getName方法,在新对象本身找不到该方法,因此向原型找
new Foo().getName() // 3
new new Foo().getName()
简单实现:new
  • 创建一个新对象,并指向函数原型
  • 绑定this到新对象
  • 返回对象
const newFn = (fn, ...arg) => {
  const obj = Object.create(fn.prototype);
  fn.apply(obj,arg)
  return obj
}

拓展:Object.create方法会创建一个对象,并且将该对象的__proto__属性指向传入的对象

const person = {
 address: {
  country:"china"
 },
 number: 111,
 say: function () {
  console.log(`it's ${this.name}, from ${this.address.country}, nums ${this.number}`)
 },
 setCountry:function (country,number) {
  this.address.country=country
  this.number = number
 }
}
// 1、p1、p2 的原型对象指向了同一个对象
const p1 = Object.create(person)
const p2 = Object.create(person)

// 2、添加属性
p1.name = "huahua"
// 3、在原型上找到setCountry函数,并且找到引用值address和原始值numbe属性,引用值会在所有实例共享
p1.setCountry("nanji",666)

p2.name = "mumu"
// 4、p2 的修改值会覆盖 p1的,最终country的值都为beiji
p2.setCountry("beiji",999)

p1.say() // it's huahua, from beiji, nums 666
p2.say() // it's mumu, from beiji, nums 999

6.2.4、实例

const value = 1;
const objContext = {
  value : 2, 
  getThis:function() {
    // 嵌套函数中的`this`不会从外层函数中继承
    function fn1() {
      console.log("🚀 ~ fn1",this.value) // undefined
    }
    fn1()

    // 把 this 保存为一个 self 变量,再利用变量的作用域机制传递给嵌套函数
    const self = this;
    function fn2() {
      console.log("🚀 ~ fn2 self",self.value) // 2
    }
    fn2()

    // 箭头函数没有自己的执行上下文,会继承调用函数中的 this
    const fn3 = () => {
      console.log("🚀 ~ fn3 箭头函数",this.value) // 2
    }
    fn3()

    // 箭头函数不会绑定局部变量,所有涉及它们的引用都会沿袭向上查找外层作用域链来处理,因此this的绑定只有一次
    function fn4() {
      return () => {
          return () => {
            return () => {
              console.log("🚀 ~ fn4 箭头函数",this.value) // 42
            };
          };
      };
    }
    fn4.call( { value: 42 } )()()()

    // 构造函数优先级最高
    function fn5(value) {
      this.value = value
    }
    const fn = new fn5(100)
    console.log("🚀 ~ fn5 构造函数", fn.value, this.value) // 100 2 
  }
}

7、原型

7.1、函数对象 & 普通对象

通过 new Function 创建的对象称之为函数对象,其他则为普通对象,普通对象的构造函数是 Object

// 函数对象
function fn(){};
const fn = () =>{};
const fn = new Function('str')

// 普通对象
const obj = {}
const obj = new Object()
const obj = new fn()

每个对象都有内置__proto__属性,指向创建它的构造函数的原型对象,但只有函数对象才有prototype属性,指向函数的原型对象

7.2、原型对象

每个原型对象默认拥有一个constructor指针,指向prototype属性所在的函数

'prototype'

7.2.1、person1.__proto__ 是什么?

因为:person1 的构造函数是 Person
所以:person1.__proto__ === Person.prototype

7.2.2、Person.__proto__ 是什么?

因为:Person 的构造函数是 Function
所以:Person.__proto__ === Function.prototype

7.2.3、Person.prototype.__proto__ 是什么?

因为:Person.prototype 是构造函数的一个实例,是个普通对象,其构造函数是 Object
所以:Person.prototype.__proto__ === Object.prototype
     Function.prototype.__proto__ === Object.prototype

7.2.4、Object.__proto__ 是什么?

因为:所有函数对象的__proto__都指向Function.prototype,它是一个空函数
所以:Object.__proto__ === Function.prototype

7.2.5、Object.prototype.__proto__ 是什么?

因为:Object.prototype 处于 原型链的顶端,为null
所以:Object.prototype.__proto__ === null

7.2.6、12个JS内置构造器对象

8个可访问构造器 同 Object 指向 Function.prototype
Number、Boolean、String、Function、Array、RegExp、Error、Date
2个以对象形式存在, 其 proto 指向 Object.prototype
Math、JSON
1个不能直接访问的 Global、1个仅在函数调用时由JS引擎创建的 Arguments

8、继承

  • 原型继承
  • 构造函数继承
  • 组合继承
  • class 继承:主要依靠extends、super(让JavaScript引擎去实现原来需要我们自己编写的原型链代码)

9、模块

9.1、模块化的意义

若不采用模块化,则引入js文件时必须确保引入顺序正确,否则无法运行。在文件数量大、依赖关系不明确的情况很难保证,因此出现了模块化

9.2、CommonJS & AMD

在ES6以前,JS没有模块体系。只有社区指定的一些模块加载方案,如用于服务器端同步加载的CommonJS和用于浏览器端异步加载的AMDCMD

9.2.1、CommonJS(Node.js)

一个文件就是一个模块,有自己的作用域,只向外暴露特定的变量和函数。拥有四个重要变量:moduleexportsrequireglobal

exports本身是一个变量对象,指向module.exports{}模块,只能通过.语法向外暴露变量。而module.exports既可通过.也可使用=赋值,其中exportsmodule的属性,指向{}模块

//在这里写上需要向外暴露的函数、变量
module.exports = { 
  add,
  update
}

// 引用自定义模块必须加./路径,不加的话只会去node_modules文件找
var math = require('./math')
// 引用核心模块时,不需要带路径
var http = require('http')

9.2.2、AMD(require.js)、CMD(sea.js)

虽然都是并行加载js文件,但AMD推崇依赖前置、提前执行,即预加载,CMD推崇依赖就近、延迟执行,即懒加载。拥有三个重要变量:指定引用路径的require.config、定义模块的definde以及加载模块的require

/** AMD写法 **/
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) { 
     // 在最前面声明并初始化了要用到的所有模块
    a.doSomething();
    if (false) {
        // 即便没用到某个模块 b,但 b 还是提前执行了
        b.doSomething()
    } 
});

/** CMD写法 **/
define(function(require, exports, module) {
    //在需要时申明
    var a = require('./a'); 
    a.doSomething();
    if (false) {
        var b = require('./b');
        b.doSomething();
    }
});

9.3、ESM

CommonJSAMD输出的是对象,引入时需查找对象属性,只能在运行时确定模块的依赖关系以及输入输出变量,即运行时加载,而ES6模块的设计思想,是尽量的静态化,它导出的不是对象,而是一个个接口,使得编译时就能确定模块的依赖关系和输入输出变量,即静态加载

9.3.1、原理解析

// index.js
import { m } from './module';

// module.js
const m = 1;
const n = 2;
export { m, n };

将上面源码进行打包

// 1. 是一个立即执行函数
(function (modules) {
  var installedModules = {};
  // 4. 处理入口文件模块
  function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // 5. 创建一个模块
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // 6. 执行入口文件模块函数
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
    module.l = true;
    // 7. 返回
    return module.exports;
  }
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) { // 判断name是不是exports自己的属性
      Object.defineProperty(exports, name, {enumerable: true, get: getter});
    }
  };
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      // Symbol.toStringTag作为对象的属性,值表示这个对象的自定义类型 [Object Module]
      // 通常只作为Object.prototype.toString()的返回值
      Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
    }
    Object.defineProperty(exports, '__esModule', {value: true});
  };
  __webpack_require__.o = function (object, property) {
    return Object.prototype.hasOwnProperty.call(object, property);
  };
  // 3. 传入入口文件id
  return __webpack_require__(__webpack_require__.s = "./index.js");
})(
  // 2. 模块对象作为参数传入
  { 
  "./index.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      // __webpack_exports__就是module.exports
      "use strict";
      // 添加了__esModule和Symbol.toStringTag属性
      __webpack_require__.r(__webpack_exports__);
      var _module__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./module.js");
    }),

  "./module.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      __webpack_require__.r(__webpack_exports__);
      // 把m/n这些变量添加到module.exports中,并设置getter为直接返回值
      __webpack_require__.d(__webpack_exports__, "m", function () {return m;});
      __webpack_require__.d(__webpack_exports__, "n", function () {return n;});
      var m = 1;
      var n = 2;
    })
});
  • 将模块对象传入一个立即执行的函数
  • 传入文件id,执行__webpack_require__函数
    • 创建一个module,并绑定thismodule.exports,同时传入modulemodule.exports对象
    • __webpack_require__.rmodule.exports对象添加一个Symbol.toStringTag属性,值为{value: 'Module'},使得module.exports调用toString可返回[Object Module]表示一个模块
    • __webpack_require__.d将要导出的变量添加到module.exports,设置getter返回同名变量的值,使得变量更改,外边的引用也会变化
    • 返回module.exports

注意喔:ESM遇到加载命令import时,只生成一个动态的只读引用,在需要调用时才去模块里取值

9.4、require和import的区别

  • CommonJS 模块化方案 require/exports 是为服务器端开发设计的。服务器模块系统同步读取模块文件内容,编译执行后得到模块接口。而ES6 模块化方案 import/export 是为浏览器设计的,浏览器模块系统异步加载脚本文件
  • require/exports 是运行时动态加载,,import/export 是静态编译
  • require/exports 输出的是一个值的拷贝,import/export 模块输出的是值的引用,即文件引用的模块值改变,require 引入的模块值不会改变,而 import 引入的模块值会改变
  • 用法不同
  • ES6 模块可以在 import 引用语句前使用模块,CommonJS 则需要先引用后使用
  • import/export 只能在模块顶层使用,不能在函数、判断语句等代码块之中引用,而require/exports可以
  • import/export 默认采用严格模式
// require/exports
const fs = require('fs')
exports.fs = fs
module.exports = fs

// import/export
import fileSystem, {readFile} from 'fs' // 引入 export default 导出的模块不用加 {},引入非 export default 导出的模块需要加 {}

10、babel

对源码字符串进行 parse,生成 AST,把对代码的修改转为对 AST 的增删改,转换完 AST 之后再打印成目标代码字符串

10.1、babel插件开发的API

10.1.1、parse 阶段

使用@babel/parser 把源码转成 AST

require('@babel/parser').parse(source, {
  sourceType: 'module', // 解析 es module 语法
  plugins: ['jsx'], // 指定jsx 等插件来解析对应的语法
});

10.1.2、transform 阶段

使用 @babel/traverse 遍历 AST,并调用 visitor 函数修改 AST@babel/types 用于创建、判断 AST 节点,提供了 isXassertX 等 api,若批量创建,则可使用@babel/template

require('@babel/traverse').default(ast, {
  // do something
})

10.1.3、generate 阶段

使用@babel/generateAST 打印为目标代码字符串,同时生成 sourcemap@babel/code-frame 用于错误时打印代码位置

const { code,map } = generator(ast, { sourceMaps: true }) 

11、正则

11.1、基础语法

  • 匹配模式
// + 前导字符必须在目标字符串中连续出现1次起
/\d+/

// * ~ 连续0次起
/\d*/

// ? ~ 0、1次
/\d?/

// ^ 定位字符串首个字符,$ 末尾字符
/^\d$/

// () 为一个捕获组,[]匹配单个字符,元素关系为或
/([\s\S]*?)["']/ // 任意字符 + "|'
  • 匹配字符
// \s 匹配空白字符(空格、换行、缩进等)、\S相反
/[\s\S]*/ // 匹配全部内容

// \w 匹配单词([A-Za-z0-9_])、\W相反
/[\w\W]*/ // 匹配全部内容

// \b 匹配不全是\w的位置
/\bnice\b/ // a nice day -> a是显式位置,a和n之间的位置则为隐式位置,即a位置到n位置前则是\b匹配的位置,同样,e位置到d位置前也是\b匹配的,因此可匹配到 nice
/\b.\bnice\b/ // a nice day -> 匹配到 a nice
  • 匹配数字
/[0-9]/  /\d/ // 单个数字
/[0-9]+/ /\d*/ // 多个数字
/[\d]{1,3}/ // 指定个数,1-3个数字

11.2、实例解析

  • 实用http路由表达式
/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)/

拆解匹配

http + (0|1个)s + :// + (0|1个) www. + (1-256个) -|a-z|A-Z|0-9|@|:|%|.|_|+|~|#|= + . + (1-6个) a-z|A-Z|0-9|(|)  + 匹配不全是\w的位置 + (任意个)-|a-z|A-Z|0-9|(|)|@|:|%|_|+|.|~|#|?|&|/|=
展开阅读全文
  • 0
    感动
  • 0
    路过
  • 0
    高兴
  • 0
    难过
  • 0
    搞笑
  • 0
    无聊
  • 0
    愤怒
  • 0
    同情
热度排行
友情链接