由于最近比较忙活没时间学习新东西,现在得空想着能不能好好整理出一些有用的东西,让记忆深刻一点,免得到时候实习找工作面试的时候一问三不知,也希望大家能指正出错误和对大家有点帮助,一起进步,加油奥里给!!!
那么废话不多说直接进入正题,如果觉得可以家人们给个三连!!!
语义化指对文本内容的结构化(内容语义化),选择合乎语义的标签(代码语义化),便于开发者阅读,维护和写出更优雅的代码的同时,让浏览器的爬虫和辅助技术更好的解析。通过使用恰当语义的HTML标签,可以有效提高可访问性、可检索性、国际化和互用性。
优点:
新特性:
<header>、<footer>、<nav>、<aside>、<article>和<section>
<audio>、<video>
层叠样式表(Cascading Style Sheets,缩写为 CSS)是一种样式表语言,用来描述 HTML 或 XML(包括如 SVG、MathML 或 XHTML 之类的 XML 分支语言)文档的呈现。CSS 描述了在屏幕、纸质、音频等其他媒体上的元素应该如何被渲染的问题。CSS3 是 CSS(层叠样式表)技术的升级版本,CSS演进的一个主要变化就是W3C决定将CSS3分成一系列模块。
新特性:
常用选择器:
优先级顺序:
!important >
内联样式 >
ID选择器 >
类选择器/属性选择器/伪类选择器 >
元素选择器/伪元素选择器 >
关系选择器/通用选择器
!important
> 内联样式 > ID选择器 > 类别选择器、属性选择器、伪类选择器 > 元素选择器 > 通配符 > 继承
示例:
// 1. 使用ID选择器:
#main-nav {
background-color: blue;
}
// 2. 元素选择器:
div{
width:100%;
height:100%;
}
// 3. 使用类别选择器:
.button {
background-color: red;
}
// 4. 使用属性选择器:
a[href^="https://"] {
color: green;
}
// 5. 使用伪类选择器:
a:hover {
color: yellow;
}
// 6. 使用后代选择器:
.main-nav ul {
padding: 0;
}
// 7. 使用子元素选择器:
.main-nav > ul {
list-style-type: none;
}
// 8. 使用兄弟选择器:
h1 + p {
font-size: 16px;
}
// 9. 使用通用选择器:
* {
box-sizing: border-box;
}
CSS盒子模型(Box Model)是网页布局的基础,可以将页面上所有元素看作一个个矩形的盒子。这些盒子由四个部分组成:内容区(content)、内边距(padding)、边框(border)、外边距(margin)。CSS盒子模型分为标准盒子模型和怪异盒子模型,这两个概念与盒子模型的计算方式有关。
在CSS3中,通过box-sizing
属性可以控制盒子模型的计算方式。CSS3中的 box-sizing 属性有三个值:content-box,border-box和inherit。
总结来说,box-sizing属性用于控制盒子模型的计算方式,更改CSS盒子模型的大小计算方式,使得需要计算的尺寸更加精确和方便。
标准盒子模型是CSS2.1规范定义的,也被称为W3C盒子模型。在标准盒子模型下,一个元素的尺寸由其content(内容)的宽度、内边距padding、边框border和外边距margin四个部分组成。其中content的大小可以通过width和height属性进行设置,padding、border和margin的大小可以通过相应的属性进行设置。
标准盒子模型的计算公式如下:
总宽度 =
width + padding-left + padding-right + border-left-width + border-right-width + margin-left + margin-right
总高度 =
height + padding-top + padding-bottom + border-top-width + border-bottom-width + margin-top + margin-bottom
示例:
// HTML 代码为:
<div class="box">盒子模型</div>
// CSS 代码为:
.box {
width: 200px;
height: 100px;
padding: 10px;
border: 5px solid #000;
margin: 0 auto;
}
怪异盒子模型也被称为IE盒子模型,是IE5~IE6浏览器采用的盒子模型,由于该模型与标准盒子模型不同,因此被称为怪异盒子模型。
标准盒子模型的计算公式如下:
总宽度 = width + margin-left + margin-right
总高度 = height + margin-top + margin-bottom
也就是说,在怪异盒子模型中,内边距和边框的大小并没有算入元素的总尺寸。(既 width 已经包含了 padding 和 border 值)
示例:
// HTML 代码为:
<div class="box">测试盒子模型</div>
// CSS 代码为:
.box {
width: 200px;
height: 100px;
padding: 10px;
border: 5px solid #000;
margin: 0 auto;
box-sizing: border-box; /* 显示使用IE怪异盒子模型 */
}
解释:当设置一个元素的box-sizing属性为 border-box 时,即可使用怪异盒子模型进行盒子尺寸的计算,而采用其他值(如 content-box)则会使用标准盒子模型进行盒子尺寸的计算。
其实在默认的 content-box 模式下,盒子模型就是标准的盒子模型,元素的宽度和高度仅包含内容,不包括内边距(padding)、边框(border)和外边距(margin)。而使用设置为 border-box 的 box-sizing 属性时,元素的宽度和高度包括了内边距、边框和内容,但不包括外边距。
具体而言,在 content-box 模式下,当我们设置宽度为200px时,它并不包括 padding、border 和 margin 的尺寸,因此该元素的实际宽度可能会比我们期望的要大一些。而在 border-box 模式下,设置的宽度200px已经包含了 padding 和 border 的尺寸,因此该元素的实际宽度也就比较准确了。
总之,box-sizing 属性可以更好地控制元素的尺寸计算方式,让开发者更加方便地实现页面布局效果。在实际开发中,应根据实际需求和页面特点灵活选择使用哪种盒子模型计算方式。
BFC和IFC是CSS布局中的概念,他们分别代表“块级格式化上下文”和“内联格式化上下文”。
区别:
BFC是块级格式化上下文,它是一个独立的布局环境,其中块级盒子垂直排列。在BFC中,盒子的垂直边距会发生折叠,浮动元素也会参与高度计算。
IFC是行内格式化上下文,它是一种水平的格式化上下文,其中行内级盒子从左到右水平排列,直到一行被填满,然后换行。在IFC中,盒子的垂直对齐方式由vertical-align属性决定。行高由包含该行内级盒子中最高的盒子决定。
BFC(Block Formatting Context),即块级格式化上下文。指的是一个独立的块级渲染区域(布局环境),它具有一定的隔离特性,内部元素的定位、清除浮动、高度塌陷等计算方式与外部元素保持独立。
BFC的原理布局规则:
扩展:Box是CSS布局的对象和基本单位,一个页面是由很多个Box组成的。元素的类型和display属性决定了这个Box的类型。
BFC的生成规则有如下几条:
示例:
// HTML 代码为:
<div class="container">
<div class="box"></div>
<div class="box"></div>
</div>
// CSS 代码为:
.container {
border: 1px solid black;
overflow: hidden;
}
.box {
width: 100px;
height: 100px;
margin: 10px;
float: left;
background-color: lightblue;
}
解释:在这个示例中,我们创建了一个BFC来包裹两个浮动元素,通过设置 overflow:hidden,让它会触发BFC,在BFC中,BFC自适应高度,浮动元素也会参与高度计算,因此解决了浮动元素引发的高度塌陷问题。
应用场景:
扩展:如果页面布局造成了浮动塌陷,除了使用清除浮动(Clearfix)技术强制容器在浮动元素之后换行,还可以为容器设置一个触发BFC的样式,就是上面那个例子中为 container 设置了 overflow: hidde 的样式。
IFC指的是一个内联元素渲染区域,它是一种水平的格式化上下文,具有一定的隔离特性,同一个IFC内部的元素在渲染时互相影响,但与外部元素不产生任何影响。在IFC中,盒子从左到右水平排列,直到一行被填满,然后换行。行内级盒子的垂直对齐方式由 vertical-align 属性决定。行高由包含该行内级盒子中最高的盒子决定。
IFC中的布局规则包括:
IFC的生成规则有如下几条:
示例:
// HTML 代码为:
<div class="container">
<div class="box"></div>
<div class="box"></div>
<div class="box"></div>
</div>
// CSS 代码为:
.container {
border: 1px solid black;
width: 300px;
}
.box {
display: inline-block;
width: 100px;
height: 100px;
margin: 10px;
background-color: lightblue;
}
解释:这里没有用flex布局,有兴趣的可以自己试一试噢!在这个例子中,我们有一个包含三个盒子的容器。盒子被设置为display: inline-block,这使它们成为行内块级元素。由于容器的宽度只有300像素,所以第三个盒子会换行。
应用场景:
总而言之,BFC和IFC在CSS布局中扮演了至关重要的角色,可以解决很多常见的布局问题,对于理解CSS的渲染流程、排版规则有很大帮助。
在我的第一篇博客文章中有介绍了前端常见的十种布局方式,所以这里就不再详细介绍了,大家可以去看看,我就简单提一下就好了:
伪类和伪元素都是CSS选择器,它们用来选择文档树以外的元素,或者选择文档树中无法用简单选择器表示的状态。但它们之间有一些重要的区别。
伪类用来选择元素的特殊状态。例如,:hover伪类用来选择鼠标悬停在其上的元素,:focus伪类用来选择获得焦点的元素。伪类通常用于添加一些特殊的样式,以反映元素的状态。
伪元素用来创建一些不在文档树中的元素,并为其添加样式。例如,::before伪元素用来在一个元素之前插入内容,::after伪元素用来在一个元素之后插入内容。伪元素通常用于添加装饰性内容。
总之,伪类和伪元素的主要区别在于它们的作用对象不同。伪类作用于已经存在的元素,而伪元素创建新的元素。
px、em和rem都是CSS中的长度单位,但它们之间有一些重要的区别。
px(像素)是一个绝对长度单位,它表示屏幕上的一个物理像素。由于不同设备的屏幕分辨率不同,所以1px在不同设备上可能表示不同的物理尺寸。
em是一个相对长度单位,它相对于当前元素的字体大小。例如,如果一个元素的字体大小为16px,那么1em就等于16px。em单位常用于设置元素的字体大小、边距和填充等属性。
rem(root em)也是一个相对长度单位,它相对于根元素(元素)的字体大小。例如,如果根元素的字体大小为16px,那么1rem就等于16px。rem单位常用于实现响应式布局。
总之,px、em和rem的主要区别在于它们的参考系不同。px是绝对长度单位,而em和rem是相对长度单位。
想要了解更多或者详细一点可以看我第一篇文章前端常见的十种布局方式中的定位布局。
常见方法:
常见方法:
ES6,全称 ECMAScript 6.0,是 JavaScript 语言的下一代标准,于 2015 年 6 月正式发布。它为 JavaScript 带来了许多新的语法特性和功能,使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。
ES6 的一些主要新语法特性包括:
- 新的原始类型和变量声明:let 和 const 关键字用于声明块级作用域的变量和常量。
- 箭头函数:使用 => 符号定义函数,可以更简洁地编写函数。
- 模板字符串:使用反引号(`)定义字符串,可以在字符串中嵌入表达式。
- 解构赋值:允许从数组或对象中提取值并赋值给变量。
- 类:使用 class 关键字定义类,支持继承、构造函数、静态方法等面向对象编程特性。
- 模块化:使用 import 和 export 关键字导入和导出模块。
- Promise:用于处理异步操作的结果。
- 迭代器和生成器:支持迭代器和生成器,可以更方便地遍历数据结构。
- Set 和 Map 数据结构:新增了 Set 和 Map 数据结构,用于存储唯一值和键值对。
迭代器和生成器的简单示例:
// 简单的迭代器示例,它实现了一个next()方法,用于遍历数组中的元素:
function makeIterator(array) {
let nextIndex = 0;
return {
next: function() {
return nextIndex < array.length ?
{value: array[nextIndex++], done: false} :
{done: true};
}
};
}
let it = makeIterator(['a', 'b', 'c']);
console.log(it.next().value); // 'a'
console.log(it.next().value); // 'b'
console.log(it.next().value); // 'c'
console.log(it.next().done); // true
// 简单的生成器示例,它使用yield表达式来暂停函数执行并返回一个值:
function* idMaker() {
let index = 0;
while (true)
yield index++;
}
let gen = idMaker();
console.log(gen.next().value); // 0
console.log(gen.next().value); // 1
console.log(gen.next().value); // 2
分为两大类:包括值类型(基本对象类型)和引用类型(复杂对象类型)
值类型:字符串(String)、数字(Number)、布尔(Boolean)、空(Null)、未定义(Undefined)、Symbol和BigInt。其中,Symbol是ES6引入的一种新的原始数据类型,表示独一无二的值。
引用数据类型:对象(Object)、数组(Array)和函数(Function),还有两个特殊的对象:正则(RegExp)和日期(Date)。
示例:
// 值类型
let myString = 'Hello, World!'; // 字符串
let myNumber = 3.14; // 数字
let myBoolean = true; // 布尔
let myNull = null; // 空
let myUndefined = undefined; // 未定义
let mySymbol = Symbol(); // Symbol
let myBigInt = 123n; // BigInt
// 引用数据类型
let myObject = {name: '幼儿园技术家', age: 25}; // 对象
let myArray = [1, 2, 3]; // 数组
let myFunction = function() {console.log('Hello, World!')}; // 函数
let myRegExp = /hello/i; // 正则表达式
let myDate = new Date(); // 日期
我相信大家很少见过 symbol 和 Bigint 吧,如果面试问到估计只有少部分大佬能聊出来(反正我不行)。
先详细解释一下吧:
示例:
let mySymbol = Symbol('mySymbol');
let obj = {};
obj[mySymbol] = 'Hello, World!';
console.log(obj[mySymbol]); // 输出'Hello, World!'
let myBigInt = 1234567890123456789012345678901234567890n;
console.log(myBigInt * 2n); // 输出2469135780246913578024691357802469135780n
好处:
Symbol的好处在于它能够创建独一无二的值,这样就可以避免属性名冲突的问题。例如,当你想要给一个对象添加一个新属性时,你可以使用Symbol来创建一个唯一的属性名,这样就不用担心这个属性名会与对象中已有的属性名冲突。
BigInt的好处在于它能够表示任意大的整数,这样就可以避免整数溢出的问题。例如,在对大整数进行数学运算时,以任意精度表示整数的能力尤为重要。有了BigInt,整数溢出将不再是一个问题。此外,你可以安全地使用高精度时间戳、大整数ID等,而不必使用任何变通方法。
1. typeof:typeof操作符可以返回一个字符串,表示未经计算的操作数的类型。优点在于它简单易用,可以快速检测基本数据类型。但它也有一些缺点,例如它无法区分Object、Array和Null,因为都会返回"object"。
示例:
console.log(typeof 'Hello, World!'); // 输出'string'
console.log(typeof 3.14); // 输出'number'
console.log(typeof true); // 输出'boolean'
console.log(typeof undefined); // 输出'undefined'
console.log(typeof null); // 输出'object'
console.log(typeof Symbol()); // 输出'symbol'
console.log(typeof 123n); // 输出'bigint'
console.log(typeof {}); // 输出'object'
console.log(typeof []); // 输出'object'
console.log(typeof function() {}); // 输出'function'
2. instanceof:instanceof操作符主要用于检测引用数据类型,它用于检测构造函数的prototype属性是否出现在某个实例对象的原型链上。因此,它并不适用于检测所有数据类型。优点在于它可以检测引用数据类型,判断一个实例是否属于某个类。但它也有一些缺点,例如它无法检测基本数据类型。
示例:
console.log([] instanceof Array); // 输出true
console.log({} instanceof Object); // 输出true
console.log(function() {} instanceof Function); // 输出true
3. Object.prototype.toString.call():这种方法可以用来检测对象的类型。优点在于它可以准确地检测所有数据类型,包括基本数据类型和引用数据类型。但它也有一些缺点,例如使用起来比较麻烦,需要调用Object.prototype.toString.call()方法,并传入要检测的值作为参数。
示例:
console.log(Object.prototype.toString.call('Hello, World!')); // 输出'[object String]'
console.log(Object.prototype.toString.call(3.14)); // 输出'[object Number]'
console.log(Object.prototype.toString.call(true)); // 输出'[object Boolean]'
console.log(Object.prototype.toString.call(undefined)); // 输出'[object Undefined]'
console.log(Object.prototype.toString.call(null)); // 输出'[object Null]'
console.log(Object.prototype.toString.call(Symbol())); // 输出'[object Symbol]'
console.log(Object.prototype.toString.call(123n)); // 输出'[object BigInt]'
console.log(Object.prototype.toString.call({})); // 输出'[object Object]'
console.log(Object.prototype.toString.call([])); // 输出'[object Array]'
console.log(Object.prototype.toString.call(function() {})); // 输出'[object Function]'
在JavaScript中,数据类型转换分为两种:隐式类型转换和显式类型转换。
隐式类型转换:指在运算过程中,JavaScript会自动将一种数据类型转换为另一种数据类型,以便进行运算。例如,在字符串和数字相加时,数字会被自动转换为字符串,然后进行字符串拼接。
示例:
let x = '3' + 4; // x的值为'34'
let y = '3' - 4; // y的值为-1
显式类型转换:指通过调用特定的函数或方法来手动进行数据类型转换。例如,可以使用Number()函数将字符串转换为数字,或使用String()函数将数字转换为字符串。
示例:
// 使用Number()函数将字符串转换为整数
let a = Number('3') + 4; // a的值为7
// 使用String()函数将整数转换为字符串
let b = String(3) + 4; // b的值为'34'
// 使用一元加号运算符将字符串转换为数字
let x = +'3'; // x的值为3
// 使用一元减号运算符将字符串转换为数字
let y = -'3'; // y的值为-3
// 使用parseInt()函数将字符串转换为整数
let a = parseInt('3.14'); // a的值为3
// 使用parseFloat()函数将字符串转换为浮点数
let b = parseFloat('3.14'); // b的值为3.14
// 使用toString()方法将数字转换为字符串
let c = (3).toString(); // c的值为'3'
深拷贝和浅拷贝是针对引用数据类型(如Object和Array)的概念。浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
当我们把一个对象赋值给一个新的变量时,赋的其实是该对象的在栈中的地址,而不是堆中的数据。也就是两个对象指向的是同一个存储空间,无论哪个对象发生改变,其实都是改变的存储空间的内容,因此,两个对象是联动的。浅拷贝是按位拷贝对象,它会创建一个新对象,这个对象有着原始对象属性值的一份精确拷贝。如果属性是基本类型,拷贝的就是基本类型的值;如果属性是内存地址(引用类型),拷贝的就是内存地址 ,因此如果其中一个对象改变了这个地址,就会影响到另一个对象。
浅拷贝可以通过多种方法实现。例如,可以使用Object.assign()方法进行浅拷贝,也可以使用扩展运算符…进行浅拷贝。此外,还可以使用Array.prototype.concat()和Array.prototype.slice()方法对数组进行浅拷贝。
示例:
// 使用Object.assign()进行浅拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = Object.assign({}, obj1);
obj1.b.c = 3;
console.log(obj2.b.c); // 输出3,因为obj2.b和obj1.b指向同一个对象
// 使用扩展运算符…进行浅拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = {…obj1};
obj1.b.c = 3;
console.log(obj2.b.c); // 输出3,因为obj2.b和obj1.b指向同一个对象
// 使用Array.prototype.concat()对数组进行浅拷贝:
let arr1 = [1, 2, { a: 3 }];
let arr2 = arr1.concat();
arr1[2].a = 4;
console.log(arr2[2].a); // 输出4,因为arr2[2]和arr1[2]指向同一个对象
// 使用Array.prototype.slice()对数组进行浅拷贝:
let arr1 = [1, 2, { a: 3 }];
let arr2 = arr1.slice();
arr1[2].a = 4;
console.log(arr2[2].a); // 输出4,因为arr2[2]和arr1[2]指向同一个对象
深拷贝可以通过多种方法实现。例如,可以使用递归的方式实现深拷贝,也可以通过JSON对象实现深拷贝,即先使用JSON.stringify()将对象转换为JSON字符串,再使用JSON.parse()将字符串解析成新的对象。
示例:
// 使用递归实现深拷贝:
function deepClone(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj;
}
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
result[key] = deepClone(obj[key]);
}
}
return result;
}
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = deepClone(obj1);
obj1.b.c = 3;
console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
// 使用JSON.stringify()和JSON.parse()实现深拷贝:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = JSON.parse(JSON.stringify(obj1));
obj1.b.c = 3;
console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
此外,还可以通过jQuery的extend方法实现深浅拷贝: extend()方法的第一个参数是一个布尔值,用来指定是否进行深拷贝。如果该参数为true,则进行深拷贝;否则进行浅拷贝。
示例:
let obj1 = { a: 1, b: { c: 2 } };
let obj2 = jQuery.extend(true, {}, obj1);
obj1.b.c = 3;
console.log(obj2.b.c); // 输出2,因为obj2是obj1的深拷贝,它们之间没有引用关系
作用域链是指在JavaScript中,变量的查找机制。当代码在一个环境中执行时,会创建变量对象的一个作用域链(scope chain)。这个作用域链保证了对执行环境有权访问的所有变量和函数的有序访问。
作用域链的前端是当前执行环境的变量对象,如果这个执行环境是函数,则将其活动对象作为变量对象。活动对象在最开始时只包含一个变量,即arguments对象(这个对象在全局环境中是不存在的)。作用域链中的下一个变量对象来自包含(外部)环境,再下一个变量对象则来自下一个包含环境。这样一直延续到全局执行环境;全局执行环境的变量对象始终都是作用域链中的最后一个对象。
其实作用域链的理解比较简单,就是当查找变量时,会从作用域链的前端开始,逐级向后查找,直到找到为止。如果在整个作用域链中都没有找到该变量,则该变量未定义。
示例1(查找成功):
let x = 1;
function outer() {
let y = 2;
console.log(x + y);
}
outer(); // 输出 3
示例2(查找失败):
function outer() {
let y = 2;
console.log(x + y);
}
outer(); // 报错:ReferenceError: x is not defined
闭包是指一个函数能够访问其定义时的词法作用域,即使这个函数在其定义时的作用域之外执行。闭包可以让你从内部函数访问外部函数作用域。
在JavaScript中,函数在创建时会保存一个指向其定义时的词法作用域的引用。当这个函数被调用时,它会使用这个引用来确定其外部变量的值。这就是闭包。
优点:
缺点:
避免闭包导致的内存泄漏:
我们常常使用的定时器、事件处理、Ajax请求等常用于异步操作用了回调函数,但是回调函数其实是可以使用闭包也可以不使用闭包的,并不是说回调一定是在使用闭包。
回调示例1(使用闭包):
let x = 1;
function doSomething(callback) {
// 执行一些操作
let result = x + 1;
// 调用回调函数
callback(result);
}
doSomething(function(result) {
console.log(result); // 输出 2
});
<!-- 在这个例子中,我们定义了一个全局变量x和一个函数doSomething。
doSomething函数接受一个回调函数作为参数。
当我们调用doSomething时,它会执行一些操作,然后调用回调函数,并将结果作为参数传递给回调函数。 -->
回调示例2(不使用闭包):
function doSomething(callback) {
// 执行一些操作
let result = 1 + 1;
// 调用回调函数
callback(result);
}
doSomething(function(result) {
console.log(result); // 输出 2
});
<!-- 在这个例子中,我们定义了一个函数doSomething,它接受一个回调函数作为参数。
当我们调用doSomething时,它会执行一些操作,然后调用回调函数,并将结果作为参数传递给回调函数。 -->
在JavaScript中,内存管理是自动进行的。当一个变量不再被引用时,它所占用的内存就会被垃圾回收器回收。
在闭包中定义的变量也是如此。当闭包不再被引用时,它所引用的外部变量也就不再被引用,因此它们所占用的内存就会被垃圾回收器回收。
所以有两种情况:
- 第一是当全局变量作为闭包变量的时候,那么闭包变量就会因为上下文的存在(一直被引用)而保存到页面关闭。
- 第二是当局部变量作为闭包变量的时候,其一是引用完毕立即回收(可以赋予null),其二是可以一直引用依然保存在内存中直到不再被引用则会回收。
第二种情况示例1(立即回收):
function fn() {
let x = 1;
return function() {
console.log(x);
}
}
for (let i = 0; i < 10; i++) {
fn()(); // 输出10次1
}
<!-- 在这个例子中,fn是一个函数,它返回一个匿名函数。
当我们在循环中调用fn()时,它每次都会返回一个新的匿名函数,并立即执行这个匿名函数。
由于这些匿名函数在执行完毕后就不再被引用,因此它们所占用的内存就会被垃圾回收器回收。 -->
第二种情况示例2(等到不再引用则回收):
function fn() {
let x = 1;
return function() {
console.log(x++);
}
}
let closure = fn();
for (let i = 0; i < 10; i++) {
closure(); // 输出 1,2,3,...,10
}
closure = null; // 释放对闭包的引用
<!-- 在这个例子中,fn是一个函数,它返回一个匿名函数。当我们调用fn()时,它返回inner函数,并将其赋值给closure变量。
当我们在循环中调用closure()时,它每次都会输出递增的值,即fn函数内部定义的变量x的值。
由于x在每次调用闭包时都会自增1,因此每次输出的都是递增的值。在循环结束后,我们将closure变量赋值为null,这样就释放了对闭包的引用。 -->
涉及for循环和闭包:
var data = [];
for (var i = 0; i < 3; i++) {
data[i] = function () {
console.log(i);
};
}
data[0](); // 输出什么?
data[1](); // 输出什么?
data[2](); // 输出什么?
// 连续输出3个3
<!-- 原因:在这段代码中,i 是全局变量,共用一个作用域。当函数被执行的时候,此时的 i 已经变成了3,导致输出的结果都是3。 -->
如果预期输出1、2、3,使用闭包改善:
var data = [];
for (var i = 0; i < 3; i++) {
(function (j) {
data[j] = function () {
console.log(j);
};
})(i);
}
data[0](); // 输出1
data[1](); // 输出2
data[2](); // 输出3
<!-- 原因:在这个例子中,我们使用了一个自执行函数和闭包来创建3个互不干扰的私有作用域。
这样,每次循环时都会创建一个新的闭包,并将当前的 i 值传递给闭包,使得每个闭包都有自己独立的 j 值。
因此,当我们调用 data[0]()、data[1]() 和 data[2]() 时,它们分别输出1、2和3。 -->
原型(prototype)是一个对象,它是用来创建其他对象的模板。每个函数都有一个 prototype 属性,它指向该函数的原型对象。
原型链是由一系列原型对象组成的链条。每个对象都有一个原型对象与之关联,这个原型对象也是一个普通对象,它也有自己的原型对象,这样层层递进,就形成了一个链条,这个链条就是原型链。
原型链的作用是实现继承。当访问一个对象的属性时,如果该属性不存在于该对象中,则会沿着原型链向上查找,直到找到该属性或者到达原型链的顶端。
原型关系: 指的是对象与其原型对象之间的关系。每个对象都有一个内部属性 [[Prototype]],它指向该对象的原型对象。在 JavaScript 中,可以通过 __proto__ 属性来访问这个内部属性。
示例:
// 假设我们有一个构造函数 Person 和一个实例对象 p:
function Person(name) {
this.name = name;
}
Person.prototype.sayName = function() {
console.log(this.name);
}
var p = new Person('Tom');
// 在这个例子中,p 的原型对象就是 Person.prototype。我们可以通过 p.__proto__ 来访问它:
console.log(p.__proto__ === Person.prototype); // true
ES6之前创建变量用的是var,之后创建变量用的是let/const,当然也会用var,那么区别在哪呢?
var,let和const都是用来声明变量的,但它们之间有一些区别。var声明的变量属于函数作用域,而let和const声明的变量属于块级作用域。此外,var声明的变量存在变量提升现象,而let和const没有。在同一块级作用域中,let变量不能重新声明,而const常量不能修改。简单的来说就是,var定义全局变量且可以覆盖,let定义块级作用域变量且不能再一次进行声明({}),const定义不允许修改的块级作用域常量。
示例:
function exampleVar() {
var x = 1;
if (true) {
var x = 2;
console.log(x); // 输出2
}
console.log(x); // 输出2
}
function exampleLet() {
let x = 1;
if (true) {
let x = 2;
console.log(x); // 输出2
}
console.log(x); // 输出1
}
function exampleConst() {
const x = 1;
if (true) {
const x = 2;
console.log(x); // 输出2
}
console.log(x); // 输出1
}
解释:
在exampleVar函数中,由于var声明的变量属于函数作用域,所以在if语句块中重新声明的变量x会覆盖函数作用域中的变量x。
而在exampleLet和exampleConst函数中,由于let和const声明的变量属于块级作用域,所以在if语句块中声明的变量x不会影响到外部作用域中的变量x。
在JavaScript中,this关键字指向函数执行时的当前对象。this的指向取决于函数调用的方式,而不是函数定义的位置。
在全局作用域中,this指向全局对象(在浏览器中是window对象,在Node.js中是global对象)。
在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象。
在作为对象方法调用时,this指向调用该方法的对象。
在构造函数中,this指向新创建的对象。
在事件处理程序中,this指向触发事件的元素。
此外,可以使用call()、apply()和bind()方法显式地设置函数调用时的this值。
示例:
// 1.在全局作用域中,this指向全局对象:
console.log(this === window); // 输出true(在浏览器中)
// 2.在函数调用中,如果函数不是作为对象的方法被调用,那么this指向全局对象:
function foo() {
console.log(this === window); // 输出true(在浏览器中)
}
foo();
// 3.在作为对象方法调用时,this指向调用该方法的对象:
let obj = {
myMethod: function() {
console.log(this === obj); // 输出true
}
};
obj.myMethod();
// 4.在构造函数中,this指向新创建的对象:
function MyConstructor() {
this.myProperty = 'Hello World!';
console.log(this instanceof MyConstructor); // 输出true
}
let myInstance = new MyConstructor();
// 5.在事件处理程序中,this指向触发事件的元素:
// 6.使用call()、apply()和bind()方法显式地设置函数调用时的this值:
function foo() {
console.log(this);
}
let obj = { a: 1 };
foo.call(obj); // 输出{ a: 1 }
foo.apply(obj); // 输出{ a: 1 }
let bar = foo.bind(obj);
bar(); // 输出{ a: 1 }
此外还有一些特殊情况会影响this的指向问题:
在严格模式下,如果函数不是作为对象的方法被调用,那么this的值为undefined。
在DOM事件处理程序中,如果使用addEventListener()方法添加事件处理程序,那么事件处理程序中的this指向触发事件的元素。但是,如果使用attachEvent()方法(仅在旧版本的IE中可用),那么事件处理程序中的this指向全局对象。
在回调函数中,this的指向取决于回调函数被调用的方式。例如,在setTimeout()和setInterval()中,回调函数中的this指向全局对象。在数组方法(如forEach()、map()、filter()等)中,回调函数中的this指向全局对象,除非显式地设置了thisArg参数。
在箭头函数中,this的值取决于箭头函数定义时所在的上下文。箭头函数不会创建自己的this值,而是从外层作用域继承this值。
如果使用了ES6的类语法,那么类中的方法默认是在严格模式下执行的,因此类方法中的this指向取决于方法调用的方式。
示例:
// 1.在严格模式下,函数调用中的this指向undefined:
'use strict';
function foo() {
console.log(this);
}
foo(); // 输出undefined
// 2.在DOM事件处理程序中,使用addEventListener()方法添加事件处理程序,事件处理程序中的this指向触发事件的元素:
// 3.在回调函数中,this的指向取决于回调函数被调用的方式:
// 在setTimeout()中,回调函数中的this指向全局对象
setTimeout(function() {
console.log(this === window); // 输出true(在浏览器中)
}, 1000);
// 在数组方法中,回调函数中的this指向全局对象,除非显式地设置了thisArg参数
let arr = [1, 2, 3];
arr.forEach(function() {
console.log(this === window); // 输出true(在浏览器中)
});
arr.forEach(function() {
console.log(this === obj);
}, obj); // 输出true
// 4.在箭头函数中,this的值取决于箭头函数定义时所在的上下文:
let obj = {
myMethod: function() {
let arrowFunction = () => {
console.log(this === obj); // 输出true
};
arrowFunction();
}
};
obj.myMethod();
// 5.在类方法中,this指向取决于方法调用的方式:
class MyClass {
myMethod() {
console.log(this);
}
}
let myInstance = new MyClass();
myInstance.myMethod(); // 输出MyClass实例
let myMethod = myInstance.myMethod;
myMethod(); // 输出undefined(在严格模式下)或全局对象(在非严格模式下)
EventLoop 即 事件循环,是指浏览器或 Node 的一种解决 javaScript 单线程运行时不会阻塞的一种机制,也就是我们经常使用异步的原理。
JavaScript 运行时包含了一个待处理消息的消息队列。每一个消息都关联着一个用以处理这个消息的回调函数。在事件循环期间的某个时刻,运行时会从最先进入队列的消息开始处理队列中的消息。被处理的消息会被移出队列,并作为输入参数来调用与之关联的函数。
这个模型与其他语言中的模型截然不同,比如 C 和 Java。它永不阻塞,处理 I/O 通常通过事件和回调来执行,所以当一个应用正等待一个 IndexedDB 查询返回或者一个 XHR 请求返回时,它仍然可以处理其他事情,比如用户输入。
在 JavaScript 引擎中,任务分为两种类型:微任务(microtask)和宏任务(macrotask)。微任务是指在当前任务执行结束后立即执行的任务,它可以看作是在当前任务的“尾巴”添加的任务。常见的微任务包括 Promise 回调和 process.nextTick。宏任务是指在下一轮事件循环中执行的任务。常见的宏任务包括 setTimeout、setInterval、setImmediate、requestAnimationFrame 等。
微任务和宏任务是绑定的,每个宏任务在执行时,会创建自己的微任务队列。比如一个宏任务在执行过程中,产生了 100 个微任务,执行每个微任务的时间是 10 毫秒,那么执行这 100 个微任务的时间就是 1000 毫秒,也可以说这 100 个微任务让宏任务的执行时间延长了 1000 毫秒。
宏任务和微任务与事件循环有着密切的关系。在事件循环中,每个宏任务执行完后,都会检查微任务队列并执行队列中的所有微任务,然后再执行下一个宏任务。这个过程会一直重复,直到队列中没有消息为止。
示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
// 输出结果:
script start
script end
promise1
promise2
setTimeout
解释:首先,同步代码 console.log('script start') 和 console.log('script end') 被执行。然后,setTimeout 被添加到宏任务队列中。接着,Promise.resolve().then 中的回调被添加到微任务队列中。当同步代码执行完后,事件循环检查微任务队列并执行队列中的所有微任务,即 console.log('promise1') 和 console.log('promise2')。最后,事件循环执行下一个宏任务,即 setTimeout 中的回调。
优先级:
Promise 的回调属于微任务,所以它会在当前宏任务执行完后立即执行。
setTimeout 属于宏任务,所以它会在下一轮事件循环中执行。
Async/Await 是基于 Promise 的语法糖,它能实现的效果都能用 then 链来实现。当遇到 await 的时候,会让出主线程,阻塞后面的代码的执行。所以 await 后面的代码相当于 promise.then() 里面的代码。
示例:
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
}
async1();
console.log('script end');
// 输出结果:
script start
async1 start
async2
script end
promise1
promise2
async1 end
setTimeout
解释: 首先,同步代码 console.log('script start')、console.log('async1 start')、console.log('async2') 和 console.log('script end') 被执行。然后,setTimeout 被添加到宏任务队列中。接着,Promise.resolve().then 中的回调被添加到微任务队列中。当同步代码执行完后,事件循环检查微任务队列并执行队列中的所有微任务,即 console.log('promise1') 和 console.log('promise2')。最后,事件循环执行下一个宏任务,即 setTimeout 中的回调。
节流(Throttle)是一种控制函数执行频率的技术。当事件被频繁触发时,节流函数会按照一定的频率来执行函数。它可以保证在一段时间内,不管事件触发了多少次,函数都只会执行一次,且是最先被触发调用的那次。
举个例子,假设你正在滚动一个页面,每滚动一段距离就会触发一个事件。如果这个事件被频繁触发,可能会导致页面卡顿。这时候,你可以使用节流来控制事件的执行频率,让它每隔一段时间才执行一次。
节流通常用于优化性能,避免因为事件触发过于频繁而导致的页面卡顿或浏览器崩溃。
场景:
滚动事件当然是 触底加载 比较多了。现在用这个作为示例:
// 节流函数
function throttle(fn, delay) {
let timer = null;
return function() {
if (!timer) {
timer = setTimeout(() => {
fn.apply(this, arguments);
timer = null;
}, delay);
}
}
}
// 加载函数
function loadMore() {
// 加载更多内容
console.log('Loading more content...');
}
// 监听滚动事件
window.addEventListener('scroll', throttle(function() {
// 滚动到页面底部时触发加载函数
if (document.documentElement.scrollTop + window.innerHeight === document.documentElement.scrollHeight) {
loadMore();
}
}, 500));
解释: 在这个例子中,我们定义了一个节流函数 throttle,它接受两个参数:一个是要执行的函数 fn,另一个是延迟时间 delay。当事件被触发时,节流函数会按照指定的频率来执行函数。然后,我们定义了一个加载函数 loadMore,用来加载更多内容。接着,我们监听了滚动事件,并使用节流函数来控制加载函数的执行频率。当滚动到页面底部时,会触发加载函数。
防抖(Debounce)是一种控制函数执行频率的技术。当事件被频繁触发时,防抖函数会推迟执行函数。只有当等待一段时间后也没有再次触发该事件,那么才会真正执行函数。
举个例子,假设你正在输入一个搜索关键词,每输入一个字符就会触发一个搜索事件。如果这个事件被频繁触发,可能会导致页面卡顿或浏览器崩溃。这时候,你可以使用防抖来控制搜索事件的执行频率,让它在用户停止输入一段时间后才执行。
防抖通常用于优化性能,避免因为事件触发过于频繁而导致的页面卡顿或浏览器崩溃。
场景:
那么就用 实时搜索 作为示例:
// 防抖函数
function debounce(fn, delay) {
let timer = null;
return function() {
clearTimeout(timer);
timer = setTimeout(() => {
fn.apply(this, arguments);
}, delay);
}
}
// 搜索函数
function search(keyword) {
// 执行搜索操作
console.log(`Searching for ${keyword}...`);
}
// 获取输入框元素
const input = document.querySelector('input');
// 监听输入事件
input.addEventListener('input', debounce(function(event) {
// 获取输入框的值
const keyword = event.target.value;
// 执行搜索操作
search(keyword);
}, 500));
解释: 在这个例子中,我们定义了一个防抖函数 debounce,它接受两个参数:一个是要执行的函数 fn,另一个是延迟时间 delay。当事件被触发时,防抖函数会推迟执行函数。如果在等待时间内再次触发该事件,那么会重新计算等待时间。然后,我们定义了一个搜索函数 search,用来执行搜索操作。接着,我们获取了输入框元素,并监听了输入事件。当用户在输入框中输入内容时,会触发输入事件。我们使用防抖函数来控制搜索函数的执行频率,让它在用户停止输入一段时间后才执行。
JavaScript 的垃圾回收机制是用来防止内存泄漏的。内存泄漏指的是当已经不需要某块内存时,这块内存还存在着。在项目中,如果存在大量不被释放的内存(堆/栈/上下文),页面性能会变得很慢。当某些代码操作不能被合理释放,就会造成内存泄漏。垃圾回收机制就是间歇性地、不定期地寻找到不再使用的变量,并释放掉它们所指向的内存。
JavaScript 的垃圾回收算法主要有两种:引用计数(reference counting)和标记清除(mark-and-sweep)。
引用计数算法通过跟踪每个值被引用的次数来工作。当声明了一个变量并将一个引用类型值赋给该变量时,则这个值的引用次数就是 1。如果同一个值又被赋给另一个变量,则该值的引用次数加 1。相反,如果包含对这个值引用的变量又取得了另外一个值,则这个值的引用次数减 1。当这个值的引用次数变成 0 时,则说明没有办法再访问这个值了,因此就可以将其占用的内存空间回收回来。
标记清除算法将“不再使用的变量”定义为“无法访问到这个变量”。垃圾收集器在运行的时候会给存储在内存中的所有变量都加上标记(可以使用任何标记方式)。然后,它会去掉环境中的变量以及被环境中的变量引用的变量的标记。而在此之后再被加上标记的变量即为准备删除的变量,原因是环境中的变量已经无法访问到这些变量了。最后,垃圾收集器完成内存清除工作,销毁那些带标记的值并回收它们所占用的内存空间。
过程:
示例:
function Person(name, age) {
this.name = name;
this.age = age;
}
var person1 = new Person('幼儿园技术家', 25);
console.log(person1.name); // 输出: 幼儿园技术家
console.log(person1.age); // 输出: 25
使用原型链。
示例:
function Animal(name) {
this.name = name;
}
Animal.prototype.sayName = function() {
console.log(this.name);
}
function Dog(name, breed) {
Animal.call(this, name);
this.breed = breed;
}
Dog.prototype = Object.create(Animal.prototype);
Dog.prototype.constructor = Dog;
Dog.prototype.bark = function() {
console.log('Woof!');
}
let dog = new Dog('Max', 'German Shepherd');
dog.sayName(); // Max
dog.bark(); // Woof!
使用 class 关键字来定义类,并使用 extends 关键字来实现继承。
示例:
class Animal {
constructor(name) {
this.name = name;
}
sayName() {
console.log(this.name);
}
}
class Dog extends Animal {
constructor(name, breed) {
super(name);
this.breed = breed;
}
bark() {
console.log('Woof!');
}
}
let dog = new Dog('Max', 'German Shepherd');
dog.sayName(); // Max
dog.bark(); // Woof!
使用混入(Mixin)。
示例:
let Animal = {
sayName: function() {
console.log(this.name);
}
}
function Dog(name, breed) {
this.name = name;
this.breed = breed;
}
Object.assign(Dog.prototype, Animal);
Dog.prototype.bark = function() {
console.log('Woof!');
}
let dog = new Dog('Max', 'German Shepherd');
dog.sayName(); // Max
dog.bark(); // Woof!
// 可以通过在 Function.prototype 上添加一个新方法来手写实现 bind 方法
Function.prototype.myBind = function(context) {
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
return function() {
var bindArgs = Array.prototype.slice.call(arguments);
return self.apply(context, args.concat(bindArgs));
}
}
var obj = {
name: '幼儿园技术家'
}
function sayName(age) {
console.log(this.name);
console.log(age);
}
var boundSayName = sayName.myBind(obj, 25);
boundSayName(); // 输出: 幼儿园技术家 \n 25
解释:在上面的示例中,我们定义了一个 myBind 方法,它接受一个参数 context,表示绑定的上下文对象。然后我们使用 apply 方法将函数的执行上下文绑定到指定的对象上,并传入相应的参数。最后我们可以调用绑定后的函数。
CommonJS和ESM是两种不同的JavaScript模块化规范。CommonJS主要用于服务器端,比如Node.js,而ESM是ECMAScript 6中引入的模块化标准,它既可以用于前端,也可以用于后端。
CommonJS和ESM之间有一些主要区别:
首先,它们的语法不同。CommonJS使用 require 和 module.exports 来导入和导出模块,而ESM使用 import 和 export 关键字。
其次,CommonJS模块是运行时加载的,而ESM模块是编译时输出接口的。此外,CommonJS是同步加载模块的,而ESM支持异步加载。
示例:
// CommonJS
var foo = require('foo');
module.exports = foo;
// ESM
import foo from 'foo';
export default foo;
在上面的闭包中我们有提到柯里化,那么这里简单介绍一下。要思考柯里化是什么?有什么用?怎么实现?
柯里化(Currying)是一种处理多元函数的方法,它是指将一个多参数的函数转化为单参数函数的方法。它是数学家柯里(Haskell Curry)提出的。
柯里化的主要作用是将一个复杂的函数拆分成多个简单的函数,使得每个函数只接受一个参数。这样做可以让我们更灵活地使用这些函数,比如可以将它们组合起来,或者将它们作为参数传递给其他函数。
示例:
function add(x, y) {
return x + y;
}
function curriedAdd(x) {
return function(y) {
return add(x, y);
}
}
var add5 = curriedAdd(5);
console.log(add5(3)); // 输出: 8
在解决this指向问题中提到了call、apply 和 bind,那么现在来介绍一下。
call、apply 和 bind 都是JavaScript中的函数方法,它们都可以用来改变函数的执行上下文(即函数内部的 this 指向)。
call 和 apply 的作用相似,它们都可以用来立即调用一个函数,并指定函数内部的 this 指向。它们的区别在于传递参数的方式不同:call 方法接受若干个参数,第一个参数是 this 指向的对象,后面的参数依次传递给函数;而 apply 方法接受两个参数,第一个参数是 this 指向的对象,第二个参数是一个数组,数组中的元素依次传递给函数。
bind 方法与 call 和 apply 不同,它不会立即调用函数,而是返回一个新的函数。这个新函数与原函数具有相同的行为,但是它内部的 this 指向被绑定到了 bind 方法的第一个参数上。除了第一个参数外,bind 方法还可以接受若干个参数,这些参数会被预先传递给新函数。
示例:
function sayName(greeting) {
console.log(`${greeting}, my name is ${this.name}`);
}
var obj = {
name: '幼儿园技术家'
}
sayName.call(obj, 'Hello'); // 输出: Hello, my name is 幼儿园技术家
sayName.apply(obj, ['Hello']); // 输出: Hello, my name is 幼儿园技术家
var boundSayName = sayName.bind(obj);
boundSayName('Hello'); // 输出: Hello, my name is 幼儿园技术家
Vue (发音为 /vjuː/,类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建,并提供了一套声明式的、组件化的编程模型,帮助你高效地开发用户界面。
Vue 是一个典型的 MVVM 模型的框架。MVVM 是 Model-View-ViewModel 的缩写,它是一种基于前端开发的架构模式,其核心是提供对 View 和 ViewModel 的双向数据绑定。这使得 ViewModel 的状态改变可以自动传递给 View,即所谓的数据双向绑定。
优点:
缺点:
既然提到了 mvvm,那么就简单说一下 MVC 以及 MVVM 和 MVC 之间的区别:
MVC 和 MVVM 都是一种设计模式,它们都旨在将应用程序分成不同的部分,以便更好地管理和维护。
MVC 是 Model-View-Controller 的缩写,它将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,Controller 负责接收用户输入并更新 Model 和 View。在 MVC 模式中,View 和 Model 是相互独立的,它们之间通过 Controller 来进行通信。
优点:
缺点:
MVVM 是 Model-View-ViewModel 的缩写,它也将应用程序分成三个部分:Model 负责存储数据和业务逻辑,View 负责展示数据,ViewModel 则负责连接 View 和 Model。与 MVC 不同的是,在 MVVM 模式中,View 和 ViewModel 之间有着双向数据绑定的联系。这意味着当 ViewModel 中的数据发生变化时,View 会自动更新;而当 View 中的数据发生变化时,ViewModel 也会自动更新。
优点:
缺点:
总之,MVC 和 MVVM 的主要区别在于它们对 View 和 Model 之间通信方式的不同处理。MVC 通过 Controller 来进行通信,而 MVVM 则通过双向数据绑定来实现通信。这两种模式各有优缺点,具体使用哪种模式取决于具体的应用场景。
Vue 的底层实现原理主要包括数据双向绑定和虚拟 DOM两部分。
数据双向绑定是指当数据发生变化时,视图会自动更新;而当视图发生变化时,数据也会自动更新。Vue 实现数据双向绑定的方式是通过数据劫持
和发布订阅模式
相结合。
虚拟 DOM 是一种用 JavaScript 对象表示 DOM 的技术。它可以让我们在不直接操作 DOM 的情况下更新视图。Vue 在更新视图时会先生成一个新的虚拟 DOM 树,然后将新旧虚拟 DOM 树进行对比,找出它们之间的差异。最后,Vue 会根据这些差异来更新真实的 DOM 树。这个过程被称为“patching”。
使用虚拟DOM有以下几个好处:
相对于手动操作真实DOM,使用虚拟DOM通常可以获得更好的性能。但这并不是绝对的,因为虚拟DOM也有一些开销,如创建虚拟DOM树和计算差异。在某些情况下,手动操作真实DOM可能会更快。但总体来说,使用虚拟DOM可以让我们更容易地构建高性能和跨平台的应用。
Vue 的生命周期指的是 Vue 实例从创建到销毁的整个过程。在这个过程中,Vue 实例会经历一系列的生命周期钩子函数,这些钩子函数可以让我们在特定的时刻执行特定的操作。
beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。
created:在实例创建完成后被立即调用。此时,实例已完成以下配置:数据观测、属性和方法的运算、watch/event 事件回调。但是,挂载阶段还没开始,$el 属性目前不可见。
beforeMount:在挂载开始之前被调用。相关的 render 函数首次被调用。
mounted:在 el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用。如果根实例挂载了一个文档内元素,当 mounted 被调用时,vm.$el 也在文档内。
beforeUpdate:在数据更新之前调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM。
updated:在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。
beforeDestroy:在实例销毁之前调用。此时实例仍然完全可用。
destroyed:在实例销毁之后调用。此时,所有的指令绑定都被解除,所有的事件监听器都被移除,所有的子实例也都被销毁。
Vuex是一个专为Vue.js应用程序开发的状态管理模式+库。使用Vuex时,每一个Vuex应用的核心就是store(仓库)。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。它可以帮助我们管理共享状态,解决多组件数据通信问题。
简单来说,Vuex就像一个容器,它包含了你的应用中大部分的状态。当Vue组件从store中读取状态时,若store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
你可以通过store.state
来获取状态对象,并通过store.commit
方法触发状态变更。在Vue组件中,可以通过this.$store
访问store实例,但不能直接改变store中的状态。改变store中的状态的唯一途径就是显式地提交mutation
,而非直接改变store.state.count
。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。
Vuex主要包括以下几个核心模块:
一些常见的Vuex使用场景包括:用户的个人信息管理模块、电商项目的购物车模块、我的订单模块(订单列表中点击取消订单,然后更新对应的订单列表)、在订单结算页获取需要的优惠券并更新订单优惠信息等。
示例:
// store.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
count: 0
},
getters: {
doubleCount: state => state.count * 2
},
mutations: {
increment(state) {
state.count++
}
},
actions: {
increment(context) {
context.commit('increment')
}
}
})
// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'
new Vue({
el: '#app',
store,
render: h => h(App)
})
// App.vue
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">Increment</button>
</div>
</template>
<script>
export default {
computed: {
count() {
return this.$store.state.count
}
},
methods: {
increment() {
this.$store.dispatch('increment')
}
}
}
</script>
除了以上说的 Vuex 进行组件之间的通讯外,常见的组件通讯还有以下几种方式:
props / $emit:父组件通过props向子组件传递数据,子组件通过$emit向父组件传递数据。
示例:
// 父组件
// 子组件
{{ msg }}
ref / $refs:父组件可以通过$refs获取子组件的实例,从而调用子组件的方法或访问子组件的数据。
示例:
// 父组件
// 子组件
eventBus事件总线($emit / $on):可以创建一个空的Vue实例作为事件总线,在组件中通过$emit触发事件,在另一个组件中通过$on监听事件,从而实现组件间通信。
示例:
// eventBus.js
import Vue from 'vue'
export const eventBus = new Vue()
// 组件A
// 组件B
$parent / $children:子组件可以通过$parent访问父组件实例,父组件可以通过$children访问子组件实例。
示例:
// 父组件
// 子组件
$attrs/ $listeners:$attrs包含了父组件中不作为prop被识别且获取的特性绑定,$listeners包含了父组件中的v-on事件监听器。
示例:
// 父组件
// 中间组件
// 子组件
{{ msg }}
provide/inject:祖先组件通过provide提供变量,然后在子孙组件中通过inject来注入变量。
示例:
// 祖先组件
// 子孙组件
computed和watch都是Vue实例的选项,用来监听数据变化并执行相应的操作。
computed:计算属性是基于它们的依赖进行缓存的。计算属性只有在它的相关依赖发生改变时才会重新求值。这就意味着只要相关依赖没有发生改变,多次访问计算属性会立即返回之前的计算结果,而不必再次执行函数。计算属性默认只有getter,不过在需要时你也可以提供一个setter。
示例:
new Vue({
el: '#app',
data: {
message: 'Hello'
},
computed: {
reversedMessage: function () {
return this.message.split('').reverse().join('')
}
}
})
watch:当你需要在数据变化时执行异步或开销较大的操作时,可以使用watch。watch选项允许我们执行异步操作(访问一个API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。
示例:
new Vue({
el: '#app',
data: {
message: 'Hello'
},
watch: {
message: function (newVal, oldVal) {
console.log('message changed from', oldVal, 'to', newVal)
}
}
})
区别:
总之,当你需要根据数据变化来改变数据时,可以使用计算属性;当你需要根据数据变化来执行异步操作或开销较大的操作时,可以使用watch。
不建议在同一元素上同时使用v-for和v-if。当它们同时存在时,v-for的优先级比v-if更高,这意味着v-if将分别重复运行于每个循环的项上。这可能会导致性能问题,因为在渲染列表时会进行更多的计算。
场景一:如果你想根据条件过滤列表并渲染过滤后的结果,可以将过滤后的结果计算为一个计算属性,然后在v-for中使用这个计算属性:
<template>
<ul>
<li v-for="item in filteredItems" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1', show: true },
{ id: 2, text: 'Item 2', show: false },
{ id: 3, text: 'Item 3', show: true }
]
}
},
computed: {
filteredItems() {
return this.items.filter(item => item.show)
}
}
}
</script>
场景二:如果你的目的是有条件地跳过循环的执行,那么可以将v-if放置在外层元素(如<template>
)或包装元素上:
<template>
<ul v-if="shouldShowItems">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
items: [
{ id: 1, text: 'Item 1' },
{ id: 2, text: 'Item 2' },
{ id: 3, text: 'Item 3' }
],
shouldShowItems: true
}
}
}
</script>
Vue.nextTick是一个全局API,用于在下一次DOM更新循环结束之后延迟执行一个回调函数。它的实现依赖于JavaScript的事件循环和微任务队列。
为了异步刷新队列,Vue会尝试使用原生的Promise.then、MutationObserver或setImmediate来实现异步延迟。如果这些方法都不可用,它会退而使用setTimeout(fn, 0)。
在Vue 3.x中,Vue.nextTick的实现类似于Vue 2.x,但使用了更现代的API来实现异步延迟。它首先尝试使用原生的Promise.then,如果不可用则退而使用setTimeout(fn, 0)。
示例:
{{ message }}
总之,Vue.nextTick的实现依赖于JavaScript的事件循环和微任务队列。它使用一个异步队列来存储所有等待执行的回调函数,并使用原生API或setTimeout来异步刷新这个队列。
在Vue组件中,data必须是一个函数,而不是一个对象。这是因为当一个组件被多次使用时,每个实例都应该维护一份被返回对象的独立的拷贝。
如果data是一个对象,那么所有组件实例将共享同一个数据对象。这意味着当一个组件实例改变了数据对象时,其他组件实例的数据也会受到影响。
为了避免这个问题,Vue要求组件的data选项必须是一个函数。当一个组件被实例化时,Vue会调用这个函数来获取组件的初始数据。由于每个组件实例都会调用这个函数来获取自己的数据,所以每个组件实例都会维护一份独立的数据拷贝。
前端路由是指在单页应用(SPA)中,通过改变URL并不向服务器发送请求,而是通过JavaScript来控制页面内容的切换。这种方式可以让用户在不离开当前页面的情况下,浏览不同的内容。
前端路由通常有两种实现方式:hash模式和history模式。
Vue的diff算法是用来比较新旧虚拟DOM树,计算出最小的更新操作来更新真实DOM的过程。它采用了深度优先遍历和双端比较的策略来优化比较过程,是Vue虚拟DOM实现的核心部分。
Vue的diff算法基于两个假设:
两个相同标签的元素会产生类似的DOM结构。
同一层级的一组子节点,它们可以通过唯一的id进行区分。
基于这两个假设,Vue的diff算法采用了深度优先遍历和双端比较的策略来比较新旧虚拟DOM树。
在比较过程中,Vue会从新旧虚拟DOM树的根节点开始,逐层进行比较。当遇到不同类型的节点时,Vue会直接替换整个节点及其子节点;当遇到相同类型但属性不同的节点时,Vue会更新节点的属性;当遇到相同类型且属性相同但子节点不同的节点时,Vue会递归地比较子节点。
在比较子节点时,Vue会使用双端比较的策略来优化比较过程。它会同时从新旧虚拟DOM树的两端开始比较,如果发现两端的节点相同,则直接移动节点;如果发现两端的节点不同,则继续比较中间部分。这种策略可以有效地减少需要比较的节点数量,从而提高diff算法的性能。
那么我们常常在for循环中要绑定一个key属性值,有什么作用呢?
其实在Vue中,key
是一个特殊的属性,用于标识列表渲染中每个节点的唯一性。这是因为在列表渲染中,列表数据可能会发生变化,导致列表项的顺序、数量或内容发生变化。如果没有key属性,Vue将无法准确地确定新旧虚拟DOM树中的节点是否相同,从而无法快速地更新虚拟DOM树。所以它可以帮助Vue更快地更新虚拟DOM树,从而提高应用的性能。
当Vue进行列表渲染时,它需要一种方式来确定新旧虚拟DOM树中的节点是否相同。如果没有key属性,Vue会默认使用“就地更新”的策略,即直接复用旧虚拟DOM树中的节点来更新新虚拟DOM树中的节点。这种方式简单快速,但在某些情况下可能会导致问题。
为了避免这些问题,我们可以使用key属性来为每个节点指定一个唯一的标识。当Vue进行列表渲染时,它会根据key属性来确定新旧虚拟DOM树中的节点是否相同。这样,Vue就可以更快地更新虚拟DOM树,从而提高应用的性能。
keep-alive
是Vue的一个内置组件,用于保留组件状态或避免重新渲染。它可以将其包裹的组件缓存起来,当组件切换时不会销毁,而是保留在内存中,以便下次切换回来时可以直接使用。
实现原理:是通过一个缓存对象来存储被缓存的组件实例。当一个组件被切换出去时,它不会被销毁,而是被保存在缓存对象中;当一个组件被切换回来时,keep-alive会先检查缓存对象中是否有这个组件的实例,如果有,则直接使用缓存的实例;如果没有,则创建一个新的实例。
示例:
<template>
<div>
<button @click="toggle">Toggle</button>
<keep-alive>
<component :is="currentView"></component>
</keep-alive>
</div>
</template>
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: {
Foo,
Bar
},
data() {
return {
currentView: 'Foo'
}
},
methods: {
toggle() {
this.currentView = this.currentView === 'Foo' ? 'Bar' : 'Foo'
}
}
}
</script>
插槽(Slot)是Vue的一个功能,用于实现组件的内容分发。它允许你在父组件中定义一些内容,然后将这些内容分发到子组件的指定位置。
默认插槽,具名插槽和匿名插槽:
示例:
// 子组件
Vue.component('my-component', {
template: `
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
`
})
// 父组件
new Vue({
el: '#app',
template: `
<my-component>
<template v-slot:header>
<h1>Header</h1>
</template>
<p>Content</p>
<template v-slot:footer>
<h1>Footer</h1>
</template>
</my-component>
`
})
React是一个由Facebook创建的JavaScript库,用于构建用户界面。它是一个用于构建UI组件的工具。
React是一个前端框架,它允许开发人员使用组件化的方式来构建复杂的用户界面。React组件是独立的、可复用的代码块,它们可以接收输入并返回React元素来描述应该在页面上显示什么。
React的核心思想是声明式编程
。这意味着开发人员只需要描述应用程序应该呈现什么样子,而不需要关心如何实现它。React会负责计算出如何高效地更新用户界面,以便它始终与最新的状态保持一致。
优点:
缺点:
React是一个JavaScript库,用于构建用户界面。它的底层实现原理包括虚拟DOM、组件化架构和响应式更新等。
React组件的生命周期可分成三个状态:Mounting(挂载)、Updating(更新)和Unmounting(卸载)。
Mounting(挂载):当组件实例被创建并插入 DOM 中时,其生命周期调用顺序如下:
Updating(更新):每当组件的 state 或 props 发生变化时,组件就会更新。当组件的 props 或 state 发生变化时会触发更新。组件更新的生命周期调用顺序如下:
Unmounting(卸载):当组件从 DOM 中移除时会调用如下方法:
React-fiber是对React核心算法的一次重新实现。它能让React中的同步渲染进行中断,并将渲染的控制权让回浏览器,从而达到不阻塞浏览器渲染的目的。Fiber能够将渲染工作分割成块并将其分散到多个帧中。同时加入了在新的更新进入时暂停,中止或重复工作的能力和为不同类型的更新分配优先级的能力。
在Fiber诞生之前,React处理一次setState(首次渲染)时会有两个阶段:调度阶段(Reconciler)和渲染阶段(Renderer)。调度阶段React用新数据生成新的Virtual DOM,遍历Virtual DOM,然后通过Diff算法,快速找出需要更新的元素,放到更新队列中去。渲染阶段React根据所在的渲染环境,遍历更新队列,将对应元素更新。在浏览器中,就是更新对应的DOM元素。
这种设计看似合理,但是对于复杂组件,需要大量的diff计算,会严重影响到页面的交互性。例如,假设更新一个组件需要1ms,如果有500个组件要更新,那就需要500ms,在这500ms的更新过程中,浏览器唯一的主线程都在专心运行更新操作,无暇去做任何其他的事情。这就是所谓的界面卡顿。
React-fiber就是为了解决渲染复杂组件时严重影响用户和浏览器交互的问题。实现原理可以简单分为以下几个步骤:
这样,React Fiber就能够充分利用浏览器每一帧的工作特性,避免渲染复杂组件时严重影响用户和浏览器交互的问题。
React组件是组成React应用程序的可重复利用的模块。它们是用于构建Web和原生交互界面的库。
React组件可以分为两种类型:函数组件和类组件。
主要的区别:
示例:
// 函数组件
function Greeting(props) {
return <h1>Hello, {props.name}</h1>;
}
const Free = props =>{
return <h1>Hello, {props.name}</h1>;
}
// 类组件
class Counter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
render() {
return (
<div>
<p>Count: {this.state.count}</p>
<button onClick={() => this.setState({ count: this.state.count + 1 })}>
Increment
</button>
</div>
);
}
}
父子组件之间:父向子,可以通过props的方式传递。子组件可以通过props对象访问这些数据。子向父,子组件可以通过调用父组件传递给它的回调函数来向父组件传递数据。
// 父向子传
function Parent() {
const message = "来自父组件的问候";
return
}
function Child(props) {
return
{props.message}
;// 子向父传
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "" };
this.handleMessage = this.handleMessage.bind(this);
}
handleMessage(newMessage) {
this.setState({ message: newMessage });
}
render() {
return (
<>
{this.state.message}
function Child(props) {
function handleClick() {
props.onMessage("来自幼儿园技术家的问候");
}
return ;
}
兄弟组件之间:兄弟组件之间的数据传递,可以利用组件的Props以及Props回调函数来进行,而这种使用方法通信的前提是:必须要有共同的父组件。父组件可以维护一个状态,并将状态作为props传递给兄弟组件。同时,父组件还可以定义一个回调函数,用于更新状态,并将该回调函数作为props传递给兄弟组件。这样,兄弟组件就可以通过调用回调函数来更新状态,从而实现兄弟组件之间的通信。
示例:
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "" };
this.handleMessage = this.handleMessage.bind(this);
}
handleMessage(newMessage) {
this.setState({ message: newMessage });
}
render() {
return (
<>
>
);
}
}
function ChildA(props) {
function handleClick() {
props.onMessage("Hello from ChildA");
}
return ;
}
function ChildB(props) {
return
{props.message}
;跨组件层级:可以使用Context API来实现跨组件层级的通信。使用createContext
方法创建一个Context对象,然后使用Provider
组件包裹根组件,并通过value属性提供要共享的数据。在任意后代组件中,使用Consumer
组件包裹整个组件,就可以获取到共享的数据。
示例:
const MessageContext = React.createContext();
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = { message: "Hello from Parent" };
}
render() {
return (
);
}
}
function Child() {
return (
<>
>
);
}
function Grandchild() { {message}
return (
);
}
全局状态管理:对于非嵌套关系的组件通信,可以使用全局状态管理库,如Redux或MobX。这些库可以在应用程序的顶层维护一个全局状态,并允许组件订阅状态变化并更新其自身。这样,即使组件之间没有直接的嵌套关系,它们也可以共享状态并进行通信。
示例:
import { createStore } from "redux";
// Redux store
const initialState = { message: "" };
function reducer(state = initialState, action) {
switch (action.type) {
case "SET_MESSAGE":
return { message: action.message };
default:
return state;
}
}
const store = createStore(reducer);
// Parent component
class Parent extends React.Component {
render() {
return (
<>
>
);
}
}
// ChildA component
function ChildA() {
function handleClick() {
store.dispatch({ type: "SET_MESSAGE", message: "Hello from ChildA" });
}
return ;
}
// ChildB component
class ChildB extends React.Component {
constructor(props) {
super(props);
this.state = { message: "" };
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
const state = store.getState();
this.setState({ message: state.message });
});
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return
{this.state.message}
;React组件复用可以提高开发效率,减少Bug和程序体积。设计接口时,可以把通用的设计元素(按钮,表单框,布局组件等)拆成接口良好定义的可复用的组件。这样,下次开发相同界面程序时就可以写更少的代码。
复用方式有以下几种:
Props:通过props将数据和回调函数传递给子组件,可以实现组件的复用。
示例:
function Greeting(props) {
return
高阶组件(HOC):高阶组件是一种用于复用组件逻辑的高级技巧。它是一个接受组件作为参数并返回一个新组件的函数。
示例:
function withGreeting(WrappedComponent) {
return function(props) {
return (
<>
>
);
};
}
Render Props:Render Props是一种在React组件之间使用一个值为函数的prop共享代码的简单技术。
示例:
function Greeting(props) {
return props.children("Hello");
}
function App() {
return (
{greeting}, World
{greeting}, React
>
)}
);
}
React Hooks是一种新的API,它允许你在函数组件中使用状态和其他React特性。
常用的钩子有:
useState(状态钩子):useState
是一个允许你在函数组件中添加状态的Hook。它返回一个状态变量和一个更新该状态变量的函数。
示例:
import { useState } from "react";
function Example() {
const [count, setCount] = useState(0);
return (
你点击了 {count} 次
useEffect(副作用钩子):useEffect
是一个允许你在函数组件中执行副作用的Hook。它接受一个函数作为参数,该函数将在组件渲染后执行。
示例:
import { useState, useEffect } from "react";
function Example() {
const [count, setCount] = useState(0);
useEffect(() => {
document.title = 你点击了 ${count} 次
;
});
return (
你点击了 {count} 次
useContext(共享状态钩子):useContext
是一个允许你在函数组件中访问上下文的Hook。它接受一个上下文对象作为参数,并返回该上下文的当前值。
示例:
import { useContext } from "react";
const ThemeContext = React.createContext("light");
function Example() {
const theme = useContext(ThemeContext);
return
Current theme: {theme}
;useReducer(action 钩子):useReducer
是一个允许你在函数组件中使用类似于Redux的状态管理模式的Hook。它接受一个reducer函数和初始状态作为参数,并返回当前状态和一个dispatch函数。
示例:
import { useReducer } from "react";
function reducer(state, action) {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
throw new Error();
}
}
function Example() {
const [state, dispatch] = useReducer(reducer, { count: 0 });
return (
<>
Count: {state.count}
>
);
}
其他钩子函数:useCallback(记忆函数),useMemo(记忆组件)和useRef(保存引用值)等。
React的diff算法是一种高效的算法,它用来计算出Virtual DOM中真正变化的部分,并只针对该部分进行原生DOM操作,而非重新渲染整个页面,从而提高了页面渲染效率。简单来说,diff算法就是通过最小代价将旧的fiber树转换为新的fiber树。
React的每次更新,都会将新的ReactElement内容与旧的fiber树作对比,比较出它们的差异后,构建新的fiber树,将差异点放入更新队列之中,从而对真实dom进行render。
diff算法在React中处于主导地位,是React V-dom和渲染的性能保证,这也是React最有魅力、最吸引人的地方。React一个很大一个的设计有点就是将diff
和V-dom
的完美结合,而高效的diff算法可以让用户更加自由的刷新页面,让开发者也能远离原生dom操作。
setState()是React中用来更新组件状态的方法。当你调用setState()时,React会将你提供的对象与当前状态合并。例如,你的状态可能包含几个独立的变量:constructor(props) {super(props);this.state = {posts: [], comments: []};} 。
那么 setState 到底是同步还是异步的呢?
React中的setState()并不是真正意义上的异步,而是一个伪异步
或者称为延迟执行
。它的执行顺序在同步代码后、异步代码前。这种现象得益于React的合成事件,React的批处理更新也得益于合成事件。
注意:setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)中的callback拿到更新后的结果。
而 setState 关于同异步也可以分两种情况讨论:
在React事件处理程序和生命周期方法中,setState()是异步的,这意味着在调用setState()后,state不会立即更新。
示例:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新前的值
}
render() {
return (
你点击了 {this.state.count} 次
在setTimeout事件或者自定义的DOM事件中,setState()是同步的,这意味着在调用setState()后,state会立即更新。
示例:
class Example extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
componentDidMount() {
setTimeout(() => {
this.setState({ count: this.state.count + 1 });
console.log(this.state.count); // 输出的是更新后的值
}, 0);
}
render() {
return (
你点击了 {this.state.count} 次
React事件绑定的原理与传统的DOM事件绑定有所不同。在传统的DOM事件中,我们通常会将事件处理程序直接绑定到DOM元素上。但是,在React中,事件处理程序并不是直接绑定到真实的DOM元素上,而是在document处监听所有支持的事件。当事件发生并冒泡到document处时,React会将事件内容封装并交由真正的处理函数运行。
React中的事件都是合成事件,不是把每一个dom的事件绑定在dom上,而是把事件统一绑定到document中,触发时通过事件冒泡到document进行触发合成事件,因为是合成事件,所以我们无法去使用e.stopPropagation去阻止,而是使用e.preventDefault
去阻止。
这种设计可以提高性能,因为它避免了在每个DOM元素上都绑定事件处理程序。此外,它还使得React能够更好地控制事件的传播和处理。
在React中,key是一个特殊的字符串属性,它可以帮助React识别哪些元素发生了变化。当你渲染一个列表时,你应该给每个列表项分配一个稳定的、唯一的key。这样,当列表项的顺序发生变化时,React就能够正确地更新列表。
key的作用是帮助React确定哪些元素需要被重新渲染。当组件更新时,React会比较新旧两个Virtual DOM树,找出它们之间的差异。如果两个元素具有不同的key,React就会认为它们是不同的元素,并重新渲染它们。
前端工程化是一种将软件工程的方法和思想应用于前端开发的过程。它主要指从前端项目开始开发到部署线上再到后期迭代维护的整个过程,从工程的角度管理前端开发,形成前端开发流程的一整套开发规范或解决方案,提高前端开发效率。
前端工程化可以提升开发体验、提高开发效率和质量、提升应用的访问性能。一切以提高效率、降低成本、质量保证为目的的手段都属于工程化。
Webpack的构建流程大致如下:
优化Webpack的构建速度有很多方法,可以从以下几个方面入手:
babel-loader
、ts-loader
等loader
的缓存选项。thread-loader
或happypack
。speed-measure-webpack-plugin
来分析Webpack构建期间各个阶段花费的时间,从而快速定位到可以优化的地方。优化Webpack的构建速度可以带来许多好处。随着项目涉及到的页面越来越多,功能和业务代码也会越来越多,相应的Webpack的构建时间也会越来越久。这个时候我们就不得不考虑性能优化的事情了。
因为这个构建时间与我们的日常开发是密切相关,当我们本地开发启动devServer或者build的时候,如果时间过长,会大大降低我们的工作效率。试想一下,我们突然碰到一个紧急bug,项目启动需要花费几分钟,改完后项目build上线也要几分钟,换谁估计都得有暴脾气了…
优化Webpack的打包体积可以带来许多好处。打包体积越小,应用程序的加载速度就越快,用户体验就越好。下面是一些优化Webpack打包体积的方法:
CommonsChunkPlugin
来提取公共代码。externals
配置来提取常用库。DllPlugin
和DllReferencePlugin
预编译资源模块。Tree-shaking
和Scope Hoisting
来剔除多余代码。例如:如果你在开发项目时将整个组件库都引入了,那么在使用Webpack打包时,可以使用Tree-shaking来自动删除没有引用的组件,从而减小打包体积。Tree-shaking是一种通过静态分析代码,删除未引用代码的技术。它可以帮助开发人员减小打包体积,提高应用程序的加载速度。
Tree-shaking的原理是基于ES6模块的静态结构特性。由于ES6模块的导入和导出是在编译时确定的,而不是在运行时确定的,因此Webpack可以在构建过程中静态分析代码,找出未被引用的模块,并将它们从最终的打包文件中删除。
不过需要注意的是,Tree-shaking只能删除未被引用的模块,而不能删除未被执行的代码。因此,如果你想要使用Tree-shaking来优化打包体积,需要注意代码组织方式,尽量避免在一个模块中混合使用被引用和未被引用的代码。
Webpack性能优化有很多方法,可以从以下几个方面入手:
resolve.alias
来减少模块解析的时间。loader
的应用范围来提高构建速度。Webpack的loader和plugin是两种不同的扩展机制,它们都可以帮助开发人员定制Webpack的构建过程。
babel-loader
(用于将ES6+代码转换为ES5代码)、css-loader
(用于加载CSS文件)、file-loader
(用于加载文件)等。HtmlWebpackPlugin
(用于生成HTML文件)、MiniCssExtractPlugin
(用于提取CSS文件)、UglifyJsPlugin
(用于压缩JavaScript代码)等。Git是一种分布式版本控制系统,它可以帮助开发人员管理和协作代码。Git可以跟踪代码的变化历史,帮助开发人员查看每次修改的内容,并在出现问题时快速恢复到之前的状态。
Git支持分支和合并,可以帮助开发人员在不同的分支上并行开发功能,然后将它们合并到主分支上。这样,开发人员可以更好地协作,并更快地完成项目。
常用的git命令:
不同类型的分支:
ESLint是一个用于识别和报告ECMAScript/JavaScript代码中模式的工具,旨在使代码更加一致并避免错误。它是完全可插拔的,每个规则都是一个插件,你可以在运行时添加更多。你还可以添加社区插件、配置和解析器来扩展ESLint的功能。
ESLint可以帮助你快速找到代码中的问题。它内置于大多数文本编辑器中,你可以将ESLint作为持续集成管道的一部分运行。许多ESLint发现的问题都可以自动修复。ESLint修复是语法感知的,因此你不会遇到传统查找和替换算法引入的错误。
作用:
补充说明一些常见的前端工程化工具和技术:
前端页面性能优化是一个复杂的过程,可以从多个方面进行优化。一些常见的优化方法包括:
回流(reflow)和重绘(repaint)是浏览器渲染过程中的两个步骤。它们都会影响页面的渲染性能,因此应尽量避免。
避免回流和重绘的一些方法包括:
UglifyJS
等工具来压缩JavaScript代码,使用CSSNano
等工具来压缩CSS代码。此外,还可以使用Webpack
等构建工具来合并多个文件为一个文件,减少HTTP请求数量。CDN
、启用Gzip压缩、使用浏览器缓存等方法来优化资源加载,提高页面加载速度。例如,可以将静态资源部署到CDN上,以加快资源加载速度;可以在服务器端启用Gzip压缩,以减小传输文件的大小;可以合理设置HTTP缓存头,以利用浏览器缓存加快页面加载速度。尽量减少对DOM的操作
,避免触发浏览器的回流和重绘。懒加载
、预加载
、按需加载
等技术来优化用户交互,提升用户体验。例如,可以使用懒加载技术来延迟加载页面中不可见的图片;可以使用预加载技术来预先加载页面中即将需要的资源;可以使用按需加载技术来动态加载页面中需要的JavaScript模块。模块化
、组件化
、设计模式
等技术来提高代码的可维护性,降低维护成本。例如,可以使用CommonJS或ES6模块化语法来组织代码;可以使用React或Vue等框架来构建可复用的组件;可以使用设计模式来编写可扩展、可维护的代码。其他一些措施:
如果各位大佬还有其他什么方法措施打在评论区吧,我会加上去的!
浏览器渲染一帧的过程包括以下几个步骤:
处理用户输入:浏览器会处理用户的输入事件,如鼠标点击、键盘输入等。
JavaScript执行:浏览器会执行页面中的JavaScript代码。
请求动画帧回调:浏览器会执行requestAnimationFrame回调函数。
样式计算:浏览器会计算元素的最终样式。
布局:浏览器会根据元素的样式和大小计算它们在页面中的位置。
绘制:浏览器会根据元素的样式和位置绘制它们。
合成:浏览器会将多个图层合并为一张图像,并显示在屏幕上。
这些步骤是浏览器渲染一帧的基本过程。不同的浏览器可能会有一些细微的差别,但总体流程是相同的。
HTTP(超文本传输协议)和HTTPS(超文本传输安全协议)都是用于在Web浏览器和网站服务器之间传输信息的协议。它们的主要区别在于安全性。
HTTP是一种明文传输协议,它不提供任何加密机制。这意味着,如果攻击者截获了HTTP传输的数据,他们可以直接读取其中的内容。因此,HTTP不适合用于传输敏感信息,如信用卡号、密码等。
优点:
缺点:
工作原理:
HTTPS则是HTTP的安全版本。它在HTTP的基础上使用了SSL/TLS协议来加密数据。HTTPS 开发的主要目的是提供对网站服务器的身份认证,保护交换数据的隐私与完整性。整个过程中,客户端和服务器之间传输的数据都是经过加密的,这意味着即使攻击者截获了HTTPS传输的数据,他们也无法读取其中的内容,除非能够破解加密算法。
优点:
缺点:
工作原理:
怎么保证安全性的?
HTTPS 通过使用对称加密、非对称加密、签名算法和证书机制来保证数据安全。在 HTTPS 请求过程中,客户端首先会请求服务端的数字证书,并生成一个随机数 R1,将随机数和自己支持的加密算法告诉服务端。服务端接收到客户端的请求后,会将自己的数字证书发送给客户端,并生成一个随机数 R2,然后根据客户端支持的加密算法选择一种加密算法,并将 R2 和加密算法告诉客户端。客户端接收到服务端的响应后,会验证服务端的数字证书是否有效,如果有效,则根据 R1、R2 和服务端选择的加密算法生成一个对称加密的密钥,并使用该密钥对后续通信进行加密。
这样,HTTPS 协议就能够保证所有信息都是加密传播,第三方无法窃听;具有校验机制,一旦被篡改,通信双方会立刻发现;配备身份证书,防止身份被冒充1。
另外,HTTP和HTTPS还有一些其他区别。例如,它们使用的端口不同:HTTP默认使用80
端口,而HTTPS默认使用443
端口。且由于HTTPS需要进行加密
和解密
操作,因此它比HTTP更耗费服务器资源。
TCP(传输控制协议)是一种面向连接的协议,它使用三次握手来建立连接,并使用四次挥手来断开连接。
过程:
过程:
全双工通信协议
,因此每个方向都需要单独进行关闭。这就需要四次挥手来完成。首先,TCP使用序列号和确认应答机制来保证数据包的有序传输。每个发送的数据包都会被分配一个序列号,接收方收到数据包后会发送一个确认应答报文,其中包含对发送方序列号的确认。这样,发送方就能够知道哪些数据包已经被接收,哪些数据包需要重传。
其次,TCP使用检验和来保证数据包的完整性。发送方在发送数据包时会计算一个检验和,并将其附加到数据包中。接收方收到数据包后会重新计算检验和并与发送方的检验和进行比较。如果两者不一致,则说明数据包在传输过程中发生了错误,接收方会丢弃该数据包并请求重传。
此外,TCP还使用超时重传机制来保证数据包的可靠传输。当发送方发送一个数据包后,它会启动一个定时器。如果在定时器超时之前没有收到接收方的确认应答报文,则认为该数据包丢失,并进行重传。
最后,TCP还使用流量控制和拥塞控制机制来保证网络的稳定性。流量控制通过调整发送窗口的大小来控制发送方的发送速率,防止接收方缓冲区溢出。拥塞控制则通过调整拥塞窗口的大小来控制网络拥塞程度,防止网络拥塞。
总之,TCP和UDP各有优缺点。TCP提供了可靠的数据传输,但速度相对较慢;而UDP速度快,但不提供可靠性保证。选择哪种协议取决于应用程序的需求。
跨域是指浏览器为了安全起见,限制了脚本内发起的跨源HTTP请求。这种限制被称为同源策略。同源策略规定,只有当协议
、域名
和端口
都相同时,两个页面才被认为是同源的。如果两个页面不同源,那么它们之间就不能进行跨域请求。
例如,运行在 https://api.example-a.com 的 JavaScript 代码使用 XMLHttpRequest 来发起一个到 https://api.example-b.com/data.json 的请求。这由于域名的不同所以不同源,这就是一个跨域请求。
但是,有时候我们需要在不同源之间进行通信。为了解决这个问题,出现了一些解决跨域问题的方法,如JSONP
、CORS
、代理
和postMessage
等。这些方法都是通过绕过浏览器的同源策略限制来实现跨域请求的。
JSONP:JSONP是一种通过动态创建<script>
标签来实现跨域请求的方法。它利用了<script>
标签的src属性不受同源策略限制的特点,可以获取到其他域下的数据。
原理:
<script>
标签,将其src属性指向目标服务器上的一个接口,并在URL中添加一个callback参数,用来指定回调函数的名称。<script>
标签被插入页面后,浏览器会自动发起一个GET请求,获取目标服务器上的数据。目标服务器在接收到请求后,会将数据包装在回调函数中返回。示例:
<!DOCTYPE html>
<html>
<head>
<title>JSONP Example</title>
</head>
<body>
<h1>JSONP Example</h1>
<p id="output"></p>
<script>
// 定义回调函数
function handleResponse(data) {
// 在回调函数中处理获取到的数据
document.getElementById('output').innerHTML = data.message;
} // 动态创建<script>标签
var script = document.createElement('script');
// 设置src属性,指定回调函数的名称
script.src = 'https://api.example.com/getData?callback=handleResponse';
// 将<script>标签插入页面
document.body.appendChild(script);
</script>
</body>
</html>
缺点:
CORS:CORS(Cross-Origin Resource Sharing,跨源资源共享)是一种基于HTTP头的机制,它允许服务器标识除了它自己以外的其他源(域、协议或端口),使得浏览器允许这些源访问加载自己的资源。服务器可以通过设置响应头中的Access-Control-Allow-Origin字段来指定哪些源可以访问它的资源。
代理:可以在服务器端设置一个代理,将前端发出的请求转发到目标服务器上,然后再将目标服务器返回的数据转发回前端。这样,前端就可以绕过浏览器的同源策略限制,实现跨域请求。
postMessage:postMessage是HTML5中新增的一个API,它允许不同源之间的窗口进行通信。可以通过监听message事件来接收其他窗口发送过来的消息。
浏览器提供了多种缓存机制,包括HTTP缓存、Cookie、Web Storage(包括localStorage和sessionStorage)和IndexedDB等。
用户会话信息
。session与上述几种缓存机制不同,它不是在浏览器中实现的,而是在服务器端实现的。浏览器缓存(Browser Caching)是为了节约网络的资源加速浏览,浏览器在用户磁盘上对最近请求过的文档进行存储,当访问者再次请求这个页面时,浏览器就可以从本地磁盘显示文档,这样就可以加速页面的阅览。
通常浏览器缓存策略分为两种:强缓存(Expires,cache-control)和协商缓存(Last-modified ,Etag),并且缓存策略都是通过设置 HTTP Header 来实现的。
当浏览器请求资源时,首先会检查资源的Expires和Cache-Control。如果命中强缓存,状态仍然返回200,但不会请求数据,在浏览器中能明显看到from cache字样。如果强缓存失效,则会携带缓存标识向服务器发起请求,由服务器根据缓存标识决定是否使用缓存。如果命中协商缓存,则返回304状态码,并且不会返回数据。
协商缓存是一种服务端的缓存策略,即通过服务端来判断某件事情是不是可以被缓存。服务端判断客户端的资源,是否和服务端资源一致,如果一致则返回304,反之返回200和最新的资源。
当网页不是首次加载时,如果设置了强缓存,数据则会从缓存中读取,不请求服务端。如果强缓存时间过期,则会请求服务端,服务端判断是否命中协商缓存,如果协商缓存时间或者哈希没变,则返回304。如果协商缓存时间对比不一样或资源变化,则数据重新被获取,返回200。
进程、线程和协程是计算机程序执行的三种不同方式。
进程和线程是两个密切相关但又不同的概念。
进程和线程之间的区别主要在于它们是不同的操作系统资源管理方式。
网络攻击是指损害网络系统安全属性的危害行为,危害行为导致网络系统的机密性、完整性、可控性、真实性、抗抵赖性等受到不同程度的破坏。
常见的网络攻击类型包括:
部分攻击的防御措施:
GET和POST是两种HTTP请求方法,它们之间有一些区别。
HTTP状态码用来表明特定HTTP请求是否成功完成。响应被归为以下五大类:
一些常见的HTTP状态码包括:
CDN是Content Delivery Network的缩写,即内容分发网络。它的基本思路是尽可能避开互联网上有可能影响数据传输速度和稳定性的瓶颈和环节,使内容传输的更快、更稳定。
CDN的工作原理就是将源站的资源缓存到CDN各个节点上,当请求命中了某个节点的资源缓存时,立即返回客户端,避免每个请求的资源都通过源站获取,避免网络拥塞、缓解源站压力,保证用户访问资源的速度和体验。
简单来说,CDN通过在全球范围内部署大量节点服务器,将网站内容缓存在这些节点服务器上,当用户访问网站时,CDN会根据用户的地理位置和网络状况等因素,智能调度用户访问离其最近的节点服务器,从而加快网站访问速度、提高网站可用性。
主要作用:
时间复杂度和空间复杂度:衡量算法的优劣。 时间复杂度和空间复杂度是用来衡量算法的优劣的两个指标。时间复杂度表示算法执行时间与数据规模之间的关系,常用大O表示法来表示。空间复杂度表示算法在运行过程中临时占用存储空间大小的量度,也常用大O表示法来表示。
常见的算法类型包括:
常见的考察内容包括:
例子:
爬楼梯:每次可以选择爬1个或2个台阶,问有多少种爬楼梯的方法。
var climbStairs = function(n) {
if (n === 1) {
return 1;
}
let dp = new Array(n + 1);
dp[1] = 1;
dp[2] = 2;
for (let i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
};
有效的括号:判断括号是否正确闭合。
var isValid = function(s) {
let stack = [];
let map = {
'(': ')',
'[': ']',
'{': '}'
}
for (let i = 0; i < s.length; i++) {
if (map[s[i]]) {
stack.push(s[i]);
} else if (s[i] !== map[stack.pop()]) {
return false;
}
}
return stack.length === 0;
};
整数转化英文表示:将整数转化为英文表示。
const singles = ["", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine"];
const teens = ["Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", "Sixteen", "Seventeen", "Eighteen", "Nineteen"];
const tens = ["", "Ten", "Twenty", "Thirty", "Forty", "Fifty", "Sixty", "Seventy", "Eighty", "Ninety"];
const thousands = ["", "Thousand", "Million", "Billion"];
var numberToWords = function(num) {
if (num === 0) {
return 'Zero';
}
let sb = '';
let unit = 1000000000;
for (let i = 3; i >= 0; i--) {
let curNum = Math.floor(num / unit);
if (curNum !== 0) {
num -= curNum * unit;
sb += toEnglish(curNum) + thousands[i] + ' ';
}
unit /= 1000;
}
return sb.trim();
};
function toEnglish(num) {
let curr = '';
let hundred = Math.floor(num / 100);
num %= 100;
if (hundred !== 0 && num !== 0) {
curr += singles[hundred] + ' Hundred ';
} else if (hundred !== 0 && num === 0) {
curr += singles[hundred] + ' Hundred';
}
let ten = Math.floor(num / 10);
if (ten >= 2 && num % 10 !== 0) {
curr += tens[ten] + ' ';
num %= 10;
} else if (ten >= 2 && num % 10 === 0) {
curr += tens[ten];
num %= 10;
}
if (num < 10 && num > 0) {
curr += singles[num] + ' ';
} else if (num >=10 && num <20) {
curr += teens[num -10] + ' ';
}
return curr;
}
Z 字形变换:将字符串按照Z字形排列后输出。
var convert = function(s, numRows) {
if (numRows === 1 || s.length < numRows) return s;
let rows = [];
let converted = '';
let reverse = false;
let count = 0;
for (let i=0; i<numRows; i++) rows[i] = [];
for (let i=0; i<s.length; i++) {
rows[count].push(s[i]);if (count === numRows-1 || count === 0) reverse=!reverse;
reverse ? count++ : count--;
}
for(let row of rows){
converted+=row.join('');
}
return converted;
};
两数之和:在数组中找出和为目标值的两个数。
var twoSum = function(nums, target) {
let map = new Map();
for (let i = 0; i < nums.length; i++) {
let complement = target - nums[i];
if (map.has(complement)) {
return [map.get(complement), i];
}
map.set(nums[i], i);
}
};
最长回文子串:找出字符串中最长的回文子串。
var longestPalindrome = function(s) {
if (s.length < 2) return s;
let start = 0;
let maxLength = 1;
function expandAroundCenter(left, right) {
while (left >= 0 && right < s.length && s[left] === s[right]) {
if (right - left + 1 > maxLength) {
maxLength = right - left + 1;
start = left;
}
left--;
right++;
}
}
for (let i = 0; i < s.length; i++) {
expandAroundCenter(i - 1, i + 1);
expandAroundCenter(i, i + 1);
}
return s.substring(start, start + maxLength);
};
以上就是目前所学的知识进行收集和整理的一些前端八股文,如若有错误请大佬们指正我会马上进行更改,若有欠缺请兄弟们补充一下我都会加上去!
最好的学习还是要把所学的知识进行整理理解,其实我也是小白,以上还有很多不大懂的地方需要进行理解学习再融会贯通,希望与各位一起进步,加油!!!
https://juejin.cn/post/6994617237793406990
https://juejin.cn/post/7016593221815910408
创作不易,若需转载请备注出处!
手机扫一扫
移动阅读更方便
你可能感兴趣的文章