问题先导
- 说一下webworkers【html】
- 对盒模型的理解【css】
- 使用translate和position来定位元素有什么区别?【css】
- 操作符
|
、||
、&
和&&
之间的区别?【js操作符】 -
==
、===
和Object.is()
之间的区别?【js操作符】 - 什么是JavaScript的包装类型?【js数据类型】
- JavaScript的隐式转换逻辑?【js数据类型】
- Vue实例中的过滤器是什么?如何使用?【Vue】
- Vue如何保存当前页面状态?【Vue】
- Vue中常见的事件修饰符及其作用【Vue】
- 手写
new
操作符【手写代码】 - 手写
Promise
对象【手写代码】 - 输出结果【Promise相关】
- 剑指 Offer 22. 链表中倒数第k个节点【算法】
- 反转链表【算法】
知识梳理
说一下webworks
Web Worker为Web内容在后台线程中运行脚本提供了一种简单的方法。线程可以执行任务而不干扰用户界面,即web worker
是后台异步执行的,不会占用主线用资源,因为worker有自己的线程。
在worker线程中你可以运行任何你喜欢的代码,不过有一些例外情况。比如:在worker内,不能直接操作DOM节点,也不能使用window
法和属性。然而你可以使用大量window对象之下的东西,包括WebSockets,IndexedDB以及FireFox OS专用的Data Store API等数据存储机制。查看Functions and classes available to workers获取详情。
workers和主线程间的数据传递通过这样的消息机制进行——双方都使用postMessage()方法发送各自的消息,使用onmessage事件处理函数来响应消息(消息被包含在Message
事件的data属性中)。这个过程中数据并不是被共享而是被复制。
只要运行在同源的父页面中,workers可以依次生成新的workers;并且可以使用XMLHttpRequest
est的responseXML和channel属性总会返回null。
在主页面与 worker 之间传递的数据是通过拷贝,而不是共享来完成的。传递给 worker
的对象需要经过序列化,接下来在另一端还需要反序列化。页面与worker
**不会共享同一个实例,最终的结果就是在每次通信结束时生成了数据的一个副本。**大部分浏览器使用结构化拷贝来实现该特性。
参考:
对盒模型的理解
所有的html元素都可以看做一个盒子,在css中,盒模型这一术语用于设计和布局时使用。CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:边距(margin),边框(border),填充(padding),和实际内容(content)四部分组成。
除了标准盒模型,还有一些怪异盒模型,比如IE盒模型,区别在于width
和height
设置时应用的范围不同,标准和模型的width
和height
只会应用内容(content)区域,而怪异盒模型会包括除了外边距之外的三个部分作为宽高值。
使用translate和position来定位元素有什么区别?
translate
是transform
的一个属性值,改变transform
或opacity
之类的属性不会触发浏览器回流(reflow),因为translate
没有改变元素的文档布局,而是通过GPU创建了一个图层进行渲染,而定位本身就是在改变元素的文档布局,并使其脱离文档流,造成回流和重绘,而且不像translate
那样能利用GPU进行硬件加速,因此,对于动画效果来说,translate
来改变元素定位是要优于position
的。
操作符|
、||
、&
和&&
之间的区别?
首先|
和&
在js中既可以作为位运算符,也可以作为逻辑运算符。当作为逻辑运算符时,|
和||
都表示逻辑或,&
和&&
都表示逻辑与,但||
和&&
具有短路的特性,即A || B
只要A为true
就不会再判断B
,而直接返回true
,同理,A && B
表达式当A
为false
就会直接返回false
,无需判断B
表达式的值。
==
、===
和Object.is()
之间的区别?
==
在进行数据比较时,会先转换为相同的数据类型再比较,这就是js的隐式数据类型转换。
而===
是全等比较,比较时会直接比较而不进行数据转换,也更为严谨。
而Object.is
和===
相差不大,数据比较时同样不会进行类型转换,但Object.is
比===
更为严谨,增加了一些特殊比较:比如Object.is(0, -0)
返回false,Object.is(NaN, NaN)
返回true,而全等比较正好相反。
什么是 JavaScript 中的包装类型?
在 JavaScript 中,基本类型是没有属性和方法的,但是为了便于操作基本类型的值,在调用基本类型的属性或方法时 JavaScript 会在后台隐式地将基本类型的值转换为对象,如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
复制代码
而这些属性和方法都是String
对象中的,这样,普通的数据类型就有了和对象一样的功能和特性。
js中的隐式转换逻辑
当遇到一些计算和比较运算符时,js就会触发隐式转换,将数据转换为相同的数据类型再进行运算。比如+、-、*、/
以及==、>、<
这些运算符。
对于基本数据类型,undefined
和null
可以转为字符串、数字或布尔值,分别为:”undefined”和”null”、0、false。而字符串、数字、布尔之间也可以相关转换,具体转换逻辑需要看两种数据类型分别是什么。
而对于引用类型,在进行运算操作时,会隐式执行toPrimitive
操作,转换为基本数据类型。在js对象中有这么一个隐式方法:[Symbol.toprimitive]。
我们可以在定义对象时定义这个函数,比如:
var obj = {
[Symbol.toPrimitive](hint) {
if (hint == "number") {
return 10;
}
if (hint == "string") {
return "hello";
}
return true;
}
};
console.log(+obj2); // 10 -- hint 参数值是 "number"
console.log(`${obj2}`); // "hello" -- hint 参数值是 "string"
console.log(obj2 + ""); // "true" -- hint 参数值是 "default"
复制代码
在对象进行运算操作时就会自动识别hint
类型并返回相应基本数据类型值。
如果不定义这个函数,那么会遵照以下原则:
hint
为number
时,先调用对象的valueOf()
,无值再调用toString()
函数。hint
为string
时,反过来,先调用对象的toString()
,无值再调用valueOf()
函数。
默认情况下,hint
都是number
类型,除了Date
对象,默认类型为string
。
Vue实例中的过滤器作用是什么?如何使用?
滤器顾名思义,就是用来过滤数据的。当我们展示数据时,可能希望数据进过过滤之后再展示出来,或者进过某些特殊操作再展示出来,必然展示数字数组时只展示整数,展示人名时首字母大写再展示。
实际上,过滤器同methods
、computed
类似,都是改变原数据的一种方法。
<input v-on:keyup="items | filteredItems(10)">
复制代码
实际上,该过滤器可以换成计算属性,具有同样的效果:
computed: {
filteredItems: function (size) {
return this.items.slice(0, size)
}
}
复制代码
更多使用细节请参考:过滤器 – Vue
Vue中如何保存页面的当前的状态
既然要保持页面的状态,也就是维护组件的状态,让其不被页面更新而变化,即页面恢复时能同时恢复组件之前的状态。
保持组件的状态需要分两种情况:
- 组件会被卸载
- 组件不会被卸载
如果组件会被卸载,那么组件的相关数据就需要缓存起来,一般有两种方法:
-
将状态数据存储在
LocalStorage
或SessionStorage
只需要在组件即将被销毁的生命周期
componentWillUnmount
(react)中在 LocalStorage / SessionStorage 中把当前组件的 state 通过 JSON.stringify() 储存下来就可以了。在这里面需要注意的是组件更新状态的时机。比如从 B 组件跳转到 A 组件的时候,A 组件需要更新自身的状态。但是如果从别的组件跳转到 B 组件的时候,实际上是希望 B 组件重新渲染的,也就是不要从 Storage 中读取信息。所以需要在 Storage 中的状态加入一个 flag 属性,用来控制 A 组件是否读取 Storage 中的状态。
优点:兼容性好,不需要额外库或工具。
缺点:数据存储需要拷贝,如果数据中依赖有特殊对象,拷贝可能会丢失相关数据;读取flag并不好控制,当页面直接打开时可能并不需要读取组件的缓存状态。
-
路由传值
通过 react-router 的 Link 组件的 prop —— to 可以实现路由间传递参数的效果。
在这里需要用到 state 参数,在 B 组件中通过 history.location.state 就可以拿到 state 值,保存它。返回 A 组件时再次携带 state 达到路由状态保持的效果。
优点:简单快捷,不需要控制标记
flag
。缺点:如果组件可以跳转多个组件,那么每个跳转的组件都要写相同处理逻辑。
如果组件不会被卸载,事情就变得简单一些,一般来说,同样有两种方案:
-
切换的组件作为子组件全屏渲染,父组件中正常储存页面状态。
-
在Vue中,还可以是用keep-alive组件来缓存页面,当组件在keep-alive内被切换时组件的activated、deactivated这两个生命周期钩子函数会被执行 被包裹在keep-alive中的组件的状态将会被保留。
<keep-alive> <router-view v-if="$route.meta.keepAlive"></router-view> </kepp-alive> 复制代码
Vue中常见的事件修饰符及其作用
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
以下是Vue提供的一些事件修饰符:
.stop
:阻止事件冒泡,相当于event.stopPropagation()
.prevent
:取消预设行为,相当于event.preventDefault()
.capture
:启用事件捕获(与冒泡相反,从外到内进行事件捕获).self
:只有当前元素才能出发,即子元素无法触发.once
:事件只执行一次.passive
:与.prevent
的作用相反,即不阻止默认行为,.passive
修饰符尤其能够提升移动端的性能。其实这等同于设置addEventListener的第三个属性passive
。
<!-- 阻止单击事件继续传播 -->
<a v-on:click.stop="doThis"></a>
<!-- 提交事件不再重载页面 -->
<form v-on:submit.prevent="onSubmit"></form>
<!-- 修饰符可以串联 -->
<a v-on:click.stop.prevent="doThat"></a>
<!-- 只有修饰符 -->
<form v-on:submit.prevent></form>
<!-- 添加事件监听器时使用事件捕获模式 -->
<!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 -->
<div v-on:click.capture="doThis">...</div>
<!-- 只当在 event.target 是当前元素自身时触发处理函数 -->
<!-- 即事件不是从内部元素触发的 -->
<div v-on:click.self="doThat">...</div>
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 -->
<!-- 而不会等待 `onScroll` 完成 -->
<!-- 这其中包含 `event.preventDefault()` 的情况 -->
<div v-on:scroll.passive="onScroll">...</div>
复制代码
手写new操作符
new
运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。
new
关键字会进行如下的操作:
- 创建一个空的简单JavaScript对象(即
{}
); - 设置该对象的constructor,链接该对象到构造函数的
prototype
对象; - 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
function objectFactory(constructor, args) {
// 判断参数是否是一个函数
if (typeof constructor !== "function") {
console.error("type error");
return;
}
// 1.创建一个对象,并将对象的原型设置为构造函数的 prototype
const newObject = Object.create(constructor.prototype);
// 2.将 this 指向新建对象,并初始化函数
const result = constructor.apply(newObject, args);
return result && (typeof result === "object" || typeof result === "function") ? result : newObject;
}
复制代码
参考:
手写Promise
关于Promise
的介绍可参考:Promise – MDN。
Promise
对象用于表示一个异步操作的最终完成 (或失败)及其结果值。有三种状态:
- padding:待定。初始状态,表示既未成功也未失败。
- fulfilled:已完成(已兑现)。回调成功的状态。回调函数为微任务。
- rejected:已失败(已拒绝)。回调失败的状态。回调函数为微任务。
Promise
的设计初衷为解决回调地狱的问题,即Promise.then
函数的实现,因此,最基本的Promise
应该有以下属性和功能:
- state:回调的状态值
- data:回调成功的返回值
- err:回调失败的返回值
- then:回调状态更新后的链式调用函数
涉及到的细节为:
- 如何切换回调状态;何时切换
- 成功回调如何触发
- 失败回调如何触发
- 微任务的执行如何模拟
// Promise的三种状态
const PromiseState = {
PENDING: 'pending',
FULFILLED: 'fulfilled',
REJECTED: 'rejected'
}
function MyPromise(executor) {
const THIS = this;
// 初始化构造函数
_defineProperty(THIS, 'state', PromiseState.PENDING); // 状态
_defineProperty(THIS, 'date', undefined); // 成功结果
_defineProperty(THIS, 'error', undefined); // 成功结果
_defineProperty(THIS, 'then', thenFun); // then
_defineProperty(THIS, 'fulfilledCalls', []); // 成功回调
_defineProperty(THIS, 'rejectedCalls', []); // 失败回调
// 执行宏任务
try {
executor(resolve, reject);
} catch (err) {
reject(err);
}
/**
* 成功回调
* @param {*} value
*/
function resolve(value) {
updateState(PromiseState.FULFILLED, value);
}
/**
* 失败回调
* @param {*} err
*/
function reject(err) {
updateState(PromiseState.REJECTED, err);
}
/**
* 更新状态
* @param {*} state 状态
* @param {*} res 处理结果
*/
function updateState(state, res) {
if(THIS.state === PromiseState.PENDING) {
THIS.state = state;
if(THIS.state = PromiseState.FULFILLED) {
THIS.data = res;
setTimeout(() => {
THIS.fulfilledCalls.forEach(fun => {
fun.call(THIS, THIS.data);
});
THIS.fulfilledCalls.length= 0;
THIS.rejectedCalls.length= 0;
}, 0);
} else if(THIS.state = PromiseState.reject) {
setTimeout(() => {
THIS.rejectedCalls.forEach(fun => {
fun.call(THIS, THIS.err);
});
THIS.rejectedCalls.length= 0;
THIS.fulfilledCalls.length= 0;
});
};
}
}
/**
* 链式调用
* @param {Function} onFulfilled
* @param {Function} onRejected
*/
function thenFun(onFulfilled, onRejected) {
if(THIS.state === PromiseState.FULFILLED) {
setTimeout(() => {
onFulfilled && onFulfilled.call(THIS, THIS.data);
}, 0);
} else if(THIS.state === PromiseState.REJECTED) {
setTimeout(() => {
onRejected && onRejected.call(THIS, THIS.error);
}, 0);
} else {
onFulfilled && THIS.fulfilledCalls.push(onFulfilled);
onRejected && THIS.rejectedCalls.push(onRejected);
}
return THIS;
}
}
function _defineProperty(obj, key, value) {
if (key in obj) {
// 正常情况下,对象直接赋值和使用Object.defineProperty是一样效果,
// 但如果某属性本身就存在于该对象或其原型链上,切属性是可以配置的,
// 那么就可以更新属性的配置如enumerable、writable
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
// 测试
const p = new MyPromise((resolve, reject) => {
console.log('Promise runing...');
setTimeout(() => {
console.log('fulfilled!');
resolve('1');
}, 100);
});
p.then((data) => {
console.log('2', data);
})
p.then((data) => {
console.log('3', data);
});
console.log('4')
复制代码
注意:这里的Promise.then
实现其实是有问题的,按照完整的写法,Promise.then
的回调返回值将作为一个新的Promise
返回,如果回调函数没有返回值,那么将返回一个padding
状态的Promise
,这里我们直接返回了当前Promise
是不完整的。完整的Promise.then
写法将在后续文章中给出。
此外,如果Promise.then
的参数不是一个函数,那么直接将当前Promise
作为返回值。
总结一下:
- 状态切换:其实是用户触发的,当然也只有用户知道何时成功,何时失败,即用户手动调用
resolve
或者reject
函数才能更新Promise
状态,并且Promise
状态一旦更新完成就无法再更新。 - 微任务的模拟:使用
setTimeout
来模拟即可 then
的多次调用:当then
函数多次调用时,可能Promise的状态还未改变,这个时候我们就需要将回调函数缓存起来,待状态发生变化后再调用。还有一个细节,调用完成后我们将回调函数缓存清空了,这是为了释放内存,因为状态切换只会执行一次,没必要保留缓存,避免内存无法回收,因此主动释放掉。
输出结果
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log("timerStart");
resolve("success");
console.log("timerEnd");
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
复制代码
输出结果:
1
2
4
timerStart
timerEnd
success
复制代码
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2')
}, 0)
});
const timer1 = setTimeout(() => {
console.log('timer1')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('start');
复制代码
输出结果:
- Promise.resolve()返回一个
fulfilled
状态的Promise
,后续的代码为微任务,加入微任务队列 - timer1是宏任务,加入宏任务队列
- 打印start,第一轮宏任务结束
- 开始微任务执行,打印promise1,timer2宏任务进入宏任务队列,微任务结束
- 开始第二轮宏任务,timer1里的回调执行,打印timer1打印,微任务进入队列
- 开始微任务,打印promise2
- 开始宏任务,打印timer2
start
promise1
timer1
promise2
timer2
复制代码
这里的关键是,宏任务和微任务的区分,以及宏任务和微任务的交替执行逻辑:宏任务执行结束后,执行可执行的微任务,然后再继续执行宏任务队列中的宏任务,以此类推。而我们常说的异步操作,也就是不会阻塞主线程执行,原因就是异步操作会被暂时放到任务队列(可能是宏任务,也可能是微任务队列),当一轮宏任务执行结束才会开始队列中的下一轮微任务的执行,微任务执行结束后又会开始宏任务队列中的执行,以此不断交替执行。
在es6规范中,宏任务是由宿主(浏览器、Node)发起的,而微任务由JavaScript自身发起。所以script中的代码、setTimeout/setInterval、postMessage等异步操作都是宏任务,而Promise.then、Promise.nextTick、Proxy中的异步调用等都是js本身发起的,所以是微任务。
剑指 Offer 22. 链表中倒数第k个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。
例如,一个链表有 6 个节点,从头节点开始,它们的值依次是 1、2、3、4、5、6。这个链表的倒数第 3 个节点是值为 4 的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
复制代码
链表结构为:
/**
* Definition for singly-linked list.
* function ListNode(val) {
* this.val = val;
* this.next = null;
* }
*/
复制代码
链表的特点是只能从前一个获取后一个,我们要返回倒数第K个节点的子链,就要知道倒数第K个节点的位置,知道了这个位置,再从这个节点遍历到链表末尾即可。
可使用双指针法,由于目标子链的相对长度是固定的,我们遍历数组的时候,维持头尾指针,当尾指针遍历到结尾时,也就知道了倒数第K个节点的位置。
/**
* @param {ListNode} head
* @param {number} k
* @return {ListNode}
*/
var getKthFromEnd = function(head, k) {
let left = head, right = head;
let next = head.next;
let len = 0;
while(right) {
if(len === k) {
left = left.next;
} else {
len++;
}
right = right.next;
}
return left;
};
复制代码
反转链表
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
输入:head = [1,2,3,4,5]
输出:[5,4,3,2,1]
复制代码
很简单的一题,遍历过程中不断将指针指向前节点即可。
/**
* @param {ListNode} head
* @return {ListNode}
*/
var reverseList = function(head) {
let pre = null;
let cur = head;
let next = null;
while(cur) {
next = cur.next;
cur.next = pre;
pre = cur;
cur = next;
}
return pre;
};
复制代码