如果没有defer或async属性,浏览器会立即加载并执行相应的脚本。它不会等待后续加载的文档元素,读取到就会开始加载和执行,这样就阻塞了后续文档的加载。
defer 和 async属性都是去异步加载外部的JS脚本文件,它们都不会阻塞页面的解析,其区别如下:
CDN一般会用来托管Web资源(包括文本、图片和脚本等),可供下载的资源(媒体文件、软件、文档等),应用程序(门户网站等)。使用CDN来加速这些资源的访问。
(1)在性能方面,引入CDN的作用在于:
(2)在安全方面,CDN有助于防御DDoS、MITM等网络攻击:
除此之外,CDN作为一种基础的云服务,同样具有资源托管、按需扩展(能够应对流量高峰)等方面的优势。
浏览器的主要功能是将用户选择的 web 资源呈现出来,它需要从服务器请求资源,并将其显示在浏览器窗口中,资源的格式通常是 HTML,也包括 PDF、image 及其他格式。用户用 URI(Uniform Resource Identifier 统一资源标识符)来指定所请求资源的位置。
HTML 和 CSS 规范中规定了浏览器解释 html 文档的方式,由 W3C 组织对这些规范进行维护,W3C 是负责制定 web 标准的组织。但是浏览器厂商纷纷开发自己的扩展,对规范的遵循并不完善,这为 web 开发者带来了严重的兼容性问题。
浏览器可以分为两部分,shell 和 内核。其中 shell 的种类相对比较多,内核则比较少。也有一些浏览器并不区分外壳和内核。从 Mozilla 将 Gecko 独立出来后,才有了外壳和内核的明确划分。
层叠顺序,英文称作 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 实现了准确式 GC,GC 算法采用了分代式垃圾回收机制。因此,V8 将内存(堆)分为新生代和老生代两部分。
(1)新生代算法
新生代中的对象一般存活时间较短,使用 Scavenge GC 算法。
在新生代空间中,内存空间分为两部分,分别为 From 空间和 To 空间。在这两个空间中,必定有一个空间是使用的,另一个空间是空闲的。新分配的对象会被放入 From 空间中,当 From 空间被占满时,新生代 GC 就会启动了。算法会检查 From 空间中存活的对象并复制到 To 空间中,如果有失活的对象就会销毁。当复制完成后将 From 空间和 To 空间互换,这样 GC 就结束了。
(2)老生代算法
老生代中的对象一般存活时间较长且数量也多,使用了两个算法,分别是标记清除算法和标记压缩算法。
先来说下什么情况下对象会出现在老生代空间中:
老生代中的空间很复杂,有如下几个空间
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 运行。
清除对象后会造成堆内存出现碎片的情况,当碎片超过一定限制后会启动压缩算法。在压缩过程中,将活的对象向一端移动,直到所有对象都移动完成然后清理掉不需要的内存。
懒加载也叫做延迟加载、按需加载,指的是在长网页中延迟加载图片数据,是一种较好的网页性能优化的方式。在比较长的网页或应用中,如果图片很多,所有的图片都被加载出来,而用户只能看到可视窗口的那一部分图片数据,这样就浪费了性能。
如果使用图片的懒加载就可以解决以上问题。在滚动屏幕之前,可视化区域之外的图片不会进行加载,在滚动屏幕时才加载。这样使得网页的加载速度更快,减少了服务器的负载。懒加载适用于图片较多,页面列表较长(长列表)的场景中。
浏览器内核主要分成两部分:
最开始渲染引擎和 JS 引擎并没有区分的很明确,后来 JS 引擎越来越独立,内核就倾向于只指渲染引擎。
let 会产生临时性死区,在当前的执行上下文中,会进行变量提升,但是未被初始化,所以在执行上下文执行阶段,执行代码如果还没有执行到变量赋值,就引用此变量就会报错,此变量未初始化。
函数在运行的时候,会首先创建执行上下文,然后将执行上下文入栈,然后当此执行上下文处于栈顶时,开始运行执行上下文。
在创建执行上下文的过程中会做三件事:创建变量对象,创建作用域链,确定 this 指向,其中创建变量对象的过程中,首先会为 arguments 创建一个属性,值为 arguments,然后会扫码 function 函数声明,创建一个同名属性,值为函数的引用,接着会扫码 var 变量声明,创建一个同名属性,值为 undefined,这就是变量提升。
描述:使用 一个指定的 this
值(默认为 window
) 和 一个或多个参数 来调用一个函数。
语法:function.call(thisArg, arg1, arg2, ...)
核心思想:
实现:
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;
}
描述:与 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()
被调用时,这个新函数的 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
命令时,它后面的函数依次执行下面的步骤:
__proto__
),指向构造函数的prototype
属性。this
关键字指向这个对象。开始执行构造函数内部的代码(为这个新对象添加属性)。实现:
// 写法一:
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。
上面几种方式都是存储少量数据的时候的存储方式,当需要在本地存储大量数据的时候,我们可以使用浏览器的 indexDB 这是浏览器提供的一种本地的数据库存储机制。它不是关系型数据库,它内部采用对象仓库的形式存储数据,它更接近 NoSQL 数据库。
题目描述:实现一个快排
实现代码如下:
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]));
题目描述:实现一个插入排序
实现代码如下:
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]));
题目描述:手写 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);
一般非基础类型进行转换时会先调用 valueOf,如果 valueOf 无法返回基本类型值,就会调用 toString
字符串和数字
[] + {} 和 {} + []
布尔值到数字
转换为布尔值
符号
宽松相等和严格相等
宽松相等允许进行强制类型转换,而严格相等不允许
字符串与数字
转换为数字然后比较
其他类型与布尔类型
对象与非对象
假值列表
众所周知 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.nextTick
,promise
,Object.observe
,MutationObserver
宏任务包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script
,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。
所以正确的一次 Event loop 顺序是这样的
通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。
Node 中的 Event loop 和浏览器中的不相同。
Node 的 Event loop 分为6个阶段,它们会按照顺序反复运行
┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<──connections─── │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘
timers 阶段会执行 setTimeout
和 setInterval
一个 timer
指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。
下限的时间有一个范围:[1, 2147483647]
,如果设定的时间不在这个范围,将被设置为1。
I/O 阶段会执行除了 close 事件,定时器和 setImmediate
的回调
idle, prepare 阶段内部实现
poll 阶段很重要,这一阶段中,系统会做两件事情
并且当 poll 中没有定时器的情况下,会发现以下两件事情
setImmediate
需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
setImmediate
需要执行,会等待回调被加入到队列中并立即执行回调如果有别的定时器需要被执行,会回到 timer 阶段执行回调。
check 阶段执行 setImmediate
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
题目描述:
<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
题目描述: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;
}
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。