关于前端:前端面试比较好的回答

41次阅读

共计 13129 个字符,预计需要花费 33 分钟才能阅读完成。

script 标签中 defer 和 async 的区别

如果没有 defer 或 async 属性,浏览器会立刻加载并执行相应的脚本。它不会期待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。

defer 和 async 属性都是去异步加载内部的 JS 脚本文件,它们都不会阻塞页面的解析,其区别如下:

  • 执行程序: 多个带 async 属性的标签,不能保障加载的程序;多个带 defer 属性的标签,依照加载程序执行;
  • 脚本是否并行执行:async 属性,示意 后续文档的加载和执行与 js 脚本的加载和执行是并行进行的 ,即异步执行;defer 属性,加载后续文档的过程和 js 脚本的加载(此时仅加载不执行) 是并行进行的(异步),js 脚本须要等到文档所有元素解析实现之后才执行,DOMContentLoaded 事件触发执行之前。

CDN 的作用

CDN 个别会用来托管 Web 资源(包含文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。应用 CDN 来减速这些资源的拜访。

(1)在性能方面,引入 CDN 的作用在于:

  • 用户收到的内容来自最近的数据中心,提早更低,内容加载更快
  • 局部资源申请调配给了 CDN,缩小了服务器的负载

(2)在平安方面,CDN 有助于进攻 DDoS、MITM 等网络攻击:

  • 针对 DDoS:通过监控剖析异样流量,限度其申请频率
  • 针对 MITM:从源服务器到 CDN 节点到 ISP(Internet Service Provider),全链路 HTTPS 通信

除此之外,CDN 作为一种根底的云服务,同样具备资源托管、按需扩大(可能应答流量顶峰)等方面的劣势。

对浏览器的了解

浏览器的次要性能是将用户抉择的 web 资源出现进去,它须要从服务器申请资源,并将其显示在浏览器窗口中,资源的格局通常是 HTML,也包含 PDF、image 及其他格局。用户用 URI(Uniform Resource Identifier 对立资源标识符)来指定所申请资源的地位。

HTML 和 CSS 标准中规定了浏览器解释 html 文档的形式,由 W3C 组织对这些标准进行保护,W3C 是负责制订 web 规范的组织。然而浏览器厂商纷纷开发本人的扩大,对标准的遵循并不欠缺,这为 web 开发者带来了重大的兼容性问题。

浏览器能够分为两局部,shell 和 内核。其中 shell 的品种绝对比拟多,内核则比拟少。也有一些浏览器并不辨别外壳和内核。从 Mozilla 将 Gecko 独立进去后,才有了外壳和内核的明确划分。

  • shell 是指浏览器的外壳:例如菜单,工具栏等。次要是提供给用户界面操作,参数设置等等。它是调用内核来实现各种性能的。
  • 内核是浏览器的外围。内核是基于标记语言显示内容的程序或模块。

元素的层叠程序

层叠程序,英文称作 stacking order,示意元素产生层叠时有着特定的垂直显示程序。上面是盒模型的层叠规定:

(1)背景和边框:建设以后层叠上下文元素的背景和边框。
(2)负的 z -index:以后层叠上下文中,z-index 属性值为负的元素。
(3)块级盒:文档流内非行内级非定位后辈元素。
(4)浮动盒:非定位浮动元素。
(5)行内盒:文档流外行内级非定位后辈元素。
(6)z-index:0:层叠级数为 0 的定位元素。
(7)正 z -index:z-index 属性值为正的定位元素。

留神: 当定位元素 z -index:auto,生成盒在以后层叠上下文中的层级为 0,不会建设新的层叠上下文,除非是根元素。

V8 的垃圾回收机制是怎么的

V8 实现了精确式 GC,GC 算法采纳了分代式垃圾回收机制。因而,V8 将内存(堆)分为新生代和老生代两局部。

(1)新生代算法

新生代中的对象个别存活工夫较短,应用 Scavenge GC 算法。

在新生代空间中,内存空间分为两局部,别离为 From 空间和 To 空间。在这两个空间中,必然有一个空间是应用的,另一个空间是闲暇的。新调配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会查看 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制实现后将 From 空间和 To 空间调换,这样 GC 就完结了。

(2)老生代算法

老生代中的对象个别存活工夫较长且数量也多,应用了两个算法,别离是标记革除算法和标记压缩算法。

先来说下什么状况下对象会呈现在老生代空间中:

  • 新生代中的对象是否曾经经验过一次 Scavenge 算法,如果经验过的话,会将对象从新生代空间移到老生代空间中。
  • To 空间的对象占比大小超过 25 %。在这种状况下,为了不影响到内存调配,会将对象从新生代空间移到老生代空间中。

老生代中的空间很简单,有如下几个空间

enum AllocationSpace {// TODO(v8:7464): Actually map this space's memory as read-only.
  RO_SPACE,    // 不变的对象空间
  NEW_SPACE,   // 新生代用于 GC 复制算法的空间
  OLD_SPACE,   // 老生代常驻对象空间
  CODE_SPACE,  // 老生代代码对象空间
  MAP_SPACE,   // 老生代 map 对象
  LO_SPACE,    // 老生代大空间对象
  NEW_LO_SPACE,  // 新生代大空间对象
  FIRST_SPACE = RO_SPACE,
  LAST_SPACE = NEW_LO_SPACE,
  FIRST_GROWABLE_PAGED_SPACE = OLD_SPACE,
  LAST_GROWABLE_PAGED_SPACE = MAP_SPACE
};

在老生代中,以下状况会先启动标记革除算法:

  • 某一个空间没有分块的时候
  • 空间中被对象超过肯定限度
  • 空间不能保障新生代中的对象挪动到老生代中

在这个阶段中,会遍历堆中所有的对象,而后标记活的对象,在标记实现后,销毁所有没有被标记的对象。在标记大型对内存时,可能须要几百毫秒能力实现一次标记。这就会导致一些性能上的问题。为了解决这个问题,2011 年,V8 从 stop-the-world 标记切换到增量标记。在增量标记期间,GC 将标记工作合成为更小的模块,能够让 JS 应用逻辑在模块间隙执行一会,从而不至于让利用呈现进展状况。但在 2018 年,GC 技术又有了一个重大突破,这项技术名为并发标记。该技术能够让 GC 扫描和标记对象时,同时容许 JS 运行。

革除对象后会造成堆内存呈现碎片的状况,当碎片超过肯定限度后会启动压缩算法。在压缩过程中,将活的对象向一端挪动,直到所有对象都挪动实现而后清理掉不须要的内存。

懒加载的概念

懒加载也叫做提早加载、按需加载,指的是在长网页中提早加载图片数据,是一种较好的网页性能优化的形式。在比拟长的网页或利用中,如果图片很多,所有的图片都被加载进去,而用户只能看到可视窗口的那一部分图片数据,这样就节约了性能。

如果应用图片的懒加载就能够解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,缩小了服务器的负载。懒加载实用于图片较多,页面列表较长(长列表)的场景中。

参考 前端进阶面试题具体解答

哪些操作会造成内存透露?

  • 第一种状况是因为应用未声明的变量,而意外的创立了一个全局变量,而使这个变量始终留在内存中无奈被回收。
  • 第二种状况是设置了 setInterval 定时器,而遗记勾销它,如果循环函数有对外部变量的援用的话,那么这个变量会被始终留在内存中,而无奈被回收。
  • 第三种状况是获取一个 DOM 元素的援用,而前面这个元素被删除,因为咱们始终保留了对这个元素的援用,所以它也无奈被回收。
  • 第四种状况是不合理的应用闭包,从而导致某些变量始终被留在内存当中。

对浏览器内核的了解

浏览器内核次要分成两局部:

  • 渲染引擎的职责就是渲染,即在浏览器窗口中显示所申请的内容。默认状况下,渲染引擎能够显示 html、xml 文档及图片,它也能够借助插件显示其余类型数据,例如应用 PDF 阅读器插件,能够显示 PDF 格局。
  • JS 引擎:解析和执行 javascript 来实现网页的动态效果。

最开始渲染引擎和 JS 引擎并没有辨别的很明确,起初 JS 引擎越来越独立,内核就偏向于只指渲染引擎。

let 闭包

let 会产生临时性死区,在以后的执行上下文中,会进行变量晋升,然而未被初始化,所以在执行上下文执行阶段,执行代码如果还没有执行到变量赋值,就援用此变量就会报错,此变量未初始化。

变量晋升

函数在运行的时候,会首先创立执行上下文,而后将执行上下文入栈,而后当此执行上下文处于栈顶时,开始运行执行上下文。

在创立执行上下文的过程中会做三件事:创立变量对象,创立作用域链,确定 this 指向,其中创立变量对象的过程中,首先会为 arguments 创立一个属性,值为 arguments,而后会扫码 function 函数申明,创立一个同名属性,值为函数的援用,接着会扫码 var 变量申明,创立一个同名属性,值为 undefined,这就是变量晋升。

call/apply/bind 的实现

call

形容 :应用 一个指定的 this 值 (默认为 window) 一个或多个参数 来调用一个函数。

语法function.call(thisArg, arg1, arg2, ...)

核心思想

  • 调用 call 的可能不是函数
  • this 可能传入 null
  • 传入不固定个数的参数
  • 给对象绑定函数并调用
  • 删除绑定的函数
  • 函数可能有返回值

实现

Function.prototype.call1 = function(context, ...args) {if(typeof this !== "function") {throw new TypeError("this is not a function");
    }
    context = context || window; // 如果传入的是 null, 则指向 window
    let fn = Symbol('fn');  // 发明惟一的 key 值, 作为结构的 context 外部办法名
    context[fn] = this;  // 为 context 绑定原函数(this)
    let res = context[fn](...args); // 调用原函数并传参, 保留返回值用于 call 返回
    delete context[fn];  // 删除对象中的函数, 不能批改对象
    return res;
}

apply

形容:与 call 相似,惟一的区别就是 call 是传入不固定个数的参数,而 apply 是传入一个参数数组或类数组。

实现

Function.prototype.apply1 = function(context, arr) {if(typeof this !== "function") {throw new TypeError("this is not a function");
    }
    context = context || window; // 如果传入的是 null, 则指向 window
    let fn = Symbol('fn');  // 发明惟一的 key 值,作为结构的 context 外部办法名
    context[fn] = this;  // 为 context 绑定原函数(this)
    let res;
    // 判断是否传入的数组是否为空
    if(!arr) {res = context[fn]();}
    else {res = context[fn](...arr); // 调用原函数并传参, 保留返回值用于 call 返回
    }
    delete context[fn];  // 删除对象中的函数, 不能批改对象
    return res;
}

bind

形容bind 办法会创立一个新的函数,在 bind() 被调用时,这个新函数的 this 被指定为 bind() 的第一个参数,而其余参数将作为新函数的参数,供调用时应用。

核心思想:

  • 调用 bind 的可能不是函数
  • bind() 除了 this 外,还可传入多个参数
  • bind() 创立的新函数可能传入多个参数
  • 新函数可能被当做结构函数调用
  • 函数可能有返回值

实现

Function.prototype.bind1 = function(context, ...args) {if (typeof that !== "function") {throw new TypeError("this is not function");
    }
    let that = this;  // 保留原函数(this)return function F(...innerArgs) {
        // 判断是否是 new 构造函数
        // 因为这里是调用的 call 办法,因而不须要判断 context 是否为空
        return that.call(this instanceof F ? this : context, ...args, ...innerArgs);
    }
}

new 实现

形容new 运算符用来创立用户自定义的对象类型的实例或者具备构造函数的内置对象的实例。

核心思想:

  • new 会产生一个新对象
  • 新对象须要可能拜访到构造函数的属性,所以须要从新指定它的原型
  • 构造函数可能会显示返回对象与根本类型的状况(以及 null)

步骤 :应用new 命令时,它前面的函数顺次执行上面的步骤:

  1. 创立一个空对象,作为将要返回的对象实例。
  2. 将这个空对象的隐式原型 (__proto__),指向构造函数的prototype 属性。
  3. 让函数外部的 this 关键字指向这个对象。开始执行构造函数外部的代码(为这个新对象增加属性)。
  4. 判断函数的返回值类型,如果是值类型,返回创立的对象。如果是援用类型,就返回这个援用类型的对象。

实现

// 写法一:function myNew() {
    // 将 arguments 对象转为数组
    let args = [].slice.call(arguments);
    // 取出构造函数
    let constructor = args.shift();

    // 创立一个空对象,继承构造函数的 prototype 属性
    let obj = {};
    obj.__proto__ = constructor.prototype;

    // 执行构造函数并将 this 绑定到新创建的对象上
    let res = constructor.call(obj, ...args);
    // let res = constructor.apply(obj, args);

    // 判断构造函数执行返回的后果。如果返回后果是援用类型,就间接返回,否则返回 obj 对象
    return (typeof res === "object" && res !== null) ? res : obj;
}

// 写法二:constructor:构造函数,...args:结构函数参数
function myNew(constructor, ...args) {
    // 生成一个空对象, 继承构造函数的 prototype 属性
    let obj = Object.create(constructor.prototype);

    // 执行构造函数并将 this 绑定到新创建的对象上
    let res = constructor.call(obj, ...args);
    // let res = constructor.apply(obj, args);

    // 判断构造函数执行返回的后果。如果返回后果是援用类型,就间接返回,否则返回 obj 对象
    return (typeof res === "object" && res !== null) ? res : obj;
}

Cookie、LocalStorage、SessionStorage 区别

浏览器端罕用的存储技术是 cookie、localStorage 和 sessionStorage。

  • cookie: 其实最开始是服务器端用于记录用户状态的一种形式,由服务器设置,在客户端存储,而后每次发动同源申请时,发送给服务器端。cookie 最多能存储 4 k 数据,它的生存工夫由 expires 属性指定,并且 cookie 只能被同源的页面访问共享。
  • sessionStorage: html5 提供的一种浏览器本地存储的办法,它借鉴了服务器端 session 的概念,代表的是一次会话中所保留的数据。它个别可能存储 5M 或者更大的数据,它在以后窗口敞开后就生效了,并且 sessionStorage 只能被同一个窗口的同源页面所访问共享。
  • localStorage: html5 提供的一种浏览器本地存储的办法,它个别也可能存储 5M 或者更大的数据。它和 sessionStorage 不同的是,除非手动删除它,否则它不会生效,并且 localStorage 也只能被同源页面所访问共享。

下面几种形式都是存储大量数据的时候的存储形式,当须要在本地存储大量数据的时候,咱们能够应用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它外部采纳对象仓库的模式存储数据,它更靠近 NoSQL 数据库。

快排 – 工夫复杂度 nlogn~ n^2 之间

题目形容: 实现一个快排

实现代码如下:

function quickSort(arr) {if (arr.length < 2) {return arr;}
  const cur = arr[arr.length - 1];
  const left = arr.filter((v, i) => v <= cur && i !== arr.length - 1);
  const right = arr.filter((v) => v > cur);
  return [...quickSort(left), cur, ...quickSort(right)];
}
// console.log(quickSort([3, 6, 2, 4, 1]));

插入排序 – 工夫复杂度 n^2

题目形容: 实现一个插入排序

实现代码如下:

function insertSort(arr) {for (let i = 1; i < arr.length; i++) {
    let j = i;
    let target = arr[j];
    while (j > 0 && arr[j - 1] > target) {arr[j] = arr[j - 1];
      j--;
    }
    arr[j] = target;
  }
  return arr;
}
// console.log(insertSort([3, 6, 2, 4, 1]));

深拷贝(思考到复制 Symbol 类型)

题目形容: 手写 new 操作符实现

实现代码如下:

function isObject(val) {return typeof val === "object" && val !== null;}

function deepClone(obj, hash = new WeakMap()) {if (!isObject(obj)) return obj;
  if (hash.has(obj)) {return hash.get(obj);
  }
  let target = Array.isArray(obj) ? [] : {};
  hash.set(obj, target);
  Reflect.ownKeys(obj).forEach((item) => {if (isObject(obj[item])) {target[item] = deepClone(obj[item], hash);
    } else {target[item] = obj[item];
    }
  });

  return target;
}

// var obj1 = {
// a:1,
// b:{a:2}
// };
// var obj2 = deepClone(obj1);
// console.log(obj1);

JS 隐式转换,显示转换

个别非根底类型进行转换时会先调用 valueOf,如果 valueOf 无奈返回根本类型值,就会调用 toString

字符串和数字

  • “+” 操作符,如果有一个为字符串,那么都转化到字符串而后执行字符串拼接
  • “-” 操作符,转换为数字,相减 (-a, a * 1 a/1) 都能进行隐式强制类型转换
[] + {} 和 {} + []

布尔值到数字

  • 1 + true = 2
  • 1 + false = 1

转换为布尔值

  • for 中第二个
  • while
  • if
  • 三元表达式
  • ||(逻辑或)&&(逻辑与)右边的操作数

符号

  • 不能被转换为数字
  • 能被转换为布尔值(都是 true)
  • 能够被转换成字符串 “Symbol(cool)”

宽松相等和严格相等

宽松相等容许进行强制类型转换,而严格相等不容许

字符串与数字

转换为数字而后比拟

其余类型与布尔类型

  • 先把布尔类型转换为数字,而后持续进行比拟

对象与非对象

  • 执行对象的 ToPrimitive(对象)而后持续进行比拟

假值列表

  • undefined
  • null
  • false
  • +0, -0, NaN
  • “”

具体阐明 Event loop

家喻户晓 JS 是门非阻塞单线程语言,因为在最后 JS 就是为了和浏览器交互而诞生的。如果 JS 是门多线程的语言话,咱们在多个线程中解决 DOM 就可能会产生问题(一个线程中新加节点,另一个线程中删除节点),当然能够引入读写锁解决这个问题。

JS 在执行的过程中会产生执行环境,这些执行环境会被程序的退出到执行栈中。如果遇到异步的代码,会被挂起并退出到 Task(有多种 task)队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出须要执行的代码并放入执行栈中执行,所以实质上来说 JS 中的异步还是同步行为。

console.log('script start');

setTimeout(function() {console.log('setTimeout');
}, 0);

console.log('script end');

以上代码尽管 setTimeout 延时为 0,其实还是异步。这是因为 HTML5 标准规定这个函数第二个参数不得小于 4 毫秒,有余会主动减少。所以 setTimeout 还是会在 script end 之后打印。

不同的工作源会被调配到不同的 Task 队列中,工作源能够分为 微工作(microtask)和 宏工作(macrotask)。在 ES6 标准中,microtask 称为 jobs,macrotask 称为 task

console.log('script start');

setTimeout(function() {console.log('setTimeout');
}, 0);

new Promise((resolve) => {console.log('Promise')
    resolve()}).then(function() {console.log('promise1');
}).then(function() {console.log('promise2');
});

console.log('script end');
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码尽管 setTimeout 写在 Promise 之前,然而因为 Promise 属于微工作而 setTimeout 属于宏工作,所以会有以上的打印。

微工作包含 process.nextTickpromiseObject.observeMutationObserver

宏工作包含 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

很多人有个误区,认为微工作快于宏工作,其实是谬误的。因为宏工作中包含了 script,浏览器会先执行一个宏工作,接下来有异步代码的话就先执行微工作。

所以正确的一次 Event loop 程序是这样的

  1. 执行同步代码,这属于宏工作
  2. 执行栈为空,查问是否有微工作须要执行
  3. 执行所有微工作
  4. 必要的话渲染 UI
  5. 而后开始下一轮 Event loop,执行宏工作中的异步代码

通过上述的 Event loop 程序可知,如果宏工作中的异步代码有大量的计算并且须要操作 DOM 的话,为了更快的 界面响应,咱们能够把操作 DOM 放入微工作中。

Node 中的 Event loop

Node 中的 Event loop 和浏览器中的不雷同。

Node 的 Event loop 分为 6 个阶段,它们会依照程序重复运行

┌───────────────────────┐
┌─>│        timers         │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     I/O callbacks     │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │     idle, prepare     │
│  └──────────┬────────────┘      ┌───────────────┐
│  ┌──────────┴────────────┐      │   incoming:   │
│  │         poll          │<──connections───     │
│  └──────────┬────────────┘      │   data, etc.  │
│  ┌──────────┴────────────┐      └───────────────┘
│  │        check          │
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└──┤    close callbacks    │
   └───────────────────────┘
timer

timers 阶段会执行 setTimeoutsetInterval

一个 timer 指定的工夫并不是精确工夫,而是在达到这个工夫后尽快执行回调,可能会因为零碎正在执行别的事务而提早。

上限的工夫有一个范畴:[1, 2147483647],如果设定的工夫不在这个范畴,将被设置为 1。

I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段外部实现

poll

poll 阶段很重要,这一阶段中,零碎会做两件事件

  1. 执行到点的定时器
  2. 执行 poll 队列中的事件

并且当 poll 中没有定时器的状况下,会发现以下两件事件

  • 如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者零碎限度
  • 如果 poll 队列为空,会有两件事产生

    • 如果有 setImmediate 须要执行,poll 阶段会进行并且进入到 check 阶段执行 setImmediate
    • 如果没有 setImmediate 须要执行,会期待回调被退出到队列中并立刻执行回调

如果有别的定时器须要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

并且在 Node 中,有些状况下的定时器执行程序是随机的

setTimeout(() => {console.log('setTimeout');
}, 0);
setImmediate(() => {console.log('setImmediate');
})
// 这里可能会输入 setTimeout,setImmediate
// 可能也会相同的输入,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

当然在这种状况下,执行程序是雷同的

var fs = require('fs')

fs.readFile(__filename, () => {setTimeout(() => {console.log('timeout');
    }, 0);
    setImmediate(() => {console.log('immediate');
    });
});
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate,所以会立刻跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输入肯定是 setImmediate,setTimeout

下面介绍的都是 macrotask 的执行状况,microtask 会在以上每个阶段实现后立刻执行。

setTimeout(()=>{console.log('timer1')

    Promise.resolve().then(function() {console.log('promise1')
    })
}, 0)

setTimeout(()=>{console.log('timer2')

    Promise.resolve().then(function() {console.log('promise2')
    })
}, 0)

// 以上代码在浏览器和 node 中打印状况是不同的
// 浏览器中打印 timer1, promise1, timer2, promise2
// node 中打印 timer1, timer2, promise1, promise2

Node 中的 process.nextTick 会先于其余 microtask 执行。

setTimeout(() => {console.log("timer1");

  Promise.resolve().then(function() {console.log("promise1");
  });
}, 0);

process.nextTick(() => {console.log("nextTick");
});
// nextTick, timer1, promise1

请实现 DOM2JSON 一个函数,能够把一个 DOM 节点输入 JSON 的格局

题目形容:

<div>
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

把上诉 dom 构造转成上面的 JSON 格局

{
  tag: 'DIV',
  children: [
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] },
        {tag: 'A', children: [] }
      ]
    }
  ]
}

实现代码如下:

function dom2Json(domtree) {let obj = {};
  obj.name = domtree.tagName;
  obj.children = [];
  domtree.childNodes.forEach((child) => obj.children.push(dom2Json(child)));
  return obj;
}

扩大思考: 如果给定的不是一个 Dom 树结构 而是一段 html 字符串 该如何解析?

那么这个问题就相似 Vue 的模板编译原理 咱们能够利用正则 匹配 html 字符串 遇到开始标签 完结标签和文本 解析结束之后生成对应的 ast 并建设相应的父子关联 一直的 advance 截取残余的字符串 直到 html 全副解析结束

代码输入后果

function a() {console.log(this);
}
a.call(null);

打印后果:window 对象

依据 ECMAScript262 标准规定:如果第一个参数传入的对象调用者是 null 或者 undefined,call 办法将把全局对象(浏览器上是 window 对象)作为 this 的值。所以,不论传入 null 还是 undefined,其 this 都是全局对象 window。所以,在浏览器上答案是输入 window 对象。

要留神的是,在严格模式中,null 就是 null,undefined 就是 undefined:

'use strict';

function a() {console.log(this);
}
a.call(null); // null
a.call(undefined); // undefined

将虚构 Dom 转化为实在 Dom

题目形容:JSON 格局的虚构 Dom 怎么转换成实在 Dom

{
  tag: 'DIV',
  attrs:{id:'app'},
  children: [
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] }
      ]
    },
    {
      tag: 'SPAN',
      children: [{ tag: 'A', children: [] },
        {tag: 'A', children: [] }
      ]
    }
  ]
}
把上诉虚构 Dom 转化成下方实在 Dom
<div id="app">
  <span>
    <a></a>
  </span>
  <span>
    <a></a>
    <a></a>
  </span>
</div>

实现代码如下:

// 真正的渲染函数
function _render(vnode) {
  // 如果是数字类型转化为字符串
  if (typeof vnode === "number") {vnode = String(vnode);
  }
  // 字符串类型间接就是文本节点
  if (typeof vnode === "string") {return document.createTextNode(vnode);
  }
  // 一般 DOM
  const dom = document.createElement(vnode.tag);
  if (vnode.attrs) {
    // 遍历属性
    Object.keys(vnode.attrs).forEach((key) => {const value = vnode.attrs[key];
      dom.setAttribute(key, value);
    });
  }
  // 子数组进行递归操作
  vnode.children.forEach((child) => dom.appendChild(_render(child)));
  return dom;
}

正文完
 0