每日一题 - 202405
5-31
如何压缩前端项目中 JS 的体积
terser或者uglify,及流行的使用 Rust 编写的swc压缩混淆化 JS。gzip或者brotli压缩,在网关处(nginx)开启- 使用
webpack-bundle-analyzer分析打包体积,替换占用较大体积的库,如moment->dayjs - 使用支持 Tree-Shaking 的库,对无引用的库或函数进行删除,如
lodash->lodash/es - 对无法 Tree Shaking 的库,进行按需引入模块,如使用
import Button from 'antd/lib/Button',此处可手写babel-plugin自动完成,但不推荐 - 使用 babel (css 为 postcss) 时采用
browserlist,越先进的浏览器所需要的 polyfill 越少,体积更小 - code spliting,路由懒加载,只加载当前路由的包,按需加载其余的 chunk,首页 JS 体积变小 (PS: 次条不减小总体积,但减小首页体积)
- 使用 webpack 的 splitChunksPlugin,把运行时、被引用多次的库进行分包,在分包时要注意避免某一个库被多次引用多次打包。此时分为多个 chunk,虽不能把总体积变小,但可提高加载性能 (PS: 此条不减小总体积,但可提升加载性能)
- 去除多余字符,eg:空格,换行、注释
- 使用更简单的表达,eg:合并声明、布尔值简化
5-30
prefetch 与 preload 的区别是什么
preload 提供了一种声明式的命令,让浏览器提前加载指定资源(加载后并不执行),在需要执行的时候再执行。提供的好处主要是
- 将加载和执行分离开,可不阻塞渲染和 document 的 onload 事件
- 提前加载指定资源,不再出现依赖的font字体隔了一段时间才刷出
<link rel="prefetch" href="style.css" as="style" />
<link rel="preload" href="main.js" as="script" />preload优先级高,是告诉浏览器页面必定需要的资源,浏览器一定会加载这些资源。一般对于 Bundle Spliting 资源与 Code Spliting 资源做 preloadprefetch优先级低,是告诉浏览器页面可能需要的资源,浏览器不一定会加载这些资源。一般用以加载其它路由资源,如当页面出现 Link,可 prefetch 当前 Link 的路由资源。(next.js 默认会对 link 做懒加载+prefetch,即当某条 Link 出现页面中,即自动 prefetch 该 Link 指向的路由资源
若不确定资源是必定会加载的,则不要错误使用 preload,以免本末倒置,给页面带来更沉重的负担。
当然,可以在 PC 中使用 preload 来刷新资源的缓存,但在移动端则需要特别慎重,因为可能会浪费用户的带宽。
preload 和 prefetch混用的话,并不会复用资源,而是会重复加载。若 css 中有应用于已渲染到 DOM 树的元素的选择器,且设置了
@font-face规则时,会触发字体文件的加载。 而字体文件加载中时,DOM 中的这些元素,是处于不可见的状态。对已知必加载的 font 文件进行预加载,除了有性能提升外,更有体验优化的效果。
5-29
简述 node/v8 中的垃圾回收机制
v8 中的垃圾回收机制分为三种
Scavenge,工作在新生代,把from space中的存活对象移至to spaceMark-Sweep,标记清除。新生代的某些对象由于过度活跃会被移至老生代,此时对老生代中活对象进行标记,并清理死对象Mark-Compact,标记整理。
当一个函数执行结束之后,JavaScript 引擎会通过向下移动 ESP 来销毁该函数保存在栈中的执行上下文。 要回收堆中的垃圾数据,就需要用到 JavaScript 中的垃圾回收器了。 代际假说(The Generational Hypothesis),是垃圾回收领域中一个重要的术语,后续垃圾回收的策略都是建立在该假说的基础之上的,所以很是重要。 代际假说有以下两个特点:
大部分对象在内存中存在的时间很短,简单来说,就是很多对象一经分配内存,很快就变得不可访问;
是不死的对象,会活得更久。 在 V8 中会把堆分为新生代和老生代两个区域,新生代中存放的是生存时间短的对象,老生代中存放的生存时间久的对象。
● 副垃圾回收器,主要负责新生代的垃圾回收。
● 主垃圾回收器,主要负责老生代的垃圾回收。
全停顿
增量标记
使用增量标记算法,可以把一个完整的垃圾回收任务拆分为很多小的任务,这些小的任务执行时间比较短,可以穿插在其他的 JavaScript 任务中间执行,增强用户体验。
5-28
JS如何在url中传递数组
在 URL 中如何传递数组这种复杂的数据,完全取决于项目中前后端成员关于复杂数据在 URL 中传输的约定,一般情况下可以使用以下方式来传递数组:
a = 3 & a = 4 & a = 5;
a = 3, 4, 5;
a[] = 3 & a[] = 4 & a[] = 5;
a[0] = 3 & a[1] = 4 & a[2] = 5;但同样,需要后端开发者写一个
querystring.parse来对指定的格式解析进行支持,同时也有对各种复杂 qs 支持较好的 package,如:qs: 据说是对 querystring 复杂对象解析最好的库
5-27
Vue3中的ref、toRef和toRefs
ref:接收一个内部值,生成对应的响应式数据,该内部值挂载在ref对象的value属性上;该对象可以用于模版和reactive。使用ref是为了解决值类型在setup、computed、合成函数等情况下的响应式丢失问题。
toRef:为响应式对象(reactive)的一个属性创建对应的ref,且该方式创建的ref与源属性保持同步。
toRefs:将响应式对象转换成普通对象,对象的每个属性都是对应的ref,两者间保持同步。使用toRefs进行对象解构。
function ref(val) {
const wrapper = {value: val}
Object.defineProperty(wrapper, '__v_isRef', {value: true})
return reactive(wrapper)
}
function toRef(obj, key) {
const wrapper = {
get value() {
return obj[key]
},
set value(val) {
obj[key] = val
}
}
Object.defineProperty(wrapper, '__v_isRef', {value: true})
return wrapper
}
function toRefs(obj) {
const ret = {}
for (const key in obj) {
ret[key] = toRef(obj, key)
}
return ret
}
// 自动脱ref
function proxyRefs(target) {
return new Proxy(target, {
get(target, key, receiver) {
const value = Reflect.get(target, key, receiver)
return value.__v_isRef ? value.value : value
},
set(target, key, newValue, receiver) {
const value = target[key]
if(value.__v_isRef) {
value.value = newValue
return true
}
return Reflect.set(target, key, newValue, receiver)
}
})
}5-26
TypeScript中any、unknown、never
any和unkonwn在TS类型中属于最顶层的Top Type,即所有的类型都是它俩的子类型。
never则相反,它作为Bottom Type是所有类型的子类型。
5-25
浏览器的事件循环机制
JavaScript是单线程的(指的是js引擎在执行代码的时候只有一个主线程,每次只能干一件事),同时还是非阻塞运行的(执行异步任务的时候,会先挂起相应任务,待异步返回结果再执行回调)
在js代码执行时,会将对象存在堆(heap)中,在栈(stack)中存放一些基础类型变量和对象的指针。在执行方法时,会根据当前方法的执行上下文,来进行一个执行。对于普通函数就是正常的入栈出栈即可,涉及到异步任务的时候,js执行会将对应的任务放到事件队列中(微任务队列、宏任务队列)。
- 常见微任务:queueMicrotask、Promise、MutationObserve等。
- 常见宏任务:ajax、setTimeout、setInterval、script(js整体代码)、IO操作、UI交互、postMessage等。
故事件循环可以理解为是一个桥梁,连接着应用程序的js和系统调用之间的通道。其过程为:
- 执行一个宏任务(一般为一段script),若没有可选的宏任务,就直接处理微任务。
- 执行中遇到微任务,就将其添加到微任务的任务队列中。
- 执行中遇到宏任务,就将其提交到宏任务队列中。
- 执行完当前执行的宏任务后,去查询当前有无需要执行的微任务,有就执行
- 检查渲染,若需要渲染,浏览器执行渲染任务
- 渲染完毕后,Js线程会去执行下一个宏任务。。。(如此循环)
console.log("script start");
const promiseA = new Promise((resolve, reject) => {
console.log("init promiseA");
resolve("promiseA");
});
const promiseB = new Promise((resolve, reject) => {
console.log("init promiseB");
resolve("promiseB");
});
setTimeout(() => {
console.log("setTimeout run");
promiseB.then(res => {
console.log("promiseB res :>> ", res);
});
console.log("setTimeout end");
}, 500);
promiseA.then(res => {
console.log("promiseA res :>> ", res);
});
queueMicrotask(() => {
console.log("queue Microtask run");
});
console.log("script end");
// script start
// init promiseA
// init promiseB
// script end
// promiseA res :>> promiseA
// queue Microtask run
// setTimeout run
// setTimeout end
// promiseB res :>> promiseB5-24
箭头函数和普通函数的区别
箭头函数不会创建自身的this,只会从上一级继承this,箭头函数的this在定义的时候就已经确认了,之后不会改变。同时箭头函数无法作为构造函数使用,没有自身的prototype,也没有arguments。
this.id = "global";
console.log("this.id :>> ", this.id); // this.id :>> global
function normalFun() {
return this.id;
}
const arrowFun = () => {
return this.id;
};
const newNormal = new normalFun();
console.log("newNormal :>> ", newNormal); // newNormal :>> normalFun {}
try {
const newArrow = new arrowFun();
} catch (error) {
console.log("error :>> ", error); // error :>> TypeError: arrowFun is not a constructor
}
console.log("normalFun :>> ", normalFun()); // normalFun :>> undefined
console.log("arrowFun() :>> ", arrowFun()); // arrowFun() :>> global
const obj = {
id: "obj",
normalFun,
arrowFun,
};
const normalFunBindObj = normalFun.bind(obj);
const arrowFunBindObj = arrowFun.bind(obj);
console.log("normalFun.call(obj) :>> ", normalFun.call(obj)); // normalFun.call(obj) :>> obj
console.log("normalFunBindObj() :>> ", normalFunBindObj()); // normalFunBindObj() :>> obj
console.log("arrowFun.call(obj) :>> :>> ", arrowFun.call(obj)); // arrowFun.call(obj) :>> :>> global
console.log("arrowFunBindObj() :>> ", arrowFunBindObj()); // arrowFunBindObj() :>> global
console.log("obj.normalFun() :>> ", obj.normalFun()); // obj.normalFun() :>> obj
console.log("obj.arrowFun() :>> ", obj.arrowFun()); // obj.arrowFun() :>> global5-23
实现一个类似关键字new功能的函数
在js中new关键字主要做了:首先创建一个空对象,这个对象会作为执行new构造函数之后返回的对象实例,将创建的空对象原型(__proto__)指向构造函数的prototype属性,同时将这个空对象赋值给构造函数内部的this,并执行构造函数逻辑,根据构造函数的执行逻辑,返回初始创建的对象或构造函数的显式返回值。
function newFn(...args) {
const constructor = args.shift();
const obj = Object.create(constructor.prototype);
const result = constructor.apply(obj, args);
return typeof result === "object" && result !== null ? result : obj;
}
function Person(name) {
this.name = name;
}
const p = newFn(Person, "Jerome");
console.log("p.name :>> ", p.name); // p.name :>> Jerome5-22
数组的forEach和map方法的区别
forEach是对数组的每一个元素执行一次给定的函数。
map是创建一个新数组,该新数组由原数组的每个元素都调用一次提供的函数返回值。
const arr = [1,2,3,4,5,6];
arr.forEach(x =>{
x = x + 1;
console.log("x :>> ", x);
})
// x :>> 2
// x :>> 3
// x :>> 4
// x :>> 5
// x :>> 6
// x :>> 7
console.log("arr :>> ", arr); // arr :>> [1,2,3,4,5,6]
const mapArr = arr.map(x =>{
x = x * 2;
return x;
})
console.log("mapArr :>> ", mapArr); // mapArr :>> [2,4,6,8,10,12]
console.log("arr :>> ", arr); // arr :>> [1,2,3,4,5,6]pop():删除数组后面的最后一个元素,返回值为被删除的那个元素。
push():将一个元素或多个元素添加到数组末尾,并返回新的长度。
shift():删除数组中的第一个元素,并返回被删除元素的值。
unshift():将一个或多个元素添加到数组的开头,并返回该数组的新长度。
splice():通过删除或替换现有元素或者原地添加新的元素来修改数组,并以数组形式返回被修改的内容。
reverse(): 反转数组。
5-21
TS中type和interface的区别
interface可以重复声明,type不行,继承方式不一样,type使用交叉类型方式,interface使用extends实现。在对象扩展的情况下,使用接口继承要比交叉类型的性能更好。建议使用interface来描述对象对外暴露的借口,使用type将一组类型重命名(或对类型进行复杂编程)。
interface iMan {
name: string;
age: number;
}
// 接口可以进行声明合并
interface iMan {
hobby: string;
}
type tMan = {
name: string;
age: number;
};
// type不能重复定义
// type tMan = {}
// 继承方式不同,接口继承使用extends
interface iManPlus extends iMan {
height: string;
}
// type继承使用&,又称交叉类型
type tManPlus = { height: string } & tMan;
const aMan: iManPlus = {
name: "aa",
age: 15,
height: "175cm",
hobby: "eat",
};
const bMan: tManPlus = {
name: "bb",
age: 15,
height: "150cm",
};5-20
理解provide与inject
1、provide和inject是一对新的API,用于在父组件中提供数据,然后在子组件中注入数据。
2、provide:是一个对象,或者是一个返回对象的函数。里面呢就包含要给子孙后代的东西,也就是属性和属性值。
3、inject:一个字符串数组,或者是一个对象。属性值可以是一个对象,包含from和default默认值。
//在父组件中,使用provide提供数据:
//name:定义提供 property的 name。
//value :property的值。
setup(){
provide('info',"值")
}
//在子组件中,使用inject注入数据
//name:接收 provide提供的属性名。
//default:设置默认值,可以不写,是可选参数。
setup(){
const info = inject("info")
inject('info',"设置默认值")
return {
info
}
}provide和inject只能在setup函数中使用,而且provide提供的数据只能在其子组件中使用。如果要在兄弟组件中共享数据,可以使用一个共享的对象或者使用Vuex等状态管理库。
5-19
js-tool-big-box工具包
js-tool-big-box工具主要解决防抖(debounce)和节流(throttle)的公共方法
防抖:
<template>
<div>
<input @keyup="handleChange" v-model="inputVal" />
</div>
</template>
<script>
import { eventBox } from 'js-tool-big-box';
export default {
data() {
return {
inputVal: ''
}
},
created() {
this.myDebounce = eventBox.debounce((data) => {
this.sendAjax(data);
}, 2000);
},
methods: {
handleChange(event) {
const val = event.target.value;
this.myDebounce(val);
},
sendAjax(data) {
console.log('发送时间::', new Date().getTime());
console.log('发送请求:', data);
},
}
}
</script>节流:
<script>
import { eventBox } from 'js-tool-big-box';
export default {
name: 'dj',
data () {
return {
inputVal: ''
}
},
created() {
this.myThrottle = eventBox.throttle((data) => {
this.sendAjax(data);
}, 2000);
},
methods: {
handleChange(event) {
const val = event.target.value;
this.myThrottle(val);
},
sendAjax(data) {
console.log('发送时间::', new Date().getTime());
console.log('发送请求:', data);
},
}
}
</script>5-18
Flutter状态管理
以下是常用的状态管理框架:
1、state状态管理
@override
InheritedWidget inheritFromWidgetOfExactType(Type targetType, { Object aspect }) {
/// 在共享 map _inheritedWidgets 中查找
final InheritedElement ancestor = _inheritedWidgets == null ? null : _inheritedWidgets[targetType];
if (ancestor != null) {
/// 返回找到的 InheritedWidget ,同时添加当前 element 处理
return inheritFromElement(ancestor, aspect: aspect);
}
_hadUnsatisfiedDependencies = true;
return null;
}
@override
InheritedWidget inheritFromElement(InheritedElement ancestor, { Object aspect }) {
_dependencies ??= HashSet<InheritedElement>();
_dependencies.add(ancestor);
/// 就是将当前 element(this) 添加到 _dependents 里
/// 也就是 InheritedElement 的 _dependents
/// _dependents[dependent] = value;
ancestor.updateDependencies(this, aspect);
return ancestor.widget;
}
@override
void notifyClients(InheritedWidget oldWidget) {
for (Element dependent in _dependents.keys) {
notifyDependent(oldWidget, dependent);
}
}2、Provider状态管理
优点: 不复杂,好理解,代码量不大的情况下,可以方便组合和控制刷新颗粒度 , 其实一开始官方也有一个 flutter-provide ,不过后来无了, Provider成了它的替代品。
缺点:相对依赖Flutter 和 Widget;需要依赖Context
class _ProviderPageState extends State<ProviderPage> {
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(builder: (_) => ProviderModel()),
],
child: Scaffold(
appBar: AppBar(
title: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
var counter = Provider.of<ProviderModel>(context);
return new Text("Provider ${counter.count.toString()}");
},
)
),
body: CountWidget(),
),
);
}
}
class CountWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Consumer<ProviderModel>(builder: (context, counter, _) {
return new Column(
children: <Widget>[
new Expanded(child: new Center(child: new Text(counter.count.toString()))),
new Center(
child: new FlatButton(
onPressed: () {
counter.add();
},
color: Colors.blue,
child: new Text("+")),
)
],
);
});
}
}
class ProviderModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void add() {
_count++;
notifyListeners();
}
}3、Bloc状态管理
BLoC算是 Flutter 早期比较知名的状态管理框架,它同样是存在 bloc 和 flutter_bloc 这样的依赖关系,它是基于事件驱动来实现的状态管理。
优点:代码更加解耦,这是事件驱动的特性,把状态更新和事件绑定,可以灵活得实现状态拦截,重试甚至撤回
缺点:需要写更多的代码,开发节奏会有点影响,接收代码的新维护人员,缺乏有效文档时容易陷入对着事件和业务蒙圈,项目后期事件容易混乱交织
BlocSelector<BlocA, BlocAState, SelectedState>(
selector: (state) {
// return selected state based on the provided state.
},
builder: (context, state) {
// return widget here based on the selected state.
},
)
MultiBlocListener(
listeners: [
BlocListener<BlocA, BlocAState>(
listener: (context, state) {},
),
BlocListener<BlocB, BlocBState>(
listener: (context, state) {},
),
BlocListener<BlocC, BlocCState>(
listener: (context, state) {},
),
],
child: ChildA(),
)4、flutter_redux状态管理
优点:解耦,对 redux 开发友好,适合中大型项目里协作开发
缺点:影响开发速度,要写一堆模版,不是很贴合 Flutter 开发思路
5、GetX状态管理
优点:瑞士军刀式护航;对新人友好;可以减少很多代码
缺点:全家桶,做的太多对于一些使用者来说是致命缺点,需要解决的 Bug 也多;“魔法”使用较多,脱离 Flutter 原本轨迹;入侵性极强
5-17
了解Flutter
Flutter是由Google推出的开源UI软件开发工具包,用于构建原生、精美的移动、web和桌面应用。它使用Dart语言作为开发语言,并通过自己的渲染引擎绘制UI。
Flutter的优势:
- 提高开发效率
- 同一份代码开发iOS和Android
- 用更少的代码做更多的事情
- 轻松迭代
- 在应用程序运行时更改代码并重新加载(通过热重载)
- 修复崩溃并继续从应用程序停止的地方进行调试
- 创建美观,高度定制的用户体验
- 受益于使用Flutter框架提供的丰富的Material Design和Cupertino(iOS风格)的widget
- 实现定制、美观、品牌驱动的设计,而不受原生控件的限制
5-16
理解keep-alive
keep-alive 是 Vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染 ,其有以下特性:
- 一般结合路由和动态组件一起使用,用于缓存组件;
- 提供 include 和 exclude 属性,两者都支持字符串或正则表达式, include 表示只有名称匹配的组件会被缓存,exclude 表示任何名称匹配的组件都不会被缓存 ,其中 exclude 的优先级比 include 高;
- 对应两个钩子函数 activated 和 deactivated ,当组件被激活时,触发钩子函数 activated,当组件被移除时,触发钩子函数 deactivated。
5-15
Vue 的父组件和子组件生命周期钩子函数执行顺序
Vue 的父组件和子组件生命周期钩子函数执行顺序可以归类为以下 4 部分:
加载渲染过程
父 beforeCreate -> 父 created -> 父 beforeMount -> 子 beforeCreate -> 子 created -> 子 beforeMount -> 子 mounted -> 父 mounted
子组件更新过程
父 beforeUpdate -> 子 beforeUpdate -> 子 updated -> 父 updated
父组件更新过程
父 beforeUpdate -> 父 updated
销毁过程
父 beforeDestroy -> 子 beforeDestroy -> 子 destroyed -> 父 destroyed
5-14
Vue 生命周期
Vue 实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载 Dom -> 渲染、更新 -> 渲染、卸载等一系列过程,我们称这是 Vue 的生命周期。
| 生命周期 | 描述 |
|---|---|
| beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
| created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
| beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
| mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
| beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
| update | 组件数据更新之后 |
| activited | keep-alive 专属,组件被激活时调用 |
| deactivated | keep-alive 专属,组件被销毁时调用 |
| beforeDestory | 组件销毁前调用 |
| destoryed | 组件销毁后调用 |
5-13
JS 双色球机选一注
描述
双色球由33个红球和16个蓝球组成,1注双色球包括6个不重复的红球和1个蓝球。 请阅读给出的页面和代码,完成 randomFn 函数,实现“随机一注”功能,要求如下:
函数返回:
`1.以字符串形式输出“随机一注”结果,选中的红蓝球用"|"隔开,红球在前,号码间用半角逗号隔开,如"06,10,13,18,23,27|05"
2.红球和蓝球号码排列顺序 需与页面展示的顺序对应`
页面交互:
1.将选中的红球和蓝球(页面中对应DOM元素)用class="active"高亮
2.将选中的球按号码从小到大排列,移至所属组的前方,结果如示意图所示
3.每次执行 randomFn 函数,输出符合要求且不完全重复
注意:
1、请使用原生JavaScript操作DOM元素,不要增加、删除DOM元素或修改css
2、请使用ES5语法
3、答题时不要使用第三方插件
4、运行浏览器为chrome浏览器
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<!-- 填写标签 -->
<div class="main">
<div class="balls red">
<span>红球</span>
<div class="balls-wp">
<b>01</b>
<b>02</b>
<b>03</b>
<b>04</b>
<b>05</b>
<b>06</b>
<b>07</b>
<b>08</b>
<b>09</b>
<b>10</b>
<b>11</b>
<b>12</b>
<b>13</b>
<b>14</b>
<b>15</b>
<b>16</b>
<b>17</b>
<b>18</b>
<b>19</b>
<b>20</b>
<b>21</b>
<b>22</b>
<b>23</b>
<b>24</b>
<b>25</b>
<b>26</b>
<b>27</b>
<b>28</b>
<b>29</b>
<b>30</b>
<b>31</b>
<b>32</b>
<b>33</b>
</div>
</div>
<div class="balls blue">
<span>蓝球</span>
<div class="balls-wp">
<b>01</b>
<b>02</b>
<b>03</b>
<b>04</b>
<b>05</b>
<b>06</b>
<b>07</b>
<b>08</b>
<b>09</b>
<b>10</b>
<b>11</b>
<b>12</b>
<b>13</b>
<b>14</b>
<b>15</b>
<b>16</b>
</div>
</div>
</div>
<script type="text/javascript">
// 填写JavaScript
randomFn();
function randomFn() {
let redballs = document.querySelectorAll(".red .balls-wp b")
let blueballs = document.querySelectorAll(".blue .balls-wp b")
let reddiv = document.querySelector(".red .balls-wp")
let bluediv = document.querySelector(".blue .balls-wp")
let red = []
let redB = []
while(red.length<6) {
let num=Math.floor(Math.random()*33)+1
if(!red.includes(num)) {
red.push(num)
let redb = redballs[num-1]
redb.classList.add("active")
redB.push(redb)
}
}
let blue = Math.floor(Math.random()*16)+1
let blueB = blueballs[blue-1]
//不能直接接在后面写 因为add返回的是undefined
blueB.classList.add("active")
//按照从大到小 指的是b标签的内部
redB = redB.sort((a,b)=>b.innerHTML-a.innerHTML)
for(let i=0;i<6;i++) {
redballs = document.querySelectorAll('.red .balls-wp b')
//insertBefore是如果原来是子节点则将子节点移动到对应节点前面(删除原本位置)
//反之如果原来不是子节点 则将新节点插入到对应节点前面
reddiv.insertBefore(redB[i],redballs[0])
}
bluediv.insertBefore(blueB,blueballs[0])
//因为涉及到06这种 而redb只是纯数值而已
return redB.map(ball => ball.innerHTML).reverse().join(',') + '|' + blueB.innerHTML
}
</script>
</body>
</html>5-12
JS 购物车
HTML模块为一个简化版的购物车,tbody为商品列表,tfoot为统计信息,系统会随机在列表中生成一些初始商品信息
1、请完成add函数,在列表后面显示items商品信息。参数items为{name: String, price: Number}组成的数组
2、请完成bind函数,点击每一行的删除按钮(包括通过add增加的行),从列表中删除对应行
3、请注意同步更新统计信息,价格保留小数点后两位
4、列表和统计信息格式请与HTML示例保持一致
5、不要直接手动修改HTML中的代码
6、不要使用第三方库
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
</head>
<body>
<!-- 填写标签 -->
<table id="jsTrolley">
<thead><tr><th>名称</th><th>价格</th><th>操作</th></tr></thead>
<tbody>
<tr><td>产品1</td><td>10.00</td><td><a href="javascript:void(0);">删除</a></td></tr>
<tr><td>产品2</td><td>30.20</td><td><a href="javascript:void(0);">删除</a></td></tr>
<tr><td>产品3</td><td>20.50</td><td><a href="javascript:void(0);">删除</a></td></tr>
</tbody>
<tfoot><tr><th>总计</th><td colspan="2">60.70(3件商品)</td></tr></tfoot>
</table>
<script type="text/javascript">
// 填写JavaScript
// 新增行
function add(items) {
var tbody = document.getElementsByTagName('tbody')[0]
var tfoot = document.getElementsByTagName('tfoot')[0]
// 获取初始数据
let count = tbody.children.length
let price = parseFloat(tfoot.innerText.match(/\d+.\d+/)[0])
// 新增行
let tr = ''
for (let i = 0; i < items.length; i++) {
count += 1
price += items[i].price
tr += `<tr><td>${items[i].name}</td><td>${items[i].price.toFixed(2)}</td><td><a href="javascript:void(0);">删除</a></td></tr>`
}
tbody.innerHTML += tr
tfoot.innerHTML = `<tr><th>总计</th><td colspan="2">${price.toFixed(2)}(${count}件商品)</td></tr>`
}
// 绑定事件,事件代理
function bind() {
var tbody = document.getElementsByTagName('tbody')[0]
var tfoot = document.getElementsByTagName('tfoot')[0]
tbody.addEventListener('click', function (e) {
let num = tbody.children.length
// 过滤点击的是否是a标签
if (e.target.tagName === "A") {
// 获取数据
let price = parseFloat(e.target.parentElement.parentElement.innerHTML.match(/\d+.\d+/)[0])
let total = tfoot.innerHTML.match(/\d+.\d+/)[0]
e.target.parentElement.parentElement.remove()
tfoot.innerHTML = `<tr><th>总计</th><td colspan="2">${(total - price).toFixed(2)}(${num - 1}件商品)</td></tr>`
}
})
}
// 执行绑定事件
bind()
</script>
</body>
</html>5-11
JS 数组排序
请补全JavaScript代码,根据预设代码中的数组,实现以下功能:
- 列表只展示数组中的name属性
- 实现点击"销量升序"按钮,列表内容按照销量升序重新渲染
- 实现点击"销量降序"按钮,列表内容按照销量降序重新渲染
注意:
- 必须使用DOM0级标准事件(onclick)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
</head>
<body>
<button class='up'>销量升序</button>
<button class='down'>销量降序</button>
<ul></ul>
<script>
var cups = [
{ type: 1, price: 100, color: 'black', sales: 3000, name: '牛客logo马克杯' },
{ type: 2, price: 40, color: 'blue', sales: 1000, name: '无盖星空杯' },
{ type: 4, price: 60, color: 'green', sales: 200, name: '老式茶杯' },
{ type: 3, price: 50, color: 'green', sales: 600, name: '欧式印花杯' }
]
var ul = document.querySelector('ul');
var upbtn = document.querySelector('.up');
var downbtn = document.querySelector('.down');
// 补全代码
function ulRender() {
ul.innerHTML = cups.map(v => `<li>${v.name}</li>`).join('');
}
upbtn.onclick = function () {
cups.sort((a, b) => a.sales - b.sales);
ulRender();
}
downbtn.onclick = function () {
cups.sort((a, b) => b.sales - a.sales);
ulRender();
}
</script>
</body>
</html>5-10
JS模块编程题
题目:
完成函数 createModule,调用之后满足如下要求:
1、返回一个对象
2、对象的 greeting 属性值等于 str1, name 属性值等于 str2
3、对象存在一个 sayIt 方法,该方法返回的字符串为 greeting属性值 + ', ' + name属性值
题解:
function createModule(str1, str2) {
let res = {
greeting: str1,
name: str2,
}
res.sayIt = function() {
return this.greeting + ', ' + this.name;
}
return res;
}5-9
computed 和 watch 的区别?
computed: 是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch: 更多的是「观察」的作用,类似于某些数据的监听回调 ,每当监听的数据变化时都会执行回调进行后续操作;
5-8
webpack的构建流程?
1、运行流程
webpack 的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来
在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条webpack机制中,去改变webpack的运作,使得整个系统扩展性良好
从启动到结束会依次执行以下三大步骤:
- 初始化流程:从配置文件和
Shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数 - 编译构建流程:从 Entry 发出,针对每个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理
- 输出流程:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到文件系统

2、初始化流程
从配置文件和 Shell 语句中读取与合并参数,得出最终的参数
配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件
webpack 将 webpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins
完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控者webpack声明周期,不执行具体的任务,只是进行一些调度工作
3、编译构建流程
根据配置中的 entry 找出所有的入口文件
module.exports = {
entry: './src/file.js'
}初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:
compile开始编译make从入口点分析模块及其依赖的模块,创建这些模块对象build-module构建模块seal封装构建结果emit把各个chunk输出到结果文件
compile 编译
执行了run方法后,首先会触发compile,主要是构建一个Compilation对象
该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务的对象
make 编译模块
当完成了上述的compilation对象后,就开始从Entry入口文件开始读取,主要执行_addModuleChain()函数,如下:
_addModuleChain(context, dependency, onModule, callback) {
// 根据依赖查找对应的工厂函数
const Dep = /** @type {DepConstructor} */ (dependency.constructor);
const moduleFactory = this.dependencyFactories.get(Dep);
// 调用工厂函数NormalModuleFactory的create来生成一个空的NormalModule对象
moduleFactory.create({
dependencies: [dependency]
}, (err, module) => {
const afterBuild = () => {
this.processModuleDependencies(module, err => {
if (err) return callback(err);
callback(null, module);
});
};
this.buildModule(module, false, null, null, err => {
afterBuild();
})
})
}过程如下:
_addModuleChain中接收参数dependency传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create方法生成一个空的module对象
回调中会把此module存入compilation.modules对象和dependencies.module对象中,由于是入口文件,也会存入compilation.entries中
随后执行buildModule进入真正的构建模块module内容的过程
build module 完成模块编译
这里主要调用配置的loaders,将我们的模块转成标准的JS模块
在用Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 Webpack后面对代码的分析
从配置的入口模块开始,分析其 AST,当遇到require等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系
4、输出流程
seal 输出资源
seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码
webpack 中的 chunk ,可以理解为配置在 entry 中的模块,或者是动态引入的模块
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表
emit 输出完成
在确定好输出内容后,根据配置确定输出的路径和文件名
在 Compiler 开始生成文件前,钩子 emit 会被执行,这是我们修改最终文件的最后一个机会
从而webpack整个打包过程则结束了
5-7
bind、call、apply 区别?
call、apply、bind作用是改变函数执行时的上下文,简而言之就是改变函数运行时的this指向
apply
apply接受两个参数,第一个参数是this的指向,第二个参数是函数接受的参数,以数组的形式传入
改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
call
call方法的第一个参数也是this的指向,后面传入的是一个参数列表
跟apply一样,改变this指向后原函数会立即执行,且此方法只是临时改变this指向一次
bind
bind方法和call很相似,第一参数也是this的指向,后面传入的也是一个参数列表(但是这个参数列表可以分多次传入)
改变this指向后不会立即执行,而是返回一个永久改变this指向的函数
从上面可以看到,
apply、call、bind三者的区别在于:
- 三者都可以改变函数的
this对象指向- 三者第一个参数都是
this要指向的对象,如果如果没有这个参数或参数为undefined或null,则默认指向全局window- 三者都可以传参,但是
apply是数组,而call是参数列表,且apply和call是一次性传入参数,而bind可以分为多次传入bind是返回绑定this之后的函数,apply、call则是立即执行
5-6
如何理解this对象?
函数的 this 关键字在 JavaScript 中的表现略有不同,此外,在严格模式和非严格模式之间也会有一些差别
在绝大多数情况下,函数的调用方式决定了 this 的值(运行时绑定)
this 关键字是函数运行时自动生成的一个内部对象,只能在函数内部使用,总指向调用它的对象;同时,this在函数执行过程中,this一旦被确定了,就不可以再更改
function baz() {
// 当前调用栈是:baz
// 因此,当前调用位置是全局作用域
console.log( "baz" );
bar(); // <-- bar的调用位置
}
function bar() {
// 当前调用栈是:baz --> bar
// 因此,当前调用位置在baz中
console.log( "bar" );
foo(); // <-- foo的调用位置
}
function foo() {
// 当前调用栈是:baz --> bar --> foo
// 因此,当前调用位置在bar中
console.log( "foo" );
}
baz(); // <-- baz的调用位置绑定规则:
根据不同的使用场合,this有不同的值,主要分为下面几种情况:
默认绑定
严格模式下,不能将全局对象用于默认绑定,this会绑定到
undefined,只有函数运行在非严格模式下,默认绑定才能绑定到全局对象隐式绑定
函数还可以作为某个对象的方法调用,这时
this就指这个上级对象特殊情况:
javascriptvar o = { a:10, b:{ a:12, fn:function(){ console.log(this.a); //undefined console.log(this); //window } } } var j = o.b.fn; j();此时
this指向的是window,这里的大家需要记住,this永远指向的是最后调用它的对象,虽然fn是对象b的方法,但是fn赋值给j时候并没有执行,所以最终指向windownew绑定
通过构建函数
new关键字生成一个实例对象,此时this指向这个实例对象显示绑定
apply()、call()、bind()是函数的一个方法,作用是改变函数的调用对象。它的第一个参数就表示改变后的调用这个函数的对象。因此,这时this指的就是这第一个参数
5-5
如何理解闭包?
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围),这样的组合就是闭包(closure)
也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域
在 JavaScript中,每当创建一个函数,闭包就会在函数创建的同时被创建出来,作为函数内部与外部连接起来的一座桥梁
function init() {
var name = "Mozilla"; // name 是一个被 init 创建的局部变量
function displayName() { // displayName() 是内部函数,一个闭包
alert(name); // 使用了父函数中声明的变量
}
displayName();
}
init();displayName() 没有自己的局部变量。然而,由于闭包的特性,它可以访问到外部函数的变量
5-4
深拷贝浅拷贝的区别?
1、浅拷贝
浅拷贝,指的是创建新的数据,这个数据有着原始数据属性值的一份精确拷贝
如果属性是基本类型,拷贝的就是基本类型的值。如果属性是引用类型,拷贝的就是内存地址
即浅拷贝是拷贝一层,深层次的引用类型则共享内存地址
在JavaScript中,存在浅拷贝的现象有:
Object.assignArray.prototype.slice(),Array.prototype.concat()- 使用拓展运算符实现的复制
2、深拷贝
深拷贝开辟一个新的栈,两个对象属完成相同,但是对应两个不同的地址,修改一个对象的属性,不会改变另一个对象的属性
常见的深拷贝方式有:
- _.cloneDeep()
- jQuery.extend()
- JSON.stringify()
- 手写循环递归
3、区别

浅拷贝和深拷贝都创建出一个新的对象,但在复制对象属性的时候,行为就不一样
浅拷贝只复制属性指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存,修改对象属性会影响原对象;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象
- 浅拷贝是拷贝一层,属性为对象时,浅拷贝是复制,两个对象指向同一个地址
- 深拷贝是递归拷贝深层次,属性为对象时,深拷贝是新开栈,两个对象指向不同的地址
5-3
Vue3中Treeshaking特性?
Tree shaking 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去
而treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕。也就是说 ,tree shaking 其实是找出使用的代码
Tree shaking是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking无非就是做了两件事:
- 编译阶段利用
ES6 Module判断哪些模块已经加载 - 判断那些模块和变量未被使用或者引用,进而删除对应代码
通过
Tree shaking,Vue3给我们带来的好处是:
- 减少程序体积(更小)
- 减少程序执行时间(更快)
- 便于将来对程序架构进行优化(更友好)
5-2
SSR解决了什么问题?
SSR主要解决了以下两种问题:
- seo:搜索引擎优先爬取页面
HTML结构,使用ssr时,服务端已经生成了和业务想关联的HTML,有利于seo - 首屏呈现渲染:用户无需等待页面所有
js加载完成就可以看到页面视图(压力来到了服务器,所以需要权衡哪些用服务端渲染,哪些交给客户端)
但是使用SSR同样存在以下的缺点:
- 复杂度:整个项目的复杂度
- 库的支持性,代码兼容
- 性能问题
- 每个请求都是
n个实例的创建,不然会污染,消耗会变得很大 - 缓存
node serve、nginx判断当前用户有没有过期,如果没过期的话就缓存,用刚刚的结果。 - 降级:监控
cpu、内存占用过多,就spa,返回单个的壳
- 每个请求都是
- 服务器负载变大,相对于前后端分离服务器只需要提供静态资源来说,服务器负载更大,所以要慎重使用
所以在我们选择是否使用SSR前,我们需要慎重问问自己这些问题:
- 需要
SEO的页面是否只是少数几个,这些是否可以使用预渲染(Prerender SPA Plugin)实现 - 首屏的请求响应逻辑是否复杂,数据返回是否大量且缓慢
5-1
SSR是什么?
Server-Side Rendering 我们称其为SSR,意为服务端渲染
由服务侧完成页面的 HTML 结构拼接的页面处理技术,发送到浏览器,然后为其绑定状态与事件,成为完全可交互页面的过程
web的3个阶段的发展史:
- 传统服务端渲染SSR 网页内容在服务端渲染完成,⼀次性传输到浏览器 打开页面查看源码,浏览器拿到的是全部的dom结构
- 单页面应用SPA 单页应用优秀的用户体验,使其逐渐成为主流,页面内容由JS渲染出来,这种方式称为客户端渲染 打开页面查看源码,浏览器拿到的仅有宿主元素#app,并没有内容
- 服务端渲染SSR SSR解决方案,后端渲染出完整的首屏的dom结构返回,前端拿到的内容包括首屏及完整spa结构,应用激活后依然按照spa方式运行
Vue SSR是一个在SPA上进行改良的服务端渲染 通过Vue SSR渲染的页面,需要在客户端激活才能实现交互 Vue SSR将包含两部分:服务端渲染的首屏,包含交互的SPA


