问题先导
- HTML5的离线储存怎么使用,它的工作原理是什么?【html】
- 伪元素和伪类的区别?【css】
- 对
requestAnimationFrame
的理解?【css】 -
isNaN
和Number.isNaN
的区别?【js】 -
==
操作符的转换原则?【js】 - Vue实例中的
computed
、watch
和methods
有什么区别?【vue】 - Vue中的
slot
是什么?有什么用处?原理是什么?【cue】 - 手写
Object.create
【手写代码】 - 手写
instanceof
【手写代码】 - 输出结果判断【输出结果】
- 字符串相加【算法】
知识梳理
HTML5的离线储存怎么使用,它的工作原理是什么?
离线缓存的目的是当浏览器处于离线状态或网络连接较慢时,利用应用程序缓存机制让web程序能在离线状态下正常访问。使用离线缓存技术一般是为了让用户在:
- 离线状态也能正常访问
- 提高访问速度
- 减轻服务器响应压力
实现离线缓存,目前主要使用 Service Workers方案,而文后提到的manifest
属性方案已弃用,建议不要使用,这里只是作为了解。
历史方案:利用manifest
属性实现App Cache(已弃用)
一般具有
window.applicationCache
对象的浏览器才支持该特性。该方案已弃用,了解即可。
启用应用程序缓存的做法是在应用程序页面的html
标签上增加manifest
属性来引入缓存配置文件:
<html manifest="example.appcache">
...
</html>
复制代码
然后编辑.appcache
文件用于描述如何启动缓存,下面是完整缓存清单:
# 缓存清单:后续列出的文件会在第一次下载完毕后缓存起来
CACHE MANIFEST
index.html
cache.html
style.css
image1.png
# 缓存白名单:列出不需要缓存的文件
NETWORK:
network.html
# 定了一个后备页面,当资源无法访问时,浏览器会使用该页面。
# 该段落的每条记录都列出两个 URI—第一个表示资源,第二个表示后备页面。
FALLBACK:
/ fallback.html
/userpage user_404.html
复制代码
CACHE MANIFEST,NETWORK 和 FALLBACK 段落可以以任意顺序出现在缓存清单文件中,并且每个段落可以在同一清单文件中出现多次。
缓存清单写好了,我们还需要按时更新,否则一直使用缓存会导致界面内容老旧,更新缓存使用JavaScript脚本来更新:
- indow.applicationCache.status记录缓存状态
- window.applicationCache.update()手动更新
function onUpdateReady() {
alert('found new version!');
}
window.applicationCache.addEventListener('updateready', onUpdateReady);
if(window.applicationCache.status === window.applicationCache.UPDATEREADY) {
onUpdateReady();
} else {
window.applicationCache.update()
}
复制代码
Service Workers
之前的尝试 — AppCache — 看起来是个不错的方法,因为它可以很容易地指定需要离线缓存的资源。但是,它假定你使用时会遵循诸多规则,如果你不严格遵循这些规则,它会把你的APP搞得一团糟。关于APPCache的更多详情,请看Jake Archibald的文章: Application Cache is a Douchebag.
Service worker 最终要去解决这些问题。虽然 Service Worker 的语法比 AppCache 更加复杂,但是你可以使用 JavaScript 更加精细地控制 AppCache 的静默行为。
使用的前提
在支持service workers
的浏览器中,很多特性可能默认是关闭的,如果该浏览器支持该特性但相关代码不生效,你需要先开启浏览器的相关设置。比如chrome
:访问 chrome://flags
并开启 experimental-web-platform-features
。
此外,出于页面安全考虑,services workers
要求必须在HTTPS
协议下才能使用。
使用细节
service worker有点类似于web worker,代码也是在独立于主线程的worker中运行。
如果'serviceWorker' in navigator
说明浏览器支持Service Worker
,使用该特性一般需要:
- 注册worker:navigator.serviceWorker.register
- 安装和激活:定义缓存文件。使用
install
事件进行安装监听,该事件会在注册之后触发。 - 劫持请求:当页面使用到缓存的资源时,会触发
fetch
事件,这时候你就可以告诉service worker
该如何使用该资源,你可以在请求发起前定义响应内容,这也是Service Worker的精细之处,你可以返回缓存到的资源,也可以重新发起网络请求而不使用缓存,或者网络不可用时返回一些准备好的意外页。 - 更新缓存:简单的使用版本编号就可以控制缓存版本。
- 删除旧缓存:还有个
activate
事件,当之前版本还在运行的时候,一般被用来做些会破坏它的事情,比如摆脱旧版的缓存。
具体使用细节请参考使用 Service Workers。这是一个简单的使用案例: sw-test。
参考:
伪元素和伪类的区别
-
伪元素:在元素的前后插入额外的内容或样式,由于这些新增的元素是真实存在的,但又不是文档源码中的,因此称为伪元素。
p::before {content:"第一章:";} p::after {content:"Hot!";} p::first-line {background:red;} p::first-letter {font-size:30px;} 复制代码
-
伪类:伪类就像为元素增加了一个类,用于表达元素的特殊状态或性质。这些类都是浏览器可识别的一些“特殊效果”,当元素处于这些状态时就会被css选择器捕获。
a:hover {color: #FF00FF} p:first-child {color: red} 复制代码
总结来说就是,伪元素是元素通过css前后额外插入的元素或样式,并非来自文档源码,而伪类是具有特殊状态或性质的元素,是已存在元素。
对requestAnimationFrame的理解
从字面意思来看,window.requestAnimationFrame
是用于请求动画帧的api,而实际上,这个api和setTimeout
定时器的功能基本一致,都能在指定时间间隔后触发回调函数。而requestAnimationFreame
的作用更为明确:专门用于处理动画的。
比如我们利用定时器循环处理元素样式,来达到动画的效果,而每执行一次的动作就可以看作一次动画帧,当定时器循环执行连贯起来,也就达到了动画的效果。既然setTimeout
或setInterval
定时器都能实现js动画
,为何又要特别使用requestAnimationFrame
呢?
我们知道,setTimeout
和setInterval
除了指定回调函数,还需要指定执行间隔,而执行间隔关系到动画的刷新频率,首先不好控制,如果频率太快电脑更新不上还容易造成卡顿感,其次,这两个定时器是异步执行的,某些场景下并不好控制。基于这些问题,HTM5t提出的requestAnimationFrame
专门对动画处理进行了优化:
- 首先,这个api不需要指定调用间隔时间。这个时间会自动适应显示屏的刷新频率,比如显示屏刷新频率为
60HZ
,也就是60次每秒,换算成一次的执行间隔就是1/60秒,即1 / 60 * 1000 s = 16.67ms
。 - 非异步调用,回调是在主线程中执行的。意味着主线程如果很繁忙时不推荐频繁进行动画重绘。
- 动画会触发页面重绘,而当浏览器标签页处于后台或处于隐藏的iframe中时,该api回调是暂停执行的,是对CPU友好的。
api使用上基本与setTimeout
一致:
const requestId = requestAnimationFrame(call); // 调用一次动画帧
cancelAnimationFrame(requestId); // 取消动画帧的调用
复制代码
如果浏览器不支持该api,我们还可以使用setTimeout
来模拟:
(function() {
if(!window.requestAnimationFrame) {
window.requestAnimationFrame = (call) => {
return setTimeout(function() {
call(performance.now());
}, 16.67);
};
}
})()
复制代码
下面是一个简单的动画示例:
<div id="anim" style="position: absolute;">
悬浮暂停动画
</div>
复制代码
const ele = document.getElementById('anim');
let startTime = undefined; // 其实执行时间
let startLeft = 0; // 起始left值
// 重绘逻辑
function render(timestamp) {
if(timestamp === undefined) {
timestamp = performance.now(); // 当前执行时间
}
if(startTime === undefined) {
startTime = timestamp - (startLeft + 500) * 10; // 恢复到暂停时的时间
}
const left = (timestamp - startTime) / 10 % 500; // 0 - 500 间波动
ele.style.left = left + 'px';
}
let animRequestId = undefined
// 暂停动画
function stopAnimation() {
if(animRequestId === undefined) {
return;
}
cancelAnimationFrame(animRequestId);
animRequestId = undefined;
startTime = undefined;
startLeft = parseFloat(ele.style.left || '0');
}
// 开启动画
function startAnimation() {
if(animRequestId === undefined) {
(function animaloop() {
render();
animRequestId = requestAnimationFrame(animaloop);
})();
}
}
ele.addEventListener('mouseover', stopAnimation);
ele.addEventListener('mouseleave', startAnimation);
window.onload = function() {
startAnimation();
};
复制代码
参考:
- requestAnimationFrame – MDN
- 深入理解定时器系列第二篇——被誉为神器的requestAnimationFrame
- requestAnimationFrame – javaScript标准参考教程
isNaN 和 Number.isNaN 函数的区别?
由于NaN
是非自反数值,使用 ==
和 ===
与其他任何值比较都将不相等,所以判断数值是否为NaN
必须通过isNaN
或Number.isNaN
函数。
isNaN
函数用于检查其参数是否是非数字值。检测之前会先转换为数字再进行检测。而Number.isNaN
直接进行比较而不会进行类型转换。
==
操作符的类型转换原则
==
和===
是不同的,==
是非全等,再进行值比较时会进行自动类型转换,即转换为相同类型的值再进行比较。
数据类型分为基本数据类型和引用类型,而引用类型之间的比较是直接比较物理引用地址的,无需转换。基本数据类型有7种:undefined、null、boolean、number、string、Symbol和BigInt。其中,可以进行数据类型转换的也就string、boolean、number这三种。
转换流程如下:
-
类型相同,无需转换,直接比较
-
null
和undefined
的比较,返回true -
string
|boolean
和其他基本数据类型的比较:string
|boolean
转数字后再比较 -
object
和基本数据类型的比较:对象执行Symbol.toPrimitive
抽象操作后再进行比较(Symbol.toPrimitive
是一个内置的 Symbol 值,它是作为对象的函数值属性存在的,当一个对象转换为对应的原始值时,会调用此函数)// 一个没有提供 Symbol.toPrimitive 属性的对象,参与运算时的输出结果 var obj1 = {}; console.log(+obj1); // NaN console.log(`${obj1}`); // "[object Object]" console.log(obj1 + ""); // "[object Object]" // 接下面声明一个对象,手动赋予了 Symbol.toPrimitive 属性,再来查看输出结果 var obj2 = { [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" 复制代码
-
返回false
我们发现,排除了特殊情况后只有string
| boolean
转number
和对象转基本类型两种情况。
除了==
,还有+
、-
等类型操作时也可能发生类型转换,类型转换是弱类型语言的一个特点,但也是一个缺点,因为不够严谨,可能转换并不是我们需要的,因此,再进行数据比较和运算时,主动转换为相同类型是更推荐的做法。
Vue实例属性computed
vs methods
vs watch
computed
是计算属性,语法就像getter/setter
那样,同时也支持函数作为参数:
{ [key: string]: Function | { get: Function, set: Function } }
计算属性的初衷在于“计算”两个字上,我们希望得到一个可以通过某种公式或逻辑计算出来的值,这个时候就可以使用计算属性。
计算属性有个特点:缓存。计算属性是基于响应式依赖进行缓存的,这意为这当依赖发生变化会自动更新计算属性值缓存,未变化则不会更新,而每次取值则直接读取值缓存即可。同样的,由于需要编译响应式更新计算属性的逻辑,计算属性中的异步代码就会失效,也就是说在计算属性中使用异步代码是无意义的。
watch
是Vue实例的属性侦听器,当侦听的属性变化时,可以触发侦听事件。利用这个属性侦听器,我们也可以在某些属性变化后更新某些动态属性,这其实是和计算属性的处理逻辑是一样的,依赖属性变化时更新缓存,而我们通过watch
不过是改成了手动更新。
因此,watch
更应该专注属性变化的处理逻辑,而不是维护某个属性值,这一任务更应该交由计算属性来完成。
methods
顾名思义,存储了实例复用的一些函数,函数当然也可以用来计算属性,但是函数就失去了缓存的功能,每次更新我们都需要重新计算,这对复杂运算来说是不换算的开销。但methods
的灵活和强大是计算属性无法比拟的,比如异步操作的数据,计算属性就无能为力了,只是相对来说,在获得计算依赖性质的属性这事上,还是交个computed
更好。
总结来说,对于需要计算依赖性的属性,大多数情况使用computed
即可,对于异步这种数据变化,无法主动更新的属性,我们只能改用watch
或者methods
,但正如字面意义那样,watch
更应该专注处理侦听逻辑。
参考:
Vue中的slot
是什么?有什么作用?原理是什么?
slot
是Vue中的内置插槽组件,是Vue的内容分发机制。Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot>
元素作为承载分发内容的出口。
比如组件navigation-link
使用时:
<navigation-link url="/profile">
Your Profile
</navigation-link>
复制代码
我们需要接收Your Profile
,就需要用到插槽。然后你可以在 <navigation-link>
的模板中这样写:
<a
v-bind:href="url"
class="nav-link"
>
<slot></slot>
</a>
复制代码
那么slot
元素的位置就会被组件实例的内容值替换。
插槽有三种类型:
-
匿名插槽:是指
slot
没有指定name
属性时的插槽,一个组件内只能有一个匿名插槽。 -
具名插槽:当组件内有多个插槽时,就需要为插槽命名,需要设置
name
属性值,因次叫具名插槽。(实际上,匿名插槽的名称为default
)有时我们需要多个插槽。例如对于一个带有如下模板的
<base-layout>
组件:<div class="container"> <header> <!-- 我们希望把页头放这里 --> </header> <main> <!-- 我们希望把主要内容放这里 --> </main> <footer> <!-- 我们希望把页脚放这里 --> </footer> </div> 复制代码
这样,我们就需要三个插槽,为了区分,就需要带上名称,也就是
name
属性:<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer> </div> 复制代码
在向具名插槽提供内容的时候,我们可以在一个
<template>
元素上使用v-slot
指令,并以v-slot
的参数的形式提供其名称:<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout> 复制代码
现在
<template>
元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有v-slot
的<template>
中的内容都会被视为默认插槽的内容。最终的渲染结果:
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer> </div> 复制代码
-
作用域插槽
有一个编译规则是:父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
slot
是组件模板中,也就是父作用域,我们知道,父作用域不经过特殊处理无法访问子作用域,但有时候在插槽模板中使用子作用域的数据是很有用的。就像现父作用域和子作用域通信那样,我们可以使用prop
属性来传递数据。在组件实例中可以使用v-slot:slot-name="slotProps"
来传递属性值,其中slotProps
就是子作用域传递给父作用域的数据对象。然后在组件模板中使用v-bind:propName="propObj"
来获取数据。比如:父作用域组件模板可以这样写:
<span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> 复制代码
使用时,可以这样绑定:
<current-user> <template v-slot:default="slotProps"> {{ slotProps.user.firstName }} </template> </current-user> 复制代码
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为
slotProps
,但你也可以使用任意你喜欢的名字。
实现原理:当子组件vm实例化时,获取到父组件传入的slot标签的内容,存放在vm.$slot
中,默认插槽为vm.$slot.default
,具名插槽为vm.$slot.xxx
,xxx 为插槽名,当组件执行渲染函数时候,遇到slot标签,使用$slot
中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
参考:
- 插槽 – Vue 。
手写Object.create
**Object.create()
**方法创建一个新对象,使用现有的对象来提供新创建的对象的__proto__
。
语法:Object.create(proto,[propertiesObject])
- proto:新建对象的(隐式)原型对象
- propertiesObject:可选,该对象的属性类型参照
Object.defineProperties()
简单来说,就是通过proto
参数来创建新对象,并且这个新对象的原型指向这个参数。我们知道__proto__
的赋值以及新对象的创建可以通过new
关键字来实现 。
if(typeof Object.create !== 'function') {
Object.create = function(proto, propertiesObject) {
if(!proto || (typeof proto !== 'object' && typeof proto !== 'function')) {
throw new Error('Object prototype may only be an Object: ' + proto);
}
// 第二个参数可以用 Object.defineproperty 来实现,
// 但一般 Object.Create 不支持,说明 Object.defineproperty 也不支持
if(propertiesObject !== undefined) {
throw new Error("This browser's implementation of Object.create is a shim and doesn't support a second argument.");
}
function F() {};
F.prototype = proto;
return new F();
}
}
复制代码
扩展:对象的__proto__
我们一般称之为对象的隐式原型,指向父类的显示原型prototype
,所以我们还能利用Object.create
来实现类的继承。
手写新函数除了理解其运作原理,还有一个很有用的作用,就是polyfill
。polyfill
意为填充物、补丁,在编程中一般用于低版本”打补丁“,“兜底”。比如Object.create
是ES6的语法,在低版本浏览器就无法运行,因此手写版的Object.create
就是这个函数的polyfill
。不过这些工作基本都有现成的库如babel
来做了。
手写instanceof方法
instanceof
运算符用于检测构造函数的 prototype
属性是否出现在某个实例对象的原型链上。即判断实例对象是否为某个函数(类)的子类实例。
关键点在于遍历实例对象的原型链,然后判断构造函数是否与原型链上的原型相等。
function my_instanceOf(A, B) {
if(!A || !B) {
return false;
}
const B_proto = B.prototype; // 获取B的构造函数原型(通常说的显示原型)
let prototype_link_next = Object.getPrototypeOf(A); // 返回对象A的原型(通常说的隐式原型)(同A.__prototype__,但非标准,不推荐使用)
// 原型链简单来说就是:子类的隐式原型 = 父类的显示原型 => 子类的原型 = 父类的构造函数原型
// 当B的构造函数原型出现在A的原型链中时,说明A为B的子类,也就是B的实例
while(prototype_link_next) {
if(B_proto === prototype_link_next) {
return true;
}
prototype_link_next = Object.getPrototypeOf(prototype_link_next);
}
return false;
}
复制代码
输出结果
代码段:
const promise = new Promise((resolve, reject) => {
console.log(1);
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
复制代码
本题考查的是Promise
的基本执行流程:Promise
对象用于表示一个异步操作的最终完成 (或失败)及其结果值。有三个状态:
padding
(待定):初始状态,宏任务执行后的异步执行等待阶段。fulfilled
(已兑现):异步执行成功。后续可执行微任务。rejected
(已拒绝):异步执行失败。后续可执行微任务。
案例代码,定义了一个Promise
对象,对象内部的代码是宏任务,是同步执行的,但是没有修改Promise
的执行回调,也就是状态未改变,那么微任务也就不会执行,所以最后的输出结果为:
1
2
4
复制代码
代码段:
const promise1 = new Promise((resolve, reject) => {
console.log('promise1')
resolve('resolve1')
})
const promise2 = promise1.then(res => {
console.log(res)
})
console.log('1', promise1);
console.log('2', promise2);
复制代码
本题考察内容除了Promise
的执行流程,还有Promise
对象的打印细节:Promise
对象打印时,会打印状态加参数。
- 等待状态:Promise { }
- 成功状态:Promise { : 参数}(注意node控制台不打印状态)
- 拒绝状态:Promise { : 参数}
所以本题的打印结果为:
promise1
1 Promise {<fulfilled>: 'resolve1'}
2 Promise {<padding>}
resolve1
复制代码
字符串相加
给定两个字符串形式的非负整数 num1 和num2 ,计算它们的和。
输入:
"9333852702227987"
"85731737104263"
输出:
"9419584439332250"
复制代码
注意本题的特点是字符串可能特别长,会超过数字最大存储范围,因此,本题不能将字符串转为数字再相加,这样会丢失精度。本题可以通过模拟数字相加的方法,逐一计算出最后的答案。
/**
* 字符串加法运算
* @param {string} str1
* @param {string} str2
*/
function addStrings(str1, str2) {
const num1 = str1.split('');
const num2 = str2.split('');
let i = num1.length - 1;
let j = num2.length - 1;
let sumStr = '';
let carry = 0;
while(i >= 0 || j >= 0) {
let a = i < 0 ? 0 : +num1[i--];
let b = j < 0 ? 0 : +num2[j--];
let sum = a + b + carry;
carry = parseInt(sum / 10);
sumStr = (sum % 10) + sumStr;
}
return carry ? carry + sumStr : sumStr;
}
复制代码