前端-基础2
7. js工作原理
7.1 为什么js是单线程
参考答案:
这主要和js的用途有关,js是作为浏览器的脚本语言,主要是实现用户与浏览器的交互,以及操作dom;这决定了它只能是单线程,否则会带来很复杂的同步问题。 举个例子:如果js被设计了多线程,如果有一个线程要修改一个dom元素,另一个线程要删除这个dom元素,此时浏览器就会一脸茫然,不知所措。所以,为了避免复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,将来也不会改变
扩展:
什么是进程?
进程:是cpu分配资源的最小单位;(是能拥有资源和独立运行的最小单位)
什么是线程?
线程:是cpu调度的最小单位;(线程是建立在进程的基础上的一次程序运行单位,一个进程中可以有多个线程)
浏览器是多进程的?
放在浏览器中,每打开一个tab页面,其实就是新开了一个进程,在这个进程中,还有ui渲染线程,js引擎线程,http请求线程等。 所以,浏览器是一个多进程的。
为了利用多核CPU的计算能力,HTML5提出Web Worker标准,允许JavaScript脚本创建多个线程,但是子线程完全受主线程控制,且不得操作DOM。所以,这个新标准并没有改变JavaScript单线程的本质。
7.2 宏微队列及执行顺序
参考答案:
JS 中用来存储待执行回调函数的队列包含 2 个不同特定的列队
- 宏列队:用来保存待执行的宏任务(回调),比如:定时器回调、DOM 事件回调、ajax 回调
- 微列队:用来保存待执行的微任务(回调),比如:promise的回调、MutationObserver 的回调
JS 执行时会区别这 2 个队列
- JS 引擎首先必须先执行所有的初始化同步任务代码
- 每次准备取出第一个宏任务执行前, 都要将所有的微任务一个一个取出来执行,也就是优先级比宏任务高,且与微任务所处的代码位置无关
下面这个例子可以看出 Promise
要先于 setTimeout
执行:
setTimeout(() => { //立即放入宏队列 console.log('timeout callback1()') Promise.resolve(3).then( value => { //立即放入微队列 console.log('Promise onResolved3()', value) } ) }, 0) setTimeout(() => { //立即放入宏队列 console.log('timeout callback2()') }, 0) Promise.resolve(1).then( value => { //立即放入微队列 console.log('Promise onResolved1()', value) setTimeout(() => { console.log('timeout callback3()', value) }, 0) } ) Promise.resolve(2).then( value => { //立即放入微队列 console.log('Promise onResolved2()', value) } ) // Promise onResolved1() 1 // Promise onResolved2() 2 // timeout callback1() // Promise onResolved3() 3 // timeout callback2() // timeout callback3() 1
7.3 死锁
参考答案:
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源而造成阻塞的现象,若无外力作用,它们都将无法继续执行
产生原因
- 竞争资源引起进程死锁
- 可剥夺和非剥夺资源
- 竞争非剥夺资源
- 竞争临时性资源
- 进程推进顺序不当
产生条件
- 互斥条件:涉及的资源是非共享的
- 涉及的资源是非共享的,一段时间内某资源只由一个进程占用,如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放
- 不剥夺条件:不能强行剥夺进程拥有的资源
- 进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放
- 请求和保持条件:进程在等待一新资源时继续占有已分配的资源
- 指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它进程占有,此时请求进程阻塞,但又对自己已获得的其它资源保持不放 环路等待条件:存在一种进程的循环链,链中的每一个进程已获得的资源同时被链中的下一个进程所请求 在发生死锁时,必然存在一个进程——资源的环形链
解决办法
只要打破四个必要条件之一就能有效预防死锁的发生
7.4 暂时性死区
参考答案:
暂时性死区的本质就是,只要一进入当前作用域,所要使用的变量就已经存在了,但是不可获取,只有等到声明变量的那一行代码出现,才可以获取和使用该变量。
扩展:
let 、const与暂时性死区
let
或 const
声明的变量拥有暂时性死区(TDZ):当进入它的作用域,它不能被访问(获取或设置)直到执行到达声明。
首先看看不具有暂时性死区的 var
:
- 当进入
var
变量的作用域(包围它的函数),立即为它创建(绑定)存储空间。变量会立即被初始化并赋值为undefined
。 - 当执行到变量声明的时候,如果变量定义了值则会被赋值。
通过 let
声明的变量拥有暂时性死区,生命周期如下:
- 当进入
let
变量的作用域(包围它的语法块),立即为它创建(绑定)存储空间。此时变量仍是未初始化的。 - 获取或设置未初始化的变量将抛出异常
ReferenceError
。 - 当执行到变量声明的时候,如果变量定义了值则会被赋值。如果没有定义值,则赋值为
undefined
。
const
工作方式与 let
类似,但是定义的时候必须赋值并且不能改变。
在 TDZ 内部,如果获取或设置变量将抛出异常:
if (true) { // enter new scope, TDZ starts // Uninitialized binding for `tmp` is created tmp = 'abc'; // ReferenceError console.log(tmp); // ReferenceError let tmp; // TDZ ends, `tmp` is initialized with `undefined` console.log(tmp); // undefined tmp = 123; console.log(tmp); // 123 }
下面的示例将演示死区(dead zone)是真正短暂的(基于时间)和不受空间条件限制(基于位置)
if (true) { // enter new scope, TDZ starts const func = function () { console.log(myVar); // OK! }; // Here we are within the TDZ and // accessing `myVar` would cause a `ReferenceError` let myVar = 3; // TDZ ends func(); // called outside TDZ }
typeof
与暂时性死区
变量在暂时性死区无法被访问,所以无法对它使用 typeof
:
if (true) { console.log(typeof tmp); // ReferenceError let tmp; }
7.5 面向对象的三个特征,分别说一下什么意思
参考答案:
概念:
封装:将对象运行所需的资源封装在程序对象中——基本上,是方法和数据。对象是“公布其接口”。其他附加到这些接口上的对象不需要关心对象实现的方法即可使用这个对象。这个概念就是“不要告诉我你是怎么做的,只要做就可以了。”对象可以看作是一个自我包含的原子。对象接口包括了公共的方法和初始化数据。
继承: 继承可以解决代码复用,让编程更加靠近人类思维。当多个类存在相同的属性(变量)和方法时,可以从这些类中抽象出父类,在父类中定义这些相同的属性和方法,所有的子类不需要重新定义这些属性和方法,只需要通过继承父类中的属性和方法。
多态: 多态是指一个引用(类型)在不同情况下的多种状态。也可以理解成:多态是指通过指向父类的引用,来调用在不同子类中实现的方法。
特点:
封装可以隐藏实现细节,使得代码模块化;
继承可以扩展已存在的代码模块(类),它们的目的都是为了——代码重用。
多态就是相同的事物,调用其相同的方法,参数也相同时,但表现的行为却不同。多态分为两种,一种是行为多态与对象的多态
8. 应用
8.1 文件异步上传怎么实现
参考答案:
- 普通表单上传
使用PHP来展示常规的表单上传是一个不错的选择。首先构建文件上传的表单,并指定表单的提交内容类型为enctype="multipart/form-data"
,表明表单需要上传二进制数据。
<form action="/index.php" method="POST" enctype="multipart/form-data"> <input type="file" name="myfile"> <input type="submit"> </form>
然后编写index.php
上传文件接收代码,使用move_uploaded_file
方法即可(php大法好...)
$imgName = 'IMG'.time().'.'.str_replace('image/','',$_FILES["myfile"]['type']); $fileName = 'upload/'.$imgName; // 移动上传文件至指定upload文件夹下,并根据返回值判断操作是否成功 if (move_uploaded_file($_FILES['myfile']['tmp_name'], $fileName)){ echo $fileName; }else { echo "nonn"; }
form表单上传大文件时,很容易遇见服务器超时的问题。通过xhr,前端也可以进行异步上传文件的操作,一般由两个思路。
- 文件编码上传
第一个思路是将文件进行编码,然后在服务端进行解码,之前写过一篇在前端实现图片压缩上传的博客,其主要实现原理就是将图片转换成base64进行传递
var imgURL = URL.createObjectURL(file); ctx.drawImage(imgURL, 0, 0); // 获取图片的编码,然后将图片当做是一个很长的字符串进行传递 var data = canvas.toDataURL("image/jpeg", 0.5);
在服务端需要做的事情也比较简单,首先解码base64,然后保存图片即可
$imgData = $_REQUEST['imgData']; $base64 = explode(',', $imgData)[1]; $img = base64_decode($base64); $url = './test.jpg'; if (file_put_contents($url, $img)) { exit(json_encode(array( url => $url ))); }
base64编码的缺点在于其体积比原图片更大(因为Base64将三个字节转化成四个字节,因此编码后的文本,会比原文本大出三分之一左右),对于体积很大的文件来说,上传和解析的时间会明显增加。
更多关于base64的知识,可以参考Base64笔记。
除了进行base64编码,还可以在前端直接读取文件内容后以二进制格式上传
// 读取二进制文件 function readBinary(text){ var data = new ArrayBuffer(text.length); var ui8a = new Uint8Array(data, 0); for (var i = 0; i < text.length; i++){ ui8a[i] = (text.charCodeAt(i) & 0xff); } console.log(ui8a) } var reader = new FileReader(); reader.onload = function(){ readBinary(this.result) // 读取result或直接上传 } // 把从input里读取的文件内容,放到fileReader的result字段里 reader.readAsBinaryString(file);
- formData异步上传
FormData对象主要用来组装一组用 XMLHttpRequest发送请求的键/值对,可以更加灵活地发送Ajax请求。可以使用FormData来模拟表单提交。
let files = e.target.files // 获取input的file对象 let formData = new FormData(); formData.append('file', file); axios.post(url, formData);
服务端处理方式与直接form表单请求基本相同。
- iframe无刷新页面
在低版本的浏览器(如IE)上,xhr是不支持直接上传formdata的,因此只能用form来上传文件,而form提交本身会进行页面跳转,这是因为form表单的target属性导致的,其取值有
- _self,默认值,在相同的窗口中打开响应页面
- _blank,在新窗口打开
- _parent,在父窗口打开
- _top,在最顶层的窗口打开
framename
,在指定名字的iframe中打开
如果需要让用户体验异步上传文件的感觉,可以通过framename
指定iframe来实现。把form的target属性设置为一个看不见的iframe,那么返回的数据就会被这个iframe接受,因此只有该iframe会被刷新,至于返回结果,也可以通过解析这个iframe内的文本来获取。
function upload(){ var now = +new Date() var id = 'frame' + now $("body").append(`<iframe style="display:none;" name="${id}" id="${id}" />`); var $form = $("#myForm") $form.attr({ "action": '/index.php', "method": "post", "enctype": "multipart/form-data", "encoding": "multipart/form-data", "target": id }).submit() $("#"+id).on("load", function(){ var content = $(this).contents().find("body").text() try{ var data = JSON.parse(content) }catch(e){ console.log(e) } }) }
扩展:
大文件上传
现在来看看在上面提到的几种上传方式中实现大文件上传会遇见的超时问题,
- 表单上传和iframe无刷新页面上传,实际上都是通过form标签进行上传文件,这种方式将整个请求完全交给浏览器处理,当上传大文件时,可能会遇见请求超时的情形
- 通过fromData,其实际也是在xhr中封装一组请求参数,用来模拟表单请求,无法避免大文件上传超时的问题
- 编码上传,我们可以比较灵活地控制上传的内容
大文件上传最主要的问题就在于:在同一个请求中,要上传大量的数据,导致整个过程会比较漫长,且失败后需要重头开始上传。试想,如果我们将这个请求拆分成多个请求,每个请求的时间就会缩短,且如果某个请求失败,只需要重新发送这一次请求即可,无需从头开始,这样是否可以解决大文件上传的问题呢?
综合上面的问题,看来大文件上传需要实现下面几个需求
- 支持拆分上传请求(即切片)
- 支持断点续传
- 支持显示上传进度和暂停上传
接下来让我们依次实现这些功能,看起来最主要的功能应该就是切片了。
文件切片
编码方式上传中,在前端我们只要先获取文件的二进制内容,然后对其内容进行拆分,最后将每个切片上传到服务端即可。
在JavaScript中,文件FIle对象是Blob对象的子类,Blob对象包含一个重要的方法slice,通过这个方法,我们就可以对二进制文件进行拆分。
下面是一个拆分文件的示例
function slice(file, piece = 1024 * 1024 * 5) { let totalSize = file.size; // 文件总大小 let start = 0; // 每次上传的开始字节 let end = start + piece; // 每次上传的结尾字节 let chunks = [] while (start < totalSize) { // 根据长度截取每次需要上传的数据 // File对象继承自Blob对象,因此包含slice方法 let blob = file.slice(start, end); chunks.push(blob) start = end; end = start + piece; } return chunks }
将文件拆分成piece大小的分块,然后每次请求只需要上传这一个部分的分块即可
let file = document.querySelector("[name=file]").files[0]; const LENGTH = 1024 * 1024 * 0.1; let chunks = slice(file, LENGTH); // 首先拆分切片 chunks.forEach(chunk=>{ let fd = new FormData(); fd.append("file", chunk); post('/mkblk.php', fd) })
服务器接收到这些切片后,再将他们拼接起来就可以了,下面是PHP拼接切片的示例代码
$filename = './upload/' . $_POST['filename'];//确定上传的文件名 //第一次上传时没有文件,就创建文件,此后上传只需要把数据追加到此文件中 if(!file_exists($filename)){ move_uploaded_file($_FILES['file']['tmp_name'],$filename); }else{ file_put_contents($filename,file_get_contents($_FILES['file']['tmp_name']),FILE_APPEND); echo $filename; }
测试时记得修改nginx的server配置,否则大文件可能会提示413 Request Entity Too Large的错误。
server { // ... client_max_body_size 50m; }
上面这种方式来存在一些问题
- 无法识别一个切片是属于哪一个切片的,当同时发生多个请求时,追加的文件内容会出错
- 切片上传接口是异步的,无法保证服务器接收到的切片是按照请求顺序拼接的
因此接下来我们来看看应该如何在服务端还原切片。
还原切片
在后端需要将多个相同文件的切片还原成一个文件,上面这种处理切片的做法存在下面几个问题
- 如何识别多个切片是来自于同一个文件的,这个可以在每个切片请求上传递一个相同文件的context参数
- 如何将多个切片还原成一个文件
- 确认所有切片都已上传,这个可以通过客户端在切片全部上传后调用mkfile接口来通知服务端进行拼接
- 找到同一个context下的所有切片,确认每个切片的顺序,这个可以在每个切片上标记一个位置索引值
- 按顺序拼接切片,还原成文件
上面有一个重要的参数,即context,我们需要获取为一个文件的唯一标识,可以通过下面两种方式获取
- 根据文件名、文件长度等基本信息进行拼接,为了避免多个用户上传相同的文件,可以再额外拼接用户信息如uid等保证唯一性
- 根据文件的二进制内容计算文件的hash,这样只要文件内容不一样,则标识也会不一样,缺点在于计算量比较大.
修改上传代码,增加相关参数
// 获取context,同一个文件会返回相同的值 function createContext(file) { return file.name + file.length } let file = document.querySelector("[name=file]").files[0]; const LENGTH = 1024 * 1024 * 0.1; let chunks = slice(file, LENGTH); // 获取对于同一个文件,获取其的context let context = createContext(file); let tasks = []; chunks.forEach((chunk, index) => { let fd = new FormData(); fd.append("file", chunk); // 传递context fd.append("context", context); // 传递切片索引值 fd.append("chunk", index + 1); tasks.push(post("/mkblk.php", fd)); }); // 所有切片上传完毕后,调用mkfile接口 Promise.all(tasks).then(res => { let fd = new FormData(); fd.append("context", context); fd.append("chunks", chunks.length); post("/mkfile.php", fd).then(res => { console.log(res); }); });
在mkblk.php接口中,我们通过context来保存同一个文件相关的切片
// mkblk.php $context = $_POST['context']; $path = './upload/' . $context; if(!is_dir($path)){ mkdir($path); } // 把同一个文件的切片放在相同的目录下 $filename = $path .'/'. $_POST['chunk']; $res = move_uploaded_file($_FILES['file']['tmp_name'],$filename);
除了上面这种简单通过目录区分切片的方法之外,还可以将切片信息保存在数据库来进行索引。接下来是mkfile.php接口的实现,这个接口会在所有切片上传后调用
// mkfile.php $context = $_POST['context']; $chunks = (int)$_POST['chunks']; //合并后的文件名 $filename = './upload/' . $context . '/file.jpg'; for($i = 1; $i <= $chunks; ++$i){ $file = './upload/'.$context. '/' .$i; // 读取单个切块 $content = file_get_contents($file); if(!file_exists($filename)){ $fd = fopen($filename, "w+"); }else{ $fd = fopen($filename, "a"); } fwrite($fd, $content); // 将切块合并到一个文件上 } echo $filename;
这样就解决了上面的两个问题:
- 识别切片来源
- 保证切片拼接顺序
断点续传
即使将大文件拆分成切片上传,我们仍需等待所有切片上传完毕,在等待过程中,可能发生一系列导致部分切片上传失败的情形,如网络故障、页面关闭等。由于切片未全部上传,因此无法通知服务端合成文件。这种情况下可以通过断点续传来进行处理。
断点续传指的是:可以从已经上传部分开始继续上传未完成的部分,而没有必要从头开始上传,节省上传时间。
由于整个上传过程是按切片维度进行的,且mkfile接口是在所有切片上传完成后由客户端主动调用的,因此断点续传的实现也十分简单:
- 在切片上传成功后,保存已上传的切片信息
- 当下次传输相同文件时,遍历切片列表,只选择未上传的切片进行上传
- 所有切片上传完毕后,再调用mkfile接口通知服务端进行文件合并
因此问题就落在了如何保存已上传切片的信息了,保存一般有两种策略
- 可以通过locaStorage等方式保存在前端浏览器中,这种方式不依赖于服务端,实现起来也比较方便,缺点在于如果用户清除了本地文件,会导致上传记录丢失
- 服务端本身知道哪些切片已经上传,因此可以由服务端额外提供一个根据文件context查询已上传切片的接口,在上传文件前调用该文件的历史上传记录
下面让我们通过在本地保存已上传切片记录,来实现断点上传的功能
// 获取已上传切片记录 function getUploadSliceRecord(context){ let record = localStorage.getItem(context) if(!record){ return [] }else { try{ return JSON.parse(record) }catch(e){} } } // 保存已上传切片 function saveUploadSliceRecord(context, sliceIndex){ let list = getUploadSliceRecord(context) list.push(sliceIndex) localStorage.setItem(context, JSON.stringify(list)) }
然后对上传逻辑稍作修改,主要是增加上传前检测是已经上传、上传后保存记录的逻辑
let context = createContext(file); // 获取上传记录 let record = getUploadSliceRecord(context); let tasks = []; chunks.forEach((chunk, index) => { // 已上传的切片则不再重新上传 if(record.includes(index)){ return } let fd = new FormData(); fd.append("file", chunk); fd.append("context", context); fd.append("chunk", index + 1); let task = post("/mkblk.php", fd).then(res=>{ // 上传成功后保存已上传切片记录 saveUploadSliceRecord(context, index) record.push(index) }) tasks.push(task); });
此时上传时刷新页面或者关闭浏览器,再次上传相同文件时,之前已经上传成功的切片就不会再重新上传了。
服务端实现断点续传的逻辑基本相似,只要在getUploadSliceRecord内部调用服务端的查询接口获取已上传切片的记录即可,因此这里不再展开。
此外断点续传还需要考虑切片过期的情况:如果调用了mkfile接口,则磁盘上的切片内容就可以清除掉了,如果客户端一直不调用mkfile的接口,放任这些切片一直保存在磁盘显然是不可靠的,一般情况下,切片上传都有一段时间的有效期,超过该有效期,就会被清除掉。基于上述原因,断点续传也必须同步切片过期的实现逻辑。
上传进度和暂停
通过xhr.upload中的progress方法可以实现监控每一个切片上传进度。
上传暂停的实现也比较简单,通过xhr.abort可以取消当前未完成上传切片的上传,实现上传暂停的效果,恢复上传就跟断点续传类似,先获取已上传的切片列表,然后重新发送未上传的切片。
由于篇幅关系,上传进度和暂停的功能这里就先不实现了。
8.2 使用setInterval请求实时数据,返回顺序不一致怎么解决
参考答案:
场景:
setInterval(function() { $.get("/path/to/server", function(data, status) { console.log(data); }); }, 10000);
上面的程序会每隔10秒向服务器请求一次数据,并在数据到达后存储。这个实现方法通常可以满足简单的需求,然而同时也存在着很大的缺陷:在网络情况不稳定的情况下,服务器从接收请求、发送请求到客户端接收请求的总时间有可能超过10秒,而请求是以10秒间隔发送的,这样会导致接收的数据到达先后顺序与发送顺序不一致。
解决方案:
使用setTimeout代替setInterval
程序首先设置10秒后发起请求,当数据返回后再隔10秒发起第二次请求,以此类推。这样的话虽然无法保证两次请求之间的时间间隔为固定值,但是可以保证到达数据的顺序。
function poll() { setTimeout(function() { $.get("/path/to/server", function(data, status) { console.log(data); // 发起下一次请求 poll(); }); }, 10000); }
WebSocket
WebSocket 协议本质上是一个基于 TCP 的协议。
为了建立一个 WebSocket 连接,客户端浏览器首先要向服务器发起一个 HTTP 请求,这个请求和通常的 HTTP 请求不同,包含了一些附加头信息,其中附加头信息"Upgrade: WebSocket"表明这是一个申请协议升级的 HTTP 请求,服务器端解析这些附加的头信息然后产生应答信息返回给客户端,客户端和服务器端的 WebSocket 连接就建立起来了,双方就可以通过这个连接通道自由的传递信息,并且这个连接会持续存在直到客户端或者服务器端的某一方主动的关闭连接。
服务器(Node.js):
var WebSocketServer = require('ws').Server; var wss = new WebSocketServer({port: 8080}); wss.on("connection", function(socket) { socket.on("message", function(msg) { console.log(msg); socket.send("Nice to meet you!"); }); });
客户端同样可以使用Node.js或者是浏览器实现,这里选用浏览器作为客户端:
// WebSocket 为客户端JavaScript的原生对象 var ws = new WebSocket("ws://localhost:8080"); ws.onopen = function (event) { ws.send("Hello there!"); } ws.onmessage = function (event) { console.log(event.data); }
8.3 防抖和节流的原理和使用场景
参考答案:
函数防抖和函数节流:优化高频率执行js代码的一种手段,js中的一些事件如浏览器的resize、scroll,鼠标的mousemove、mouseover,input输入框的keypress等事件在触发时,会不断地调用绑定在事件上的回调函数,极大地浪费资源,降低前端性能。为了优化体验,需要对这类事件进行调用次数的限制。
防抖:
在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。
根据函数防抖思路设计出第一版的最简单的防抖代码:
var timer; // 维护同一个timer function debounce(fn, delay) { clearTimeout(timer); timer = setTimeout(function(){ fn(); }, delay); }
上面例子中的debounce就是防抖函数,在document中鼠标移动的时候,会在onmousemove最后触发的1s后执行回调函数testDebounce;如果我们一直在浏览器中移动鼠标(比如10s),会发现会在10 + 1s后才会执行testDebounce函数(因为clearTimeout(timer)),这个就是函数防抖。
在上面的代码中,会出现一个问题,var timer只能在setTimeout的父级作用域中,这样才是同一个timer,并且为了方便防抖函数的调用和回调函数fn的传参问题,我们应该用闭包来解决这些问题。
优化后的代码:
function debounce(fn, delay) { var timer; // 维护一个 timer return function () { var _this = this; // 取debounce执行作用域的this var args = arguments; if (timer) { clearTimeout(timer); } timer = setTimeout(function () { fn.apply(_this, args); // 用apply指向调用debounce的对象,相当于_this.fn(args); }, delay); }; }
使用闭包后,解决传参和封装防抖函数的问题,这样就可以在其他地方随便将需要防抖的函数传入debounce了。
节流:
每隔一段时间,只执行一次函数。
定时器实现节流函数:
function throttle(fn, delay) { var timer; return function () { var _this = this; var args = arguments; if (timer) { return; } timer = setTimeout(function () { fn.apply(_this, args); timer = null; // 在delay后执行完fn之后清空timer,此时timer为假,throttle触发可以进入计时器 }, delay) } }
时间戳实现节流函数:
function throttle(fn, delay) { var previous = 0; // 使用闭包返回一个函数并且用到闭包函数外面的变量previous return function() { var _this = this; var args = arguments; var now = new Date(); if(now - previous > delay) { fn.apply(_this, args); previous = now; } } }
异同比较
相同点:
- 都可以通过使用 setTimeout 实现。
- 目的都是,降低回调执行频率。节省计算资源。
不同点:
- 函数防抖,在一段连续操作结束后,处理回调,利用clearTimeout 和 setTimeout实现。函数节流,在一段连续操作中,每一段时间只执行一次,频率较高的事件中使用来提高性能。
- 函数防抖关注一定时间连续触发的事件只在最后执行一次,而函数节流侧重于一段时间内只执行一次。
常见应用场景
函数防抖的应用场景:
连续的事件,只需触发一次回调的场景有:
- 搜索框搜索输入。只需用户最后一次输入完,再发送请求
- 手机号、邮箱验证输入检测
- 窗口大小Resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。
函数节流的应用场景:
间隔一段时间执行一次回调的场景有:
- 滚动加载,加载更多或滚到底部监听
- 谷歌搜索框,搜索联想功能
- 高频点击提交,表单重复提交
8.4 浅拷贝,深拷贝(实现方式)
参考答案:
浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
区别:浅拷贝只复制对象的第一层属性、深拷贝可以对对象的属性进行递归复制;
实现浅拷贝方法
(1)Object.assign方法
var obj = { a: 1, b: 2 } var obj1 = Object.assign({},obj); boj1.a = 3; console.log(obj.a) // 3
(2)for in方法
// 只复制第一层的浅拷贝 function simpleCopy(obj1) { var obj2 = Array.isArray(obj1) ? [] : {}; for (let i in obj1) { obj2[i] = obj1[i]; } return obj2; } var obj1 = { a: 1, b: 2, c: { d: 3 } } var obj2 = simpleCopy(obj1); obj2.a = 3; obj2.c.d = 4; alert(obj1.a); // 1 alert(obj2.a); // 3 alert(obj1.c.d); // 4 alert(obj2.c.d); // 4
实现深拷贝方法
(1)采用递归去拷贝所有层级属性
function deepClone(obj){ let objClone = Array.isArray(obj)?[]:{}; if(obj && typeof obj==="object"){ for(key in obj){ if(obj.hasOwnProperty(key)){ //判断ojb子元素是否为对象,如果是,递归复制 if(obj[key]&&typeof obj[key] ==="object"){ objClone[key] = deepClone(obj[key]); }else{ //如果不是,简单复制 objClone[key] = obj[key]; } } } } return objClone; } let a=[1,2,3,4], b=deepClone(a); a[0]=2; console.log(a,b);
(2)使用JSON.stringify和JSON.parse实现深拷贝:JSON.stringify把对象转成字符串,再用JSON.parse把字符串转成新的对象;
function deepCopy(obj1){ let _obj = JSON.stringify(obj1); let obj2 = JSON.parse(_obj); return obj2; } var a = [1, [1, 2], 3, 4]; var b = deepCopy(a); b[1][0] = 2; alert(a); // 1,1,2,3,4 alert(b); // 2,2,2,3,4
(3)热门的函数库lodash,也有提供_.cloneDeep用来做深拷贝;
var _ = require('lodash'); var obj1 = { a: 1, b: { f: { g: 1 } }, c: [1, 2, 3] }; var obj2 = _.cloneDeep(obj1); console.log(obj1.b.f === obj2.b.f); // false
8.5 获取当前页面url
参考答案:
- window.location.href (设置或获取整个 URL 为字符串)
var test = window.location.href; alert(test); // 返回:http://i.cnblogs.com/EditPosts.aspx?opt=1
- window.location.protocol (设置或获取 URL 的协议部分)
var test = window.location.protocol; alert(test); //返回:http:
- window.location.host (设置或获取 URL 的主机部分)
var test = window.location.host; alert(test); //返回:i.cnblogs.com
- window.location.port (设置或获取与 URL 关联的端口号码)
var test = window.location.port; alert(test); //返回:空字符(如果采用默认的80端口 (update:即使添加了:80),那么返回值并不是默认的80而是空字符)
- window.location.pathname (设置或获取与 URL 的路径部分(就是文件地址))
var test = window.location.pathname; alert(test); //返回:/EditPosts.aspx
- window.location.search (设置或获取 href 属性中跟在问号后面的部分)
var test = window.location.search; alert(test); //返回:?opt=1 (PS:获得查询(参数)部分,除了给动态语言赋值以外,我们同样可以给静态页面,并使用javascript来获得相信应的参数值。)
- window.location.hash (设置或获取 href 属性中在井号“#”后面的分段)
var test = window.location.hash; alert(test); //返回:空字符(因为url中没有)
js获取url中的参数值*
正则法
function getQueryString(name) { var reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)', 'i'); var r = window.location.search.substr(1).match(reg); if (r != null) { return unescape(r[2]); } return null; } // 这样调用: alert(GetQueryString("参数名1")); alert(GetQueryString("参数名2")); alert(GetQueryString("参数名3"));
split拆分法
function GetRequest() { var url = location.search; //获取url中"?"符后的字串 var theRequest = new Object(); if (url.indexOf("?") != -1) { var str = url.substr(1); strs = str.split("&"); for(var i = 0; i < strs.length; i ++) { theRequest[strs[i].split("=")[0]] = unescape(strs[i].split("=")[1]); } } return theRequest; } var Request = new Object(); Request = GetRequest();<br>// var id=Request["id"]; // var 参数1,参数2,参数3,参数N; // 参数1 = Request['参数1']; // 参数2 = Request['参数2']; // 参数3 = Request['参数3']; // 参数N = Request['参数N'];
指定取
比如说一个url:http://i.cnblogs.com/?j=js, 我们想得到参数j的值,可以通过以下函数调用。
function GetQueryString(name) { var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i"); var r = window.location.search.substr(1).match(reg); //获取url中"?"符后的字符串并正则匹配 var context = ""; if (r != null) context = r[2]; reg = null; r = null; return context == null || context == "" || context == "undefined" ? "" : context; } alert(GetQueryString("j"));
单个参数的获取方法
function GetRequest() { var url = location.search; //获取url中"?"符后的字串 if (url.indexOf("?") != -1) {? //判断是否有参数 var str = url.substr(1); //从第一个字符开始 因为第0个是?号 获取所有除问号的所有符串 strs = str.split("=");? //用等号进行分隔 (因为知道只有一个参数 //所以直接用等号进分隔 如果有多个参数 要用&号分隔 再用等号进行分隔) alert(strs[1]);???? //直接弹出第一个参数 (如果有多个参数 还要进行循环的) } }
8.6 js中两个数组怎么取交集+(差集、并集、补集)
参考答案:
最普遍的做法
使用 ES5 语法来实现虽然会麻烦些,但兼容性最好,不用考虑浏览器 JavaScript 版本。也不用引入其他第三方库。
直接使用 filter、concat 来计算
var a = [1,2,3,4,5] var b = [2,4,6,8,10] //交集 var c = a.filter(function(v){ return b.indexOf(v) > -1 }) //差集 var d = a.filter(function(v){ return b.indexOf(v) == -1 }) //补集 var e = a.filter(function(v){ return !(b.indexOf(v) > -1) }) .concat(b.filter(function(v){ return !(a.indexOf(v) > -1)})) //并集 var f = a.concat(b.filter(function(v){ return !(a.indexOf(v) > -1)}));
对 Array 进行扩展
//数组功能扩展 //数组迭代函数 Array.prototype.each = function(fn){ fn = fn || Function.K; var a = []; var args = Array.prototype.slice.call(arguments, 1); for(var i = 0; i < this.length; i++){ var res = fn.apply(this,[this[i],i].concat(args)); if(res != null) a.push(res); } return a; }; //数组是否包含指定元素 Array.prototype.contains = function(suArr){ for(var i = 0; i < this.length; i ++){ if(this[i] == suArr){ return true; } } return false; } //不重复元素构成的数组 Array.prototype.uniquelize = function(){ var ra = new Array(); for(var i = 0; i < this.length; i ++){ if(!ra.contains(this[i])){ ra.push(this[i]); } } return ra; }; //两个数组的交集 Array.intersect = function(a, b){ return a.uniquelize().each(function(o){return b.contains(o) ? o : null}); }; //两个数组的差集 Array.minus = function(a, b){ return a.uniquelize().each(function(o){return b.contains(o) ? null : o}); }; //两个数组的补集 Array.complement = function(a, b){ return Array.minus(Array.union(a, b),Array.intersect(a, b)); }; //两个数组并集 Array.union = function(a, b){ return a.concat(b).uniquelize(); };
使用 ES6 语法实现
ES6 中可以借助扩展运算符(...)以及 Set 的特性实现相关计算,代码也会更加简单些。
var a = [1,2,3,4,5] var b = [2,4,6,8,10] console.log("数组a:", a); console.log("数组b:", b); var sa = new Set(a); var sb = new Set(b); // 交集 let intersect = a.filter(x => sb.has(x)); // 差集 let minus = a.filter(x => !sb.has(x)); // 补集 let complement = [...a.filter(x => !sb.has(x)), ...b.filter(x => !sa.has(x))]; // 并集 let unionSet = Array.from(new Set([...a, ...b]));
使用 jQuery 实现
var a = [1,2,3,4,5] var b = [2,4,6,8,10] console.log("数组a:", a); console.log("数组b:", b); // 交集 let intersect = $(a).filter(b).toArray(); // 差集 let minus = $(a).not(b).toArray(); // 补集 let complement = $(a).not(b).toArray().concat($(b).not(a).toArray()); // 并集 let unionSet = $.unique(a.concat(b));
8.7 用正则和非正则实现123456789.12=》1,234,567,890.12
参考答案:
非正则:
如果数字带有小数点的话,可以使用toLocaleString()方法实现这个需求。
b.toLocaleString();
正则:
不带小数点
num.toString().replace(/(\d)(?=(?:\d{3})+$)/g,'$1,')
带小数点
- 判读是否带有小数点
- 没有小数点,就用正则匹配实
function numFormat(num) { var c = (num.toString().indexOf ('.') !== -1) ? num.toLocaleString() : num.toString().replace(/(\d)(?=(?:\d{3})+$)/g, '$1,'); return c; }
8.8 写一个判断是否是空对象的函数
参考答案:
function isEmpty(value) { return ( value === null || value === undefined || (typeof value === 'object' && Object.keys(value).length === 0) ) }
8.9 代码题:颜色值16进制转10进制rgb
参考答案:
function toRGB(color) { var regex = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/ //匹配十六进制的正则 match = color.match(regex) // 判断是否是十六进制颜色值 return match ? 'rgb('+parseInt(match[1], 16)+','+parseInt(match[2], 16)+','+parseInt(match[3], 16)+')' : color }
8.10 传入 [1,[[2],3,4],5] ,返回 [1,2,3,4,5]
参考答案:
递归
我们最一开始能想到的莫过于循环数组元素,如果还是一个数组,就递归调用该方法:
// 方法 1 var arr = [1, [2, [3, 4]]]; function flatten(arr) { var result = []; for (var i = 0, len = arr.length; i < len; i++) { if (Array.isArray(arr[i])) { result = result.concat(flatten(arr[i])) } else { result.push(arr[i]) } } return result; } console.log(flatten(arr))
toString
如果数组的元素都是数字,那么我们可以考虑使用 toString 方法,因为:
[1, [2, [3, 4]]].toString() // "1,2,3,4"
调用 toString 方法,返回了一个逗号分隔的扁平的字符串,这时候我们再 split,然后转成数字不就可以实现扁平化了吗?
// 方法2 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.toString().split(',').map(function(item){ return +item }) } console.log(flatten(arr))
然而这种方法使用的场景却非常有限,如果数组是 [1, '1', 2, '2'] 的话,这种方法就会产生错误的结果。
reduce
既然是对数组进行处理,最终返回一个值,我们就可以考虑使用 reduce 来简化代码:
// 方法3 var arr = [1, [2, [3, 4]]]; function flatten(arr) { return arr.reduce(function(prev, next){ return prev.concat(Array.isArray(next) ? flatten(next) : next) }, []) } console.log(flatten(arr))
...
ES6 增加了扩展运算符,用于取出参数对象的所有可遍历属性,拷贝到当前对象之中:
var arr = [1, [2, [3, 4]]]; console.log([].concat(...arr)); // [1, 2, [3, 4]]
我们用这种方法只可以扁平一层,但是顺着这个方法一直思考,我们可以写出这样的方法:
// 方法4 var arr = [1, [2, [3, 4]]]; function flatten(arr) { while (arr.some(item => Array.isArray(item))) { arr = [].concat(...arr); } return arr; } console.log(flatten(arr))
undercore
那么如何写一个抽象的扁平函数,来方便我们的开发呢,所有又到了我们抄袭 underscore 的时候了~
在这里直接给出源码和注释,但是要注意,这里的 flatten 函数并不是最终的 _.flatten,为了方便多个 API 进行调用,这里对扁平进行了更多的配置。
/** * 数组扁平化 * @param {Array} input 要处理的数组 * @param {boolean} shallow 是否只扁平一层 * @param {boolean} strict 是否严格处理元素,下面有解释 * @param {Array} output 这是为了方便递归而传递的参数 * 源码地址:https://github.com/jashkenas/underscore/blob/master/underscore.js#L528 */ function flatten(input, shallow, strict, output) { // 递归使用的时候会用到output output = output || []; var idx = output.length; for (var i = 0, len = input.length; i < len; i++) { var value = input[i]; // 如果是数组,就进行处理 if (Array.isArray(value)) { // 如果是只扁平一层,遍历该数组,依此填入 output if (shallow) { var j = 0, length = value.length; while (j < length) output[idx++] = value[j++]; } // 如果是全部扁平就递归,传入已经处理的 output,递归中接着处理 output else { flatten(value, shallow, strict, output); idx = output.length; } } // 不是数组,根据 strict 的值判断是跳过不处理还是放入 output else if (!strict){ output[idx++] = value; } } return output; }
解释下 strict,在代码里我们可以看出,当遍历数组元素时,如果元素不是数组,就会对 strict 取反的结果进行判断,如果设置 strict 为 true,就会跳过不进行任何处理,这意味着可以过滤非数组的元素,举个例子:
var arr = [1, 2, [3, 4]]; console.log(flatten(arr, true, true)); // [3, 4]
那么设置 strict 到底有什么用呢?不急,我们先看下 shallow 和 strct 各种值对应的结果:
- shallow true + strict false :正常扁平一层
- shallow false + strict false :正常扁平所有层
- shallow true + strict true :去掉非数组元素
- shallow false + strict true : 返回一个[]
我们看看 underscore 中哪些方法调用了 flatten 这个基本函数:
_.flatten
首先就是 _.flatten:
_.flatten = function(array, shallow) { return flatten(array, shallow, false); };
在正常的扁平中,我们并不需要去掉非数组元素。
_.union
接下来是 _.union:
该函数传入多个数组,然后返回传入的数组的并集,
举个例子:
_.union([1, 2, 3], [101, 2, 1, 10], [2, 1]); => [1, 2, 3, 101, 10]
如果传入的参数并不是数组,就会将该参数跳过:
_.union([1, 2, 3], [101, 2, 1, 10], 4, 5); => [1, 2, 3, 101, 10]
为了实现这个效果,我们可以将传入的所有数组扁平化,然后去重,因为只能传入数组,这时候我们直接设置 strict 为 true,就可以跳过传入的非数组的元素。
// 关于 unique 可以查看《JavaScript专题之数组去重》[](https://github.com/mqyqingfeng/Blog/issues/27) function unique(array) { return Array.from(new Set(array)); } _.union = function() { return unique(flatten(arguments, true, true)); }
_.difference
是不是感觉折腾 strict 有点用处了,我们再看一个 _.difference:
语法为:
_.difference(array, *others)
效果是取出来自 array 数组,并且不存在于多个 other 数组的元素。跟 _.union 一样,都会排除掉不是数组的元素。
举个例子:
_.difference([1, 2, 3, 4, 5], [5, 2, 10], [4], 3); => [1, 3]
实现方法也很简单,扁平 others 的数组,筛选出 array 中不在扁平化数组中的值:
function difference(array, ...rest) { rest = flatten(rest, true, true); return array.filter(function(item){ return rest.indexOf(item) === -1; }) }
8.11 倒计时,一开始就进行
参考答案:
题意:一旦进入页面倒计时就开始,因此在window.onload方法中调用倒计时方法
<script> window.onload = function () { countDown(); function addZero(i) { return i < 10 ? "0" + i: i + ""; } function countDown() { var nowtime = new Date(); var endtime = new Date("2019/03/16,17:57:00"); var lefttime = parseInt((endtime.getTime() - nowtime.getTime()) / 1000); var d = parseInt(lefttime / (24*60*60)) var h = parseInt(lefttime / (60 * 60) % 24); var m = parseInt(lefttime / 60 % 60); var s = parseInt(lefttime % 60); d = addZero(d) h = addZero(h); m = addZero(m); s = addZero(s); document.querySelector(".count").innerHTML = `活动倒计时 ${d}天 ${h} 时 ${m} 分 ${s} 秒`; if (lefttime <= 0) { document.querySelector(".count").innerHTML = "活动已结束"; return; } setTimeout(countDown, 1000); } } </script>
8.12 沙箱隔离怎么做的什么原理
参考答案:
沙箱,即sandbox,顾名思义,就是让你的程序跑在一个隔离的环境下,不对外界的其他程序造成影响,通过创建类似沙盒的独立作业环境,在其内部运行的程序并不能对硬盘产生永久性的影响。
实现沙箱的三种方法
- 借助with + new Function
首先从最简陋的方法说起,假如你想要通过eval和function直接执行一段代码,这是不现实的,因为代码内部可以沿着作用域链往上找,篡改全局变量,这是我们不希望的,所以你需要让沙箱内的变量访问都在你的监控范围内;不过,你可以使用with API,在with的块级作用域下,变量访问会优先查找你传入的参数对象,之后再往上找,所以相当于你变相监控到了代码中的“变量访问”:
function compileCode (src) { src = 'with (exposeObj) {' + src + '}' return new Function('exposeObj', src) }
接下里你要做的是,就是暴露可以被访问的变量exposeObj,以及阻断沙箱内的对外访问。通过es6提供的proxy特性,可以获取到对对象上的所有改写:
function compileCode (src) { src = `with (exposeObj) { ${src} }` return new Function('exposeObj', src) } function proxyObj(originObj){ let exposeObj = new Proxy(originObj,{ has:(target,key)=>{ if(["console","Math","Date"].indexOf(key)>=0){ return target[key] } if(!target.hasOwnProperty(key)){ throw new Error(`Illegal operation for key ${key}`) } return target[key] }, }) return exposeObj } function createSandbox(src,obj){ let proxy = proxyObj(obj) compileCode(src).call(proxy,proxy) //绑定this 防止this访问window }
通过设置has函数,可以监听到变量的访问,在上述代码中,仅暴露个别外部变量供代码访问,其余不存在的属性,都会直接抛出error。其实还存在get、set函数,但是如果get和set函数只能拦截到当前对象属性的操作,对外部变量属性的读写操作无法监听到,所以只能使用has函数了。接下来我们测试一下:
const testObj = { value:1, a:{ b: } } createSandbox("value='haha';console.log(a)",testObj)
看起来一切似乎没有什么问题,但是问题出在了传入的对象,当调用的是console.log(a.b)的时候,has方法是无法监听到对b属性的访问的,假设所执行的代码是不可信的,这时候,它只需要通过a.b.proto就可以访问到Object构造函数的原型对象,再对原型对象进行一些篡改,例如将toString就能影响到外部的代码逻辑的。
createSandbox(` a.b.__proto__.toString = ()=>{ new (()=>{}).constructor("var script = document.createElement('script'); script.src = 'http://xss.js'; script.type = 'text/javascript'; document.body.appendChild(script);")() } `,testObj) console.log(testObj.a.b.__proto__.toString())
例如上面所展示的代码,通过访问原型链的方式,实现了沙箱逃逸,并且篡改了原型链上的toString方法,一旦外部的代码执行了toString方法,就可以实现xss攻击,注入第三方代码;由于在内部定义执行的函数代码逻辑,仍然会沿着作用于链查找,为了绕开作用域链的查找,笔者通过访问箭头函数的constructor的方式拿到了构造函数Function,这个时候,Funtion内所执行的xss代码,在执行的时候,便不会再沿着作用域链往上找,而是直接在全局作用域下执行,通过这样的方式,实现了沙箱逃逸以及xss攻击。
你可能会想,如果我切断原型链的访问,是否就杜绝了呢?的确,你可以通过Object.create(null)的方式,传入一个不含有原型链的对象,并且让暴露的对象只有一层,不传入嵌套的对象,但是,即使是基本类型值,数字或字符串,同样也可以通过proto查找到原型链,而且,即使不传入对象,你还可以通过下面这种方式绕过:
({}).__proto__.toString= ()=>{console.log(111)};
可见,new Function + with的这种沙箱方式,防君子不防小人,当然,你也可以通过对传入的code代码做代码分析或过滤?假如传入的代码不是按照的规定的数据格式(例如json),就直接抛出错误,阻止恶意代码注入,但这始终不是一种安全的做法。
- 借助iframe实现沙箱
前面介绍一种劣质的、不怎么安全的方法构造了一个简单的沙箱,但是在前端最常见的方法,还是利用iframe来构造一个沙箱
<iframe sandbox src="..."></iframe>
但是这也会带来一些限制:
script脚本不能执行
不能发送ajax请求
不能使用本地存储,即localStorage,cookie等
不能创建新的弹窗和window
不能发送表单
不能加载额外插件比如flash等
不过别方,你可以对这个iframe标签进行一些配置:
接下里你只需要结合postMessage API,将你需要执行的代码,和需要暴露的数据传递过去,然后和你的iframe页面通信就行了。
1)需要注意的是,在子页面中,要注意不要让执行代码访问到contentWindow对象,因为你需要调用contentWindow的postMessageAPI给父页面传递信息,假如恶意代码也获取到了contentWindow对象,相当于就拿到了父页面的控制权了,这个时候可大事不妙。
2)当使用postMessageAPI的时候,由于sandbox的origin默认为null,需要设置allow-same-origin允许两个页面进行通信,意味着子页面内可以发起请求,这时候需要防范好CSRF,允许了同域请求,不过好在,并没有携带上cookie。
3)当调用postMessageAPI传递数据给子页面的时候,传输的数据对象本身已经通过结构化克隆算法复制
简单的说,通过postMessageAPI传递的对象,已经由浏览器处理过了,原型链已经被切断,同时,传过去的对象也是复制好了的,占用的是不同的内存空间,两者互不影响,所以你不需要担心出现第一种沙箱做法中出现的问题。
- nodejs中的沙箱
nodejs中使用沙箱很简单,只需要利用原生的vm模块,便可以快速创建沙箱,同时指定上下文。
const vm = require('vm'); const x = 1; const sandbox = { x: 2 }; vm.createContext(sandbox); // Contextify the sandbox. const code = 'x += 40; var y = 17;'; vm.runInContext(code, sandbox); console.log(sandbox.x); // 42 console.log(sandbox.y); // 17 console.log(x); // 1; y is not defined.
vm中提供了runInNewContext、runInThisContext、runInContext三个方法,三者的用法有个别出入,比较常用的是runInNewContext和runInContext,可以传入参数指定好上下文对象。
但是vm是绝对安全的吗?不一定。
const vm = require('vm'); vm.runInNewContext("this.constructor.constructor('return process')().exit()")
通过上面这段代码,我们可以通过vm,停止掉主进程nodejs,导致程序不能继续往下执行,这是我们不希望的,解决方案是绑定好context上下文对象,同时,为了避免通过原型链逃逸(nodejs中的对象并没有像浏览器端一样进行结构化复制,导致原型链依然保留),所以我们需要切断原型链,同时对于传入的暴露对象,只提供基本类型值。
let ctx = Object.create(null); ctx.a = 1; // ctx上不能包含引用类型的属性 vm.runInNewContext("this.constructor.constructor('return process')().exit()", ctx);
让我们来看一下TSW中是怎么使用的:
const vm = require('vm'); const SbFunction = vm.runInNewContext('(Function)', Object.create(null)); // 沙堆 ... if (opt.jsonpCallback) { code = `var result=null; var ${opt.jsonpCallback}=function($1){result=$1}; ${responseText}; return result;`; obj = new SbFunction(code)(); } ...
通过runInNewContext返回沙箱中的构造函数Function,同时传入切断原型链的空对象防止逃逸,之后再外部使用的时候,只需要调用返回的这个函数,和普通的new Function一样调用即可。
8.13 实现一个 JS 的sleep
参考答案:
普通版
function sleep(sleepTime) { for(var start = new Date; new Date - start <= sleepTime;) {} } var t1 = +new Date() sleep(3000) var t2 = +new Date() console.log(t2 - t1)
优点:简单粗暴,通俗易懂。
缺点:这是最简单粗暴的实现,确实 sleep 了,也确实卡死了,CPU 会飙升,无论你的服务器 CPU 有多么 Niubility。
Promise 版本
function sleep(time) { return new Promise(resolve => setTimeout(resolve, time)) } const t1 = +new Date() sleep(3000).then(() => { const t2 = +new Date() console.log(t2 - t1) })
优点:这种方式实际上是用了 setTimeout,没有形成进程阻塞,不会造成性能和负载问题。
缺点:虽然不像 callback 套那么多层,但仍不怎么美观,而且当我们需要在某过程中需要停止执行(或者在中途返回了错误的值),还必须得层层判断后跳出,非常麻烦,而且这种异步并不是那么彻底,还是看起来别扭
Async/Await 版本
function sleep(delay) { return new Promise(reslove => { setTimeout(reslove, delay) }) } !async function test() { const t1 = +new Date() await sleep(3000) const t2 = +new Date() console.log(t2 - t1) }()
缺点: ES7 语法存在兼容性问题,有 babel 一切兼容性都不是问题
更优雅的写法
function sleep (time) { return new Promise((resolve) => setTimeout(resolve, time)); } // 用法 sleep(500).then(() => { // 这里写sleep之后需要去做的事情 })
不要忘了开源的力量
const sleep = require("sleep") const t1 = +new Date() sleep.msleep(3000) const t2 = +new Date() console.log(t2 - t1)
优点:能够实现更加精细的时间精确度,而且看起来就是真的 sleep 函数,清晰直白。
8.14 实现一个数组对象的去重,相同value的只保留最后一个,最好有多个思路
参考答案:
1.遍历数组法
它是最简单的数组去重方法(indexOf方法)
实现思路:新建一个数组,遍历去要重的数组,当值不在新数组的时候(indexOf为-1)就加入该新数组中;
var arr=[2,8,5,0,5,2,6,7,2]; function unique1(arr){ var hash=[]; for (var i = 0; i < arr.length; i++) { if(hash.indexOf(arr[i])==-1){ hash.push(arr[i]); } } return hash; }
2.数组下标判断法
调用indexOf方法,性能和方法1差不多
实现思路:如果当前数组的第 i 项在当前数组中第一次出现的位置不是 i,那么表示第 i 项是重复的,忽略掉。否则存入结果数组。
function unique2(arr){ var hash=[]; for (var i = 0; i < arr.length; i++) { if(arr.indexOf(arr[i])==i){ hash.push(arr[i]); } } return hash; }
3.排序后相邻去除法
实现思路:给传入的数组排序,排序后相同的值会相邻,然后遍历排序后数组时,新数组只加入不与前一值重复的值。
function unique3(arr){ arr.sort(); var hash=[arr[0]]; for (var i = 1; i < arr.length; i++) { if(arr[i]!=hash[hash.length-1]){ hash.push(arr[i]); } } return hash; }
4.优化遍历数组法(推荐)
实现思路:双层循环,外循环表示从0到arr.length,内循环表示从i+1到arr.length
将没重复的右边值放入新数组。(检测到有重复值时终止当前循环同时进入外层循环的下一轮判断)
function unique4(arr){ var hash=[]; for (var i = 0; i < arr.length; i++) { for (var j = i+1; j < arr.length; j++) { if(arr[i]===arr[j]){ ++i; } } hash.push(arr[i]); } return hash; }
5.ES6实现
基本思路:ES6提供了新的数据结构Set。它类似于数组,但是成员的值都是唯一的,没有重复的值。
Set函数可以接受一个数组(或类似数组的对象)作为参数,用来初始化。
function unique5(arr){ var x = new Set(arr); return [...x]; }
扩展:如果重复,则去掉该元素
数组下标去重
function unique22(arr){ var hash=[]; for (var i = 0; i < arr.length; i++) { if(arr.indexOf(arr[i])==arr.lastIndexOf(arr[i])){ hash.push(arr[i]); } } return hash; }
8.15 function rand(min, max, N):生成长度是N,且在min、max内不重复的整数随机数组
参考答案:
把考点拆成了4个小项;需要用递归算法实现:
a) 生成一个长度为n的空数组arr。
b) 生成一个(min-max)之间的随机整数rand。
c) 把随机数rand插入到数组arr内,如果数组arr内已存在与rand相同的数字,则重新生成随机数rand并插入到 arr内[需要使用递归实现,不能使用for/while等循环]
d) 最终输出一个长度为n,且内容不重复的数组arr。
function buildArray(arr, n, min, max) { var num = Math.floor(Math.random() * (max - min + 1)) + min; if (!arr.includes(num)) { arr.push(num); } return arr.length === n ? arr : buildArray(arr, n, min, max); } var result = buildArray([], 5, 2, 32); console.table(result);
8.16 闭包的理解
参考答案:
闭包:
一个函数和对其周围状态(lexical environment,词法环境)的引用捆绑在一起(或者说函数被引用包围), 这样的组合就是闭包(closure)。也就是说,闭包让你可以在一个内层函数中访问到其外层函数的作用域。在 JavaScript 中,每当创建一个函数,闭包就会在函数创建的同时被创建出来。
闭包的特点:
让外部访问函数内部变量成为可能;
可以避免使用全局变量,防止全局变量污染;
可以让局部变量常驻在内存中;
会造成内存泄漏(有一块内存空间被长期占用,而不被释放)
应用场景
- 埋点(是网站分析的一种常用的数据采集方法)计数器
function count() { var num = 0; return function () { return ++num } } var getNum = count(); var getNewNum = count(); document.querySelectorAll('button')[0].onclick = function(){ console.log('点击加入购物车次数: '+getNum()); } document.querySelectorAll('button')[1].onclick = function(){ console.log('点击付款次数: '+getNewNum()); }
- 事件+循环
按照以下方式添加事件,打印出来的i不是按照序号的
形成原因就是操作的是同一个词法环境,因为onclick后面的函数都是一个闭包,但是操作的是同一个词法环境
var lis = document.querySelectorAll('li'); for (var i = 0; i < lis.length; i++) { lis[i].onclick = function () { alert(i) } }
解决办法:
使用匿名函数之后,就形成一个闭包, 操作的就是不同的词法环境
var lis = document.querySelectorAll('li'); for (var i = 0; i < lis.length; i++) { (function (j) { lis[j].onclick = function () { alert(j) } })(i) }
8.17 字符串中的单词逆序输出(手写)
参考答案:
方法一:
function strReverse(str) { return str.split("").reverse().join("") }
方法二:
function strReverse(str) { var i=str.length; var nstr = ""; i=i-1; for (var x = i; x >=0; x--) { nstr+=str.charAt(x) } return nstr }
方法三:
function strReverse(str) { if(str.length == 0)return null; var i = str.length; var dstr = ""; while(--i >= 0) { dstr += str.charAt(i); } return dstr; }
方法四:
function strReverse(str) { return str.split('').reduce((prev, next) => next + prev); }
方法五:
function strReverse(str) { var newstr=""; for(var i=0;i<str.length;i++){ newstr=str.charAt(i)+newstr; } return newstr }
方法六:
function strReverse(str) { if(str.length===1){ return str } return str.slice(-1)+strReverse(str.slice(0,-1)); }
8.18 给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度
参考答案:
思路分析:
对字符串进行遍历,使用String.prototype.indexOf()
实时获取遍历过程中的无重复子串并存放于str
,并保存当前状态最长无重复子串的长度为res
,当遍历结束时,res的值即为无重复字符的最长子串的长度。
代码示例:
/** * @param {string} s * @return {number} */ var lengthOfLongestSubstring = function(s) { var res = 0; // 用于存放当前最长无重复子串的长度 var str = ""; // 用于存放无重复子串 var len = s.length; for(var i = 0; i < len; i++) { var char = s.charAt(i); var index = str.indexOf(char); if(index === -1) { str += char; res = res < str.length ? str.length : res; } else { str = str.substr(index + 1) + char; } } return res; };
8.19 去掉字符串前后的空格
参考答案:
第五种方法在处理长字符串时效率最高
第一种:循环检查替换
//供使用者调用 function trim(s){ return trimRight(trimLeft(s)); } //去掉左边的空白 function trimLeft(s){ if(s == null) { return ""; } var whitespace = new String(" \t\n\r"); var str = new String(s); if (whitespace.indexOf(str.charAt(0)) != -1) { var j=0, i = str.length; while (j < i && whitespace.indexOf(str.charAt(j)) != -1){ j++; } str = str.substring(j, i); } return str; } //去掉右边的空白 www.2cto.com function trimRight(s){ if(s == null) return ""; var whitespace = new String(" \t\n\r"); var str = new String(s); if (whitespace.indexOf(str.charAt(str.length-1)) != -1){ var i = str.length - 1; while (i >= 0 && whitespace.indexOf(str.charAt(i)) != -1){ i--; } str = str.substring(0, i+1); } return str; }
第二种:正则替换
<SCRIPT LANGUAGE="JavaScript"> String.prototype.Trim = function() { return this.replace(/(^\s*)|(\s*$)/g, ""); } String.prototype.LTrim = function() { return this.replace(/(^\s*)/g, ""); } String.prototype.RTrim = function() { return this.replace(/(\s*$)/g, ""); } </SCRIPT>
第三种:使用jquery
$.trim(str) //jquery内部实现为: function trim(str){ return str.replace(/^(\s|\u00A0)+/,'').replace(/(\s|\u00A0)+$/,''); }
第四种:使用motools
function trim(str){ return str.replace(/^(\s|\xA0)+|(\s|\xA0)+$/g, ''); }
第五种:裁剪字符串方式
function trim(str){ str = str.replace(/^(\s|\u00A0)+/,''); for(var i=str.length-1; i>=0; i--){ if(/\S/.test(str.charAt(i))){ str = str.substring(0, i+1); break; } } return str; }
8.20 "判断输出console.log(0 == [])console.log([1] == [1])"
参考答案:
console.log([]==[]); // false console.log([]== 0); // true
解析:
原始值的比较是值的比较:
它们的值相等时它们就相等(==)
对象和原始值不同,对象的比较并非值的比较,而是引用的比较:
即使两个对象包含同样的属性及相同的值,它们也是不相等的
即使两个数组各个索引元素完全相等,它们也是不相等的,所以[]!=[]
[]==0,是数组进行了隐士转换,空数组会转换成数字0,所以相等
8.21 三数之和
参考答案:
题目描述
给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组。
注意:答案中不可以包含重复的三元组。
//例如, 给定数组 nums = [-1, 0, 1, 2, -1, -4], //满足要求的三元组集合为: [ [-1, 0, 1], [-1, -1, 2] ]
解答
这题我们才用排序+双指针的思路来做,遍历排序后的数组,定义指针l和r,分别从当前遍历元素的下一个元素和数组的最后一个元素往中间靠拢,计算结果跟目标对比。
var threeSum = function(nums) { if(nums.length < 3){ return []; } let res = []; // 排序 nums.sort((a, b) => a - b); for(let i = 0; i < nums.length; i++){ if(i > 0 && nums[i] == nums[i-1]){ // 去重 continue; } if(nums[i] > 0){ // 若当前元素大于0,则三元素相加之后必定大于0 break; } // l为左下标,r为右下标 let l = i + 1; r = nums.length - 1; while(l<r){ let sum = nums[i] + nums[l] + nums[r]; if(sum == 0){ res.push([nums[i], nums[l], nums[r]]); while(l < r && nums[l] == nums[l+1]){ l++ } while(l < r && nums[r] == nums[r-1]){ r--; } l++; r--; } else if(sum < 0){ l++; } else if(sum > 0){ r--; } } } return res; };
9. 模块化
9.1 CommonJS规范
参考答案:
CommonJS规范加载模块是同步的,只有加载完成,才能执行后面的操作。
CommonJS
规范中的module
、exports
和require
- 每个文件就是一个模块,有自己的作用域。每个模块内部,
module
变量代表当前模块,是一个对象,它的exports
属性(即module.exports
)是对外的接口。 module.exports
属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports
变量。- 为了方便,
Node
为每个模块提供一个exports
变量,指向module.exports
。
let exports = module.exports;
require
命令用于加载模块文件。
使用示例:
//name.js exports.name = function(){return '李婷婷'}; //导出 //getName.js let getName = require('name'); //引入
注:不能直接将exports
变量指向一个值,因为这样等于切断了exports
与module.exports
的联系:如下
exports = function(x){console.log(x)}
如果一个模块的对外接口,就是一个单一的值,不能使用exports
输出,只能使用module.exports
输出。
CommonJS模块导入用require
,导出用module.exports
。导出的对象需注意,如果是静态值,而且非常量,后期可能会有所改动的,请使用函数动态获取,否则无法获取修改值。导入的参数,是可以随意改动的,所以使用时要注意
9.2 ES6 module 和 CommonJS module 的区别
参考答案:
为CommonJS的
require
语法是同步的,所以就导致了CommonJS模块规范只适合用在服务端,而ES6模块无论是在浏览器端还是服务端都是可以使用的,但是在服务端中,还需要遵循一些特殊的规则才能使用 ;CommonJS 模块输出的是一个值的拷贝,而ES6 模块输出的是值的引用;
CommonJS 模块是运行时加载,而ES6 模块是编译时输出接口,使得对JS的模块进行静态分析成为了可能
因为两个模块加载机制的不同,所以在对待循环加载的时候,它们会有不同的表现。CommonJS遇到循环依赖的时候,只会输出已经执行的部分,后续的输出或者变化,是不会影响已经输出的变量。而ES6模块相反,使用
import
加载一个变量,变量不会被缓存,真正取值的时候就能取到最终的值;关于模块顶层的
this
指向问题,在CommonJS顶层,this
指向当前模块;而在ES6模块中,this
指向undefined
;关于两个模块互相引用的问题,在ES6模块当中,是支持加载CommonJS模块的。但是反过来,CommonJS并不能
require
ES6模块,在NodeJS中,两种模块方案是分开处理的。
9.3 ES6 module、CommonJS module 循环引用的问题
参考答案:
循环加载指的是a脚本的执行依赖b脚本,b脚本的执行依赖a脚本
CommonJS模块是加载时执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。
ES6模块对导出模块,变量,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用。
CommonJS模块规范主要适用于后端Node.js,后端Node.js是同步模块加载,所以在模块循环引入时模块已经执行完毕。推荐前端工程中使用ES6的模块规范,通过安装Babel转码插件支持ES6模块引入的语法。
解析:
- CommonJS模块的加载原理
CommonJS模块就是一个脚本文件,require命令第一次加载该脚本时就会执行整个脚本,然后在内存中生成该模块的一个说明对象。
{ id: '', //模块名,唯一 exports: { //模块输出的各个接口 ... }, loaded: true, //模块的脚本是否执行完毕 ... }
以后用到这个模块时,就会到对象的exports属性中取值。即使再次执行require命令,也不会再次执行该模块,而是到缓存中取值。
CommonJS模块是加载时执行,即脚本代码在require时就全部执行。一旦出现某个模块被“循环加载”,就只输出已经执行的部分,没有执行的部分不会输出。
案例说明:
案例来源于Node官方说明:nodejs.org/api/modules…
//a.js exports.done = false; var b = require('./b.js'); console.log('在a.js中,b.done = %j', b.done); exports.done = true; console.log('a.js执行完毕!') //b.js exports.done = false; var a = require('./a.js'); console.log('在b.js中,a.done = %j', a.done); exports.done = true; console.log('b.js执行完毕!') //main.js var a = require('./a.js'); var b = require('./b.js'); console.log('在main.js中,a.done = %j, b.done = %j', a.done, b.done);
输出结果如下:
//node环境下运行main.js node main.js 在b.js中,a.done = false b.js执行完毕! 在a.js中,b.done = true a.js执行完毕! 在main.js中,a.done = true, b.done = true
JS代码执行顺序如下:
1)main.js中先加载a.js,a脚本先输出done变量,值为false,然后加载b脚本,a的代码停止执行,等待b脚本执行完成后,才会继续往下执行。
2)b.js执行到第二行会去加载a.js,这时发生循环加载,系统会去a.js模块对应对象的exports属性取值,因为a.js没执行完,从exports属性只能取回已经执行的部分,未执行的部分不返回,所以取回的值并不是最后的值。
3)a.js已执行的代码只有一行,exports.done = false;所以对于b.js来说,require a.js只输出了一个变量done,值为false。往下执行console.log('在b.js中,a.done = %j', a.done);控制台打印出:
在b.js中,a.done = false
4)b.js继续往下执行,done变量设置为true,console.log('b.js执行完毕!'),等到全部执行完毕,将执行权交还给a.js。此时控制台输出:
b.js执行完毕!
5)执行权交给a.js后,a.js接着往下执行,执行console.log('在a.js中,b.done = %j', b.done);控制台打印出:
在a.js中,b.done = true
6)a.js继续执行,变量done设置为true,直到a.js执行完毕。
a.js执行完毕!
7)main.js中第二行不会再次执行b.js,直接输出缓存结果。最后控制台输出:
在main.js中,a.done = true, b.done = true
总结:
1)在b.js中,a.js没有执行完毕,只执行了第一行,所以循环加载中,只输出已执行的部分。
2)main.js第二行不会再次执行,而是输出缓存b.js的执行结果。exports.done = true;
- ES6模块的循环加载
ES6模块与CommonJS有本质区别,ES6模块对导出变量,方法,对象是动态引用,遇到模块加载命令import时不会去执行模块,只是生成一个指向被加载模块的引用,需要开发者保证真正取值时能够取到值,只要引用是存在的,代码就能执行。
案例说明:
//even.js import {odd} from './odd'; var counter = 0; export function even(n){ counter ++; console.log(counter); return n == 0 || odd(n-1); } 复制代码 //odd.js import {even} from './even.js'; export function odd(n){ return n != 0 && even(n-1); } 复制代码 //index.js import * as m from './even.js'; var x = m.even(5); console.log(x); var y = m.even(4); console.log(y);
执行index.js,输出结果如下:
babel-node index.js 1 2 3 false 4 5 6 true
可以看出counter的值是累加的,ES6是动态引用。如果上面的引用改为CommonJS代码,会报错,因为在odd.js里,even.js代码并没有执行。改成CommonJS规范加载的代码为:
//even.js var odd = require('./odd.js'); var counter = 0; module.exports = function even(n){ counter ++; console.log(counter); return n == 0 || odd(n-1); } //odd.js var even = require('./even.js'); module.exports = function odd(n){ return n != 0 && even(n-1); } //index.js var even = require('./even.js'); var x = even(5); console.log(x); var y = even(5); console.log(y);
执行index.js,输出结果如下:
$ babel-node index.js 1 /Users/name/Projects/node/ES6/odd.1.js:6 return n != 0 && even(n - 1); ^ TypeError: even is not a function at odd (/Users/name/Projects/node/ES6/odd.1.js:4:22)
css
10. 概念
10.1 继承相关
css的继承:就是给父级设置一些属性,子级继承了父级的该属性,这就是我们的css中的继承。 官方解释,继承是一种规则,它允许样式不仅应用于特定的html标签元素,而且应用于其后代元素。
无继承性的属性
1、display:规定元素应该生成的框的类型
2、文本属性:
vertical-align:垂直文本对齐
text-decoration:规定添加到文本的装饰
text-shadow:文本阴影效果
white-space:空白符的处理
unicode-bidi:设置文本的方向
3、盒子模型的属性:width、height、margin 、margin-top、margin-right、margin-bottom、margin-left、border、 border-style、border-top-style、border-right-style、border-bottom-style、border-left-style、border-width、border-top-width、border-right-right、border-bottom-width、border-left-width、border-color、border-top-color、border-right-color、border-bottom-color、border-left-color、border-top、border-right、border-bottom、border-left、padding、padding-top、padding-right、padding-bottom、padding-left
4、背景属性:background、background-color、background-image、background-repeat、background-position、background-attachment
5、定位属性:float、clear、position、top、right、bottom、left、min-width、min-height、max-width、max-height、overflow、clip、z-index
6、生成内容属性:content、counter-reset、counter-increment
7、轮廓样式属性:outline-style、outline-width、outline-color、outline
8、页面样式属性:size、page-break-before、page-break-after
9、声音样式属性:pause-before、pause-after、pause、cue-before、cue-after、cue、play-during
有继承性的属性
1、字体系列属性
font:组合字体
font-family:规定元素的字体系列
font-weight:设置字体的粗细
font-size:设置字体的尺寸
font-style:定义字体的风格
font-variant:设置小型大写字母的字体显示文本,这意味着所有的小写字母均会被转换为大写,但是所有使用小型大写 字体的字母与其余文本相比,其字体尺寸更小。
font-stretch:对当前的 font-family 进行伸缩变形。所有主流浏览器都不支持。
font-size-adjust:为某个元素规定一个 aspect 值,这样就可以保持首选字体的 x-height。
2、文本系列属性
text-indent:文本缩进
text-align:文本水平对齐
line-height:行高
word-spacing:增加或减少单词间的空白(即字间隔)
letter-spacing:增加或减少字符间的空白(字符间距)
text-transform:控制文本大小写
direction:规定文本的书写方向
color:文本颜色 a元素除外
3、元素可见性:visibility
4、表格布局属性:caption-side、border-collapse、border-spacing、empty-cells、table-layout
5、列表布局属性:list-style-type、list-style-image、list-style-position、list-style
6、生成内容属性:quotes
7、光标属性:cursor
8、页面样式属性:page、page-break-inside、windows、orphans
9、声音样式属性:speak、speak-punctuation、speak-numeral、speak-header、speech-rate、volume、voice-family、 pitch、pitch-range、stress、richness、、azimuth、elevation
所有元素可以继承的属性
1、元素可见性:visibility
2、光标属性:cursor
内联元素可以继承的属性
1、字体系列属性
2、除text-indent、text-align之外的文本系列属性
块级元素可以继承的属性
1、text-indent、text-align
10.2 css预处理工具
参考答案:
CSS 预处理器是一个能让你通过预处理器自己独有的语法来生成CSS的程序。
css预处理器种类繁多,三种主流css预处理器是Less、Sass(Scss)及Stylus;它们各自的背景如下:
Sass:
2007年诞生,最早也是最成熟的CSS预处理器,拥有ruby社区的支持和compass这一最强大的css框架,目前受LESS影响,已经进化到了全面兼容CSS的SCSS(SCSS 需要使用分号和花括号而不是换行和缩进)。
Less:
2009年出现,受SASS的影响较大,但又使用CSS的语法,让大部分开发者和设计师更容易上手,在ruby社区之外支持者远超过SASS。其缺点是比起SASS来,可编程功能不够。优点是简单和兼容CSS,反过来也影响了SASS演变到了SCSS的时代,著名的Twitter Bootstrap就是采用LESS做底层语言的。
Stylus:
2010年产生,来自Node.js社区,主要用来给Node项目进行CSS预处理支持,在此社区之内有一定支持者,在广泛的意义上人气还完全不如SASS和LESS。
比较
在使用 CSS 预处理器之前最重要的是理解语法,幸运的是基本上大多数预处理器的语法跟 CSS 都差不多。
首先 Sass 和 Less 都使用的是标准的 CSS 语法,因此如果可以很方便的将已有的 CSS 代码转为预处理器代码,默认 Sass 使用 .sass 扩展名,而 Less 使用 .less 扩展名。
h1 { color: #0982C1; }
这是一个再普通不过的,不过 Sass 同时也支持老的语法,就是不包含花括号和分号的方式:
h1 color: #0982c1
而 Stylus 支持的语法要更多样性一点,它默认使用 .styl 的文件扩展名,下面是 Stylus 支持的语法
/* style.styl */ h1 { color: #0982C1; } /* omit brackets */ h1 color: #0982C1; /* omit colons and semi-colons */ h1 color #0982C1
可以在同一个样式单中使用不同的变量,例如下面的写法也不会报错:
h1 { color #0982c1 } h2 font-size: 1.2em
10.3 行内元素和块级元素什么区别,然后怎么相互转换
参考答案:
块级元素
1.总是从新的一行开始,即各个块级元素独占一行,默认垂直向下排列;
2.高度、宽度、margin及padding都是可控的,设置有效,有边距效果;
3.宽度没有设置时,默认为100%;
4.块级元素中可以包含块级元素和行内元素。
行内元素
1.和其他元素都在一行,即行内元素和其他行内元素都会在一条水平线上排列;
2.高度、宽度是不可控的,设置无效,由内容决定。
3.根据标签语义化的理念,行内元素最好只包含行内元素,不包含块级元素。
转换
当然块级元素与行内元素之间的特性是可以相互转换的。HTML可以将元素分为行内元素、块状元素和行内块状元素三种。
使用display属性能够将三者任意转换:
(1)display:inline;转换为行内元素;
(2)display:block;转换为块状元素;
(3)display:inline-block;转换为行内块状元素。
10.4 块元素哪些属性可以继承?
参考答案:
text-indent、text-align、visibility、cursor
10.5 盒模型
参考答案:
概念
CSS盒模型本质上是一个盒子,封装周围的HTML元素,它包括:
外边距(margin)
、边框(border)
、内边距(padding)
、实际内容(content)
四个属性。
CSS盒模型:标准模型 + IE模型1.1 W3C盒子模型(标准盒模型)
1.2 IE盒子模型(怪异盒模型)
2. 标准模型和IE模型的区别
计算宽度和高度的不同
标准盒模型:盒子总宽度/高度 = width/height + padding + border + margin
。( 即 width/height 只是 内容高度,不包含 padding 和 border 值 )
IE盒子模型:盒子总宽度/高度 = width/height + margin = (内容区宽度/高度 + padding + border) + margin
。( 即 width/height 包含了 padding 和 border 值 )
CSS如何设置这两种模型
标准:
box-sizing: content-box;
( 浏览器默认设置 )
IE:box-sizing: border-box;
JS如何获取盒模型对应的宽和高
(1)
dom.style.width/height
只能取到行内样式的宽和高,style 标签中和 link 外链的样式取不到。
(2)dom.currentStyle.width/height
(只有IE兼容)取到的是最终渲染后的宽和高
(3)window.getComputedStyle(dom).width/height
同(2)但是多浏览器支持,IE9 以上支持。
(4)dom.getBoundingClientRect().width/height
也是得到渲染后的宽和高,大多浏览器支持。IE9 以上支持,除此外还可以取到相对于视窗的上下左右的距离。
(6)dom.offsetWidth/offsetHeight
包括高度(宽度)、内边距和边框,不包括外边距。最常用,兼容性最好。BFC(边距重叠解决方案)
5.1 BFC基本概念
BFC: 块级格式化上下文
BFC基本概念:BFC
是CSS
布局的一个概念,是一块独立的渲染区域,是一个环境,里面的元素不会影响到外部的元素 。
父子元素和兄弟元素边距重叠,重叠原则取最大值。空元素的边距重叠是取margin
与 padding 的最大值。5.2 BFC原理(渲染规则|布局规则):
(1)内部的
Box
会在垂直方向,从顶部开始一个接着一个地放置;
(2)Box
垂直方向的距离由margin
(外边距)决定,属于同一个BFC
的两个相邻Box
的margin
会发生重叠;
(3)每个元素的margin Box
的左边, 与包含块border Box
的左边相接触,(对于从左到右的格式化,否则相反)。即使存在浮动也是如此;
(4)BFC 在页面上是一个隔离的独立容器,外面的元素不会影响里面的元素,反之亦然。文字环绕效果,设置float
;
(5)BFC 的区域不会与float Box
重叠(清浮动);
(6)计算BFC
的高度时,浮动元素也参与计算。5.3 CSS在什么情况下会创建出BFC(即脱离文档流)
0、根元素,即 HTML 元素(最大的一个
BFC
)
1、浮动(float 的值不为 none
)
2、绝对定位元素(position 的值为 absolute 或 fixed
)
3、行内块(display 为 inline-block
)
4、表格单元(display 为 table、table-cell、table-caption、inline-block 等 HTML 表格相关的属性
)
5、弹性盒(display 为 flex 或 inline-flex
)
6、默认值。内容不会被修剪,会呈现在元素框之外(overflow 不为 visible
)5.4 BFC作用(使用场景)
1、自适应两(三)栏布局(避免多列布局由于宽度计算四舍五入而自动换行)
2、避免元素被浮动元素覆盖
3、可以让父元素的高度包含子浮动元素,清除内部浮动(原理:触发父div
的BFC
属性,使下面的子div
都处在父div
的同一个BFC
区域之内)
4、去除边距重叠现象,分属于不同的BFC
时,可以阻止margin
重叠IFC
6.1 IFC基本概念
IFC: 行内格式化上下文
IFC基本概念:
6.2 IFC原理(渲染规则|布局规则):
(1)内部的 Box
会在水平方向,从含块的顶部开始一个接着一个地放置;
(2)这些 Box
之间的水平方向的 margin
,border
和padding
都有效;
(3)Box
垂直对齐方式:以它们的底部、顶部对齐,或以它们里面的文本的基线(baseline
)对齐(默认, 文本与图片对其),例:line-heigth
与 vertical-align
。
10.6 样式优先级
参考答案:
样式类型
样式类型分为三类
- 行间
<h1 style="font-size:12px;color:#000;">我的行间CSS样式。</h1>
- 内联
<style type="text/css"> h1{font-size:12px; color:#000; } </style>
- 外部
<link rel="stylesheet" href="css/style.css">
选择器类型
- 1、ID #id
- 2、class .class
- 3、标签 p
- 4、通用 *
- 5、属性 [type="text"]
- 6、伪类 :hover
- 7、伪元素 ::first-line
- 8、子选择器、相邻选择器
权重计算规则
第一等:代表内联样式,如: style=””,权值为1000。
第二等:代表ID选择器,如:#content,权值为0100。
第三等:代表类,伪类和属性选择器,如.content,权值为0010。
第四等:代表类型选择器和伪元素选择器,如div p,权值为0001。
通配符、子选择器、相邻选择器等的。如*、>、+,权值为0000。
继承的样式没有权值。
比较规则
遵循如下法则:
- 选择器都有一个权值,权值越大越优先;
- 当权值相等时,后出现的样式表设置要优于先出现的样式表设置;
- 创作者的规则高于浏览者:即网页编写者设置的 CSS 样式的优先权高于浏览器所设置的样式;
- 继承的 CSS 样式不如后来指定的 CSS 样式;
- 在同一组属性设置中标有!important规则的优先级最大
- 通配符、子选择器、相邻选择器等的。虽然权值为0000,但是也比继承的样式优先。
!important
- !important 的作用是提升优先级,换句话说。加了这句的样式的优先级是最高的(比内联样式的优先级还高)。
<style> p{ color:red !important; } </style> <p style="color:blue;">我显示红色</p>
- ie7+和别的浏览器对important的这种作用的支持度都很好。只有ie6有些bug
p{ color:red !important; color:blue; }//会显示blue
但是这并不说明ie6不支持important,只是支持上有些bug。看下面
p{ color:red !important; } p{ color:blue; } //这样就会显示的是red。说明ie6还是支持important的。</pre>
10.7 盒子塌陷是什么?
参考答案:
盒子塌陷
本应该在父盒子内部的元素跑到了外部。
关于盒子塌陷的几种解决方法
(1)最简单,直接,粗暴的方法就是盒子大小写死,给每个盒子设定固定的width和height,直到合适为止,这样的好处是简单方便,兼容性好,适合只改动少量内容不涉及盒子排布的版面。缺点是非自适应,浏览器的窗口大小直接影响用户体验。
(2)给外部的父盒子也添加浮动,让其也脱离标准文档流,这种方法方便,但是对页面的布局不是很友好,不易维护。
(3)给父盒子添加overflow属性。
overflow:auto; 有可能出现滚动条,影响美观。
overflow:hidden; 可能会带来内容不可见的问题。
(4)父盒子里最下方引入清除浮动块。最简单的有:
<br style="clear:both;"/>
有很多人是这么解决的,但是我们并不推荐,因为其引入了不必要的冗余元素 。
(5)用after伪元素清除浮动
给外部盒子的after伪元素设置clear属性,再隐藏它
这其实是对空盒子方案的改进,一种纯CSS的解决方案,不用引入冗余元素。
.clearfix {*zoom: 1;} .clearfix:before,.clearfix:after { display: table; line-height: 0; content: ""; } .clearfix:after {clear: both;}
这也是bootstrap框架采用的清除浮动的方法。
这是一种纯CSS的解决浮动造成盒子塌陷方法,没有引入任何冗余元素,推荐使用此方法来解决CSS盒子塌陷。
备注:第五种方法虽好,但是低版本IE不兼容,具体选择哪种解决方法,可根据实际情况决定。
(6) 给父盒子添加border
(7) 给父盒子设置padding-top
10.8 为什么会出现盒子塌陷?
参考答案:
当父元素没设置足够大小的时候,而子元素设置了浮动的属性,子元素就会跳出父元素的边界(脱离文档流),尤其是当父元素的高度为auto时,而父元素中又没有其它非浮动的可见元素时,父盒子的高度就会直接塌陷为零, 我们称这是CSS高度塌陷。
10.9 css 伪类与伪元素区别
参考答案:
- 伪类(pseudo-classes)
- 其核⼼就是⽤来选择DOM树之外的信息,不能够被普通选择器选择的⽂档之外的元素,⽤来添加⼀些选择器的特殊效果。
- ⽐如:hover :active :visited :link :visited :first-child :focus :lang等
- 由于状态的变化是⾮静态的,所以元素达到⼀个特定状态时,它可能得到⼀个伪类的样式;当状态改变时,它⼜会失去这个样式。
- 由此可以看出,它的功能和class有些类似,但它是基于⽂档之外的抽象,所以叫 伪类。
- 伪元素(Pseudo-elements)
- DOM树没有定义的虚拟元素
- 核⼼就是需要创建通常不存在于⽂档中的元素,
- ⽐如::before ::after 它选择的是元素指定内容,表示选择元素内容的之前内容或之后内容。
- 伪元素控制的内容和元素是没有差别的,但是它本身只是基于元素的抽象,并不存在于⽂档中,所以称为伪元素。⽤于将特殊的效果添加到某些选择器
- 伪类与伪元素的区别
- 表示⽅法
- CSS2 中伪类、伪元素都是以单冒号:表示,
- CSS2.1 后规定伪类⽤单冒号表示,伪元素⽤双冒号::表示,
- 浏览器同样接受 CSS2 时代已经存在的伪元素(:before, :after, :first�line, :first-letter 等)的单冒号写法。
- CSS2 之后所有新增的伪元素(如::selection),应该采⽤双冒号的写法。
- CSS3中,伪类与伪元素在语法上也有所区别,伪元素修改为以::开头。浏览器对以:开头的伪元素也继续⽀持,但建议规范书写为::开头
- 定义不同
- 伪类即假的类,可以添加类来达到效果
- 伪元素即假元素,需要通过添加元素才能达到效果
- 总结:
- 伪类和伪元素都是⽤来表示⽂档树以外的"元素"。
- 伪类和伪元素分别⽤单冒号:和双冒号::来表示。
- 伪类和伪元素的区别,关键点在于如果没有伪元素(或伪类),
- 是否需要添加元素才能达到效果,如果是则是伪元素,反之则是伪类
- 伪类和伪元素都不出现在源⽂件和DOM树中。也就是说在html源⽂件中是看不到伪类和伪元素的。
- 伪类其实就是基于普通DOM元素⽽产⽣的不同状态,他是DOM元素的某⼀特征。
- 伪元素能够创建在DOM树中不存在的抽象对象,⽽且这些抽象对象是能够访问到的。
10.10 行内元素的margin 和 padding
参考答案:
- 水平方向:水平方向上,都有效;
- 垂直方向:垂直方向上,都无效;(
padding-top
和padding-bottom
会显示出效果,但是高度不会撑开,不会对周围元素有影响)
10.11 min-width/max-width 和 min-height/max-height 属性间的覆盖规则?
参考答案:
- max-width 会覆盖 width,即使 width 是行内样式或者设置了 !important。
- min-width 会覆盖 max-width,此规则发生在 min-width 和 max-width 冲突的时候;
10.12 浏览器是怎样解析CSS选择器的?
参考答案:
CSS选择器的解析是从右向左解析的。若从左向右的匹配,发现不符合规则,需要进行回溯,会损失很多性能。若从右向左匹配,先找到所有的最右节点,对于每一个节点,向上寻找其父节点直到找到根元素或满足条件的匹配规则,则结束这个分支的遍历。两种匹配规则的性能差别很大,是因为从右向左的匹配在第一步就筛选掉了大量的不符合条件的最右节点(叶子节点),而从左向右的匹配规则的性能都浪费在了失败的查找上面。而在 CSS解析完毕后,需要将解析的结果与DOM Tree的内容-起进行分析建立-棵Render Tree,最终用来进行绘图。在建立Render Tree时(WebKit 中的「Attachment」过程), 浏览器就要为每个DOM Tree中的元素根据CSS的解析结果(Style Rules)来确定生成怎样的Render Tree。
11.布局
11.1 未知高度元素垂直居中、垂直居中的实现方式有哪些?
参考答案:
1、绝对定位+css3 transform:translate(-50%,-50%)
.wrap{ position:relative; } .child{ position: absolute; top:50%; left:50%; -webkit-transform:translate(-50%,-50%); }
2、css3 的flex布局
.wrap{ display:flex; justify-content:center; } .child{ align-self:center; }
3、table布局
<div class="wrap"> <div class="child"> <div>sadgsdgasgd</div> </div> </div> .wrap{ display:table; text-align:center; } .child{ background:#ccc; display:table-cell; vertical-align:middle; } .child div{ width:300px; height:150px; background:red; margin:0 auto; }
11.2 实现图片垂直居中
参考答案:
1. 使用flex实现图片垂直居中
利用 display: flex;align-items: center 实现垂直居中。flex可能不是实现垂直居中最好的选择,因为IE8,9并不支持它。
html代码:
<div class="flexbox"> <img src="1.jpg" alt=""></div>
css代码:
body{ background:#999} .flexbox{width: 300px;height: 250px;background:#fff;display: flex;align-items: center} .flexbox img{width: 100px;height: 100px;align-items: center;}
2. 利用Display: table;实现img图片垂直居中
给最外层的div设置display属性为table;img的父元素div设置display:table-cell,vertical-align: middle;如果你也想实现水平居中,你可以给最外层的div元素添加text-align: center属性
html代码:
<div class="tablebox"> <div id="imgbox"> <img src="1.jpg" alt=""> </div></div>
css代码:
.tablebox{width: 300px;height: 250px;background: #fff;display: table} #imgbox{display: table-cell;vertical-align: middle;} #imgbox img{width: 100px}
3. 用绝对定位实现垂直居中(推荐-兼容性好)
给img的父元素添加相对定位属性(position: relative),同时,要给子元素也就是图片img元素添加绝对定位属性(position: absolute)。
将图片元素的top属性设置为50%。
现在我们需要给img元素设置一个负的margin-top值,这个值为你想要实现垂直居中的元素高度的一半,*如果不确定元素的高度,可以不使用margin-top,而是使用transform:translateY(-50%);属性。
记住:如果你想要同时实现水平居中,那么你可以用实现垂直居中的一样的技巧来实现。
HTML代码:
<div class="posdiv"> <img src="1.jpg" alt=""></div>
css代码:
body{background: #ccc;} .posdiv{width: 300px;height: 250px;background: #fff;position: relative; margin:0 auto} .posdiv img{width: 100px;position: absolute;top: 50%;margin-top: -50px;}
11.3 设置斑马线表格(纯css)
参考答案:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>斑马线表格</title> <style type="text/css"> *{ margin: 0; padding: 0; /*清处浏览器默认设置*/ } table{ /*表格的外边距和大小*/ margin: 10px 0 0 0; width: 100%; border-spacing: 0; border-collapse: collapse; /*collapse 表格单元格边框合并 border-spacing 表格单元格间距为零 */ } caption{ font: 30px "楷体"; padding: 5px; /*表格标题*/ } td{ width: 32%; height: 50px; /*单元格大小*/ } tbody td{ border: 1px solid; /*表格主体的边框*/ } thead{ background-color: #A2A5A7; /*表格头部*/ } tr:hover{ background-color: #66D9EF; cursor: pointer; /*鼠标悬停在表格上时,表格的背景和鼠标的形状*/ } table tbody tr:nth-child(even){ background-color: #8F908A; box-shadow: inset 0 5px rgba(255,255,255,0.5); /*even为偶数行 odd为奇数行 设置表格的主体部分偶数行的样式 shadow 阴影 inset将外部阴影改为内部阴影 */ } thead tr th:first-child { /*表头部分th 第一个th左上角设置圆角*/ border-radius: 15px 0 0 0; } thead tr td:last-child{ /*最后一个单元格右上角设置圆角*/ border-radius: 0 15px 0 0; } </style> </head> <body> <table> <caption>斑马线表格</caption> <thead> <tr> <th></th> <td></td> <td></td> </tr> </thead> <tbody> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> <tr> <td></td> <td></td> <td></td> </tr> </tbody> <tfoot> <tr> <td></td> <td></td> <td></td> </tr> </tfoot> </table> </body> </html>
11.4 文本元素如何居中
参考答案:
CSS设置文字水平居中
在CSS中可以使用text-align属性来设置文字水平居中。该属性规定元素中的文本的水平对齐方式,通过使用center值设置文本居中。
text-align是一个基本的属性,它会影响一个元素中的文本行互相间的对齐方式。值left、right和center会导致元素中的文本分别左对齐、右对齐和居中,想要使文本居中,直接使用center即可。
该属性设置文本和img标签等一些内联对象(或与之类似的元素)的居中。
该属性有如下几个特点:
1)text-align的center应用在一个容器上,它只针对容器里面的文字以及容器里面的display为inline或者inline-block的容器,如果里面的容器display为block,则里面的容器的内容不会居中。
2)text-align具有向下传递性,会不断地向子元素传递。如果设置一个div,则其子div中的内容也会居中。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>css 水平居中</title> <style> .box { width: 400px; height: 100px; background: pink; text-align:center; } </style> </head> <body> <div class="box">css 水平居中了--文本文字</div> </body> </html>
CSS设置字体垂直居中
2.1 单行文字垂直居中
对于单行文本,我们只需要将文本行高(line-height属性)和所在区域高度(height)设置一致就可以了
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>css 垂直居中</title> <style> .box { width: 300px; height: 300px; background: paleturquoise; line-height:300px; } </style> </head> <body> <div class="box">css 垂直居中了--文本文字</div> </body> </html>
2.2 多行文本垂直居中
说明:多行文本垂直居中分为两种情况,一个是父级元素高度不固定,随着内容变化;另一个是父级元素高度固定。
1) 父级元素高度不固定
父级高度不固定的时,高度只能通过内部文本来撑开。所以,我们可以通过设置内填充(padding)的值来使文本看起来垂直居中,只需设置padding-top和padding-bottom的值相等:
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>css 垂直居中</title> <style> .box { width: 300px; margin: 50px auto; background: paleturquoise; padding: 50px 20px; } </style> </head> <body> <div class="box">css 垂直居中了--文本文字,文本文字,文本文字,文本文字,文本文字,文本文字</div> </body> </html>
2) 父级元素高度固定
使用vertical-align:middle +display:table-cell 使文字垂直居中
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>css 垂直居中</title> <style> .box { width: 300px; height: 300px; background: paleturquoise; vertical-align:middle; display:table-cell; } </style> </head> <body> <div class="box">css 垂直居中了--文本文字,文本文字,文本文字,文本文字,文本文字,文本文字。</div> </body> </html>
说明:vertical-align:middle +display:table-cell能够使单行文字、多行文字都居中。但是因为 table-cell 是 inline 类型,所以会导致原来的块级元素每个 div 一行移动到了同一行。如果需要分列两行,需要在外面额外添加容器对位置进行控制。
11.5 用flex实现九宫格讲思路
参考答案:
利用了padding-top和flex-wrap:wrap,当设置background-color时,是包括盒子模型中的content和padding的,但是为什么不设置height呢?因为父元素没有高度,所以定义height:30%是没有用的,且若想每个block都为正方形,最好的方式就是设置padding-top/padding-bottom:a%,因为此时的百分比是父元素宽度的百分比,而width也为父元素宽度的百分比,所以block可以成为正方形。
<!DOCTYPE html> <html> <style> .block { padding-top: 30%; margin-top: 3%; border-radius: 10%; background-color: orange; width: 30%; } .container-flex2 { display: flex; flex-wrap: wrap; justify-content: space-around; } </style> <body> <div class="container-flex2"> <div class="block"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> <div class="block"></div> </div> </body> </html>
11.6 CSS实现一个等腰三角形
参考答案:
主要是通过把宽高设置成0,边框宽度设置宽一些,设置其中三个边透明,只留一个边显示
等边三角形是特殊的等腰三角形,它的三条边都相等,顶角为60度,而高是边长的3^(1/2)/2倍,约等于0.866……假设底为160px,则高约为138.56px,因此要做边长为160px的等边三角形,可以这么做:
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <title>测试</title> <style type="text/css"> div { width:0px;height:0px;margin:100px auto; border-left:80px solid transparent; border-right:80px solid transparent; border-bottom:138.56px solid #A962CE; /*--三角形的高--*/ } </style> </head> <body> <div> </div> </body> </html>
扩展:
用CSS实现一个等边三角形:
根据各个边之间的长度关系,我们易知:需要展示的边框的宽度:相邻的透明的边框的宽度 = √3 :1
.triangle{ width: 0px; height: 0px; border-left: 10px solid transparent; border-right: 10px solid transparent;; border-top: 17.32px solid transparent; border-bottom: 17.32px solid red; }
11.7 实现扇形、圆形
参考答案:
圆形:
border-radius圆角的四个值按顺序取值分别为:左上、右上、右下、左下。这里只设置一个值,代表四个角的取值都为为50%
原理:border-radius: 50% 弯曲元素的边框以创建圆。
由于圆在任何给定点具有相同的半径,故宽和高都需要保证一样的值,不同的值将创建椭圆。
<div class="circle"></div> <style> .circle { border-radius: 50%; width: 80px; height: 80px; background: #666; } </style>
扇形:
利用border-radius,实现90度角的扇形:
原理:
左上角是圆角,其余三个角都是直角:左上角的值为宽和高一样的值,其他三个角的值不变(等于0)。
<div class="sector"></div> <style> .sector{ border-radius:80px 0 0; width: 80px; height: 80px; background: #666; }</style>
- 绘制任意角度的扇形
<div class="shanxing shanxing1"> <div class="sx1"></div> <div class="sx2"></div> </div> <!--*绘制一个85度扇形*/--p> <div class="shanxing shanxing2"> <div class="sx1"></div> <div class="sx2"></div> </div> <!--*绘制一个向右扇形,90度扇形*--> <div class="shanxing shanxing3"> <div class="sx1"></div> <div class="sx2"></div> </div> <!--*绘制一个颜色扇形 */--p> <div class="shanxing shanxing4"> <div class="sx1"></div> <div class="sx2"></div> </div> <!--/*绘制一个不同颜色半圆夹角 */--> <div class="shanxing shanxing5"> <div class="sx1"></div> <div class="sx2"></div> </div> <style> .shanxing{ position: relative; width: 200px; height: 200px; border-radius: 100px; background-color: yellow; } .sx1{ position: absolute; width: 200px; height: 200px; transform: rotate(0deg); clip: rect(0px,100px,200px,0px); /*这个clip属性用来绘制半圆,在clip的rect范围内的内容显示出来,使用clip属性,元素必须是absolute的 */ border-radius: 100px; background-color: #f00; /*-webkit-animation: an1 2s infinite linear; */ } .sx2{ position: absolute; width: 200px; height: 200px; transform: rotate(0deg); clip: rect(0px,100px,200px,0px); border-radius: 100px; background-color: #f00; /*-webkit-animation: an2 2s infinite linear;*/ } /*绘制一个60度扇形*/ .shanxing1 .sx1{transform: rotate(-30deg);} .shanxing1 .sx2{transform: rotate(-150deg);} /*绘制一个85度扇形*/ .shanxing2 .sx1{transform: rotate(-45deg);} .shanxing2 .sx2{transform: rotate(-140deg);} /*绘制一个向右扇形,90度扇形*/ .shanxing3 .sx1{transform: rotate(45deg);} .shanxing3 .sx2{transform: rotate(-45deg);} /*绘制一个颜色扇形 */ .shanxing4 .sx1{transform: rotate(45deg);background-color: #fff;} .shanxing4 .sx2{transform: rotate(-45deg);background-color: #fff;} /*绘制一个不同颜色半圆夹角 */ .shanxing5 .sx1{transform: rotate(45deg);background-color: #f00;} .shanxing5 .sx2{transform: rotate(-45deg);background-color: #0f0; </style>
11.8 旋转45度
参考答案:
CSS中使用rotate方法来实现对元素的旋转,在参数中加入角度值,旋转方式为顺时针旋转。
<!DOCTYPE html> 2 <html> 3 <head> 4 <meta charset="utf-8" /> 5 <title>Transform旋转</title> 6 <style> 7 div { 8 width: 300px; 9 margin: 150px auto; 10 background-color: yellow; 11 text-align: center; 12 -webkit-transform: rotate(45deg); /* for Chrome || Safari */ 13 -moz-transform: rotate(45deg); /* for Firefox */ 14 -ms-transform: rotate(45deg); /* for IE */ 15 -o-transform: rotate(45deg); /* for Opera */ 16 } 17 </style> 18 </head> 19 <body> 20 <div>黄色div</div> 21 </body> 22 </html>
11.9 画 0.5px 的直线
参考答案:
- 使用scale缩放
<style> .hr.scale-half { height: 1px; transform: scaleY(0.5); } </style> <p>1px + scaleY(0.5)</p> <div class="hr scale-half"></div>
Chrome/Safari都变虚了,只有Firefox比较完美看起来是实的而且还很细,效果和直接设置0.5px一样。所以通过transform: scale会导致Chrome变虚了,而粗细几乎没有变化。但是如果加上transform-origin: 50% 100%:
.hr.scale-half { height: 1px; transform: scaleY(0.5); transform-origin: 50% 100%; }
chrome现在的效果如下
- 线性渐变linear-gradient
<style> .hr.gradient { height: 1px; background: linear-gradient(0deg, #fff, #000); } </style> <p>linear-gradient(0deg, #fff, #000)</p> <div class="hr gradient"></div>
inear-gradient(0deg, #fff, #000)的意思是:渐变的角度从下往上,从白色#fff渐变到黑色#000,而且是线性的,在高清屏上,1px的逻辑像素代表的物理(设备)像素有2px,由于是线性渐变,所以第1个px只能是#fff,而剩下的那个像素只能是#000,这样就达到了画一半的目的。
- boxshadow
<style> .hr.boxshadow { height: 1px; background: none; box-shadow: 0 0.5px 0 #000; } </style> <p>box-shadow: 0 0.5px 0 #000</p> <div class="hr boxshadow"></div>
- viewport
<meta name="viewport" content="width=device-width,initial-sacle=0.5">
其中width=device-width表示将viewport视窗的宽度调整为设备的宽度,这个宽度通常是指物理上宽度。默认的缩放比例为1时,如iphone 6竖屏的宽度为750px,它的dpr=2,用2px表示1px,这样设置之后viewport的宽度就变成375px。但是我们可以改成0.5,viewport的宽度就是原本的750px,所以1个px还是1px,正常画就行,但这样也意味着UI需要按2倍图的出,整体面面的单位都会放大一倍。
11.10 css 切换主题
参考答案:
方式一:主题层
这应该是实现主题功能的一种最常用的手段了。首先,我们的站点会有一个最初的基础样式(或者叫默认样式);然后通过添加一些后续的额外的CSS来覆盖与重新定义部分样式。
具体实现
首先,我们引入基础的样式 components.*
文件
@import "components.tabs"; @import "components.buttons"
其中 components.tabs
文件内容如下
.tab { margin: 0; padding: 0; background-color: gray; }
然后,假设我们的某个主题的样式文件存放于 theme.*
文件:
对应于 components.tabs
,theme.tabs
文件内容如下
.tab { background-color: red; }
因此,我们只需要引入主题样式文件即可
@import "components.tabs"; @import "components.buttons" @import "theme.tabs";
这样当前的样式就变为了
.tab { margin: 0; padding: 0; /* background-color: gray; */ background-color: red; }
优点
- 实现方式简单
- 可以实现将主题应用与所有元素
缺点
- 过多的冗余代码
- 许多的CSS其实是无用的,浪费了带宽
- 把样式文件切分到许多文件中,更加琐碎
方式二:有状态的主题
该方式可以实现基于条件选择不同的主题皮肤,并允许用户在客户端随时切换主题。非常适合需要客户端样式切换功能,或者需要对站点某一部分(区域)进行独立样式设置的场景。
具体实现
还是类似上一节中 Tab 的这个例子,我们可以将 Tab 部分的 (S)CSS 改为如下形式:
.tab { background-color: gray; .t-red & { background-color: red; } .t-blue & { background-color: blue; } }
这里我们把.t-red
与.t-blue
称为 Tab 元素的上下文环境(context)。Tab 元素会根据 context 的不同展示出不同的样式。
最后我们给body
元素加上这个开关
<body class="t-red"> <ul class="tabs">...</ul> </body>
此时 Tab 的颜色为红色。
当我们将t-red
改为t-blue
时,Tab 就变为了蓝色主题。
进一步的,我们可以创建一些 (S)CSS 的 util class(工具类)来专门控制一些 CSS 属性,帮助我们更好地控制主题。例如我们使用如下的.u-color-current
类来控制不同主题下的字体颜色
.u-color-current { .t-red & { color: red; } .t-blue & { color: blue; } }
这样,当我们在不同主题上下文环境下使用.u-color-current
时,就可以控制元素展示出不同主题的字体颜色
<body class="t-red"> <h1 class="page-title u-color-current">...</h1> </body>
上面这段代码会控制<h1>
元素字体颜色为红色主题时的颜色。
优点
- 将许多主题放在了同一处代码中
- 非常适合主题切换的功能
- 非常适合站点局部的主题化
- 可以实现将主题应用于所有元素
缺点
- 有时有点也是缺点,将许多主题混杂在了同一块代码中
- 可能会存在冗余
方式三:配置主题
这种方式其实是在开发侧来实现主题样式的区分与切换的。基于不同的配置,配合一些开发的自动化工具,我们可以在开发时期根据配置文件,编译生成不同主题的 CSS 文件。
它一般会结合使用一些 CSS 预处理器,可以对不同的 UI 元素进行主题分离,并且向客户端直接提供主题样式下最终的 CSS。
具体实现
我们还是以 Sass 为例:
首先会有一份 Sass 的配置文件,例如settings.config.scss
,在这份配置中定义当前的主题值以及一些其他变量
$theme: red;
然后对于一个 Tab 组件,我们这么来写它的 Sass 文件
.tab { margin: 0; padding: 0; @if ($theme == red) { background-color: red; } @else { background-color: gray; } }
这时,我们在其之前引入相应的配置文件后
@import "settings.config"; @import "components.tabs";
Tab 组件就会呈现出红色主题。
当然,我们也可以把我们的settings.config.scss
做的更健壮与易扩展一些
$config: ( theme: red, env: dev, ) // 从$config中获取相应的配置变量 @function config($key) { @return map-get($config, $key); }
与之前相比,这时候使用起来只需要进行一些小的修改,将直接使用theme
变量改为调用config
方法
.tab { margin: 0; padding: 0; @if (config(theme) == red) { background-color: red; } @else { background-color: gray; } }
优点
- 访问网站时,只会传输所需的 CSS,节省带宽
- 将主题的控制位置放在了一个地方(例如上例中的
settings.config.scss
文件) - 可以实现将主题应用于所有元素
缺点
- 在 Sass 中会有非常多逻辑代码
- 只支持有限数量的主题
- 主题相关的信息会遍布代码库中
- 添加一个新主题会非常费劲
方式四:主题调色板
这种方式有些类似于我们绘图时,预设了一个调色板(palette),然后使用的颜色都从其中取出一样。
在实现主题功能时,我们也会有一个类似的“调色板”,其中定义了主题所需要的各种属性值,之后再将这些信息注入到项目中。
当你经常需要为客户端提供完全的定制化主题,并且经常希望更新或添加主题时,这种模式会是一个不错的选择。
具体实现
在方式三中,我们在一个独立的配置文件中设置了一些“环境”变量,来标示当前所处的主题。而在方式四中,我们会更进一步,抽取出一个专门的 palette 文件,用于存放不同主题的变量信息。
例如,现在我们有一个settings.palette.red.scss
文件
$color: red; $color-tabs-background: $color-red;
然后我们的components.tabs.scss
文件内容如下
.tabs { margin: 0; padding: 0; backgroung-color: $color-tabs-background; }
这时候,我们只需要引入这两个文件即可
@import "settings.palette.red"; @import "components.tabs";
可以看到,components.tabs.scss
中并没有关于主题的逻辑判断,我们只需要专注于编辑样式,剩下就是选择所需的主题调色板(palette)即可。
优点
- 编译出来的样式代码无冗余
- 非常适合做一些定制化主题,例如一个公司采购了你们的系统,你可以很方便实现一个该公司的主题
- 可以从一个文件中完全重制出你需要的主题样式
缺点
- 由于主要通过设定不同变量,所以代码确定后,能实现的修改范围会是有限的
方式五:用户定制化
这种模式一般会提供一个个性化配置与管理界面,让用户能自己定义页面的展示样式。
“用户定制化”在社交媒体产品、SaaS 平台或者是 Brandable Software 中最为常见。
具体实现
要实现定制化,可以结合方式二中提到的 util class。
首先,页面中支持自定义的元素会被预先添加 util class,例如 Tab 元素中的u-user-color-background
<ul class="tabs u-user-color-background">...</ul>
此时,u-user-color-background
还并未定义任何样式。而当用户输入了一个背景色时,我们会创建一个``标签,并将 hex 值注入其中
<style id="my-custom"> .u-user-color-background { background-color: #00ffff; } </style>
这时用户就得到了一个红色的 Tab。
优点
- 不需要开发人员的输入信息(是用户定义的)
- 允许用户拥有自己“独一无二”的站点
- 非常实用
缺点
- 不需要开发人员的输入信息也意味着你需要处理更多的“不可控”情况
- 会有许多的冗余
- 会浪费 CSS 的带宽
- 失去部分 CSS 的浏览器缓存能力
11.11 布局: 三栏布局(平均分布)
参考答案:
flex:1 : 设置父级弹性盒,子盒子三个各占1份
<div class="Grid"> <div class="Grid-cell">1/3</div> <div class="Grid-cell">1/3</div> <div class="Grid-cell">1/3</div> </div>
.Grid { display: flex; } .Grid-cell { flex: 1; background: #eee; margin: 10px; }
flex 百分比
<div class="Grid"> <div class="Grid-cell col3"></div> <div class="Grid-cell col3"></div> <div class="Grid-cell clo3"></div> </div>
.col3 { flex: 0 0 33.3%; }
流式布局
<div class="Grid"> <div class="Grid-cell col3"></div> <div class="Grid-cell col3"></div> <div class="Grid-cell clo3"></div> </div>
.col3 { width: 33.33% }
11.12 移动端 1px 问题
参考答案:
问题:1px 的边框,在高清屏下,移动端的1px 会很粗
产生原因
那么为什么会产生这个问题呢?主要是跟一个东西有关,DPR(devicePixelRatio) 设备像素比,它是默认缩放为100%的情况下,设备像素和CSS像素的比值。
window.devicePixelRatio=物理像素 /CSS像素 复制代码
目前主流的屏幕DPR=2 (iPhone 8),或者3 (iPhone 8 Plus)。拿2倍屏来说,设备的物理像素要实现1像素,而DPR=2,所以css 像素只能是 0.5。一般设计稿是按照750来设计的,它上面的1px是以750来参照的,而我们写css样式是以设备375为参照的,所以我们应该写的0.5px就好了啊! 试过了就知道,iOS 8+系统支持,安卓系统不支持。
解决方案
WWDC对iOS统给出的方案
在 WWDC大会上,给出来了1px方案,当写 0.5px的时候,就会显示一个物理像素宽度的 border,而不是一个css像素的 border。 所以在iOS下,你可以这样写。
border:0.5px solid #E5E5E5
可能你会问为什么在3倍屏下,不是0.3333px 这样的?经过测试,在Chrome上模拟iPhone 8Plus,发现小于0.46px的时候是显示不出来。
总结:
- 优点:简单,没有副作用
- 缺点:支持iOS 8+,不支持安卓。后期安卓follow就好了。
使用边框图片
border: 1px solid transparent; border-image: url('./../../image/96.jpg') 2 repeat;
总结:
- 优点:没有副作用
- 缺点:border颜色变了就得重新制作图片;圆角会比较模糊。
使用box-shadow实现
box-shadow: 0 -1px 1px -1px #e5e5e5, //上边线 1px 0 1px -1px #e5e5e5, //右边线 0 1px 1px -1px #e5e5e5, //下边线 -1px 0 1px -1px #e5e5e5; //左边线
总结
- 优点:使用简单,圆角也可以实现
- 缺点:模拟的实现方法,仔细看谁看不出来这是阴影不是边框。
使用伪元素
1条border
.setOnePx{ position: relative; &::after{ position: absolute; content: ''; background-color: #e5e5e5; display: block; width: 100%; height: 1px; /*no*/ transform: scale(1, 0.5); top: 0; left: 0; } }
可以看到,将伪元素设置绝对定位,并且和父元素的左上角对齐,将width 设置100%,height设置为1px,然后进行在Y方向缩小
0.5倍
。4 条border
.setBorderAll{ position: relative; &:after{ content:" "; position:absolute; top: 0; left: 0; width: 200%; height: 200%; transform: scale(0.5); transform-origin: left top; box-sizing: border-box; border: 1px solid #E5E5E5; border-radius: 4px; } }
同样为伪元素设置绝对定位,并且和父元素左上角对其。将伪元素的长和宽先放大2倍,然后再设置一个边框,以左上角为中心,缩放到原来的
0.5倍
总结:
- 优点:全机型兼容,实现了真正的1px,而且可以圆角。
- 缺点:暂用了after 伪元素,可能影响清除浮动。
设置viewport的scale值
这个解决方案是利用viewport+rem+js 实现的。
<html> <head> <title>1px question</title> <meta http-equiv="Content-Type" content="text/html;charset=UTF-8"> <meta name="viewport" id="WebViewport" content="initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no"> <style> html { font-size: 1px; } * { padding: 0; margin: 0; } .top_b { border-bottom: 1px solid #E5E5E5; } .a,.b { box-sizing: border-box; margin-top: 1rem; padding: 1rem; font-size: 1.4rem; } .a { width: 100%; } .b { background: #f5f5f5; width: 100%; } </style> <script> var viewport = document.querySelector("meta[name=viewport]"); //下面是根据设备像素设置viewport if (window.devicePixelRatio == 1) { viewport.setAttribute('content', 'width=device-width,initial-scale=1, maximum-scale=1, minimum-scale=1, user-scalable=no'); } if (window.devicePixelRatio == 2) { viewport.setAttribute('content', 'width=device-width,initial-scale=0.5, maximum-scale=0.5, minimum-scale=0.5, user-scalable=no'); } if (window.devicePixelRatio == 3) { viewport.setAttribute('content', 'width=device-width,initial-scale=0.3333333333333333, maximum-scale=0.3333333333333333, minimum-scale=0.3333333333333333, user-scalable=no'); } var docEl = document.documentElement; var fontsize = 32* (docEl.clientWidth / 750) + 'px'; docEl.style.fontSize = fontsize; </script> </head> <body> <div class="top_b a">下面的底边宽度是虚拟1像素的</div> <div class="b">上面的边框宽度是虚拟1像素的</div> </body> </html>
总结
- 优点:全机型兼容,直接写
1px
不能再方便 - 缺点:适用于新的项目,老项目可能改动大
- 优点:全机型兼容,直接写
11.13 BFC
参考答案:
简介
在解释BFC之前,先说一下文档流。我们常说的文档流其实分为定位流、浮动流、普通流三种。而普通流其实就是指BFC中的FC。FC(Formatting Context),直译过来是格式化上下文,它是页面中的一块渲染区域,有一套渲染规则,决定了其子元素如何布局,以及和其他元素之间的关系和作用。常见的FC有BFC、IFC,还有GFC和FFC。
BFC(Block Formatting Context)块级格式化上下文,是用于布局块级盒子的一块渲染区域。MDN上的解释:BFC是Web页面 CSS 视觉渲染的一部分,用于决定块盒子的布局及浮动相互影响范围的一个区域。
注意:一个BFC的范围包含创建该上下文元素的所有子元素,但不包括创建了新BFC的子元素的内部元素。这从另一方角度说明,一个元素不能同时存在于两个BFC中。因为如果一个元素能够同时处于两个BFC中,那么就意味着这个元素能与两个BFC中的元素发生作用,就违反了BFC的隔离作用。
三种文档流的定位方案
常规流(Normal flow)
- 在常规流中,盒一个接着一个排列;
- 在块级格式化上下文里面, 它们竖着排列;
- 在行内格式化上下文里面, 它们横着排列;
- 当position为static或relative,并且float为none时会触发常规流;
- 对于静态定位(static positioning),position: static,盒的位置是常规流布局里的位置;
- 对于相对定位(relative positioning),position: relative,盒偏移位置由top、bottom、left、right属性定义。即使有偏移,仍然保留原有的位置,其它常规流不能占用这个位置。
浮动(Floats)
- 左浮动元素尽量靠左、靠上,右浮动同理
- 这导致常规流环绕在它的周边,除非设置 clear 属性
- 浮动元素不会影响块级元素的布局
- 但浮动元素会影响行内元素的布局,让其围绕在自己周围,撑大父级元素,从而间接影响块级元素布局
- 最高点不会超过当前行的最高点、它前面的浮动元素的最高点
- 不超过它的包含块,除非元素本身已经比包含块更宽
- 行内元素出现在左浮动元素的右边和右浮动元素的左边,左浮动元素的左边和右浮动元素的右边是不会摆放浮动元素的
绝对定位(Absolute positioning)
- 绝对定位方案,盒从常规流中被移除,不影响常规流的布局;
- 它的定位相对于它的包含块,相关CSS属性:top、bottom、left、right;
- 如果元素的属性position为absolute或fixed,它是绝对定位元素;
- 对于position: absolute,元素定位将相对于上级元素中最近的一个relative、fixed、absolute,如果没有则相对于body;
BFC触发方式
3.1 根元素,即HTML标签
3.2 浮动元素:float值为
left
、right
3.3 overflow值不为 visible,为
auto
、scroll
、hidden
3.4 display值为
inline-block
、table-cell
、table-caption
、table
、inline-table
、flex
、inline-flex
、grid
、inline-grid
3.5 定位元素:position值为
absolute
、fixed
注意:display:table也可以生成BFC的原因在于Table会默认生成一个匿名的table-cell,是这个匿名的table-cell生成了BFC。
约束规则
浏览器对BFC区域的约束规则:
生成BFC元素的子元素会一个接一个的放置。
垂直方向上他们的起点是一个包含块的顶部,两个相邻子元素之间的垂直距离取决于元素的margin特性。在BFC中相邻的块级元素的外边距会折叠(Mastering margin collapsing)
生成BFC元素的子元素中,每一个子元素左外边距与包含块的左边界相接触(对于从右到左的格式化,右外边距接触右边界),即使浮动元素也是如此(尽管子元素的内容区域会由于浮动而压缩),除非这个子元素也创建了一个新的BFC(如它自身也是一个浮动元素)。
规则解读:
- 内部的Box会在垂直方向上一个接一个的放置
- 内部的Box垂直方向上的距离由margin决定。(完整的说法是:属于同一个BFC的两个相邻Box的margin会发生折叠,不同BFC不会发生折叠。)
- 每个元素的左外边距与包含块的左边界相接触(从左向右),即使浮动元素也是如此。(这说明BFC中子元素不会超出他的包含块,而position为absolute的元素可以超出他的包含块边界)
- BFC的区域不会与float的元素区域重叠
- 计算BFC的高度时,浮动子元素也参与计算
作用
BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面元素,反之亦然。我们可以利用BFC的这个特性来做很多事。
5.1 阻止元素被浮动元素覆盖
一个正常文档流的block元素可能被一个float元素覆盖,挤占正常文档流,因此可以设置一个元素的float、 display、position值等方式触发BFC,以阻止被浮动盒子覆盖。
5.2 可以包含浮动元素
通过改变包含浮动子元素的父盒子的属性值,触发BFC,以此来包含子元素的浮动盒子。
5.3 阻止因为浏览器因为四舍五入造成的多列布局换行的情况
有时候因为多列布局采用小数点位的width导致因为浏览器因为四舍五入造成的换行的情况,可以在最后一 列触发BFC的形式来阻止换行的发生。比如下面栗子的特殊情况
5.4 阻止相邻元素的margin合并
属于同一个BFC的两个相邻块级子元素的上下margin会发生重叠,(设置writing-mode:tb-rl时,水平 margin会发生重叠)。所以当两个相邻块级子元素分属于不同的BFC时可以阻止margin重叠。
这里给任一个相邻块级盒子的外面包一个div,通过改变此div的属性使两个原盒子分属于两个不同的BFC,以此来阻止margin重叠。
11.14 移动端适配方案
参考答案:
适配思路
设计稿(750*1334) ---> 开发 ---> 适配不同的手机屏幕,使其显得合理
原则
- 开发时方便,写代码时设置的值要和标注的 160px 相关
- 方案要适配大多数手机屏幕,并且无 BUG
- 用户体验要好,页面看着没有不适感
思路
- 写页面时,按照设计稿写固定宽度,最后再统一缩放处理,在不同手机上都能用
- 按照设计稿的标准开发页面,在手机上部分内容根据屏幕宽度等比缩放,部分内容按需要变化,需要缩放的元素使用 rem, vw 相对单位,不需要缩放的使用 px
- 固定尺寸+弹性布局,不需要缩放
viewport 适配
根据设计稿标准(750px 宽度)开发页面,写完后页面及元素自动缩小,适配 375 宽度的屏幕
在 head 里设置如下代码
<meta name="viewport" content="width=750,initial-scale=0.5">
initial-scale = 屏幕的宽度 / 设计稿的宽度
为了适配其他屏幕,需要动态的设置 initial-scale 的值
<head> <script> const WIDTH = 750 const mobileAdapter = () => { let scale = screen.width / WIDTH let content = `width=${WIDTH}, initial-scale=${scale}, maximum-scale=${scale}, minimum-scale=${scale}` let meta = document.querySelector('meta[name=viewport]') if (!meta) { meta = document.createElement('meta') meta.setAttribute('name', 'viewport') document.head.appendChild(meta) } meta.setAttribute('content',content) } mobileAdapter() window.onorientationchange = mobileAdapter //屏幕翻转时再次执行 </script> </head>
缺点就是边线问题,不同尺寸下,边线的粗细是不一样的(等比缩放后),全部元素都是等比缩放,实际显示效果可能不太好
vw 适配(部分等比缩放)
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标注是基于此宽度标注)
- 开始开发,对设计稿的标注进行转换,把px换成vw。比如页面元素字体标注的大小是32px,换成vw为 (100/750)*32 vw
- 对于需要等比缩放的元素,CSS使用转换后的单位
- 对于不需要缩放的元素,比如边框阴影,使用固定单位px
关于换算,为了开发方便,利用自定义属性,CSS变量
<head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"> <script> const WIDTH = 750 //:root { --width: 0.133333 } 1像素等于多少 vw document.documentElement.style.setProperty('--width', (100 / WIDTH)) </script> </head>
注意此时,meta 里就不要去设置缩放了
业务代码里就可以写
header { font-size: calc(28vw * var(--width)) }
实现了按需缩放
rem 适配
- 开发者拿到设计稿(假设设计稿尺寸为750px,设计稿的元素标是基于此宽度标注)
- 开始开发,对设计稿的标注进行转换
- 对于需要等比缩放的元素,CSS使用转换后的单位
- 对于不需要缩放的元素,比如边框阴影,使用固定单位px
假设设计稿的某个字体大小是 40px, 手机屏幕上的字体大小应为 420/750*40 = 22.4px (体验好),换算成 rem(相对于 html 根节点,假设 html 的 font-size = 100px,)则这个字体大小为 0.224 rem
写样式时,对应的字体设置为 0.224 rem 即可,其他元素尺寸也做换算...
但是有问题
举个 ,设计稿的标注 是40px,写页面时还得去做计算,很麻烦(全部都要计算)
能不能规定一下,看到 40px ,就应该写 40/100 = 0.4 rem,这样看到就知道写多少了(不用计算),此时的 html 的 font-size 就不能是 100px 了,应该为 (420*100)/750 = 56px,100为我们要规定的那个参数
根据不同屏幕宽度,设置 html 的 font-size 值
<head> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, minimum-scale=1"> <script> const WIDTH = 750 //设计稿尺寸 const setView = () => { document.documentElement.style.fontSize = (100 * screen.width / WIDTH) + 'px' } window.onorientationchange = setView setView() </script> </head>
对于需要等比缩放的元素,CSS使用转换后的单位
header { font-size: .28rem; }
对于不需要缩放的元素,比如边框阴影,使用固定单位px
header > span.active { color: #fff; border-bottom: 2px solid rgba(255, 255, 255, 0.3); }
假设 html 的 font size = 1px 的话,就可以写 28 rem 了,更方便了,但是浏览器对字体大小有限制,设为 1px 的话,在浏览器中是失效的,会以 12px(或者其他值) 做一个计算 , 就会得到一个很夸张的结果,所以可以把 html 写的大一些
使用 sass 库时
JS 处理还是一样的,但看着好看些
@function px2rem($px) { @return $px * 1rem / 100; } header { font-size: px2rem(28); }
以上的三种适配方案,都是等比缩放,放到 ipad 上时(设计稿以手机屏幕设计的),页面元素会很大很丑,有些场景下,并不需要页面整体缩放(viewport 自动处理的也很好了),所以有时只需要合理的布局即可。
弹性盒适配(合理布局)
<meta name="viewport" content="width=device-width">
使用 flex 布局
section { display: flex; }
总结一下,什么样的页面需要做适配(等比缩放)呢
- 页面中的布局是栅格化的
换了屏幕后,到底有多宽多高很难去做设置,整体的都需要改变,所以需要整体的缩放
- 头屏大图,宽度自适应,高度固定的话,对于不同的屏幕,形状就会发生改变(放到ipad上就变成长条了),宽度变化后,高度也要保持等比例变化
以上所有的适配都是宽度的适配,但是在某些场景下,也会出现高度的适配
比如大屏,需要适配很多的电视尺寸,要求撑满屏幕,不能有滚动条,此时若换个屏幕
此时需要考虑小元素用 vh, 宽和高都用 vh 去表示,中间的大块去自适应,这就做到了大屏的适配,屏幕变小了,整体变小了(体验更好),中间这块撑满了屏幕
对于更复杂的场景,需要更灵活考虑,没有一种适配方式可以囊括所有场景。
12. 属性
12.1 清除浮动
参考答案:
除浮动其实叫做闭合浮动更合适,因为是把浮动的元素圈起来,让父元素闭合出口和入口不让他们出来影响其他的元素。
在CSS中,clear属性用于清除浮动,其基本语法格式如下:
选择器{clear:属性值;} /*属性值为left,清除左侧浮动的影响 属性值为right,清除右侧浮动的影响 属性值为both,同时清除左右两侧浮动的影响*/
额外标签法
1.1 末尾标签法
通过在浮动元素的末尾添加一个空的标签。这是W3C推荐的做法,虽然比较简单,但是添加了无意义的标 签,结构化比较差,所以不推荐使用。下面三种写法都适用:1. <div style="clear:both"></div> 2. .clear { clear:both } <div class="clear"></div> 3..clear{ clear:both } <br class="clear" /> <!--也可以使用br等别的块级元素来清除浮动-->
2.2 内部标签法,把div放进父盒子里,盒子会撑开,一般也不会用。
overflow
给父级元素添加overflow样式方法。
代码比较简洁,可以通过触发BFC方式,但是因为本身overflow的本质是溢出隐藏的效果,所以有的时候也会有一些问题存在,比如内容增多的时候不会自动换行导致内容被隐藏掉,无法显示出要溢出的元素。.father { overflow: auto; /* 加上这句话,就可以清除浮动 overflow = hidden|auto|scroll 都可以实现*/ }
伪元素法(最常用)
3.1 使用after伪元素清除浮动
after是在父元素中加一个盒子,这个元素是通过css添加上去的,符合闭合浮动思想,结构语义化正确。
父元素中加一个类名为clearfix 。但是这个方法IE6,7不识别,要进行兼容,使用zoom:1触发 hasLayout来清除浮动
代表网站:百度、淘宝、网易等.clearfix:after{ content:"."; /*尽量不要为空,一般写一个点*/ height:0; /*盒子高度为0,看不见*/ display:block; /*插入伪元素是行内元素,要转化为块级元素*/ visibility:hidden; /*content有内容,将元素隐藏*/ clear:both; } .clearfix { *zoom: 1; /* *只有IE6,7识别 */ }
3.2 after伪元素空余字符法
父元素中加一个类名为clearfix,也需要兼容IE6、7.clearfix::after{ content:"\200B"; /* content:'\0200'; 也可以 */ display:block; height:0; clear:both; } .clearfix { *zoom: 1; }
3.3 使用before和after双伪元素清除浮动(推荐)
完全符合闭合浮动思想的方法。
父元素中加一个类名为clearfix,需要兼容IE6、7
代表网站:小米、腾讯.clearfix:before, .clearfix:after { content: ""; display: table; } .clearfix:after { clear: both; } .clearfix { *zoom: 1; }
12.2 padding , margin 百分比单位依据
参考答案:
在CSS 盒模型中,依据CSS2.2文档,margin与padding的百分比指定值时,一律参考包含盒的宽度。
示例:
.father{ height: 100px; width: 200px; border: solid; } .son{ margin: 20%; padding: 20%; width: 50%; height: 50%; }
如下图,包括padding-top/bottom,margin-top/bottom在内,所有padding和margin均是参考的包含块的宽度,故它们的值为200px * 20% = 40px。
12.3 父子边距重合
参考答案:
效果:
边界重叠是指两个或多个盒子(可能相邻也可能嵌套)的相邻边界(其间没有任何非空内容、补白、边框)重合在一起而形成一个单一边界。
父子元素的边界重叠
<style> .parent { background: #e7a1c5; } .parent .child { background: #c8cdf5; height: 100px; margin-top: 10px; } </style> <section class="parent"> <article class="child"></article> </section>
以为期待的效果:
而实际上效果如下:
在这里父元素的高度不是 110px,而是 100px,在这里发生了高度坍塌。
产生原因:
是如果块元素的 margin-top
与它的第一个子元素的 margin-top
之间没有 border
、padding
、inline
content
、 clearance
来分隔,或者块元素的 margin-bottom 与它的最后一个子元素的 margin-bottom 之间没有 border
、padding
、inline
content
、height
、min-height
、 max-height
分隔,那么外边距会塌陷。子元素多余的外边距会被父元素的外边距截断。
解决办法:
父子元素的边界重叠得解决方案: 在父元素上加上 overflow:hidden;使其成为 BFC。
12.4 css字体大小设置(三种).em rem px
参考答案:
px(绝对长度单位)
相信对于前端来说px这个单位是大家并不陌生,px这个单位,兼容性可以说是相当可以,大家对px的了解肯 定是没有很大的问题的。
em(相对长度单位)
使用:
浏览器的默认字体都是16px,那么1em=16px,以此类推计算12px=0.75em,10px=0.625em,2em=32px;
这样使用很复杂,很难很好的与px进行对应,也导致书写、使用、视觉的复杂(0.75em、0.625em全是小数点);
为了简化font-size的换算,我们在body中写入一下代码
body {font-size: 62.5%; } /* 公式16px*62.5%=10px */
这样页面中1em=10px,1.2em=12px,1.4em=14px,1.6em=16px,使得视觉、使用、书写都得到了极大的帮助。
例子如下:
<div class="font1" style='font-size:1.6em'>我是1.6em</div>
缺点:
em的值并不是固定的;
em会继承父级元素的字体大小(参考物是父元素的font-size;);
em中所有的字体都是相对于父元素的大小决定的;所以如果一个设置了font-size:1.2em的元素在另一个设置了font-size:1.2em的元素里,而这个元素又在另一个设置了font-size:1.2em的元素里,那么最后计算的结果是1.2X1.2X1.2=1.728em
<div class="big"> 我是大字体 <div class="small">我是小字体</div> </div>
样式为
<style> body {font-size: 62.5%; } /* 公式:16px*62.5%=10px */ .big{font-size: 1.2em} .small{font-size: 1.2em} </style>
但运行结果small的字体大小为:1.2em*1.2em=1.44em
rem(相对长度单位)
使用:
浏览器的默认字体都是16px,那么1rem=16px,以此类推计算12px=0.75rem,10px=0.625rem,2rem=32px;
这样使用很复杂,很难很好的与px进行对应,也导致书写、使用、视觉的复杂(0.75rem、0.625em全是小数点) ;
为了简化font-size的换算,我们在根元素html中加入font-size: 62.5%;
html {font-size: 62.5%; } /* 公式16px*62.5%=10px */
这样页面中1rem=10px,1.2rem=12px,1.4rem=14px,1.6rem=16px;使得视觉、使用、书写都得到了极大的帮助;
<div class="font1" style='font-size:1.6rem'>我是1.6rem=16px</div>
特点:
rem单位可谓集相对大小和绝对大小的优点于一身
和em不同的是rem总是相对于根元素(如:root{}),而不像em一样使用级联的方式来计算尺寸。这种相对单位使用起来更简单。
rem支持IE9及以上,意思是相对于根元素html(网页),不会像em那样,依赖于父元素的字体大小,而造成混乱。使用起来安全了很多。
<div class="big"> 我是14px=1.4rem<div class="small">我是12px=1.2rem</div> </div>
<style> html {font-size: 10px; } /* 公式16px*62.5%=10px */ .big{font-size: 1.4rem} .small{font-size: 1.2rem} </style>
注意:
- 值得注意的浏览器支持问题: IE8,Safari 4或 iOS 3.2中不支持rem单位。
- 如果你的用户群都使用最新版的浏览器,那推荐使用rem,如果要考虑兼容性,那就使用px,或者两者同时使用。
12.5 css3新特性
参考答案:
CSS3 边框
在 css3 中新增的边框属性如下:
创建圆角
语法:
border-radius : length length;
length: 由浮点数字和单位标识符组成的长度值(如:20px)。不可为负值,如果为负值则与0展示效果一样。第一个值设置其水平半径,第二个值设置其垂直半径,如果第二个值省略则默认第二个值等于第一个值。
div{ border: 1px solid; /* 设置每个圆角水平半径和垂直半径都为30px */ border-radius: 30px; }
border-radius
是4个角的缩写方法。四个角的表示顺序与border
类似按照border-top-left-radius
、border-top-right-radius
、border-bottom-right-radius
、border-bottom-left-radius
的顺序来设置:div{ border: 1px solid; /* 如果 / 前后的值都存在,那么 / 前面的值设置其水平半径,/ 后面值设置其垂直半径,如果没有 / ,则水平和垂直半径相等 */ border-radius: 10px 15px 20px 30px / 20px 30px 10px 15px; /* 上面写法等价于下面的写法,第一个值是水平半径,第二个值是垂直半径 */ border-top-left-radius: 10px 20px; border-top-right-radius: 15px 30px; border-bottom-right-radius: 20px 10px; border-bottom-left-radius: 30px 15px; }
border-radius
指定不同数量的值遵循对角相等的原则,即指定了值的取指定值,没有指定值的与对角值相等,对角相等模型边框阴影
通过属性
box-shadow
向边框添加阴影。语法:
{box-shadow : [inset] x-offset y-offset blur-radius extension-radius spread-radiuscolor}
说明:对象选择器 {box-shadow:[投影方式] X轴偏移量 Y轴偏移量 模糊半径 阴影扩展半径 阴影颜色}
div{ /* 内阴影,向右偏移10px,向下偏移10px,模糊半径5px,阴影缩小10px */ box-shadow: inset 10px 10px 5px -10px #888888; }
边框图片
语法:
border-image : border-image-source || border-image-slice [ / border-image-width] || border-image-repeat
border-image : none | image [ number | percentage]{1,4} [ / border-width>{1,4} ] ? [ stretch | repeat | round ]{0,2}
div{ border-image:url(border.png) 30 30 round; border-image: url(border.png) 20/10px repeat; }
- CSS3 背景
background-size
属性
在 CSS3 之前,背景图片的尺寸是由图片的实际尺寸决定的。在 CSS3 中,可以设置背景图片的尺寸,这就允许我们在不同的环境中重复使用背景图片。可以像素或百分比规定尺寸。如果以百分比规定尺寸,那么尺寸相对于父元素的宽度和高度。
div{ background:url(bg_flower.gif); /* 通过像素规定尺寸 */ background-size:63px 100px; /* 通过百分比规定尺寸 */ background-size:100% 50%; background-repeat:no-repeat; }
background-origin
属性
规定背景图片的定位区域,背景图片可以放置于 content-box
、padding-box
或 border-box
区域,
div{ background:url(bg_flower.gif); background-repeat:no-repeat; background-size:100% 100%; /* 规定背景图片的定位区域 */ background-origin:content-box; }
background-clip
属性
与background-origin
属性相似,规定背景颜色的绘制区域,区域划分与background-origin
属性相同。
div{ background-color:yellow; background-clip:content-box; }
CSS3 多重背景图片
CSS3 允许为元素设置多个背景图像
body{ background-image:url(bg_flower.gif),url(bg_flower_2.gif); }
- CSS3 文本效果
text-shadow
属性
给为本添加阴影,能够设置水平阴影、垂直阴影、模糊距离,以及阴影的颜色。
h1{ text-shadow: 5px 5px 5px #FF0000; }
text-wrap 属性
设置区域内的自动换行。
语法:text-wrap: normal | none | unrestricted | suppress | break-word;
/* 允许对长单词进行拆分,并换行到下一行 */ p {word-wrap:break-word;}
值 | 描述 |
---|---|
normal | 只在允许的换行点进行换行。 |
none | 不换行。元素无法容纳的文本会溢出。 |
break-word | 在任意两个字符间换行。 |
suppress | 压缩元素中的换行。浏览器只在行中没有其他有效换行点时进行换行。 |
- CSS3 字体
字体定义
在 CSS3 之前,web 设计师必须使用已在用户计算机上安装好的字体。但是通过 CSS3,web 设计师可以使用他 们喜欢的任意字体。当找到或购买到希望使用的字体时,可将该字体文件存放到 web 服务器上,它会在需要时 被自动下载到用户的计算机上。字体需要在 CSS3 @font-face 规则中定义。
/* 定义字体 */ @font-face{ font-family: myFont; src: url('Sansation_Light.ttf'), url('Sansation_Light.eot'); /* IE9+ */ } div{ font-family:myFont; }
使用粗体字体
"Sansation_Light.ttf"文件 是定义的正常字体,"Sansation_Bold.ttf" 是另一个字体文件,它包含了 Sansation 字体的粗体字符。只要 font-family 为 "myFirstFont" 的文本需要显示为粗体,浏览器就会使用该字体。
(其实没弄清楚这样处理的原因,经测试直接在html中通过 b 标签也可以实现加粗的效果)
/* 定义正常字体 */ @font-face{ font-family: myFirstFont; src: url('/example/css3/Sansation_Light.ttf'), url('/example/css3/Sansation_Light.eot'); /* IE9+ */ } /* 定义粗体时使用的字体 */ @font-face{ font-family: myFirstFont; src: url('/example/css3/Sansation_Bold.ttf'), url('/example/css3/Sansation_Bold.eot'); /* IE9+ */ /* 标识属性 */ font-weight:bold; } div{ font-family:myFirstFont; }
- CSS3 2D 转换
通过 CSS3 转换,我们能够对元素进行移动、缩放、转动、拉长或拉伸,转换是使元素改变形状、尺寸和位置的一种效果。
translate() 方法
通过 translate(x , y) 方法,元素根据给定的 left(x 坐标) 和 top(y 坐标) 位置参数从其当前位置移动,x为正值向右移动,为负值向左移动;y为正值向下移动,为负值向上移动;
div{ transform: translate(50px,100px); -ms-transform: translate(50px,100px); /* IE 9 */ -webkit-transform: translate(50px,100px); /* Safari and Chrome */ -o-transform: translate(50px,100px); /* Opera */ -moz-transform: translate(50px,100px); /* Firefox */ }
rotate() 方法
控制元素顺时针旋转给定的角度。为正值,元素将顺时针旋转。为负值,元素将逆时针旋转。
div{ transform: rotate(30deg); -ms-transform: rotate(30deg); /* IE 9 */ -webkit-transform: rotate(30deg); /* Safari and Chrome */ -o-transform: rotate(30deg); /* Opera */ -moz-transform: rotate(30deg); /* Firefox */ }
scale() 方法
根据给定的宽度(X 轴)和高度(Y 轴)参数,控制元素的尺寸的增加、减少。
div{ transform: scale(2,4); -ms-transform: scale(2,4); /* IE 9 */ -webkit-transform: scale(2,4); /* Safari 和 Chrome */ -o-transform: scale(2,4); /* Opera */ -moz-transform: scale(2,4); /* Firefox */ }
skew() 方法
根据给定的水平线(X 轴)和垂直线(Y 轴)参数设置元素翻转给定的角度。
/* 设置围绕 X 轴把元素翻转 30 度,围绕 Y 轴翻转 20 度。 */ div{ transform: skew(30deg,20deg); -ms-transform: skew(30deg,20deg); /* IE 9 */ -webkit-transform: skew(30deg,20deg); /* Safari and Chrome */ -o-transform: skew(30deg,20deg); /* Opera */ -moz-transform: skew(30deg,20deg); /* Firefox */ }
matrix() 方法
matrix() 方法把所有 2D 转换方法组合在一起。matrix() 方法需要六个参数,包含数学函数,允许旋转、缩放、移动以及倾斜元素。
/* 使用 matrix 方法将 div 元素旋转 30 度 */ div{ transform:matrix(0.866,0.5,-0.5,0.866,0,0); -ms-transform:matrix(0.866,0.5,-0.5,0.866,0,0); /* IE 9 */ -moz-transform:matrix(0.866,0.5,-0.5,0.866,0,0); /* Firefox */ -webkit-transform:matrix(0.866,0.5,-0.5,0.866,0,0); /* Safari and Chrome */ -o-transform:matrix(0.866,0.5,-0.5,0.866,0,0); /* Opera */ }
2D Transform 方法汇总
函数 | 描述 |
---|---|
matrix(n,n,n,n,n,n) | 定义 2D 转换,使用六个值的矩阵。 |
translate(x,y) | 定义 2D 转换,沿着 X 和 Y 轴移动元素。 |
translateX(n) | 定义 2D 转换,沿着 X 轴移动元素。 |
translateY(n) | 定义 2D 转换,沿着 Y 轴移动元素。 |
scale(x,y) | 定义 2D 缩放转换,改变元素的宽度和高度。 |
scaleX(n) | 定义 2D 缩放转换,改变元素的宽度。 |
scaleY(n) | 定义 2D 缩放转换,改变元素的高度。 |
rotate(angle) | 定义 2D 旋转,在参数中规定角度。 |
skew(x-angle,y-angle) | 定义 2D 倾斜转换,沿着 X 和 Y 轴。 |
skewX(angle) | 定义 2D 倾斜转换,沿着 X 轴。 |
skewY(angle) | 定义 2D 倾斜转换,沿着 Y 轴。 |
- CSS3 3D 转换
CSS3 允许使用 3D 转换来对元素进行格式化
rotateX() 方法
/* 设置元素围绕其 X 轴以给定的度数进行旋转 */ div{ transform: rotateX(120deg); -webkit-transform: rotateX(120deg); /* Safari 和 Chrome */ -moz-transform: rotateX(120deg); /* Firefox */ }
rotateY() 旋转
/* 设置元素围绕其 Y 轴以给定的度数进行旋转 */ div{ transform: rotateY(130deg); -webkit-transform: rotateY(130deg); /* Safari 和 Chrome */ -moz-transform: rotateY(130deg); /* Firefox */ }
CSS3 过渡
通过 CSS3可以在不使用 Flash 动画或 JavaScript 的情况下,当元素从一种样式变换为另一种样式时为元素添加效果。
要实现这一点,必须规定以下两项内容:
- 设置添加过渡效果的 CSS 属性;
- 设置过渡效果的时长;
注意: 如果时长未设置,则不会有过渡效果,因为默认值是 0。
单项改变
/* 设置将变化效果添加在“宽度”上,时长为2秒;该时长在其他属性上并不适用 */ div{ transition: width 2s; -moz-transition: width 2s; /* Firefox 4 */ -webkit-transition: width 2s; /* Safari 和 Chrome */ -o-transition: width 2s; /* Opera */ } /* 配合在一起使用的效果就是当鼠标移上去的时候宽度变为300px,这个过程耗时2秒 */ div:hover{ width:300px; }
注意: 当鼠标移出元素时,它会逐渐变回原来的样式。
多项改变
如需向多个样式添加过渡效果,请添加多个属性,由逗号隔开。
/* 同时向宽度、高度和转换添加过渡效果 */ div{ transition: width 2s, height 2s, transform 2s; -moz-transition: width 2s, height 2s, -moz-transform 2s; -webkit-transition: width 2s, height 2s, -webkit-transform 2s; -o-transition: width 2s, height 2s,-o-transform 2s; } /* 当鼠标移上时宽度和高度都变成200px,同时旋转180度,每个属性变化都耗时2秒 */ div:hover{ width:200px; height:200px; transform:rotate(180deg); -moz-transform:rotate(180deg); /* Firefox 4 */ -webkit-transform:rotate(180deg); /* Safari and Chrome */ -o-transform:rotate(180deg); /* Opera */ }
过渡属性详解
transition
是简写属性,
语法: transition : transition-property | transition-duration | transition-timing-function | transition-delay;
/* 设置在宽度上添加过渡效果,时长为1秒,过渡效果时间曲线为linear,等待2秒后开始过渡 */ div{ transition: width 1s linear 2s; -moz-transition: width 1s linear 2s; /* Firefox 4 */ -webkit-transition: width 1s linear 2s; /* Safari and Chrome */ -o-transition: width 1s linear 2s; /* Opera */ }
属性 | 描述 |
---|---|
transition | 简写属性,用于在一个属性中设置四个过渡属性。 |
transition-property | 规定应用过渡的 CSS 属性的名称。 |
transition-duration | 定义过渡效果花费的时间。默认是 0。 |
transition-timing-function | 规定过渡效果的时间曲线。默认是 "ease"。 |
transition-delay | 规定过渡效果何时开始。默认是 0。 |
CSS3 动画
通过 CSS3可以创建动画,这些动画可以取代网页中的画图片、Flash 动画以及 JavaScript。
CSS3 中通过@keyframes 规则来创建动画。在 @keyframes 中规定某项 CSS 样式,就能创建由当前样式(动画开始前的样式)逐渐改为新样式(需要变到的样式)的动画效果。
通过from , to关键字设置动画发生的时间
/* 通过@keyframes 创建动画 */ @keyframes myfirst{ from {background: red;} to {background: yellow;} } /* Firefox */ @-moz-keyframes myfirst { from {background: red;} to {background: yellow;} } /* Safari 和 Chrome */ @-webkit-keyframes myfirst { from {background: red;} to {background: yellow;} } /* Opera */ @-o-keyframes myfirst { from {background: red;} to {background: yellow;} } /* 将创建的动画绑定到选择器,并至少指定以下两项 CSS3 动画属性 1.指定动画的名称; 2.指定动画的时长; */ div{ animation: myfirst 5s; -moz-animation: myfirst 5s; /* Firefox */ -webkit-animation: myfirst 5s; /* Safari 和 Chrome */ -o-animation: myfirst 5s; /* Opera */ }
通过百分比设置动画发生的时间
动画是使元素从一种样式逐渐变化为另一种样式的效果。可以改变任意多的样式任意多的次数。可以用关键词 "from" 和 "to"来设置动画变化发生的时间,其效果等同于 0% 和 100%。0% 是动画的开始,100% 是动画的完成。为了得到最佳的浏览器支持,应该始终定义 0% 和 100% 选择器。
/* 当动画为 25% 及 50% 时改变背景色,然后当动画 100% 完成时再次改变 */ @keyframes myfirst{ 0% {background: red;} 25% {background: yellow;} 50% {background: blue;} 100% {background: green;} } /* 同时改变背景色和位置 */ @keyframes myfirst{ 0% {background: red; left:0px; top:0px;} 25% {background: yellow; left:200px; top:0px;} 50% {background: blue; left:200px; top:200px;} 75% {background: green; left:0px; top:200px;} 100% {background: red; left:0px; top:0px;} }
动画属性详解
animation
是除了 animation-play-state
属性所有动画属性的简写属性。
语法: animation : animation-name | animation-duration | animation-timing-function | animation-delay | animation-iteration-count | animation-direction
/* 应用的动画为myfirst,一个动画周期为5秒,动画的速度曲线为linear,动画2秒后播放,播放次数为infinite,即无限循环,动画下一周期是否逆向播放取值alternate,即逆向播放 */ div{ animation: myfirst 5s linear 2s infinite alternate; /* Firefox: */ -moz-animation: myfirst 5s linear 2s infinite alternate; /* Safari 和 Chrome: */ -webkit-animation: myfirst 5s linear 2s infinite alternate; /* Opera: */ -o-animation: myfirst 5s linear 2s infinite alternate; }
属性 | 描述 |
---|---|
@keyframes | 规定动画。 |
animation | 所有动画属性的简写属性,除了 animation-play-state 属性。 |
animation-name | 规定 @keyframes 动画的名称。 |
animation-duration | 规定动画完成一个周期所花费的秒或毫秒。默认是 0。 |
animation-timing-function | 规定动画的速度曲线。默认是 "ease"。 |
animation-delay | 规定动画何时开始。默认是 0。 |
animation-iteration-count | 规定动画被播放的次数。默认是 1。 |
animation-direction | 规定动画是否在下一周期逆向地播放。默认是 "normal"。 |
animation-play-state | 规定动画是否正在运行或暂停。默认是 "running"。 |
animation-fill-mode | 规定对象动画时间之外的状态。 |
CSS3 多列
通过 CSS3够创建多个列来对文本进行布局,就像我们经常看到的报纸的布局一样。
CSS3 创建多列
column-count
属性规定元素应该被分隔的列数。
/* 将div中的文本分为3列 */ div{ column-count:3; -moz-column-count:3; /* Firefox */ -webkit-column-count:3; /* Safari 和 Chrome */ }
CSS3 规定列之间的间隔
column-gap
属性规定列之间的间隔。
/* 设置列之间的间隔为 40 像素 */ div{ column-gap:40px; -moz-column-gap:40px; /* Firefox */ -webkit-column-gap:40px; /* Safari 和 Chrome */ }
CSS3 列规则
column-rule
属性设置列之间的宽度、样式和颜色规则。
语法: column-rule : column-rule-width | column-rule-style | column-rule-color
div{ column-rule:3px outset #ff0000; -moz-column-rule:3px outset #ff0000; /* Firefox */ -webkit-column-rule:3px outset #ff0000; /* Safari and Chrome */ }
属性 | 描述 |
---|---|
column-count | 规定元素应该被分隔的列数。 |
column-fill | 规定如何填充列。 |
column-gap | 规定列之间的间隔。 |
column-rule | 设置所有 column-rule-* 属性的简写属性。 |
column-rule-width | 规定列之间规则的宽度。 |
column-rule-style | 规定列之间规则的样式。 |
column-rule-color | 规定列之间规则的颜色。 |
column-span | 规定元素应该横跨的列数。 |
column-width | 规定列的宽度。 |
columns | 语法 : column-width column-count。 |
- CSS3 用户界面
CSS3 resize
在 CSS3中resize
属性设置是否可由用户调整元素尺寸。
/* 设置div可以由用户调整大小 */ div{ resize:both; overflow:auto; }
CSS3 box-sizing
box-sizing
属性允许您以确切的方式定义适应某个区域的具体内容。边框计算在width中
/* 规定两个并排的带边框方框 */ div{ box-sizing:border-box; -moz-box-sizing:border-box; /* Firefox */ -webkit-box-sizing:border-box; /* Safari */ width:50%; float:left; }
CSS3 outline-offset
outline-offset
属性对轮廓进行偏移,并在超出边框边缘的位置绘制轮廓。
轮廓与边框有两点不同:
- 轮廓不占用空间;
- 轮廓可能是非矩形;
/* 规定边框边缘之外 15 像素处的轮廓 */ div{ border:2px solid black; outline:2px solid red; outline-offset:15px; }
12.6 css:inline-block 的 div 之间的空隙,原因及解决
参考答案:
display:inline-block布局的元素在chrome下会出现几像素的间隙,原因是因为我们在编辑器里写代码的时候,同级别的标签不写在同一 行以保持代码的整齐可读性,即inline-block布局的元素在编辑器里不在同一行,即存在换行符,因此这就是著名的inline-block“换行 符/空格间隙问题”。如果inline-block元素间有空格或是换行产生了间隙,那是正常的,应该的。如果没有空格与间隙才是不正常的(IE6/7 block水平元素)。
解决方法:
1、把img标签的display属性改成block:
img{dispaly:block;}
2、把div中的字体大小设为0:
div{font-size:0;}
3、如果是img,修改img的vertical-align属性:
img{vertical-align:buttom;} img{vertical-align:middle;} img{vertical-align:top;}
- 移除标签间的空格
<ul> <li>这是一个li</li><li>这是另一个li</li><li>这是另另一个li</li><li>这是另另另一个li</li> </ul> // 方式二:在标签结束处消除换行符 <ul> <li>这是一个li </li><li>这是另一个li </li><li>这是另另一个li </li><li>这是另另另一个li</li> </ul> // 方式三:HTML注释标签 <ul> <li>这是一个li</li><!-- --><li>这是另一个li</li><!-- --><li>这是另另一个li</li><!-- --><li>这是另另另一个li</li> </ul>
webAPI
13.1 window.open
参考答案:
这个方法是用来打开新窗口的
最基本的弹出窗口
window.open('page.html');
经过设置后的弹出窗口
window.open('page.html', 'newwindow', 'height=100, width=400, top=0, left=0, toolbar=no, menubar=no, scrollbars=no, resizable=no, location=no, status=no') //该句写成一行代码 //参数解释: // window.open 弹出新窗口的命令; //'page.html' 弹出窗口的文件名; //'newwindow' 弹出窗口的名字(不是文件名),非必须,可用空''代替; //height=100 窗口高度; //width=400 窗口宽度; //top=0 窗口距离屏幕上方的象素值; //left=0 窗口距离屏幕左侧的象素值; //toolbar=no 是否显示工具栏,yes为显示; //menubar,scrollbars 表示菜单栏和滚动栏。 //resizable=no 是否允许改变窗口大小,yes为允许; //location=no 是否显示地址栏,yes为允许; //status=no 是否显示状态栏内的信息(通常是文件已经打开),yes为允许
用函数控制弹出窗口
<html> <head> <script LANGUAGE="JavaScript"> <!-- function openwin() { window.open ("page.html", "newwindow", "height=100, width=400, toolbar =no, menubar=no, scrollbars=no, resizable=no, location=no, status=no") //写成一行 } //--> </script> </head> <body οnlοad="openwin()"> 任意的页面内容... </body> </html>
解释:这里定义了一个函数openwin(), 函数内容就是打开一个窗口。在调用它之前没有任何用途。怎么调用呢? 方法一:<body οnlοad="openwin()"> 浏览器读页面时弹出窗口; 方法二:<body οnunlοad="openwin()"> 浏览器离开页面时弹出窗口; 方法三:用一个连接调用: <a href="#" οnclick="openwin()"> 打开一个窗口 注意:使用的"#"是虚连接。 方法四:用一个按扭调用: <input type="button" οnclick="openwin()" value="打开窗口" />
弹出两个窗口
<script LANGUAGE="JavaScript"> <!-- function openwin() { window.open ("page.html", "newwindow", "height=100, width=100, top=0, left=0,toolbar=no, menubar=no, scrollbars=no, resizable=no, location=n o, status=no")//写成一行 window.open ("page2.html", "newwindow2", "height=100, width=100, top=100, left=100,toolbar=no, menubar=no, scrollbars=no, resizable=no, loca tion=no, status=no")//写成一行 } //--> </script>
为避免弹出的2个窗口覆盖,用top和left控制一下弹出的位置不要相互覆盖即可。最后用上面的说过的四种方法调用即可。 注意:2个窗口的name(newwindow与 newwindow2)不要相同,或者干脆全部为空。
主窗口打开文件1.htm,同时弹出小窗口page.html
function openwin(){ window.open("page.html","","width=200,height=200") }
//加入body区: <a href="1.htm" οnclick="openwin()">open</a>即可。
弹出的窗口之定时关闭控制
将一小段代码加入弹出的页面(注意是加入page.html的HTML中,可不是主页面中,否则......),让它在10秒后自动关闭
function closeit(){ setTimeout("selft.close()", 10000) //毫秒 }
<body οnlοad="closeit()">
在弹出窗口中加上一个关闭按扭
<input type="button" value="关闭" οnclick="window.close()">
内包含的弹出窗口---一个页面两个窗口
上面的例子都包含两个窗口,一个是主窗口,另一个是弹出的小窗口。通过下面的例子,你可以在一个页面内完成上面的效果
<html> <head> <SCRIPT LANGUAGE="JavaScript"> function openwin() { OpenWindow=window.open("", "newwin", "height=250, width=250,toolbar=no ,scrollbars="+scroll+",menubar=no"); //写成一行 OpenWindow.document.write("<TITLE>例子</TITLE>") OpenWindow.document.write("<BODY BGCOLOR=#ffffff>") OpenWindow.document.write("<h1>Hello!</h1>") OpenWindow.document.write("New window opened!") OpenWindow.document.write("</BODY>") OpenWindow.document.write("</HTML>") OpenWindow.document.close() } </SCRIPT> </head> <body> <a href="#" οnclick="openwin()">打开一个窗口</a> <input type="button" οnclick="openwin()" value="打开窗口"> </body> </html>
终极应用---弹出的窗口这Cookie控制
function openwin(){ window.open("page.html","","width=200,height=200") } function get_cookie(Name){ var search=Name+"="; var returnvalue=""; if(document.cookie.length>0){ if (offset != -1) { offset += search.length end = document.cookie.indexOf(";", offset); if (end == -1) end = document.cookie.length; returnvalue=unescape(document.cookie.substring(offset, end)); } } return returnvalue; } function ladpopup(){ if(get_cookie('popped=yes')){ openwin() document.cookie="popped=yes"; } }
<body οnlοad="loadpopup()">
13.2 实现 Eventemitter 类,有on、emit、off 方法
参考答案:
- on(event,fn):监听event事件,事件触发时调用fn函数;
- once(event,fn):为指定事件注册一个单次监听器,单次监听器最多只触发一次,触发后立即解除监听器;
- emit(event,arg1,arg2,arg3...):触发event事件,并把参数arg1,arg2,arg3....传给事件处理函数;
- off(event,fn):停止监听某个事件
class EventEmitter{ constructor(){ this._envents = {} } on(event,callback){ //监听event事件,触发时调用callback函数 let callbacks = this._events[event] || [] callbacks.push(callback) this._events[event] = callbacks return this } off(event,callback){ //停止监听event事件 let callbacks = this._events[event] this._events[event] = callbacks && callbacks.filter(fn => fn !== callback) return this } emit(...args){ //触发事件,并把参数传给事件的处理函数 const event = args[0] const params = [].slice.call(args,1) const callbacks = this._events[event] callbacks.forEach(fn => fn.apply(this.params)) return this } once(event,callback){ //为事件注册单次监听器 let wrapFanc = (...args) => { callback.apply(this.args) this.off(event,wrapFanc) } this.on(event,wrapFanc) return this } }
13.3 查找给定的两个节点的第一个公共父节点
参考答案:
解题思路
递归循环树的节点,因二叉树不能重复的特性,当前节点为 p or q ,返回当前节点 父节点循环中,如果找到一个,则查找其他子树 其他子树没有找到另外一个,就证明当前节点为找到的子树是最近公共祖先 两个都找到了,对应当前节点是两个节点的父节点这种情况,则返回当前节点。 代码
var lowestCommonAncestor = function(root, p, q) { if (!root || root === p || root === q) return root let left = lowestCommonAncestor(root.left, p, q) let right = lowestCommonAncestor(root.right, p, q) if (!left) return right if (!right) return left return root };