每日一题 - 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 space
Mark-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 :>> promiseB
5-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() :>> global
5-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 :>> Jerome
5-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
时候并没有执行,所以最终指向window
new绑定
通过构建函数
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.assign
Array.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