前端面试题
-
14.数组中第K个最大元素
在未排序的数组中找到第 k 个最大的元素。请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。
题目很清晰,需要找到第K大的元素,最简单的思路就是排序,然后就能根据下标定位到第K大的数。这样做是可行的,但我们需要思考是否有优化空间。
题目要求的是查找第K大的数,实际上,如果我们不需要完全排好序就可以确认第K大位置的元素,就不需要再继续排序浪费操作次数。
不用完全排序,也就是排序是递进的过程,主要有两种排序算法:快排和堆排序。
快排,也就是快速排序算法,使用的是分而治之的思想,
- 从序列中选择一个基数
- 把数字较小的放到左边,较大的放到右边
- 对左右区间重复以上步骤,直到区间数只有一个时,排序结束
/** * 查找序列中第K大的数字 * @param {number[]} nums * @param {number} k * @returns {number} */ function findKthLargest(nums, k) { return quickSort(nums)[nums.length - k]; }; /** * 快速排序 * @param {number[]} nums 待排序数组 * @param {number?} left 区间左指针 * @param {number?} right 区间右指针 * @returns {number[]} */ function quickSort(nums, left, right) { if(Object.is(left, undefined)) { left = 0; } if(Object.is(right, undefined)) { right = nums.length - 1; } if(left >= right) { return nums; } let baseIndex = left; // 基数指针 const base = nums[baseIndex]; for(let i = left;i < right + 1; i++) { // 小于基数,放到基数左边,基数被往右“挤”一位 if(nums[i] < base) { // 交换 nums[baseIndex] = nums[i]; // less baseIndex++; nums[i] = nums[baseIndex]; // more nums[baseIndex] = base; // base } // 大于基数本身就在右侧,无需移动 } quickSort(nums, left, baseIndex-1); quickSort(nums, baseIndex+1, right); return nums; }
排有多种不同的位置交换方案,上面使用的是一次遍历法,从左扫到右,遇到比基数小的放到左边即可,值得注意的是,由于是往前放,需要把基数和右区间的数后移,右区间后移只需要把基数移到右区间最前端(基数后边那个数)移到右区间最后端(遍历指针的地方),基数后移一位即可。
此外,还有一种碰撞双指针法,左指针指向左区间最右侧,右指针指向右区间最左侧。所以初始时左右指针在数组区间的左右两侧。首先从左侧开始遍历,需要找较大值,找到需要移到右区间,也就是移到右指针的位置,同时,调整基数位置到左指针处。然后开始遍历右侧,找较小值,找到需要放到左区间,也就是i指针的位置,同时,调整基数的位置到右指针处。重复,直到左右指针碰撞,说明左右侧均找完。
function quickSort(nums, left, right) { if(Object.is(left, undefined)) { left = 0; } if(Object.is(right, undefined)) { right = nums.length - 1; } if(left >= right) { return nums; } let i = left, j = right; const base = nums[j]; while(i < j) { // 寻找左侧比基数大的值 while(i < j && nums[i] <= base) { i++; } nums[j] = nums[i]; nums[i] = base; // 寻找右侧比基数小的值 while(j > i && nums[j] >= base) { j--; } nums[i] = nums[j]; nums[j] = base; } quickSort(nums, left, j-1); quickSort(nums, j+1, right); return nums; }
基于快速排序的快速选择
我们知道,快速排序是分治思想,一步一步进行排序的,其中有个数据是明确的,那就是基数的位置。每进行一次的快排,我们就可以得到基数的位置,如果要找的数在K左侧,那我们就只需要快排左区间,如果在右侧,就只需要快排右区间,直到基数就是要找的数字为止。
实际上,我们只需要把快排函数稍微修改即可:
/** * 基于快速排序的快速查找 * @param {number[]} nums 待排序数组 * @param {number} k * @param {number?} left 区间左指针 * @param {number?} right 区间右指针 * @returns {number} */ function findKthLargest(nums, k, left, right) { if(Object.is(left, undefined)) { left = 0; } if(Object.is(right, undefined)) { right = nums.length - 1; } if(left >= right) { return nums[right]; } let i = left, j = right; const base = nums[j]; while(i < j) { // 寻找左侧比基数大的值 while(i < j && nums[i] <= base) { i++; } nums[j] = nums[i]; nums[i] = base; // 寻找右侧比基数小的值 while(j > i && nums[j] >= base) { j--; } nums[i] = nums[j]; nums[j] = base; } const d = j - (nums.length - k); if(d == 0) { return base; } else if(d > 0) { return findKthLargest(nums, k, left, j-1); } else { return findKthLargest(nums, k, j+1, right); } }
由于利用到了K值信息以及快排的特点,我们只需要对左区间或右区间进行快排就能找到答案,而无需整个数组完全排序结束。
堆排序
堆排序是利用了堆这种数据结构:
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子的值,称为大顶堆(大根堆)。或者每个结点的值都小于或等于其左右孩子的值,称为小顶堆(小根堆)。
根据堆的特点,我们知道,大根堆能保证根元素为最大值,小根堆能保证根元素为最小值,这样,我们通过不断构建堆结构,同时不断缩小堆的规模,当堆的规模为1时,排序结束。这就是堆排序的逻辑。
- 把一个无序序列构建成一个大根堆(升序)或小根堆(降序)
- 将堆顶元素放到序列末尾
- 序列长度缩小1,重复以上步骤,直到序列长度为1,结束排序过程。
看到堆排序有点冒泡排序的韵味,都是找最大值,然后存起来,同时不断缩小查找序列的范围。然而堆排序和快排一样,时间复杂度仅为`O(nlogn)。这是因为堆这种结构带来的优化效果:当第一次构建堆之后,后续只是调整首位交换带来的变化,而无需像第一步那样重建堆。重建堆和调整堆是有很大区别的:重建是对无序序列,需要从最后一个非叶子节点开始调整,是从下往上调整,但之后的调整堆由于只有根元素发生了变化,而其他非叶子节点都已经是堆结构了,所以只需要从上往下调整,直到某个非叶子节点也变成堆结构。
堆排序实际上也可以原地排序,由于是完全二叉树,非叶子节点与左右孩子的对应关系十分明确,无需借助多余的堆结构。
/** * 查找序列中第K大的数字 * @param {number[]} nums * @param {number} k * @returns {number} */ function findKthLargest(nums, k) { // 1.构建大根堆 let level = nums.length; buildHeap(nums, level); // 2.交换首尾元素, 缩小堆级别并维护堆,重复步骤2直至堆级别为1 while(level > 1) { // 交换首尾 level--; const root = nums[0]; nums[0] = nums[level]; nums[level] = root; // 重新维护堆 adjustHeap(nums, level, 0); } // 返回第K大的数 return nums[nums.length - k]; }; /** * 构建大根堆 * @param {number[]} nums 序列 * @param {number} level 构建级别|范围|长度:[0 ~ level) */ function buildHeap(nums, level) { // 节点(i) => (左孩子)2*i + 1, (右孩子)2*i + 2 // 最后一个非叶子节点,也就是至少存在左孩子 => 2*i + 1 <= len - 1 => i <= len/2 - 1 const lastNodeIndex = Math.ceil(level/2 - 1); for(let i = lastNodeIndex; i >= 0; i--) { adjustHeap(nums, level, i); } } /** * 调整大根堆 * @param {number[]} nums 序列 * @param {number} level 构建级别|范围|长度:[0 ~ level) * @param {number} i 当前结点下标 */ function adjustHeap(nums, level, i) { const lastNodeIndex = Math.ceil(level/2 - 1); if(i <= lastNodeIndex) { let nodeVal = nums[i]; // 交换孩子结点中的最大值 const left = 2 * i + 1; const right = 2 * i + 2; let sweapIndex = i; // 交换结点的坐标 if(left < level && nums[left] > nodeVal) { sweapIndex = left; nodeVal = nums[left]; } if(right < level && nums[right] > nodeVal) { sweapIndex = right; nodeVal = nums[right]; } // 交换 if(sweapIndex != i) { nums[sweapIndex] = nums[i]; nums[i] = nodeVal; adjustHeap(nums, level, sweapIndex); } } }
-
13.Vue双向绑定的实现原理
Vue的双向绑定原理简单来说就是当数据发生变化时能检测到数据变化,然后做出响应。而js中的Object.defineProperty
的getter
和setter
正是用于监听数据的读取操作的,Vue也是基于这两个api来实现数据的监听,进而实现即时响应。
双向绑定的实现有两个过程:
- 数据劫持(Observer):也就是数据监听的定义,即使用Object.defineProperty的getter和setter来实现数据劫持(Vue3.0已使用Proxy代理对象来实现数据劫持)。
- 视图更新逻辑:当数据发生变化,就会被Observer作为观察者监听到,然后发送消息给Dep,Dep作为经纪人再将信息发送给所有订阅者,订阅者就会触发视图的更新:re-render,所以数据变化能触发视图更新。
- 双向绑定:也就是反过来视图变化也能更新数据,视图是Dom,所以视图变化我们可以通过原生Dom的事件来实现监听,然后触发数据变化,数据变化的变化又引起视图层的变化,也就实现了双向绑定效果。
-
12.undefined和null的区别?
typeof null
的结果为什么是object
?
首先从定义来说,undefined是指未初始化的变量,而null是指空对象,虽然都是基本数据类型,但本质上是不一样的数据类型。
这一点从typeof null
为object
也可以看出。本质上,也就是从存储方式上来说,null的存储方式和undefined也是不同的。
在第一版的js设计中,使用32位作为存储单元,并使用低三位(1-3位)表示值的类型:
-
000:Object类型,后续位数用于存储指向对象的引用,而null的后31位全是0,用于表示无引用,也就是空对象。
-
1:int类型,后续位数存储一个31位的有符号整数。
-
010:double类型。后续位数存储一个双精度浮点数。
-
100:string类型。
-
110:布尔值。
而undefined使用整数
-2^30
表示,也就是说需要32位才能表示这个数字,这超出了int类型的范围。(尽管如此,我还是不太清楚这里具体是怎么区别undefined和null的,因为-2^30
用32位二进制表示为11000000000000000000000000000000
,同样的低三位为000
,如果只按照低三位作为判断标准,那么undefined
同样判断为object
类型才对,没找到相关说明,难受。目前的猜测是当进行类型判断时首先判断这个数字是否与-2^30
相等,相等就直接返回undefined
,不相等再进一步根据低三位数值来判断数据类型,不过这种设计思路取决于开最初的设计者,无需太过关注。)
-
11.单行和多行文本溢出如何处理?
文本溢出最常见的方式就是替换为省略号。文本溢出属性为text-overflow
,有三个可选值:
- clip:默认值,裁剪溢出文本,即溢出文本会被隐藏起来
- ellipsis:省略号的意思,溢出部分替换为省略号,这也是最常用的文本溢出处理方式
- string:实验中的属性,可用指定字符特换溢出文本
除此之外,溢出文本一般还需要搭配两个属性才能正常工作,
- overflow:溢出处理,可选值有
visible
、hidden
、scroll
和auto
。一般来说,为了保证溢出文本正确被替换为省略号,需要隐藏起来才能称之为溢出文本。 white-space
:空白处理,同样的道理,为了保证溢出文本不显示出来,需要设置为不换行,即nowrap
值才行。
多行文本有时候也需要溢出显示为省略号,但这个时候whire-space
对于多行来说就不起作用了,为了达到这个效果,我们需要另外使用几个属性:
text-overflow: ellpsis;
overflow: hidden;
/** 显示方式设置为box,子元素垂直排列*/
display: -webkit-box;
-webkit-box-orient: vertical;
/** 需要显示到的行数*/
-webkit-line-clamp: 3;
复制代码
由于display: box
、box-orient
和line-clamp
都是实验中的属性,一些浏览器并未支持,所以存在兼容性问题。
实例:
<div id="app" style="width: 200px;border: solid 1px #acc;display: -webkit-box;-webkit-box-orient: vertical;text-overflow: ellipsis;-webkit-line-clamp: 3;overflow: hidden;"> <p>这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,</p> <p>这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,</p> <p>这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,这是一行文字,</p> </div>
总结来说就是,文本溢出处理需要使用text-ellipsis
属性,常见的是设置为ellipsis
,即省略号,但文本溢出属性需要“文本发生溢出”时才会生效,对应单行文本,通过overflow:hidden
和white-space:nowarp
来让单行文本达到溢出状态。
而对于多行文本,需要将父元素设置为box
布局,且子元素排列方式box-orient
设置为垂直排列,最后,再设置显示的行数line-clamp
,这样后面为显示的行就会被替换为省略号了。
-
10.script标签中defer和async属性的区别?【
html
】
script
标签一般用于加载js脚本,我们知道js脚本是阻塞式加载和执行的,即页面解析到script
标签时会暂停页面的解析,先加载脚本并执行脚本后再继续页面的解析。
而H5新增的两个属性:defer
和async
,可以让脚本异步加载,但脚本的执行方式有所不同,defer
是延迟的意思,所以脚本会在页面加载结束再执行,而async
是异步的意思,脚本只会异步加载,并立即执行。
页面的加载、脚本加载和脚本执行示意图如下所示:
总结来说就是,defer
和async
属性让script
标签能异步加载,但async
立即执行,而defer
是延迟到页面加载结束再执行。
值得注意的是,当两个属性同时存在时,async
的优先级更高。
-
9.算法题:无重复字符的最长子串
给定一个字符串,请你找出其中不含有重复字符的最长子串的长度。
暴力解法通常就是万能解法,但一般都不是我们需要的算法,然而通过对暴力算法的分析,我们可以往往能找到解决问题的关键,进而加以优化,就能得到较为优质的答案。
本题的暴力解法是:循环两次,找到所有的子串,然后判断该子串是否包含重复字符,并记录出现的最长无重复字符子串。其中的关键点有:获取子串;判断是否具有重复字符;记录最长子串。
子串的获取我们实际上通过双指针遍历一次就能实现,而不用遍历两次。判断是否存在重复子串我们可以用hash表来协助。这样,双指针的移动逻辑就成了本题的解题关键:当我们未遇到重复字符时,遍历指针不断后移即可,当遇到了重复字符,前指针就指向重复字符之后的那个字符以保障整个子串不重复。
简单来说就是通过滑动双指针的方案,在遍历过程以维护子串没有重复字符为目标而滑动指针,当遍历结束,判断也随之结束,这样,时间复杂度就变为了O(n)
。
/** * @param {string} s * @return {number} */ var lengthOfLongestSubstring = function(s) { let i = 0; let hash = []; let maxLen = 0; for(let j = 0; j < s.length; j++) { const c = s[j]; // 出现重复 if(hash.includes(c)) { maxLen = Math.max(maxLen, j - i ); const now = hash.indexOf(c) + 1; i += now; // 滑动左指针 // 左指针之前的hash值都要移除 hash = hash.splice(now); } // 记录出现的字符 hash.push(c) } return Math.max(maxLen, s.length - i); };
-
8.阐述Vue的基本实现原理【
vue
】
以往的前端都是使用js直接控制DOM来实现页面的变化,js也更像是为页面服务的小弟(脚本)。但随着web应用的复杂化,js和页面的复杂程度和以往相比都不是一个量级的,有组织有架构的工程化开发就变得十分必要,随着MV*
类架构的发展,很多桌面应用都迁移到了web端,变成了web app,也就是单页面应用(single page web application)。
在这些变化中,数据驱动视图成为主流设计,比如Vue采用的MVVM
模型也是如此:
Vue实例创建时:
- 解析指令(Compiler):通过
template
和data
等参数构建渲染函数Render Function
。 - 劫持数据变化(Obverser):并通过
Object.defineProperty
的getter/setter
来劫持数据变化。(Vue3.0通过Proxy代理对象来实现) - 发布订阅模式实现数据响应:上面做数据劫持的目的就是为了监听数据变化,也就是发布订阅中的观察者身份
Obverser
,当数据变化后,会发送通知给Dep
对象,也就是发布订阅模式中的事件中心(经纪人),然后又Dep
告知订阅者Watcher
对象,Watcher
便响应式地触发了re-render
重新渲染的过程。
注:从上图来看,Vue更像是观察者模式,比如Obverser
调用Notify
来告知Watcher
,实际上,这里只是省略了Dep
对象。而观察者模式和发布订阅模式虽然从结构上来说,发布订阅多了一个事件中心对象Dep
,也就是观察者和订阅者之间没有直接关联了,解耦合了,但从意图上来看,这两种模式都是为了实现一对多依赖关系而设计,因此,往往可以不必过于纠结于此,发布订阅模式可看作是观察者模式的升级版。
另外,渲染函数更新的其实是Virtual Dom
即虚拟Dom,然后才是通过虚拟Dom更新视图,这有助于视图更新的性能优化,更多细节可参考:virtual dom - github
参考:
-
7.判断js数据类型的方法有哪些?【
js
】
js数据类型分为两种:基本数据类型和引用数据类型。一般情况下,判断基本数据类型使用typeof
关键字,而判断引用类型使用instanceof
关键字。
-
typeof
typeof 2 === 'number' typeof 'jinx' === 'string' typeof null === 'object' typeof Ayyay === 'function' 复制代码
实际上,
typeof
运算符的原理同Object.prototype.toString
一样:var toString = Object.prototype.toString; console.log(toString.call(2)); // [object Number] console.log(toString.call(true)); // [object Boolean] console.log(toString.call('str')); // [object String] console.log(toString.call([])); // [object Array] console.log(toString.call(function(){})); // [object Function] console.log(toString.call({})); // [object Object] console.log(toString.call(undefined)); // [object Undefined] console.log(toString.call(null)); // [object Null] 复制代码
-
instanceof
运算符用于检测构造函数的prototype
属性是否出现在某个实例对象的原型链上。值得注意的是,该运算符只适用于实例对象,也就是说判断基本数据类型无效。object instanceof constructor
:某个实例对象 instanceof 某个构造函数。
-
6.css中有哪些常见的可继承属性,不可继承属性?【
css
】
样式继承的目的是为了更好地适应页面变化,也就是修改样式时让页面更平滑地过渡:设置父元素时希望子元素样式保持统一,那么就继承,如果继承后页面属性变化比较突兀,就不应该继承,比如我设置了div
的字体大小,那么是希望其子元素的字体大小均被继承的,但对于盒模型属性、定位属性这种“比较私有”的属性,如果继承了,反而比较突兀。
常见的可继承属性有:
- 字体系列属性:
font-size
等 - 文本系列属性:
text-align
等 - 其他:
visibility
、cursor
等
其余基本基本都是默认不可继承的,因为样式继承容易造成界面发生非预期的不可控变化。
实际上,所有属性都是可继承的,上面说的不可继承是指默认不可继承,如果需要继承某属性,我们可以使用inherit
属性值来规定:
div > p {
border: inherit;
}
复制代码
如果希望所有属性都继承,则使用all: inherit;
来控制。
-
5.对html语义化的理解,有哪些常见的语义化标签?【
html
】
语义化就是我们通过标签本身就知道标签所代码的内容具有什么意义,即用使用特定的功能属性的标签做特定的事。比如使用h1
标签我们就知道这是一个标题,使用header
就知道这是页眉,使用footer
就知道这是页脚。
语义化标签就是具有特定功能或属性的标签,语义化标签有两个好处:
- 一是对机器友好。语义化能让搜索引擎爬虫更有效地捕获页面结构,有利于SEO;利于页面结构分析,如识别文章标题,生成目录等等。
- SEO(英文 Search Engine Optimization)字面理解很简单的,就是“搜索引擎优化”,最简单的理解就是“搜索自然排名”。
- 二是对开发者友好。语义化让文档结构更清晰,便于整理和优化页面结构。
常见的语义化标签有:
<header>页眉</header>
<nav>导航标记</nav>
<section>文档中的区段、小节</section>
<main> 标签规定文档的主要内容区域</main>
<article>定义外部的内容,其中的内容独立于文档的其余部分。文章、评论等</article>
<aside>侧边栏</aside>
<footer>页脚</footer>
<cite>参考文献的引用</cite>
<blockquote> 标签定义块引用。</blockquote>
<code>代码片段</code>
复制代码
我们最常用的两个无语义标签div
和span
,如果你知道某处文档片段的意图,应该使用正确的语义化标签来替代或者包裹这些无语义标签,如果没有具体的语义标签,也可以通过增加类名来给标签分类,以到语义化标签的属性标记效果。
-
4.合并两个有序数组【
算法
】
给定两个有序整数数组
nums1
和nums2
,请你将nums2
合并到nums1
中,使nums1
成为一个有序数组。
注意题目已经告知两个数组已经是有序的了,所以我们可以使用双指针法就能从两个数组中找打最大值,一个指针指向nums1
末端,一个指针指向mums2
末端,比较两个数组的最大值,并放到数组末端,直到nums2
放置结束即可。
function merge(nums1, m, muns2, n) {
let k = m + n - 1
let p1 = m -1, p2 = n - 1
while(p2 >= 0) {
if(p1 < 0){
nums1[k--] = nums2[p2--]
} else {
nums[k--] = nums1[p1] > nums2[p2] ? nums1[p1--] : nums2[p2--];
}
}
}
-
3.js的基本数据类型有哪些?有何区别?【
js
】
目前最新的ECMAScript
标准定义了9中数据类型。
其中,有7种基本数据类型:
- undefined:未定义
- null:空对象
- boolean:布尔
- number:数字
- string:字符串
- bigint:大整数。es6新增的类型,一般用于描述大于
2^53 - 1
的整数,这原本是 Javascript中可以用Number
,但如果不使用BigInt()
函数或整数字面量n
来封装数字,返回类型还会是number
- symbol:符号。symbol也是es6新增的基本类型,由
Symbol()
返回一个唯一的值,所以symbol类型永远不会重复。
2种引用类型:
- Object:对象。
- Function:函数。
我们可以简单的把Object理解为实例,而Function理解为未实例化的构造器,也就是其他语言常说的类。
为何将数据类型划分为基本数据类型和引用数据类型?这是因为它们在内存中的存储方式是不一样的,
- 基本数据类型由于占用空间不会太大,且具有相对固定的空间大小,因此是直接存放在**栈(stack)**中的
- 引用数据类型由于占用空间大、占用空间大小不稳定,如果频繁创建会造成性能问题,所以实体数据将存放到**堆(heap)**数据结构中,而在栈中只用记录该数据的堆地址即可,使用的时候再根据堆地址去堆内存中查找。
扩展:从数据结构和内存来比较栈和堆,
堆和栈的概念存在于数据结构中和操作系统内存中:
- 栈结构中的数据存取方式为先进后出。
- 堆是一个优先队列,是按优先级来进行排序的,优先级可以按照大小来规定。完全二叉树是堆的一种实现方式。
在操作系统中,内存被分为栈区和堆区:
- 栈区内存由编译器自动分配释放,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
- 堆区内存一般由程序员分配释放,若程序员不释放,程序结束时可能由垃圾回收机制回收。'
-
2.css选择器有哪些?这些选择器的优先级如何判断?【css
】
-
四类选择器
css选择器可以分为四大类:
- 基本选择器
- 分组选择器
- 组合选择器
- 伪选择器
最常用的就是基本选择器,一般来说有五种基本选择器:
- 通用选择器:
*
,匹配所有元素 - 元素选择器:使用元素名称进行元素匹配,比如
p
、a
- 类选择器:通过类名匹配元素,比如
.class
- 属性选择器:通过元素的属性来匹配元素,如
a[href]
id
选择器:通过元素id来匹配元素,如#app
。值得注意的是,id选择器不是匹配一个元素而是多个(当然我我们不希望页面有相同id的元素)。
这五种基本选择器是另外几种选择器使用的基础。
为了扩展选择器的适用范围,比如我希望同时选择
p
元素和类为red
的元素,这时就需要把基本选择器组合起来,我们使用,
把选择器分开,就得到了选择器的组合,这就是分组选择器:// 分组选择器
p, .red { color: red; }同时,有时候为了选择器更精确,也就是缩写选择器的范围,我们就需要使用组合选择器,组合选择器主要有四种(两后代两兄弟):
- 后代选择器:使用空格进行组合,比如
#app div
- 直接子元素组合器:使用
>
进行组合,比如div > p
- 兄弟选择器:使用
~
进行组合,比如p ~ p
能匹配到p
元素之后的兄弟元素。(注意,只能匹配到之后的兄弟元素) - 紧邻兄弟选择器:有时候为了选择紧邻的兄弟元素,我们使用
+
来进行组合,比如p + a
此外,还有些元素比较特殊,比如被访问过的
a
标签,悬浮状态下的div
等等,这些都是某些场景下的特殊状态,为了捕获到这些具有特殊状态的标签,我们需要使用伪类选择器,这些伪类描述了不同状态的标签,比如:hover
表示鼠标悬浮状态下的标签,a:visited
表示被访问过的a
标签,div:first-child
表示div
的第一个子元素。除了伪类,还有一种元素称之为伪元素,伪元素是真实存在的元素,但又不能被html表达出来,比如
p
元素的首字母,首行,这种元素称不上元素,因为只是元素的一部分,但又是真实存在的,因此称为伪元素,目前html提供了五种伪元素选择器:::brefore
:插入元素之前的内容::after
:插入元素之后内容::first-letter
:元素的首字母::first-line
:元素的首行::selection
:元素被选中的部分
而伪类是记录特殊状态的元素,目前有几十种,这里不再一一列举。
选择器优先级
由于我们编写css时可能会有多个选择器匹配到同一个元素,而又具有不同的样式,这时候就需要判断哪个选择器优先级更高,就选哪个选择器指定的样式。
我们知道css叫层叠样式表,也就是样式是层叠覆盖的,如果两个选择器的权重一致,则后面的选择器会覆盖前面的选择器。此外,继承而来的样式相当于样式的初始化,优先级是最低的。
选择器的权重参考值如下所示,权重越高,优先级越高:
- 元素选择器:元素选择器、伪元素选择器,权重为1
- 类选择器:类选择器、伪类选择器、属性选择器,权重为10
- id选择器:权重为100
- 内联样式:权重为1000
!important
标记权重值最高
总结来说就是:
!important
> 内联样式 > id选择器 > 类/属性选择器 > 元素选择器。而通配符选择器是没有权重的。 -
1.html标签的
src
属性和href
属性的区别?【html
】
src
属性是source的缩写,意为来源,一般作为媒体元素HTMLDediaElement
的媒体资源映射URL。比如图片、脚本的资源路径:
<img src="images/like.gif"> <iframe src="inner.html"> <script src="index.min.js"></script>
src
加载媒体资源时是阻塞式的,也就是页面的其他资源会暂停处理,这就是为什么一般把script
脚本标签放到文档末尾加载的一个原因。
而href
是Hypetext Reference
的缩写,意为超文本引用,由于是非媒体资源,外部引用,也就是页面无需呈现出来的资源,所以加载这种资源的时候是非阻塞式的。比如link
和a
标签:
<link rel="stylesheet" src="style/main.css">
<a href="./home/login">login</a>
总结来说就是,src
属性是媒体资源的路径映射,是阻塞式加载的,而href
是超链,是非阻塞式的。
我们可以理解为媒体资源是需要实实在在显示在页面上的,是DOM结构中的实实在在的数据,因此会阻塞加载,而超链相当于一个属性,一条引用,并不需要把真实的资源呈现到页面上,也就无需阻塞页面加载了。
值得注意的一问题是,js脚本并不是媒体资源,也不是DOM需要的数据,为什么js
脚本要用src
而css
就可以用href
呢?实际上,这是设计之初的决定,可能设计者认为js脚本的执行会改变页面,也就是改变DOM,当然需要随页面一起加载,而CSS只是页面的样式,并不会改变DOM,也就属于超链了,从这层含义理解,就知道是否阻塞页面和引用的资源大小无关了,而是和是否实际改变了页面DOM有关。
作者:Dongoog
链接:https://juejin.cn/post/6960688863433981983
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
------------恢复内容结束------------