Hi~ 好久不见。
上一次整理的100道前端面试题在掘金火了之后,很多小伙伴反馈看答案不方便,其实主要是因为我整理的答案和解析内容非常全面,导致每一道题的篇幅都很长,阅读体验不太好,所以才给大家把答案放到github上。
最近解锁了掘金的新功能——折叠内容,我将在这篇文章中尽可能多的的放置答案和解析。
1.如何获取 html 元素实际的样式值?
公司:京东
分类:JavaScript
查看解析
代码实现
实际的样式值 可以理解为 浏览器的计算样式
style 对象中包含支持 style 属性的元素为这个属性设置的样式信息,但不包含从其他样式表层叠继承的同样影响该元素的样式信息。
DOM2 Style 在 document.defaultView 上增加了 getComputedStyle() 方法。这个方法接收两个参数:要取得计算样式的元素和伪元素字符串(如":after")。如果不需要查询伪元素,则第二个参数可以传 null。getComputedStyle()方法返回一个 CSSStyleDeclaration 对象(与 style 属性的类型一样),包含元素的计算样式。
<!DOCTYPE html>
<html>
<head>
<title>Computed Styles Example</title>
<style type="text/css">
#myDiv {
background-color: blue;
width: 100px;
height: 200px;
}
</style>
</head>
<body>
<div
id="myDiv"
style="background-color: red; border: 1px solid black"
></div>
</body>
<script>
let myDiv = document.getElementById("myDiv");
let computedStyle = document.defaultView.getComputedStyle(myDiv, null);
console.log(computedStyle.backgroundColor); // "red"
console.log(computedStyle.width); // "100px"
console.log(computedStyle.height); // "200px"
console.log(computedStyle.border); // "1px solid black"(在某些浏览器中)
/* 兼容写法 */
function getStyleByAttr(obj, name) {
return window.getComputedStyle
? window.getComputedStyle(obj, null)[name]
: obj.currentStyle[name];
}
let node = document.getElementById("myDiv");
console.log(getStyleByAttr(node, "backgroundColor"));
console.log(getStyleByAttr(node, "width"));
console.log(getStyleByAttr(node, "height"));
console.log(getStyleByAttr(node, "border
</script>
</html>
2.说一下对 React Hook 的理解,它的实现原理,和生命周期有哪些区别?
公司:高德、头条
分类:React
查看解析
一、React Hook
1.1 什么是 React Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
从官网的这句话中,我们可以明确的知道,Hook
增加了函数式组件中state
的使用,在之前函数式组件是无法拥有自己的状态,只能通过props
以及context
来渲染自己的UI
,而在业务逻辑中,有些场景必须要使用到state
,那么我们就只能将函数式组件定义为class
组件。而现在通过Hook
,我们可以轻松的在函数式组件中维护我们的状态,不需要更改为class
组件。
React16.8 加入 hooks,让 React 函数式组件更加灵活
hooks 之前,React 存在很多问题
- 在组件间复用状态逻辑很难
- 复杂组件变得难以理解,高阶组件和函数组件的嵌套过深。
- class 组件的 this 指向问题
- 难以记忆的生命周期
hooks 很好的解决了上述问题,hooks 提供了很多方法
- useState 返回有状态值,以及更新这个状态值的函数
- useEffect 接受包含命令式,可能有副作用代码的函数。
- useContext 接受上下文对象(从 React.createContext 返回的值)并返回当前上下文值,
- useReducer useState 的替代方案。接受类型为(state,action) => newState 的 reducer,并返回与 dispatch 方法配对的当前状态。
- useCallback 返回一个回忆的 memoized 版本,该版本仅在其中一个输入发生更改时才会更改。纯函数的输入输出确定性
- useMemo 纯的一个记忆函数
- useRef 返回一个可变的 ref 对象,其.current 属性被初始化为传递的参数
- useImperativeMethods 自定义使用 ref 时公开给父组件的实例值
- useMutationEffect 更新兄弟组件之前,它在 React 执行其 DOM 改变的同一阶段同步触发
- useLayoutEffect DOM 改变后同步触发。使用它来从 DOM 读取布局并同步重新渲染
1.2.React Hook 要解决什么问题
React Hooks
要解决的问题是状态共享,这里的状态共享是指只共享状态逻辑复用,并不是指数据之间的共享。我们知道在React Hooks
之前,解决状态逻辑复用问题,我们通常使用higher-order components
和render-props
。
既然已经有了这两种解决方案,为什么React
开发者还要引入React Hook
?对于higher-order components
和render-props
,React Hook
的优势在哪?
PS:Hook 最大的优势其实还是对于状态逻辑的复用便捷,还有代码的简洁,以及帮助函数组件增强功能,
我们先来看一下React
官方给出的React Hook
的demo
import { useState } from "React";
function Example() {
// Declare a new state variable, which we'll call "count"
const [count, setCount] = useState(0);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>Click me</button>
</div>
);
}
我们再来看看不用React Hook
的话,如何实现
class Example extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0,
};
}
render() {
return (
<div>
<p>You clicked {this.state.count} times</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Click me
</button>
</div>
);
}
}
可以看到,在React Hook
中,class Example
组件变成了函数式组件,但是这个函数式组件却拥有的自己的状态,同时还可以更新自身的状态。这一切都得益于useState
这个Hook
,useState
会返回一对值:当前状态和一个让你更新它的函数,你可以在事件处理函数中或其他一些地方调用这个函数。它类似 class
组件的 this.setState
,但是它不会把新的 state
和旧的 state
进行合并
1.3 实现原理
Hooks 的基本类型:
type Hooks = {
memoizedState: any, // 指向当前渲染节点 Fiber
baseState: any, // 初始化 initialState, 已经每次 dispatch 之后 newState
baseUpdate: Update<any> | null, // 当前需要更新的 Update ,每次更新完之后,会赋值上一个 update,方便 react 在渲染错误的边缘,数据回溯
queue: UpdateQueue<any> | null, // UpdateQueue 通过
next: Hook | null, // link 到下一个 hooks,通过 next 串联每一 hooks
};
type Effect = {
tag: HookEffectTag, // effectTag 标记当前 hook 作用在 life-cycles 的哪一个阶段
create: () => mixed, // 初始化 callback
destroy: (() => mixed) | null, // 卸载 callback
deps: Array<mixed> | null,
next: Effect, // 同上
};
React Hooks 全局维护了一个 workInProgressHook 变量,每一次调取 Hooks API 都会首先调取 createWorkInProgressHooks 函数。
function createWorkInProgressHook() {
if (workInProgressHook === null) {
// This is the first hook in the list
if (firstWorkInProgressHook === null) {
currentHook = firstCurrentHook;
if (currentHook === null) {
// This is a newly mounted hook
workInProgressHook = createHook();
} else {
// Clone the current hook.
workInProgressHook = cloneHook(currentHook);
}
firstWorkInProgressHook = workInProgressHook;
} else {
// There's already a work-in-progress. Reuse it.
currentHook = firstCurrentHook;
workInProgressHook = firstWorkInProgressHook;
}
} else {
if (workInProgressHook.next === null) {
let hook;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
currentHook = currentHook.next;
if (currentHook === null) {
// This is a newly mounted hook
hook = createHook();
} else {
// Clone the current hook.
hook = cloneHook(currentHook);
}
}
// Append to the end of the list
workInProgressHook = workInProgressHook.next = hook;
} else {
// There's already a work-in-progress. Reuse it.
workInProgressHook = workInProgressHook.next;
currentHook = currentHook !== null ? currentHook.next : null;
}
}
return workInProgressHook;
}
假设我们需要执行以下 hooks 代码:
function FunctionComponet() {
const [ state0, setState0 ] = useState(0);
const [ state1, setState1 ] = useState(1);
useEffect(() => {
document.addEventListener('mousemove', handlerMouseMove, false);
...
...
...
return () => {
...
...
...
document.removeEventListener('mousemove', handlerMouseMove, false);
}
})
const [ satte3, setState3 ] = useState(3);
return [state0, state1, state3];
}
当我们了解 React Hooks 的简单原理,得到 Hooks 的串联不是一个数组,但是是一个链式的数据结构,从根节点 workInProgressHook 向下通过 next 进行串联。这也就是为什么 Hooks 不能嵌套使用,不能在条件判断中使用,不能在循环中使用。否则会破坏链式结构。
二、和生命周期的区别
函数组件 的本质是函数,没有 state 的概念的,因此不存在生命周期一说,仅仅是一个 render 函数而已。
但是引入 Hooks 之后就变得不同了,它能让组件在不使用 class 的情况下拥有 state,所以就有了生命周期的概念,所谓的生命周期其实就是 useState、 useEffect() 和 useLayoutEffect() 。
即:Hooks 组件(使用了 Hooks 的函数组件)有生命周期,而函数组件(未使用 Hooks 的函数组件)是没有生命周期的。
下面,是具体的 class 与 Hooks 的生命周期对应关系:
3.移动端适配方案具体实现以及对比
公司:头条
分类:Css
查看解析
常见的移动端适配方案
- media queries
- flex 布局
- rem + viewport
- vh vw
- 百分比
一、Meida Queries
meida queries 的方式可以说是我早期采用的布局方式,它主要是通过查询设备的宽度来执行不同的 css 代码,最终达到界面的配置。
核心语法:
@media only screen and (max-width: 374px) {
/* iphone5 或者更小的尺寸,以 iphone5 的宽度(320px)比例设置样式*/
}
@media only screen and (min-width: 375px) and (max-width: 413px) {
/* iphone6/7/8 和 iphone x */
}
@media only screen and (min-width: 414px) {
/* iphone6p 或者更大的尺寸,以 iphone6p 的宽度(414px)比例设置样式 */
}
优点:
- media query 可以做到设备像素比的判断,方法简单,成本低,特别是针对移动端和 PC 端维护同一套代码的时候。目前像 Bootstrap 等框架使用这种方式布局
- 图片便于修改,只需修改 css 文件
- 调整屏幕宽度的时候不用刷新页面即可响应式展示
缺点:
- 代码量比较大,维护不方便
- 为了兼顾大屏幕或高清设备,会造成其他设备资源浪费,特别是加载图片资源
- 为了兼顾移动端和 PC 端各自响应式的展示效果,难免会损失各自特有的交互方式
二、Flex 弹性布局
以天猫的实现方式进行说明:
它的 viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
高度定死,宽度自适应,元素都采用 px 做单位。
随着屏幕宽度变化,页面也会跟着变化,效果就和 PC 页面的流体布局差不多,在哪个宽度需要调整的时候使用响应式布局调调就行(比如网易新闻),这样就实现了『适配』。
三、rem+viewport 缩放
实现原理:
根据 rem 将页面放大 dpr 倍, 然后 viewport 设置为 1/dpr.
- 如 iphone6 plus 的 dpr 为 3, 则页面整体放大 3 倍, 1px(css 单位)在 plus 下默认为 3px(物理像素)
- 然后 viewport 设置为 1/3, 这样页面整体缩回原始大小. 从而实现高清。
这样整个网页在设备内显示时的页面宽度就会等于设备逻辑像素大小,也就是 device-width。这个 device-width 的计算公式为:
设备的物理分辨率/(devicePixelRatio * scale)
,在 scale 为 1 的情况下,device-width = 设备的物理分辨率/devicePixelRatio
。
四、rem 实现
rem
是相对长度单位,rem
方案中的样式设计为相对于根元素font-size
计算值的倍数。根据屏幕宽度设置html
标签的font-size
,在布局时使用 rem 单位布局,达到自适应的目的。
viewport 是固定的:<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
。
通过以下代码来控制 rem 基准值(设计稿以 720px 宽度量取实际尺寸)
!(function (d) {
var c = d.document;
var a = c.documentElement;
var b = d.devicePixelRatio;
var f;
function e() {
var h = a.getBoundingClientRect().width,
g;
if (b === 1) {
h = 720;
}
if (h > 720) h = 720; //设置基准值的极限值
g = h / 7.2;
a.style.fontSize = g + "px";
}
if (b > 2) {
b = 3;
} else {
if (b > 1) {
b = 2;
} else {
b = 1;
}
}
a.setAttribute("data-dpr", b);
d.addEventListener(
"resize",
function () {
clearTimeout(f);
f = setTimeout(e, 200);
},
false
);
e();
})(window);
css 通过 sass 预编译,设置量取的 px 值转化 rem 的变量$px: (1/100)+rem;
优点:
- 兼容性好,页面不会因为伸缩发生变形,自适应效果更佳。
缺点:
- 不是纯 css 移动适配方案,需要在头部内嵌一段
js
脚本监听分辨率的变化来动态改变根元素的字体大小,css
样式和js
代码有一定耦合性,并且必须将改变font-size
的代码放在css
样式之前。 - 小数像素问题,浏览器渲染最小的单位是像素,元素根据屏幕宽度自适应,通过
rem
计算后可能会出现小数像素,浏览器会对这部分小数四舍五入,按照整数渲染,有可能没那么准确。
五、纯 vw 方案
视口是浏览器中用于呈现网页的区域。
- vw : 1vw 等于 视口宽度 的 1%
- vh : 1vh 等于 视口高度 的 **1% **
- vmin : 选取 vw 和 vh 中 最小 的那个
- vmax : 选取 vw 和 vh 中 最大 的那个
虽然 vw 能更优雅的适配,但是还是有点小问题,就是宽,高没法限制。
$base_vw = 375;
@function vw ($px) {
return ($px/$base_vw) * 100vw
};
优点:
- 纯
css
移动端适配方案,不存在脚本依赖问题。 - 相对于
rem
以根元素字体大小的倍数定义元素大小,逻辑清晰简单。
缺点:
- 存在一些兼容性问题,有些浏览器不支持
六、vw + rem 方案
// scss 语法
// 设置html根元素的大小 750px->75 640px->64
// 将屏幕分成10份,每份作为根元素的大小。
$vw_fontsize: 75
@function rem($px) {
// 例如:一个div的宽度为100px,那么它对应的rem单位就是(100/根元素的大小)* 1rem
@return ($px / $vw_fontsize) * 1rem;
}
$base_design: 750
html {
// rem与vw相关联
font-size: ($vw_fontsize / ($base_design / 2)) * 100vw;
// 同时,通过Media Queries 限制根元素最大最小值
@media screen and (max-width: 320px) {
font-size: 64px;
}
@media screen and (min-width: 540px) {
font-size: 108px;
}
}
// body 也增加最大最小宽度限制,避免默认100%宽度的 block 元素跟随 body 而过大过小
body {
max-width: 540px;
min-width: 320px;
}
七、百分比
使用百分比%定义宽度,高度用px
固定,根据可视区域实时尺寸进行调整,尽可能适应各种分辨率,通常使用max-width
/min-width
控制尺寸范围过大或者过小。
优点:
- 原理简单,不存在兼容性问题
缺点:
- 如果屏幕尺度跨度太大,相对设计稿过大或者过小的屏幕不能正常显示,在大屏手机或横竖屏切换场景下可能会导致页面元素被拉伸变形,字体大小无法随屏幕大小发生变化。
- 设置盒模型的不同属性时,其百分比设置的参考元素不唯一,容易使布局问题变得复杂。
4.var arr =[[‘A’,’B’],[‘a’,’b’],[1,2]]
求二维数组的全排列组合 结果:Aa1,Aa2,Ab1,Ab2,Ba1,Ba2,Bb1,Bb2
公司:美团
分类:算法
查看解析
参考代码实现
- 实现方式一
function foo(arr) {
// 用于记录初始数组长度, 用于将数组前两组已经获取到全排列的数组进行截取标识
var len = arr.length;
// 当递归操作后, 数组长度为1时, 直接返回arr[0], 只有大于1继续处理
if (len >= 2) {
// 每次只做传入数组的前面两个数组进行全排列组合, 即arr[0]和arr[1]的全排列组合
var len1 = arr[0].length;
var len2 = arr[1].length;
var items = new Array(len1 * len2); // 创建全排列组合有可能次数的数组
var index = 0; // 记录每次全排列组合后的数组下标
for (var i = 0; i < len1; i++) {
for (var j = 0; j < len2; j++) {
if (Array.isArray(arr[0])) {
// 当第二次进来后, 数组第一个元素必定是数组包着数组
items[index] = arr[0][i].concat(arr[1][j]); // 对于已经是第二次递归进来的全排列直接追加即可
} else {
items[index] = [arr[0][i]].concat(arr[1][j]); // 这里因为只需要去arr[0]和arr[1]的全排列, 所以这里就直接使用concat即可
}
index++; // 更新全排列组合的下标
}
}
// 如果数组大于2, 这里新的newArr做一个递归操作
var newArr = new Array(len - 1); // 递归的数组比传进来的数组长度少一, 因为上面已经将传进来的数组的arr[0]和arr[1]进行全排列组合, 所以这里的newArr[0]就是上面已经全排列好的数组item
for (var i = 2; i < arr.length; i++) {
// 这里的for循环是为了截取下标1项后的数组进行赋值给newArr
newArr[i - 1] = arr[i];
}
newArr[0] = items; // 因为上面已经将传进来的数组的arr[0]和arr[1]进行全排列组合, 所以这里的newArr[0]就是上面已经全排列好的数组item
// 重新组合后的数组进行递归操作
return foo(newArr);
} else {
// 当递归操作后, 数组长度为1时, 直接返回arr[0],
return arr[0];
}
}
var arr = [
["A", "B"],
["a", "b"],
[1, 2],
];
console.log(foo(arr));
- 实现方式二
const getResult = (arr1, arr2) => {
if (!Array.isArray(arr1) || !Array.isArray(arr2)) {
return;
}
if (!arr1.length) {
return arr2;
}
if (!arr2.length) {
return arr1;
}
let result = [];
for (let i = 0; i < arr1.length; i++) {
for (let j = 0; j < arr2.length; j++) {
result.push(String(arr1[i]) + String(arr2[j]));
}
}
return result;
};
const findAll = (arr) =>
arr.reduce((total, current) => {
return getResult(total, current);
}, []);
var arr = [
["A", "B"],
["a", "b"],
[1, 2],
];
console.log(findAll(arr));
- 实现方式三
var arr = [
["A", "B"],
["a", "b"],
[1, 2],
];
let res = [],
lengthArr = arr.map((d) => d.length);
let indexArr = new Array(arr.length).fill(0);
function addIndexArr() {
indexArr[0] = indexArr[0] + 1;
let i = 0;
let overflow = false;
while (i <= indexArr.length - 1) {
if (indexArr[i] >= lengthArr[i]) {
if (i < indexArr.length - 1) {
indexArr[i] = 0;
indexArr[i + 1] = indexArr[i + 1] + 1;
} else {
overflow = true;
}
}
i++;
}
return overflow;
}
function getAll(arr, indexArr) {
let str = "";
arr.forEach((item, index) => {
str += item[indexArr[index]];
});
res.push(str);
let overflow = addIndexArr();
if (overflow) {
return;
} else {
return getAll(arr, indexArr);
}
}
getAll(arr, indexArr);
console.log(res);
5.说下工作流程(开发流程、代码规范、打包流程等)
公司:腾讯微视
分类:工程化
查看解析
一、拿到原型图,先自我解析需求,画出思维导图,流程图
- 在未拿到 UI 给定的 PSD 时,可以先理清我们的需求
- 依赖的外部资源
- 后端提供的接口
- UI 出图的大概布局
- 后期频繁改动的地方
- 依赖的外部资源
- 需要实现的效果
- 下拉刷新
- 动画效果
- 吸顶效果
- 懒加载、预加载、防抖、节流
二、产品召集项目相关人员,开需求讨论会,产品讲解原型
- 理解产品的需求,提出质疑:这是什么功能,怎么做,为啥这么做
- 评估实现难度和实现成本,是否有潜在技术问题/风险
- 对比自己整理的需求图,如果有和自己想的不符合的,提出疑问
- 理解 PM 提出此次需求的目的,明白哪些内容是重点,哪些次要,可以适当取舍
- 如果产品要求提供时间,简单项目可以预估,复杂项目不可马上给出时间,需要仔细评估,评估时间包含开发、自测、测试人员测试、修复 bug、上线准备
三、会后进一步整理需求
- 细化细节,整理有疑问的地方,与产品、设计等其他人进行确认
- 评估项目完成时间--影响因素:需要的人力、 中间插入的需求、 开发、 自测、 测试人员测试、 修复 bug、 上线准备、 其他风险(如技术选型错误等)
- 初步制定排期表
四、需求二次确认(开发中遇到不确定的,依旧需要找相关人员进行需求确认,杜绝做无用功)
- IM 工具沟通确认
- 邮件确认
- 小型需求/项目相关讨论会
- 确定最终排期表
五、开发
- 技术选型
- 搭建开发环境:工具链
- 搭建项目架构
- 业务模块划分
- 优先级排序
- 新项目介入,需要当前项目和介入项目的相关负责人 Pk 优先级,随后调整项目排期
- 开发过程中发现工作量与预期有严重出入,需要尽早向其他项目人员反馈,方便其修改时间安排
- 定制开发规范
- 开发规范
- commit 提交格式:
[改动文件类型]:[改动说明]
- 单分支开发或者多分支开发
- 小项目、并行开发少,则只在 master 主分支开发
- 中大项目,需求复杂,并行功能多,则需要分为 master、developer、开发者分支;需要开发者自创一个分支开发,合并到 developer,确认无问题后,发布到 master,最后上线
- commit 提交格式:
- 代码规范
- jsconfig.json
.postcssrc.js
.babelrc
.prettierrc
(vscode 插件 prettier-code fomatter)— 注意与 eslint 要保持一致.editorconfig
.eslintrc.js
(强制开启验证模式)
- 源码管理
- 版本管理
- 安全管理
- 开发规范
六、自测
- 手动测试
- 单元测试
- 集成测试
七、提测---测试人员测试
- 开发人员修复 bug
- 期间不可接手耗时大的需求
- 有不确定优先级高低的需求,需要各个需求方互相 pk 优先级,再确定做与不做,不能因此拖延项目完成点
- 测试修复 bug 时间可能比开发时间还长,因此开发者预估开发时间不能乐观
八、上线
-
上线准备 a. 域名申请 b. 备案申请 c. 服务器申请 d. 部署
-
测试线上环境
- 有 bug 回到修复 bug 环节
-
日志监控
- 调用栈
- sourcemap
- 本地日志
- 用户环境、IP
- 低成本接入
- 统计功能
- 报警功能
九、维护
- 技术创新(对现有的技术领域以及具体项目实现方法进行优化)
- 提高效率:例如 jenkins 构建部署
- 减少成本
- 提升稳定性
- 安全性
6.Vue 中父组件可以监听到子组件的生命周期吗?
公司:水滴筹
分类:Vue
查看解析
实现方式
一、使用 on 和 emit
// Parent.vue
<Child @mounted="doSomething"/>
// Child.vue
mounted() {
this.$emit("mounted");
}
二、使用 hook 钩子函数
// Parent.vue
<Child @hook:mounted="doSomething" ></Child>
doSomething() {
console.log('父组件监听到 mounted 钩子函数 ...');
},
// Child.vue
mounted(){
console.log('子组件触发 mounted 钩子函数 ...');
},
// 以上输出顺序为:
// 子组件触发 mounted 钩子函数 ...
// 父组件监听到 mounted 钩子函数 ...
7.怎么给 Vue 定义全局方法
分类:Vue
查看解析
实现方式
一、将方法挂载到 Vue.prototype 上面
缺点:调用这个方法的时候没有提示
// global.js
const RandomString = (encode = 36, number = -8) => {
return Math.random() // 生成随机数字, eg: 0.123456
.toString(encode) // 转化成36进制 : "0.4fzyo82mvyr"
.slice(number);
},
export default {
RandomString,
...
}
// 在项目入口的main.js里配置
import Vue from "vue";
import global from "@/global";
Object.keys(global).forEach((key) => {
Vue.prototype["$global" + key] = global[key];
});
// 挂载之后,在需要引用全局变量的模块处(App.vue),不需再导入全局变量模块,而是直接用this就可以引用了,如下:
export default {
mounted() {
this.$globalRandomString();
},
};
二、利用全局混入mixin
优点:因为mixin里面的methods会和创建的每个单文件组件合并。这样做的优点是调用这个方法的时候有提示
// mixin.js
import moment from 'moment'
const mixin = {
methods: {
minRandomString(encode = 36, number = -8) {
return Math.random() // 生成随机数字, eg: 0.123456
.toString(encode) // 转化成36进制 : "0.4fzyo82mvyr"
.slice(number);
},
...
}
}
export default mixin
// 在项目入口的main.js里配置
import Vue from 'vue'
import mixin from '@/mixin'
Vue.mixin(mixin)
export default {
mounted() {
this.minRandomString()
}
}
三、使用Plugin方式
Vue.use的实现没有挂载的功能,只是触发了插件的install方法,本质还是使用了Vue.prototype。
// plugin.js
function randomString(encode = 36, number = -8) {
return Math.random() // 生成随机数字, eg: 0.123456
.toString(encode) // 转化成36进制 : "0.4fzyo82mvyr"
.slice(number);
}
const plugin = {
// install 是默认的方法。
// 当外界在 use 这个组件或函数的时候,就会调用本身的 install 方法,同时传一个 Vue 这个类的参数。
install: function(Vue){
Vue.prototype.$pluginRandomString = randomString
...
},
}
export default plugin
// 在项目入口的main.js里配置
import Vue from 'vue'
import plugin from '@/plugin'
Vue.use(plugin)
export default {
mounted() {
this.$pluginRandomString()
}
}
四、任意 vue 文件中写全局函数
// 创建全局方法
this.$root.$on("test", function () {
console.log("test");
});
// 销毁全局方法
this.$root.$off("test");
// 调用全局方法
this.$root.$emit("test");
8.说一下 vm.$set 原理
公司:极光推送
分类:Vue
查看解析
vm.$set()
解决了什么问题
在 Vue.js 里面只有 data 中已经存在的属性才会被 Observe 为响应式数据,如果你是新增的属性是不会成为响应式数据,因此 Vue 提供了一个 api(vm.$set)来解决这个问题。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Vue Demo</title>
<script src="https://cdn.jsdelivr.net/npm/vue"></script>
</head>
<body>
<div id="app">
{{user.name}} {{user.age}}
<button @click="addUserAgeField">增加一个年纪字段</button>
</div>
<script>
const app = new Vue({
el: "#app",
data: {
user: {
name: "test",
},
},
mounted() {},
methods: {
addUserAgeField() {
// this.user.age = 20 这样是不起作用, 不会被Observer
this.$set(this.user, "age", 20); // 应该使用
},
},
});
</script>
</body>
</html>
原理
vm.$set()在 new Vue()时候就被注入到 Vue 的原型上。
源码位置: vue/src/core/instance/index.js
import { initMixin } from "./init";
import { stateMixin } from "./state";
import { renderMixin } from "./render";
import { eventsMixin } from "./events";
import { lifecycleMixin } from "./lifecycle";
import { warn } from "../util/index";
function Vue(options) {
if (process.env.NODE_ENV !== "production" && !(this instanceof Vue)) {
warn("Vue is a constructor and should be called with the `new` keyword");
}
this._init(options);
}
initMixin(Vue);
// 给原型绑定代理属性$props, $data
// 给Vue原型绑定三个实例方法: vm.$watch,vm.$set,vm.$delete
stateMixin(Vue);
// 给Vue原型绑定事件相关的实例方法: vm.$on, vm.$once ,vm.$off , vm.$emit
eventsMixin(Vue);
// 给Vue原型绑定生命周期相关的实例方法: vm.$forceUpdate, vm.destroy, 以及私有方法_update
lifecycleMixin(Vue);
// 给Vue原型绑定生命周期相关的实例方法: vm.$nextTick, 以及私有方法_render, 以及一堆工具方法
renderMixin(Vue);
export default Vue;
- stateMixin()
Vue.prototype.$set = set;
Vue.prototype.$delete = del;
- set()
源码位置: vue/src/core/observer/index.js
export function set(target: Array<any> | Object, key: any, val: any): any {
// 1.类型判断
// 如果 set 函数的第一个参数是 undefined 或 null 或者是原始类型值,那么在非生产环境下会打印警告信息
// 这个api本来就是给对象与数组使用的
if (
process.env.NODE_ENV !== "production" &&
(isUndef(target) || isPrimitive(target))
) {
warn(
`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`
);
}
// 2.数组处理
if (Array.isArray(target) && isValidArrayIndex(key)) {
// 类似$vm.set(vm.$data.arr, 0, 3)
// 修改数组的长度, 避免索引>数组长度导致splcie()执行有误
//如果不设置length,splice时,超过原本数量的index则不会添加空白项
target.length = Math.max(target.length, key);
// 利用数组的splice变异方法触发响应式, 这个前面讲过
target.splice(key, 1, val);
return val;
}
//3.对象,且key不是原型上的属性处理
// target为对象, key在target或者target.prototype上。
// 同时必须不能在 Object.prototype 上
// 直接修改即可, 有兴趣可以看issue: https://github.com/vuejs/vue/issues/6845
if (key in target && !(key in Object.prototype)) {
target[key] = val;
return val;
}
// 以上都不成立, 即开始给target创建一个全新的属性
// 获取Observer实例
const ob = (target: any).__ob__;
// Vue 实例对象拥有 _isVue 属性, 即不允许给Vue 实例对象添加属性
// 也不允许Vue.set/$set 函数为根数据对象(vm.$data)添加属性
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== "production" &&
warn(
"Avoid adding reactive properties to a Vue instance or its root $data " +
"at runtime - declare it upfront in the data option."
);
return val;
}
//5.target是非响应式数据时
// target本身就不是响应式数据, 直接赋值
if (!ob) {
target[key] = val;
return val;
}
//6.target对象是响应式数据时
//定义响应式对象
defineReactive(ob.value, key, val);
//watcher执行
ob.dep.notify();
return val;
}
- 工具函数
// 判断给定变量是否是未定义,当变量值为 null时,也会认为其是未定义
export function isUndef(v: any): boolean %checks {
return v === undefined || v === null;
}
// 判断给定变量是否是原始类型值
export function isPrimitive(value: any): boolean %checks {
return (
typeof value === "string" ||
typeof value === "number" ||
// $flow-disable-line
typeof value === "symbol" ||
typeof value === "boolean"
);
}
// 判断给定变量的值是否是有效的数组索引
export function isValidArrayIndex(val: any): boolean {
const n = parseFloat(String(val));
return n >= 0 && Math.floor(n) === n && isFinite(val);
}
- 关于(ob && ob.vmCount)
export function observe(value: any, asRootData: ?boolean): Observer | void {
// 省略...
if (asRootData && ob) {
// vue已经被Observer了,并且是根数据对象, vmCount才会++
ob.vmCount++;
}
return ob;
}
- 在初始化 Vue 的过程中有
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
//opts.data为对象属性
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}
- initData(vm)
function initData(vm: Component) {
let data = vm.$options.data;
data = vm._data = typeof data === "function" ? getData(data, vm) : data || {};
// 省略...
// observe data
observe(data, true /* asRootData */);
}
从源码可以看出 set 主要逻辑如下:
- 类型判断
- target 为数组:调用 splice 方法
- target 为对象,且 key 不是原型上的属性处理:直接修改
- target 不能是 Vue 实例,或者 Vue 实例的根数据对象,否则报错
- target 是非响应式数据时,我们就按照普通对象添加属性的方式来处理
- target 为响应数据,且key 为新增属性,我们 key 设置为响应式,并手动触发其属性值的更新
总结
vm.$set(target、key、value)
- 当 target 为数组时,直接调用数组方法 splice 实现;
- 如果目标是对象,会先判读属性是否存在、对象是否是响应式
- 最终如果要对属性进行响应式处理,则是通过调用 defineReactive 方法进行响应式处理
- defineReactive 方法就是 Vue 在初始化对象时,给对象属性采用 Object.defineProperty 动态添加 getter 和 setter 的功能所调用的方法
9.深拷贝如何解决循环引用?
公司:极光推送
分类:JavaScript
查看解析
循环引用问题
看个例子
function deepCopy(obj){
const res = Array.isArray(obj) ? [] : {};
for(let key in obj){
if(typeof obj[key] === 'object'){
res[key] = deepCopy(obj[key]);
}else{
res[key] = obj[key];
}
}
return res
}
var obj = {
a:1,
b:2,
c:[1,2,3],
d:{aa:1,bb:2},
};
obj.e = obj;
console.log('obj',obj); // 不会报错
const objCopy = deepCopy(obj);
console.log(objCopy); //Uncaught RangeError: Maximum call stack size exceeded
从例子可以看到,当存在循环引用的时候,deepCopy会报错,栈溢出。
- obj对象存在循环引用时,打印它时是不会栈溢出
- 深拷贝obj时,才会导致栈溢出
循环应用问题解决
- 即:目标对象存在循环应用时报错处理
大家都知道,对象的key是不能是对象的。
{{a:1}:2}
// Uncaught SyntaxError: Unexpected token ':'
参考解决方式一:使用weekmap:
解决循环引用问题,我们可以额外开辟一个存储空间,来存储当前对象和拷贝对象的对应关系
这个存储空间,需要可以存储key-value
形式的数据,且key
可以是一个引用类型,
我们可以选择 WeakMap
这种数据结构:
- 检查
WeakMap
中有无克隆过的对象 - 有,直接返回
- 没有,将当前对象作为
key
,克隆对象作为value
进行存储 - 继续克隆
function isObject(obj) {
return (typeof obj === 'object' || typeof obj === 'function') && obj !== null
}
function cloneDeep(source, hash = new WeakMap()) {
if (!isObject(source)) return source;
if (hash.has(source)) return hash.get(source); // 新增代码,查哈希表
var target = Array.isArray(source) ? [] : {};
hash.set(source, target); // 新增代码,哈希表设值
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
if (isObject(source[key])) {
target[key] = cloneDeep(source[key], hash); // 新增代码,传入哈希表
} else {
target[key] = source[key];
}
}
}
return target;
}
参考解决方式二:
可以用 Set,发现相同的对象直接赋值,也可用 Map
const o = { a: 1, b: 2 };
o.c = o;
function isPrimitive(val) {
return Object(val) !== val;
}
const set = new Set();
function clone(obj) {
const copied = {};
for (const [key, value] of Object.entries(obj)) {
if (isPrimitive(value)) {
copied[key] = value;
} else {
if (set.has(value)) {
copied[key] = { ...value };
} else {
set.add(value);
copied[key] = clone(value);
}
}
}
return copied;
}
10.单元测试如何测试?代码覆盖率如何?
公司:编程猫
分类:工程化
查看解析
一、为什么要单元测试?
有单元测试加持可以保障交付代码质量,增强自己和他人的信心。我们选择第三方库的时候不也是会优先选择有测试保障的吗?未来对代码进行改动时也可以节省回归测试的时间。
二、怎么测?
在做单元测试时尽量以集成测试为主,对少量难以被集成测试覆盖或需要分发的代码做单元测试,同时也可以有少量的端到端测试辅助。
尽量不测试代码实现,测试代码实现可能会让测试用例很快就失效。比如断言变量,当变量名发生变更时会导致测试不通过,但是可能功能并没有发生改变。
2.1 要写些什么样的测试?
以用户视角测试程序的功能,而非上帝视角。
对一个组件,传入不同参数渲染 dom ,对用户而言可能可以看到某些特定文字或可以做某些操作。此时可以断言 dom 是否出现了某些字,做动作(如点击、输入 、提交表单等)是否有正确的响应。
2.2 不要写什么样的测试?
不要测试实现细节。比如以上帝视角检查 redux store
上的数据、state
的数据等,而这些在最终用户眼里是不存在的,用户能感知的只是所表现的功能。
三、测试框架和周边配套
Jest
是 facebook 出品的测试框架。开箱即用,自带断言库、mock、覆盖率报告等功能。
由于前端要测试 UI,需要在模拟浏览器环境中渲染出 dom,所以需要一个这样的库。存在很多这样的库,常用的有 Enzyme
、@testing-library/react
。
四、测试覆盖/效率报告
Jest
自带测试报告,但是众多的项目分散在 gitlab 中给查看报告带来了麻烦。需要考虑有一个集中的地方查看测试报告。这里结合了 sonar
和 reportportal
归集测试报告,可以通过一个集中的地方查看所有项目的测试报告。
其中结合 sonar
的代码扫描功能可以查看测试覆盖率等报告信息。reportportal
可以查看测试执行率,另外官方宣称自带 AI 分析报告,可以得出多维度的统计信息。
11.说说 React 状态逻辑复用问题
公司:编程猫
分类:React
查看解析
React 状态逻辑复用
一、Mixins
虽然 React 本身有些函数式味道,但为了迎合用户习惯,早期只提供了 React.createClass() API 来定义组件: 自然而然地,(类)继承就成了一种直觉性的尝试。而在 JavaScript 基于原型的扩展模式下,类似于继承的 Mixin 方案就成了首选:
// 定义Mixin
var Mixin1 = {
getMessage: function () {
return "hello world";
},
};
var Mixin2 = {
componentDidMount: function () {
console.log("Mixin2.componentDidMount()");
},
};
// 用Mixin来增强现有组件
var MyComponent = React.createClass({
mixins: [Mixin1, Mixin2],
render: function () {
return <div>{this.getMessage()}</div>;
},
});
但存在诸多缺陷
组件与 Mixin 之间存在隐式依赖(Mixin 经常依赖组件的特定方法,但在定义组件时并不知道这种依赖关系)多个 Mixin 之间可能产生冲突(比如定义了相同的 state 字段)Mixin 倾向于增加更多状态,这降低了应用的可预测性(The more state in your application, the harder it is to reason about it.),导致复杂度剧增。
隐式依赖导致依赖关系不透明,维护成本和理解成本迅速攀升:难以快速理解组件行为,需要全盘了解所有依赖 Mixin 的扩展行为,及其之间的相互影响。组件自身的方法和 state 字段不敢轻易删改,因为难以确定有没有 Mixin 依赖它 Mixin 也难以维护,因为 Mixin 逻辑最后会被打平合并到一起,很难搞清楚一个 Mixin 的输入输出。
毫无疑问,这些问题是致命的 所以,React v0.13.0 放弃了 Mixin(继承),转而走向 HOC(组合)。
二、Higher - Order Components
// 定义高阶组件
var Enhance = (ComposedComponent) =>
class extends Component {
constructor() {
this.state = { data: null };
}
componentDidMount() {
this.setState({ data: "Hello" });
}
render() {
return <ComposedComponent {...this.props} data={this.state.data} />;
}
};
class MyComponent {
render() {
if (!this.data) return <div>Waiting...</div>;
return <div>{this.data}</div>;
}
}
// 用高阶组件来增强普通组件,进而实现逻辑复用
export default Enhance(MyComponent);
理论上,只要接受组件类型参数并返回一个组件的函数都是高阶组件((Component, ...args) => Component
),但为了方便组合,推荐Component => Component
形式的 HOC,通过偏函数应用来传入其它参数,例如:React Redux's connect
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
优点:
- 组件树结构 降低耦合和复杂度;
- 代码复用,逻辑抽象化
- 渲染劫持,属性代理,劫持组件的 props 和 state
- 装饰器,可以作为装饰器来使用;
- 函数柯里化
缺点:HOC 虽然没有那么多致命问题,但也存在一些小缺陷:
- 扩展性限制
- 不要在 render 中使用,每次 render 会重新创建一个高阶组件,导致组件和子组件状态丢失,影响性能;
- 静态方法会丢失,新组件没有静态方法,需要手动处理;
- refs 不会往下传递,需要使用 forwardRef
- 多次嵌套,增加复杂度和理解成本;
- 未使用命名空间的话,可能出现命名冲突,覆盖旧属性;
- 不可见性,不知道外面包了啥,黑盒;
三、Render Props
“render prop” 是指⼀种在 React 组件之间使⽤⼀个值为函数的 prop 共享代码的简单技术;
class Mouse extends React.Component {
constructor(props) {
super(props);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.state = { x: 0, y: 0 };
}
handleMouseMove(event) {
this.setState({
x: event.clientX,
y: event.clientY,
});
}
render() {
return (
<div style={{ height: "100%" }} onMouseMove={this.handleMouseMove}>
{/* Instead of providing a static representation of what <Mouse> renders, use the `render` prop to dynamically determine what to render. */}
{this.props.render(this.state)}
</div>
);
}
}
优点:数据共享、代码复⽤,将组件内的 state 作为 props 传递给调⽤者,将渲染逻辑交给调⽤者
缺点:⽆法在 return 语句外访问数据、嵌套写法不够优雅;
四、Hooks
function MyResponsiveComponent() {
const width = useWindowWidth();
// Our custom Hook
return <p>Window width is {width}</p>;
}
function useWindowWidth() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
});
return width;
}
比起上面提到的其它方案,Hooks 让组件内逻辑复用不再与组件复用捆绑在一起,是真正在从下层去尝试解决(组件间)细粒度逻辑的复用问题。
此外,这种声明式逻辑复用方案将组件间的显式数据流与组合思想进一步延伸到了组件内,契合 React 理念。
优点如下:
- 解决嵌套问题,简洁,代码量更少: React Hooks 解决了 HOC 和 Render Props 的嵌套问题,更加简洁
- 解耦: React Hooks 可以更方便地把 UI 和状态分离,做到更彻底的解耦
- 组合: Hooks 中可以引用另外的 Hooks 形成新的 Hooks,组合变化万千
- 解决类组件的 3 个问题: React Hooks 为函数组件而生,从而解决了类组件的几大问题:
- this 指向容易错误
- 业务逻辑被分割在不同声明周期中,使得代码难以理解和维护
- 代码复用成本高(高阶组件容易使代码量剧增)
Hooks 也并非完美,只是就目前而言,其缺点如下:
- 还有两个类组件的生命周期函数不能用 hooks 替代,getSnapshotBeforeUpdate 和 componentDidCatch
- 额外的学习成本(Functional Component 与 Class Component 之间的困惑)
- 写法上有限制(不能在条件、循环、嵌套函数中使用),只能在函数顶层使用,增加了重构旧代码的成本;因为 react 需要利用调用顺序来更新状态和调用钩子函数;放到循环或条件分支中,可能导致调用顺序不一致,导致奇怪的 bug;
- 破坏了 PureComponent、React.memo 浅比较的性能优化效果(为了取最新的 props 和 state,每次 render()都要重新创建事件处函数)
- 在闭包场景可能会引用到旧的 state、props 值内部实现上不直观(依赖一份可变的全局状态,不再那么“纯”)
- React.memo 并不能完全替代 shouldComponentUpdate(因为拿不到 state change,只针对 props change)
- useState API 设计上不太完美
- 使用 useState 时,数组对象,使用 push、pop、splice 直接更新,无效;比如
let [nums, setNums] = useState([0,1,2]); nums.push(1) 无效,必须使用 nums=[...nums, 1]
,再 setNums(nums);类组件中直接 push 是没问题的 - 不能使用装饰器
- 函数组件 ref 需要 forwardRef
12.组件库设计有什么原则?
公司:编程猫
分类:JavaScript
查看解析
组件库设计原则
1.1 标准性
任何一个组件都应该遵守一套标准,可以使得不同区域的开发人员据此标准开发出一套标准统一的组件
1.2 独立性
- 描述了组件的细粒度,遵循单一职责原则,保持组件的纯粹性
- 属性配置等API对外开放,组件内部状态对外封闭,尽可能的少与业务耦合
1.3 复用与易用
- UI差异,消化在组件内部(注意并不是写一堆if/else)
- 输入输出友好,易用
1.4 适用SPOT法则
Single Point Of Truth,就是尽量不要重复代码,出自《The Art of Unix Programming》
1.5 避免暴露组件内部实现
1.6 避免直接操作DOM,避免使用ref
使用父组件的 state 控制子组件的状态而不是直接通过 ref 操作子组件
1.7 入口处检查参数的有效性,出口处检查返回的正确性
1.8 无环依赖原则(ADP)
1.9 稳定抽象原则(SAP)
- 组件的抽象程度与其稳定程度成正比,
- 一个稳定的组件应该是抽象的(逻辑无关的)
- 一个不稳定的组件应该是具体的(逻辑相关的)
- 为降低组件之间的耦合度,我们要针对抽象组件编程,而不是针对业务实现编程
1.10 避免冗余状态
- 如果一个数据可以由另一个 state 变换得到,那么这个数据就不是一个 state,只需要写一个变换的处理函数,在 Vue 中可以使用计算属性
- 如果一个数据是固定的,不会变化的常量,那么这个数据就如同 HTML 固定的站点标题一样,写死或作为全局配置属性等,不属于 state
- 如果兄弟组件拥有相同的 state,那么这个state 应该放到更高的层级,使用 props 传递到两个组件中
1.11合理的依赖关系
父组件不依赖子组件,删除某个子组件不会造成功能异常
1.12 扁平化参数
除了数据,避免复杂的对象,尽量只接收原始类型的值
1.13 良好的接口设计
- 把组件内部可以完成的工作做到极致,虽然提倡拥抱变化,但接口不是越多越好
- 如果常量变为 props 能应对更多的场景,那么就可以作为 props,原有的常量可作为默认值。
- 如果需要为了某一调用者编写大量特定需求的代码,那么可以考虑通过扩展等方式构建一个新的组件。
- 保证组件的属性和事件足够的给大多数的组件使用。
1.14 API尽量和已知概念保持一致
13.写出代码输出值,并说明原因
function F() {
this.a = 1;
}
var obj = new F();
console.log(obj.prototype);
公司:富途
分类:JavaScript
查看解析
答案
undefined
参考分析
构造函数实例一般没有prototype属性。除了Function构造函数
只有函数才有prototype属性,这个属性值为一个object对象 实例对象时没有这个属性
实例对象通过__proto__这个内部属性([[prototype]]
)来串起一个原型链的,通过这个原型链可以查找属性,
方法 通过new操作符初始化一个函数对象的时候就会构建出一个实例对象,
函数对象的prototype属性指向的对象就是这个实例对象的原型对象,也就是__proto__指向的对象
经典与原型链图
14.把 10 万次 for 循环的代码插到 html 中间,会有什么现象?出现卡顿现象怎么解决?添加 defer 属性之后脚本会在什么时候执行?采用 defer 之后,用户点击页面会怎么样?如果禁用 WebWoker,还有其他方法吗?
公司:富途
分类:JavaScript
查看解析
一、十万次循环代码插入 body 中,页面会出现卡顿
十万次循环代码插入 body 中,页面会出现卡顿,代码后的 DOM 节点加载不出来
二、解决
设置 script 标签 defer 属性,浏览器其它线程将下载脚本,待到文档解析完成脚本才会执行。
三、采用 defer 之后,用户点击问题
-
若 button 中的点击事件在 defer 脚本前定义,则在 defer 脚本加载完后,响应点击事件。
-
若 button 中的点击事件在 defer 脚本后定义,则用户点击 button 无反应,待脚本加载完后,再次点击有响应。
-
代码示例
<!-- test.html -->
<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<div class="test1">test1</div>
<div id="hello"></div>
<script>
// 待defer脚本下载完成后响应
function alertMsg() {
alert("123");
}
</script>
<input type="button" id="button1" onclick="alertMsg()" />
<script src="./test.js" defer></script>
<div class="test2">test2</div>
</body>
<style>
.test1 {
color: red;
font-size: 50px;
}
.test2 {
color: yellow;
font-size: 50px;
}
</style>
</html>
// test.js
for (let i = 0; i < 100000; i++) {
console.log(i);
}
document.getElementById("hello").innerHTML = "hello world";
四、如果禁用 WebWoker,还有其他方法吗?
4.1 使用 Concurrent.Thread.js
- Concurrent.Thread.js 用来模拟多线程,进行多线程开发。
Concurrent.Thread.create(function () {
$("#test").click(function () {
alert(1);
});
for (var i = 0; i < 100000; i++) {
console.log(i);
}
});
4.2 使用虚拟列表
若该情形是渲染十万条数据的情况下,则可以使用虚拟列表。虚拟列表即只渲染可视区域的数据,使得在数据量庞大的情况下,减少 DOM 的渲染,使得列表流畅地无限滚动。
实现方案:
基于虚拟列表是渲染可视区域的特性,我们需要做到以下三点
- 需计算顶部和底部不可视区域留白的高度,撑起整个列表高度,使其高度与没有截断数据时一样,这两个高度分别命名为 topHeight、bottomHeight
- 计算截断开始位置 start 和结束位置 end,则可视区域的数据为 list.slice(start,end)
- 滚动过程中需不断更新 topHeight、bottomHeight、start、end,从而更新可视区域视图。当然我们需要对比老旧 start、end 来判断是否需要更新。
topHeight 的计算比较简单,就是滚动了多少高度,topHeight=scrollTop。
start 的计算依赖于 topHeight 和每项元素的高度 itemHeight,假设我们向上移动了两个列表项,则 start 为 2,如此,我们有 start = Math.floor(topHeight / itemHeight)
。
end 的计算依赖于屏幕的高度能显示多少个列表项,我们称之为 visibleCount,则有 visibleCount = Math.ceil(clientHeight / itemHeight)
,向上取整是为了避免计算偏小导致屏幕没有显示足够的内容,则 end = start + visibleCount
。
bottomHeight 需要我们知道整个列表没有被截断前的高度,减去其顶部的高度,计算顶部的高度有了 end 就很简单了,假设我们的整个列表项的数量为 totalItem,则 bottomHeight = (totalItem - end - 1) \* itemHeight
。
会出现的问题:
但是当这样实现的时候,会发现有两个问题:
- 滚动的时候可视区域的顶部或者底部会出现留白。
- 每次滚动到需要把空白处替换成实际的列表项的时候,页面会出现抖动,这个原因是每个列表项高度不一致,要替换的时候,替换的列表项比 itemHeight 大或者小,并且是在可见区域内替换的,浏览器就会抖动下,这个解决办法可以通过把替换的时机提前,即在我们不可见的顶部进行替换。
来分析下,对于第一个问题,会出现留白的情况,那么我们可以在顶部或者底部预留一定的位置,而第二个问题,也是可以通过在顶部和底部预留一定的空间,所以解决这个问题只要一个方案就可以解决了,那就是顶部和底部都预留一定的位置。
假设 reserveTop 为顶部预留的位置数,reserveBottom 为底部预留的位置数,那么我们上面的数据的计算就要重新定义了,具体如何计算,请看下图。
reserveTop 和 reserveBottom 尽量大点(当然也不要太大),或者知道列表项的最高高度为多少,就按这个最高高度来。当你发现你滚动的时候顶部有留白,就调大 reserveTop 的数值,当你发现滚动的时候底部有留白,那就调大 reserveBottom 的数值。
15.有 100 瓶水,其中有一瓶有毒,小白鼠只要尝一点带毒的水 3 天后就会死亡,至少要多少只小白鼠才能在 3 天内鉴别出哪瓶水有毒?
公司:富途
分类:其它
查看解析
答案
7 只
分析
每个老鼠只有死或活 2 种状态,因此每个老鼠可以看作一个 bit,取 0 或 1N 个老鼠可以看作 N 个 bit,可以表达 2^N 种状态(其中第 n 个状态代表第 n 个瓶子有毒)因此所有老鼠能表示的状态数能大于等于 100 即可。
代码实现
let n = 1;
while (Math.pow(2, n) < 100) {
n++;
}
console.log(n);
通俗点的理解:
给 100 个瓶分别标上如下标签(7 位长度): 0000001 (第 1 瓶) 0000010 (第 2 瓶) 0000011 (第 3 瓶) ...... 1100100 (第 100 瓶)
从编号最后 1 位是 1 的所有的瓶子里面取出 1 滴混在一起(比如从第一瓶,第三瓶,。。。里分别取出一滴混在一起)并标上记号为 1。以此类推,从编号第一位是 1 的所有的瓶子里面取出 1 滴混在一起并标上记号为 7。现在得到有 7 个编号的混合液,小白鼠排排站,分别标上 7,6,。。。1 号,并分别给它们灌上对应号码的混合液。三天过去了,过来验尸吧:
从左到右,死了的小白鼠贴上标签 1,没死的贴上 0,最后得到一个序号,把这个序号换成 10 进制的数字,就是有毒的那瓶水的编号。
检验一下:假如第一瓶有毒,按照 0000001 (第 1 瓶),说明第 1 号混合液有毒,因此小白鼠的生死符为 0000001(编号为 1 的小白鼠挂了),0000001 二进制标签转换成十进制=1 号瓶有毒;假如第三瓶有毒,0000011 (第 3 瓶),第 1 号和第 2 号混合液有毒,因此小白鼠的生死符为 0000011(编号为 1,2 的鼠兄弟挂了),0000011 二进制标签转换成十进制=3 号瓶有毒。
所以结果就是 2^7 = 128 >= 100,至少需要 7 只小白鼠。
16.Promise.allSettled 了解吗?手写 Promise.allSettled
公司:快手
分类:JavaScript
查看解析
Promise.allSettled(iterable)概念
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
const allSettledPromise = Promise.allSettled([resolved, rejected]);
allSettledPromise.then(function (results) {
console.log(results);
});
// [
// { status: 'fulfilled', value: 42 },
// { status: 'rejected', reason: -1 }
// ]
Promise.allSettled()
方法接受一组Promise
实例作为参数,返回一个新的 Promise 实例。- 只有等到所有这些参数实例都返回结果,不管是
fulfilled
还是rejected
,包装实例才会结束。 - 返回的新
Promise
实例,一旦结束,状态总是fulfilled
,不会变成rejected
。 - 新的
Promise
实例给监听函数传递一个数组results
。该数组的每个成员都是一个对象,对应传入Promise.allSettled
的 Promise 实例。每个对象都有 status 属性,对应着fulfilled
和rejected
。fulfilled
时,对象有value
属性,rejected
时有reason
属性,对应两种状态的返回值。 - 有时候我们不关心异步操作的结果,只关心这些操作有没有结束时,这个方法会比较有用。
手写实现
const formatSettledResult = (success, value) =>
success
? { status: "fulfilled", value }
: { status: "rejected", reason: value };
Promise.all_settled = function (iterators) {
const promises = Array.from(iterators);
const num = promises.length;
const resultList = new Array(num);
let resultNum = 0;
return new Promise((resolve) => {
promises.forEach((promise, index) => {
Promise.resolve(promise)
.then((value) => {
resultList[index] = formatSettledResult(true, value);
if (++resultNum === num) {
resolve(resultList);
}
})
.catch((error) => {
resultList[index] = formatSettledResult(false, error);
if (++resultNum === num) {
resolve(resultList);
}
});
});
});
};
const resolved = Promise.resolve(42);
const rejected = Promise.reject(-1);
Promise.all_settled([resolved, rejected]).then((results) => {
console.log(results);
});
17.浏览器为什么要阻止跨域请求?如何解决跨域?每次跨域请求都需要到达服务端吗?
公司:快手
分类:JavaScript
查看解析
一、什么是跨域
跨域是针对浏览器的“同源策略”提出的说法。之所以有“同源策略”这种模式是基于网络安全方面的考虑。所谓的同源策略关注三点:
- 协议 (
http:www.baidu.com & https.www.baidu.com http
协议不同,跨域) - 域名 (
https://www.aliyun.com & https://developer.aliyun.com
域名不同,跨域) - 端口 (
http://localhost:8080 & http://localhost:8000
端口号不同,跨域)
二、哪些网络资源涉及到跨域
“同源策略”对于跨域网络资源的设定非常的清晰。
这些场景涉及到跨域禁止操作:
- 无法获取非同源网页的 cookie、localstorage 和 indexedDB。
- 无法访问非同源网页的 DOM (iframe)。
- 无法向非同源地址发送 AJAX 请求 或 fetch 请求(可以发送,但浏览器拒绝接受响应)。
为什么要阻止跨域呢?上文我们说过是基于安全策略:比如一个恶意网站的页面通过 iframe 嵌入了银行的登录页面(二者不同源),如果没有同源限制,恶意网页上的 javascript 脚本就可以在用户登录银行的时候获取用户名和密码。
三、如何解决跨域
针对跨越问题我们该如何解决,主流的方案有以下:
1、 通过 jsonp 跨域 2、 document.domain + iframe 跨域 3、 location.hash + iframe 4、 window.name + iframe 跨域 5、 postMessage 跨域 6、 跨域资源共享(CORS) 7、 nginx 代理跨域 8、 nodejs 中间件代理跨域 9、 WebSocket 协议跨域
四、关于跨域需要明确的问题
跨域并非浏览器限制了发起跨站请求,而是跨站请求可以正常发起,但是返回结果被浏览器拦截了。
每次需求都会发出,服务器端也会做出响应,只是浏览器端在接受响应的时候会基于同源策略进行拦截。
注意:有些浏览器不允许从 HTTPS 的域跨域访问 HTTP,比如 Chrome 和 Firefox,这些浏览器在请求还未发出的时候就会拦截请求,这是一个特例。
18.浏览器缓存了解吗?强缓存一般存放在哪里?计算整个文件得到 etag 会耗费性能,怎么解决?如果我不想要使用缓存了,每次都请求最新的,怎么做?no-store 和 no-cache 的区别是什么?
公司:快手
分类:网络&安全
查看解析
一、浏览器缓存
浏览器缓存主要分为四个阶段:
- 强制缓存阶段:先在本地查找该资源,如果有发现该资源,而且该资源还没有过期,就使用这一个资源,完全不会发送 http 请求到服务器。
- 协商缓存阶段:如果在本地缓存找到对应的资源,但是不知道该资源是否过期或者已经过期,则发一个 http 请求到服务器,然后服务器判断这个请求,如果请求的资源在服务器上没有改动过,则返回 304,让浏览器使用本地找到的那个资源。
- 启发式缓存阶段:当缓存过期时间的字段一个都没有的时候,浏览器下次并不会直接进入协商阶段,而是先进入启发式缓存阶段,它根据响应头中 2 个时间字段 Date 和 Last-Modified 之间的时间差值,取其值的 10%作为缓存时间周期。也就是说,当存有 Last-Modified 字段的时候,即使是断网,且强缓存都失效后,也有一定时间是直接读取缓存文件的。etag 是没有这个阶段的。
- 缓存失败阶段:当服务器发现请求的资源已经修改过,或者这是一个新的请求(再本来没有找到资源),服务器则返回该资源的数据,并且返回 200, 当然这个是指找到资源的情况下,如果服务器上没有这个资源,则返回 404。
二、强缓存一般放在哪里
强缓存一般存放于 Memory Cache 或者 Disk Cache。
三、计算整个文件得到 etag 会耗费性能,怎么解决
etag 可以通过文件的 Last-Modified 和 content-length 计算。
Nginx官方默认的ETag计算方式是为"文件最后修改时间16进制-文件长度16进制"。例:ETag: “59e72c84-2404”
注意:
不管怎么样的算法,在服务器端都要进行计算,计算就有开销,会带来性能损失。因此为了榨干这一点点性能,不少网站完全把Etag禁用了(比如Yahoo!),这其实不符合HTTP/1.1的规定,因为HTTP/1.1总是鼓励服务器尽可能的开启Etag。
四、不使用缓存的方式,让每次请求都是最新的
不使用缓存常见的方法是通过 url 拼接 random 的方式或者设置 Cache-Control 设置 no-cache。
五、no-stroe & no-cache
- no-store 禁止浏览器和中间服务器缓存。每次都从服务器获取。
- 注意,no-store 才是真正的完完全全的禁止本地缓存。
- no-cache 每次请求都会验证该缓存是否过期。可以在本地缓存,可以在代理服务器缓存,但是这个缓存要服务器验证才可以使用
- 注意,no-cache 不是不缓存的意思。
19.说一下平时项目是怎么优化的?优化之后是怎么度量的?首屏时间是怎么计算的?
公司:快手
分类:其它
查看解析
针对每个过程进行优化
网页从加载到呈现会经历一系列过程,针对每个过程进行优化
- 网络连接
- 请求优化
- 响应优化
- 浏览器渲染
通过 performance.timing
API,可以获取各个阶段的执行时间:
{
navigationStart: 1578537857229; //上一个文档卸载(unload)结束时的时间戳
unloadEventStart: 1578537857497; //表征了unload事件抛出时的时间戳
unloadEventEnd: 1578537857497; //表征了unload事件处理完成时的时间戳
redirectStart: 0; // 重定向开始时的时间戳
redirectEnd: 0; //重定向完成时的时间戳
fetchStart: 1578537857232; //准备好HTTP请求来获取(fetch)文档的时间戳
domainLookupStart: 1578537857232; //域名查询开始的时间戳
domainLookupEnd: 1578537857232; //域名查询结束的时间戳
connectStart: 1578537857232; //HTTP请求开始向服务器发送时的时间戳
connectEnd: 1578537857232; //浏览器与服务器之间的连接建立时的时间戳
secureConnectionStart: 0; //安全链接的握手时的U时间戳
requestStart: 1578537857253; //HTTP请求(或从本地缓存读取)时的时间戳
responseStart: 1578537857491; //服务器收到(或从本地缓存读取)第一个字节时的时间戳。
responseEnd: 1578537857493; //响应结束
domLoading: 1578537857504; //DOM结构开始解析时的时间戳
domInteractive: 1578537858118; //DOM结构结束解析、开始加载内嵌资源时的时间戳
domContentLoadedEventStart: 1578537858118; //DOMContentLoaded 事件开始时间戳
domContentLoadedEventEnd: 1578537858118; //当所有需要立即执行的脚本已经被执行(不论执行顺序)时的时间戳
domComplete: 1578537858492; //当前文档解析完成的时间戳
loadEventStart: 1578537858492; //load事件被发送时的时间戳
loadEventEnd: 1578537858494; //当load事件结束时的时间戳
}
1.1 网络连接方面优化
主要是针对重定向、DNS、TCP 连接进行优化
- 避免重定向
- DNS 查找优化:页面采用预解析
dns-prefetch
,同时将同类型的资源放到一起,减少domain
数量也是可以减少 DNS 查找 - 使用 CDN(内容分发网络)
- HTTP/1.1 版本,客户端可以通过
Keep-Alive
选项和服务器建立长连接,让多个资源通过一个 TCP 连接传输。
1.2 请求方面优化
减少浏览器向浏览器发送的请求数目以及请求资源的大小是请求优化的核心思想
- 合理使用文件的压缩和合并
- 合理运用浏览器对于资源并行加载的特性,在资源的加载的数量和资源的大小之间做一个合理的平衡
- 在移动端页面中,将首屏的请求资源控制在 5 个以内,每个资源在 Gzip 之后的大小控制在 28.5KB 之内,可以显著的提升首屏时间。
- 压缩图片,使用雪碧图,小图片使用 Base64 内联
- 组件延迟加载
- 给 Cookie 瘦身
- 静态资源使用 CDN 等方式放在和当前域不同的域上,以避免请求静态资源时携带 Cookie
- 善用 CDN 提升浏览器资源加载能力
- 资源分散到多个不同的 CDN 中,最大化的利用浏览器的并行加载能力
- 合理运用缓存策略缓存静态资源,Ajax 响应等
- 利用 Manifest + 本地存储做持久化缓存
- 将对访问实时性要求不高的其他资源,如图片、广告脚本等内容存放在 IndexDB 或 WebSQL 中,IndexDB 后 WebSQL 的存储容量比 LocalStorage 大得多,可以用来存放这些资源。
- 使用 localForage 操作持久化缓存
- 库文件放入 CDN 或者开启强缓
1.3 响应优化
- 优化服务端处理流程,如使用缓存、优化数据库查询、减少查询次数等
- 优化响应资源的大小,如对响应的资源开启 Gzip 压缩等。
1.3.1 页面加载的核心指标
- TTFB
- 首个字节
- FP
- 首次绘制,只有 div 跟节点,对应 vue 生命周期的 created
- FCP
- 首次有内容的绘制,页面的基本框架,但是没有数据内容,对应 vue 生命周期的 mounted
- FMP
- 首次有意义的绘制,包含所有元素和数据内容,对应 vue 生命周期的 updated
- TTI
- 首次能交互时间
- Long Task
>=50ms 的任务
- SSR&CSR
- 服务端渲染和客户端渲染
- Isomorphic javascript
- 同构化
1.4 浏览器首屏渲染优化
1.4.1 首屏时间:
指用户打开网站开始,到浏览器首屏内容渲染完成的时间。对于用户体验来说,首屏时间是用户对一个网站的重要体验因素。通常一个网站,如果首屏时间在 5 秒以内是比较优秀的,10 秒以内是可以接受的,10 秒以上就不可容忍了。超过 10 秒的首屏时间用户会选择刷新页面或立刻离开。
1.4.2首屏时间计算:
- 首屏模块标签标记法
通常适用于首屏内容不需要通过拉取数据才能生存以及页面不考虑图片等资源加载的情况,我们会在 HTML 文档中对应首屏内容的标签结束位置,使用内联的 JavaScript 代码记录当前时间戳。如下所示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>首屏</title>
<script type="text/javascript">
window.pageStartTime = Date.now();
</script>
<link rel="stylesheet" href="common.css" />
<link rel="stylesheet" href="page.css" />
</head>
<body>
<!-- 首屏可见模块1 -->
<div class="module-1"></div>
<!-- 首屏可见模块2 -->
<div class="module-2"></div>
<script type="text/javascript">
window.firstScreen = Date.now();
</script>
<!-- 首屏不可见模块3 -->
<div class="module-3"></div>
<!-- 首屏不可见模块4 -->
<div class="module-4"></div>
</body>
</html>
此时首屏时间等于 firstScreen - performance.timing.navigationStart
事实上首屏模块标签标记法 在业务中的情况比较少,大多数页面都需要通过接口拉取数据才能完整展示
- 统计首屏内加载最慢的图片的时间:
通常我们首屏内容加载最慢的就是图片资源,因此我们会把首屏内加载最慢的图片的时间当做首屏的时间。
DOM 树构建完成后将会去遍历首屏内的所有图片标签,并且监听所有图片标签 onload 事件,最终遍历图片标签的加载时间的最大值,并用这个最大值减去 navigationStart
即可获得近似的首屏时间。
此时首屏时间等于 加载最慢的图片的时间点 - performance.timing.navigationStart
- 自定义首屏内容计算法
由于统计首屏内图片完成加载的时间比较复杂。因此我们在业务中通常会通过自定义模块内容,来简化计算首屏时间。如下面的做法:
- 忽略图片等资源加载情况,只考虑页面主要 DOM
- 只考虑首屏的主要模块,而不是严格意义首屏线以上的所有内容
1.4.3首屏优化方案:
- 页面直出:骨架屏或者 SSR
- 首帧渲染优化
- 资源动态加载
- 浏览器缓存
- 优化 JavaScript 脚本执行时间
- 减少重排重绘
- 硬件加速提升动画性能等页面渲染方面的优化方案
1.5浏览器渲染优化
- 优化 JavaScript 脚本执行时间
- 减少重排重绘
- 硬件加速提升动画性能等
20.怎么计算组件在视口内出现了几次?IntersectionObserver 怎么使用的?怎么知道一个 DOM 节点出现在视口内?
公司:快手
分类:JavaScript
查看解析
一、监听 Scroll
要了解某个元素是否进入了"视口"(viewport),即用户能不能看到它,传统的实现方法是,监听到scroll
事件后,调用目标元素的getBoundingClientRect()
方法,得到它对应于视口左上角的坐标,再判断是否在视口之内。然后声明一个全局变量,每出现一次就加一,就可以得出在视口出现了几次。这种方法的缺点是,由于scroll
事件密集发生,计算量很大,容易造成性能问题。
于是便有了 IntersectionObserver API
二、IntersectionObserver
2.1 API
var io = new IntersectionObserver(callback, option);
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的observe
方法可以指定观察哪个 DOM 节点。
// 开始观察
io.observe(document.getElementById("example"));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
上面代码中,observe
的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
io.observe(elementA);
io.observe(elementB);
2.2 Callback 参数
目标元素的可见性变化时,就会调用观察器的回调函数callback
。
callback
一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。
var io = new IntersectionObserver((entries) => {
console.log(entries);
});
callback
函数的参数(entries
)是一个数组,每个成员都是一个IntersectionObserverEntry
对象。如果同时有两个被观察的对象的可见性发生变化,entries
数组就会有两个成员。
IntersectionObserverEntry
对象提供目标元素的信息,一共有六个属性。
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
每个属性的含义如下。
time
:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒target
:被观察的目标元素,是一个 DOM 节点对象rootBounds
:根元素的矩形区域的信息,getBoundingClientRect()
方法的返回值,如果没有根元素(即直接相对于视口滚动),则返回null
boundingClientRect
:目标元素的矩形区域的信息intersectionRect
:目标元素与视口(或根元素)的交叉区域的信息intersectionRatio
:目标元素的可见比例,即intersectionRect
占boundingClientRect
的比例,完全可见时为1
,完全不可见时小于等于0
2.3 Option 对象
IntersectionObserver
构造函数的第二个参数是一个配置对象。它可以设置以下属性。
2.3.1 threshold 属性:
threshold
属性决定了什么时候触发回调函数。它是一个数组,每个成员都是一个门槛值,默认为[0]
,即交叉比例(intersectionRatio
)达到0
时触发回调函数。
new IntersectionObserver(
(entries) => {
/* ... */
},
{
threshold: [0, 0.25, 0.5, 0.75, 1],
}
);
用户可以自定义这个数组。比如,[0, 0.25, 0.5, 0.75, 1]
就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
2.3.2 root 属性、rootMargin 属性:
很多时候,目标元素不仅会随着窗口滚动,还会在容器里面滚动(比如在iframe
窗口里滚动)。容器内滚动也会影响目标元素的可见性。
IntersectionObserver API 支持容器内滚动。root
属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。
var opts = {
root: document.querySelector(".container"),
rootMargin: "500px 0px",
};
var observer = new IntersectionObserver(callback, opts);
上面代码中,除了root
属性,还有rootMargin属性。后者定义根元素的margin
,用来扩展或缩小rootBounds
这个矩形的大小,从而影响intersectionRect
交叉区域的大小。它使用 CSS 的定义方法,比如10px 20px 30px 40px
,表示 top、right、bottom 和 left 四个方向的值。
这样设置以后,不管是窗口滚动或者容器内滚动,只要目标元素可见性变化,都会触发观察器。
---------------------------------- 一条讲武德的分割线 ------------------------------
因掘金发文的字数限制,剩下的答案大家扫码即可查看。
21.versions 是一个项目的版本号列表,因多人维护,不规则,动手实现一个版本号处理函数
var versions = ["1.45.0", "1.5", "6", "3.3.3.3.3.3.3"];
// 要求从小到大排序,注意'1.45'比'1.5'大
function sortVersion(versions) {
// TODO
}
// => ['1.5','1.45.0','3.3.3.3.3.3','6']
公司:头条
分类:JavaScript
22.什么是微服务,微服务跟单体应用的区别是啥,用微服务有啥好处?
分类:Node
23.动手实现一个 repeat 方法
function repeat(func, times, wait) {
// TODO
}
const repeatFunc = repeat(alert, 4, 3000);
// 调用这个 repeatFunc ("hellworld"),会alert4次 helloworld, 每次间隔3秒
公司:头条
分类:JavaScript
24.请列出目前主流的 JavaScript 模块化实现的技术有哪些?说出它们的区别?
公司:玄武科技
分类:JavaScript
25.请描述下 JavaScript 中 Scope、Closure、Prototype 概念,并说明 JavaScript 封装、继承实现原理。
公司:玄武科技
分类:JavaScrip