强类型语言和弱类型语言的区别
- 强类型语言:强类型语言也称为强类型定义语言,是一种总是强制类型定义的语言,要求变量的使用要严格符合定义,所有变量都必须先定义后使用。Java和C++等语言都是强制类型定义的,也就是说,一旦一个变量被指定了某个数据类型,如果不经过强制转换,那么它就永远是这个数据类型了。例如你有一个整数,如果不显式地进行转换,你不能将其视为一个字符串。
- 弱类型语言:弱类型语言也称为弱类型定义语言,与强类型定义相反。JavaScript语言就属于弱类型语言。简单理解就是一种变量类型可以被忽略的语言。比如JavaScript是弱类型定义的,在JavaScript中就可以将字符串'12'和整数3进行连接得到字符串'123',在相加的时候会进行强制类型转换。
两者对比:强类型语言在速度上可能略逊色于弱类型语言,但是强类型语言带来的严谨性可以有效地帮助避免许多错误。
浏览器的主要组成部分
- ⽤户界⾯ 包括地址栏、前进/后退按钮、书签菜单等。除了浏览器主窗⼝显示的您请求的⻚⾯外,其他显示的各个部分都属于⽤户界⾯。
- 浏览器引擎 在⽤户界⾯和呈现引擎之间传送指令。
- 呈现引擎 负责显示请求的内容。如果请求的内容是 HTML,它就负责解析 HTML 和 CSS 内容,并将解析后的内容显示在屏幕上。
- ⽹络 ⽤于⽹络调⽤,⽐如 HTTP 请求。其接⼝与平台⽆关,并为所有平台提供底层实现。
- ⽤户界⾯后端 ⽤于绘制基本的窗⼝⼩部件,⽐如组合框和窗⼝。其公开了与平台⽆关的通⽤接⼝,⽽在底层使⽤操作系统的⽤户界⾯⽅法。
- JavaScript 解释器。⽤于解析和执⾏ JavaScript 代码。
- 数据存储 这是持久层。浏览器需要在硬盘上保存各种数据,例如 Cookie。新的 HTML 规范 (HTML5) 定义了“⽹络数据库”,这是⼀个完整(但是轻便)的浏览器内数据库。
值得注意的是,和⼤多数浏览器不同,Chrome 浏览器的每个标签⻚都分别对应⼀个呈现引擎实例。每个标签⻚都是⼀个独⽴的进程。
进程之前的通信方式
(1)管道通信
管道是一种最基本的进程间通信机制。管道就是操作系统在内核中开辟的一段缓冲区,进程1可以将需要交互的数据拷贝到这段缓冲区,进程2就可以读取了。
管道的特点:
- 只能单向通信
- 只能血缘关系的进程进行通信
- 依赖于文件系统
- 生命周期随进程
- 面向字节流的服务
- 管道内部提供了同步机制
(2)消息队列通信
消息队列就是一个消息的列表。用户可以在消息队列中添加消息、读取消息等。消息队列提供了一种从一个进程向另一个进程发送一个数据块的方法。 每个数据块都被认为含有一个类型,接收进程可以独立地接收含有不同类型的数据结构。可以通过发送消息来避免命名管道的同步和阻塞问题。但是消息队列与命名管道一样,每个数据块都有一个最大长度的限制。
使用消息队列进行进程间通信,可能会收到数据块最大长度的限制约束等,这也是这种通信方式的缺点。如果频繁的发生进程间的通信行为,那么进程需要频繁地读取队列中的数据到内存,相当于间接地从一个进程拷贝到另一个进程,这需要花费时间。
(3)信号量通信
共享内存最大的问题就是多进程竞争内存的问题,就像类似于线程安全问题。我们可以使用信号量来解决这个问题。信号量的本质就是一个计数器,用来实现进程之间的互斥与同步。例如信号量的初始值是 1,然后 a 进程来访问内存1的时候,我们就把信号量的值设为 0,然后进程b 也要来访问内存1的时候,看到信号量的值为 0 就知道已经有进程在访问内存1了,这个时候进程 b 就会访问不了内存1。所以说,信号量也是进程之间的一种通信方式。
(4)信号通信
信号(Signals )是Unix系统中使用的最古老的进程间通信的方法之一。操作系统通过信号来通知进程系统中发生了某种预先规定好的事件(一组事件中的一个),它也是用户进程之间通信和同步的一种原始机制。
(5)共享内存通信
共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问(使多个进程可以访问同一块内存空间)。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。
(6)套接字通信
上面说的共享内存、管道、信号量、消息队列,他们都是多个进程在一台主机之间的通信,那两个相隔几千里的进程能够进行通信吗?答是必须的,这个时候 Socket 这家伙就派上用场了,例如我们平时通过浏览器发起一个 http 请求,然后服务器给你返回对应的数据,这种就是采用 Socket 的通信方式了。
如何防御 CSRF 攻击?
CSRF 攻击可以使用以下方法来防护:
- 进行同源检测,服务器根据 http 请求头中 origin 或者 referer 信息来判断请求是否为允许访问的站点,从而对请求进行过滤。当 origin 或者 referer 信息都不存在的时候,直接阻止请求。这种方式的缺点是有些情况下 referer 可以被伪造,同时还会把搜索引擎的链接也给屏蔽了。所以一般网站会允许搜索引擎的页面请求,但是相应的页面请求这种请求方式也可能被攻击者给利用。(Referer 字段会告诉服务器该网页是从哪个页面链接过来的)
- 使用 CSRF Token 进行验证,服务器向用户返回一个随机数 Token ,当网站再次发起请求时,在请求参数中加入服务器端返回的 token ,然后服务器对这个 token 进行验证。这种方法解决了使用 cookie 单一验证方式时,可能会被冒用的问题,但是这种方法存在一个缺点就是,我们需要给网站中的所有请求都添加上这个 token,操作比较繁琐。还有一个问题是一般不会只有一台网站服务器,如果请求经过负载平衡转移到了其他的服务器,但是这个服务器的 session 中没有保留这个 token 的话,就没有办法验证了。这种情况可以通过改变 token 的构建方式来解决。
- 对 Cookie 进行双重验证,服务器在用户访问网站页面时,向请求域名注入一个Cookie,内容为随机字符串,然后当用户再次向服务器发送请求的时候,从 cookie 中取出这个字符串,添加到 URL 参数中,然后服务器通过对 cookie 中的数据和参数中的数据进行比较,来进行验证。使用这种方式是利用了攻击者只能利用 cookie,但是不能访问获取 cookie 的特点。并且这种方法比 CSRF Token 的方法更加方便,并且不涉及到分布式访问的问题。这种方法的缺点是如果网站存在 XSS 漏洞的,那么这种方式会失效。同时这种方式不能做到子域名的隔离。
- 在设置 cookie 属性的时候设置 Samesite ,限制 cookie 不能作为被第三方使用,从而可以避免被攻击者利用。Samesite 一共有两种模式,一种是严格模式,在严格模式下 cookie 在任何情况下都不可能作为第三方 Cookie 使用,在宽松模式下,cookie 可以被请求是 GET 请求,且会发生页面跳转的请求所使用。
参考:前端进阶面试题详细解答
webpack配置入口出口
<code style="margin-left:0">module.exports={ //入口文件的配置项 entry:{}, //出口文件的配置项 output:{}, //模块:例如解读CSS,图片如何转换,压缩 module:{}, //插件,用于生产模版和各项功能 plugins:[], //配置webpack开发服务功能 devServer:{} } 简单描述了一下这几个属性是干什么的。 描述一下npm run dev / npm run build执行的是哪些文件 通过配置proxyTable来达到开发环境跨域的问题,然后又可以扩展和他聊聊跨域的产生,如何跨域 最后可以在聊聊webpack的优化,例如babel-loader的优化,gzip压缩等等</code>
深/浅拷贝
首先判断数据类型是否为对象,如果是对象(数组|对象),则递归(深/浅拷贝),否则直接拷贝。
<code style="margin-left:0">function isObject(obj) { return typeof obj === "object" && obj !== null; }</code>
这个函数只能判断 obj
是否是对象,无法判断其具体是数组还是对象。
localStorage sessionStorage cookies 有什么区别?
<code style="margin-left:0">localStorage:以键值对的方式存储 储存时间没有限制 永久生效 除非自己删除记录 sessionStorage:当页面关闭后被清理与其他相比不能同源窗口共享 是会话级别的存储方式 cookies 数据不能超过4k 同时因为每次http请求都会携带cookie 所有cookie只适合保存很小的数据 如会话标识</code>
AJAX
<code style="margin-left:0">const getJSON = function(url) { return new Promise((resolve, reject) => { const xhr = XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); xhr.open('GET', url, false); xhr.setRequestHeader('Accept', 'application/json'); xhr.onreadystatechange = function() { if (xhr.readyState !== 4) return; if (xhr.status === 200 || xhr.status === 304) { resolve(xhr.responseText); } else { reject(new Error(xhr.responseText)); } } xhr.send(); }) }</code>
实现数组原型方法
forEach
<code style="margin-left:0">Array.prototype.forEach2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) // this 就是当前的数组 const len = O.length >>> 0 // 后面有解释 let k = 0 while (k < len) { if (k in O) { callback.call(thisArg, O[k], k, O); } k++; } }</code>
O.length >>> 0 是什么操作?就是无符号右移 0 位,那有什么意义嘛?就是为了保证转换后的值为正整数。其实底层做了 2 层转换,第一是非 number 转成 number 类型,第二是将 number 转成 Uint32 类型
map
基于 forEach 的实现能够很容易写出 map 的实现:
<code style="margin-left:0">- Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.map2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 - let k = 0 + let k = 0, res = [] while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + res[k] = callback.call(thisArg, O[k], k, O); } k++; } + return res }</code>
filter
同样,基于 forEach 的实现能够很容易写出 filter 的实现:
<code style="margin-left:0">- Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.filter2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 - let k = 0 + let k = 0, res = [] while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + if (callback.call(thisArg, O[k], k, O)) { + res.push(O[k]) + } } k++; } + return res }</code>
some
同样,基于 forEach 的实现能够很容易写出 some 的实现:
<code style="margin-left:0">- Array.prototype.forEach2 = function(callback, thisArg) { + Array.prototype.some2 = function(callback, thisArg) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 let k = 0 while (k < len) { if (k in O) { - callback.call(thisArg, O[k], k, O); + if (callback.call(thisArg, O[k], k, O)) { + return true + } } k++; } + return false }</code>
reduce
<code style="margin-left:0">Array.prototype.reduce2 = function(callback, initialValue) { if (this == null) { throw new TypeError('this is null or not defined') } if (typeof callback !== "function") { throw new TypeError(callback + ' is not a function') } const O = Object(this) const len = O.length >>> 0 let k = 0, acc if (arguments.length > 1) { acc = initialValue } else { // 没传入初始值的时候,取数组中第一个非 empty 的值为初始值 while (k < len && !(k in O)) { k++ } if (k > len) { throw new TypeError( 'Reduce of empty array with no initial value' ); } acc = O[k++] } while (k < len) { if (k in O) { acc = callback(acc, O[k], k, O) } k++ } return acc }</code>
哪些情况会导致内存泄漏
<code style="margin-left:0">1、意外的全局变量:由于使用未声明的变量,而意外的创建了一个全局变量,而使这个变量一直留在内存中无法被回收 2、被遗忘的计时器或回调函数:设置了 setInterval 定时器,而忘记取消它,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。 3、脱离 DOM 的引用:获取一个 DOM 元素的引用,而后面这个元素被删除,由于一直保留了对这个元素的引用,所以它也无法被回收。 4、闭包:不合理的使用闭包,从而导致某些变量一直被留在内存当中。</code>
CDN的使用场景
- 使用第三方的CDN服务:如果想要开源一些项目,可以使用第三方的CDN服务
- 使用CDN进行静态资源的缓存:将自己网站的静态资源放在CDN上,比如js、css、图片等。可以将整个项目放在CDN上,完成一键部署。
- 直播传送:直播本质上是使用流媒体进行传送,CDN也是支持流媒体传送的,所以直播完全可以使用CDN来提高访问速度。CDN在处理流媒体的时候与处理普通静态文件有所不同,普通文件如果在边缘节点没有找到的话,就会去上一层接着寻找,但是流媒体本身数据量就非常大,如果使用回源的方式,必然会带来性能问题,所以流媒体一般采用的都是主动推送的方式来进行。
代码输出结果
<code style="margin-left:0">(function(){ var x = y = 1; })(); var z; console.log(y); // 1 console.log(z); // undefined console.log(x); // Uncaught ReferenceError: x is not defined</code>
这段代码的关键在于:var x = y = 1; 实际上这里是从右往左执行的,首先执行y = 1, 因为y没有使用var声明,所以它是一个全局变量,然后第二步是将y赋值给x,讲一个全局变量赋值给了一个局部变量,最终,x是一个局部变量,y是一个全局变量,所以打印x是报错。
代码输出结果
<code style="margin-left:0">function a() { var temp = 10; function b() { console.log(temp); // 10 } b(); } a(); function a() { var temp = 10; b(); } function b() { console.log(temp); // 报错 Uncaught ReferenceError: temp is not defined } a();</code>
在上面的两段代码中,第一段是可以正常输出,这个应该没啥问题,关键在于第二段代码,它会报错Uncaught ReferenceError: temp is not defined。这时因为在b方法执行时,temp 的值为undefined。
Promise.all
描述:所有 promise
的状态都变成 fulfilled
,就会返回一个状态为 fulfilled
的数组(所有promise
的 value
)。只要有一个失败,就返回第一个状态为 rejected
的 promise
实例的 reason
。
实现:
<code style="margin-left:0">Promise.all = function(promises) { return new Promise((resolve, reject) => { if(Array.isArray(promises)) { if(promises.length === 0) return resolve(promises); let result = []; let count = 0; promises.forEach((item, index) => { Promise.resolve(item).then( value => { count++; result[index] = value; if(count === promises.length) resolve(result); }, reason => reject(reason) ); }) } else return reject(new TypeError("Argument is not iterable")); }); }</code>
说一下怎么取出数组最多的一项?
<code style="margin-left:0">// 我这里只是一个示例 const d = {}; let ary = ['赵', '钱', '孙', '孙', '李', '周', '李', '周', '周', '李']; ary.forEach(k => !d[k] ? d[k] = 1 : d[k]++); const result = Object.keys(d).sort((a, b) => d[b] - d[a]).filter((k, i, l) => d[k] === d[l[0]]); console.log(result)</code>
代码输出问题
<code style="margin-left:0">function Parent() { this.a = 1; this.b = [1, 2, this.a]; this.c = { demo: 5 }; this.show = function () { console.log(this.a , this.b , this.c.demo ); } } function Child() { this.a = 2; this.change = function () { this.b.push(this.a); this.a = this.b.length; this.c.demo = this.a++; } } Child.prototype = new Parent(); var parent = new Parent(); var child1 = new Child(); var child2 = new Child(); child1.a = 11; child2.a = 12; parent.show(); child1.show(); child2.show(); child1.change(); child2.change(); parent.show(); child1.show(); child2.show();</code>
输出结果:
<code style="margin-left:0">parent.show(); // 1 [1,2,1] 5 child1.show(); // 11 [1,2,1] 5 child2.show(); // 12 [1,2,1] 5 parent.show(); // 1 [1,2,1] 5 child1.show(); // 5 [1,2,1,11,12] 5 child2.show(); // 6 [1,2,1,11,12] 5</code>
这道题目值得神帝,他涉及到的知识点很多,例如this的指向、原型、原型链、类的继承、数据类型等。
解析:
- parent.show(),可以直接获得所需的值,没啥好说的;
- child1.show(),
Child
的构造函数原本是指向Child
的,题目显式将Child
类的原型对象指向了Parent
类的一个实例,需要注意Child.prototype
指向的是Parent
的实例parent
,而不是指向Parent
这个类。 - child2.show(),这个也没啥好说的;
- parent.show(),
parent
是一个Parent
类的实例,Child.prorotype
指向的是Parent
类的另一个实例,两者在堆内存中互不影响,所以上述操作不影响parent
实例,所以输出结果不变; - child1.show(),
child1
执行了change()
方法后,发生了怎样的变化呢? - this.b.push(this.a),由于this的动态指向特性,this.b会指向
Child.prototype
上的b数组,this.a会指向child1
的a属性,所以Child.prototype.b
变成了1,2,1,11; - this.a = this.b.length,这条语句中
this.a
和this.b
的指向与上一句一致,故结果为child1.a
变为4; - this.c.demo = this.a++,由于
child1
自身属性并没有c这个属性,所以此处的this.c
会指向Child.prototype.c
,this.a
值为4,为原始类型,故赋值操作时会直接赋值,Child.prototype.c.demo
的结果为4,而this.a
随后自增为5(4 + 1 = 5)。 child2
执行了change()
方法, 而child2
和child1
均是Child
类的实例,所以他们的原型链指向同一个原型对象Child.prototype
,也就是同一个parent
实例,所以child2.change()
中所有影响到原型对象的语句都会影响child1
的最终输出结果。- this.b.push(this.a),由于this的动态指向特性,this.b会指向
Child.prototype
上的b数组,this.a会指向child2
的a属性,所以Child.prototype.b
变成了1,2,1,11,12; - this.a = this.b.length,这条语句中
this.a
和this.b
的指向与上一句一致,故结果为child2.a
变为5; - this.c.demo = this.a++,由于
child2
自身属性并没有c这个属性,所以此处的this.c
会指向Child.prototype.c
,故执行结果为Child.prototype.c.demo
的值变为child2.a
的值5,而child2.a
最终自增为6(5 + 1 = 6)。
Vue的父子组件生命周期钩子函数执行顺序?
<code style="margin-left:0"><!-- 加载渲染过程 --> <!-- 父beforeCreate -> 父created -> 父beforeMount -> 子beforeCreate -> 子created -> 子beforeMount -> 子mounted -> 父mounted --> <!-- 子组件更新过程 --> <!-- 父beforeUpdate -> 子beforeUpdate -> 子updaed -> 父updated --> <!-- 父组件跟新过程 --> <!-- 父beforeUpdate -> 父updated --> <!-- 销毁过程 --> <!-- 父beforeDestroy -> 子beforeDestroy -> 子destroyed ->父destroyed --></code>
说一下slice splice split 的区别?
<code style="margin-left:0">// slice(start,[end]) // slice(start,[end])方法:该方法是对数组进行部分截取,该方法返回一个新数组 // 参数start是截取的开始数组索引,end参数等于你要取的最后一个字符的位置值加上1(可选)。 // 包含了源函数从start到 end 所指定的元素,但是不包括end元素,比如a.slice(0,3); // 如果出现负数就把负数与长度相加后再划分。 // slice中的负数的绝对值若大于数组长度就会显示所有数组 // 若参数只有一个,并且参数大于length,则为空。 // 如果结束位置小于起始位置,则返回空数组 // 返回的个数是end-start的个数 // 不会改变原数组 var arr = [1,2,3,4,5,6] /*console.log(arr.slice(3))//[4,5,6] 从下标为0的到3,截取3之后的数console.log(arr.slice(0,3))//[1,2,3] 从下标为0的地方截取到下标为3之前的数console.log(arr.slice(0,-2))//[1,2,3,4]console.log(arr.slice(-4,4))//[3,4]console.log(arr.slice(-7))//[1,2,3,4,5,6]console.log(arr.slice(-3,-3))// []console.log(arr.slice(8))//[]*/ // 个人总结:slice的参数如果是正数就从左往右数,如果是负数的话就从右往左边数, // 截取的数组与数的方向一致,如果是2个参数则截取的是数的交集,没有交集则返回空数组 // ps:slice也可以切割字符串,用法和数组一样,但要注意空格也算字符 // splice(start,deletecount,item) // start:起始位置 // deletecount:删除位数 // item:替换的item // 返回值为被删除的字符串 // 如果有额外的参数,那么item会插入到被移除元素的位置上。 // splice:移除,splice方法从array中移除一个或多个数组,并用新的item替换它们。 //举一个简单的例子 var a=['a','b','c']; var b=a.splice(1,1,'e','f'); console.log(a) //['a', 'e', 'f', 'c'] console.log(b) //['b'] var a = [1, 2, 3, 4, 5, 6]; //console.log("被删除的为:",a.splice(1, 1, 8, 9)); //被删除的为:2 // console.log("a数组元素:",a); //1,8,9,3,4,5,6 // console.log("被删除的为:", a.splice(0, 2)); //被删除的为:1,2 // console.log("a数组元素:", a) //3,4,5,6 console.log("被删除的为:", a.splice(1, 0, 2, 2)) //插入 第二个数为0,表示删除0个 console.log("a数组元素:", a) //1,2,2,2,3,4,5,6 // split(字符串) // string.split(separator,limit):split方法把这个string分割成片段来创建一个字符串数组。 // 可选参数limit可以限制被分割的片段数量。 // separator参数可以是一个字符串或一个正则表达式。 // 如果separator是一个空字符,会返回一个单字符的数组,不会改变原数组。 var a="0123456"; var b=a.split("",3); console.log(b);//b=["0","1","2"] // 注意:String.split() 执行的操作与 Array.join 执行的操作是相反的。</code>
代码输出结果
<code style="margin-left:0">function Dog() { this.name = 'puppy' } Dog.prototype.bark = () => { console.log('woof!woof!') } const dog = new Dog() console.log(Dog.prototype.constructor === Dog && dog.constructor === Dog && dog instanceof Dog)</code>
输出结果:true
解析: 因为constructor是prototype上的属性,所以dog.constructor实际上就是指向Dog.prototype.constructor;constructor属性指向构造函数。instanceof而实际检测的是类型是否在实例的原型链上。
constructor是prototype上的属性,这一点很容易被忽略掉。constructor和instanceof 的作用是不同的,感性地来说,constructor的限制比较严格,它只能严格对比对象的构造函数是不是指定的值;而instanceof比较松散,只要检测的类型在原型链上,就会返回true。
ES6 之前使用 prototype 实现继承
Object.create() 会创建一个 “新” 对象,然后将此对象内部的 [Prototype] 关联到你指定的对象(Foo.prototype)。Object.create(null) 创建一个空 [Prototype] 链接的对象,这个对象无法进行委托。
<code style="margin-left:0">function Foo(name) { this.name = name; } Foo.prototype.myName = function () { return this.name; } // 继承属性,通过借用构造函数调用 function Bar(name, label) { Foo.call(this, name); this.label = label; } // 继承方法,创建备份 Bar.prototype = Object.create(Foo.prototype); // 必须设置回正确的构造函数,要不然在会发生判断类型出错 Bar.prototype.constructor = Bar; // 必须在上一步之后 Bar.prototype.myLabel = function () { return this.label; } var a = new Bar("a", "obj a"); a.myName(); // "a" a.myLabel(); // "obj a"</code>
常见的水平垂直方式有几种?
<code style="margin-left:0">//利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 translate 来调整元素的中心点到页面的中心。该方法需要考虑浏览器兼容问题。 .parent { position: relative; } .child { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); } //利用绝对定位,设置四个方向的值都为 0,并将 margin 设置为 auto,由于宽高固定,因此对应方向实现平分,可以实现水平和垂直方向上的居中。该方法适用于盒子有宽高的情况: .parent { position: relative; } .child { position: absolute; top: 0; bottom: 0; left: 0; right: 0; margin: auto; } //利用绝对定位,先将元素的左上角通过 top:50%和 left:50%定位到页面的中心,然后再通过 margin 负值来调整元素的中心点到页面的中心。该方法适用于盒子宽高已知的情况 .parent { position: relative; } .child { position: absolute; top: 50%; left: 50%; margin-top: -50px; /* 自身 height 的一半 */ margin-left: -50px; /* 自身 width 的一半 */ } //使用 flex 布局,通过 align-items:center 和 justify-content:center 设置容器的垂直和水平方向上为居中对齐,然后它的子元素也可以实现垂直和水平的居中。该方法要**考虑兼容的问题**,该方法在移动端用的较多: .parent { display: flex; justify-content:center; align-items:center; } //另外,如果父元素设置了flex布局,只需要给子元素加上`margin:auto;`就可以实现垂直居中布局 .parent{ display:flex; } .child{ margin: auto; }</code>
未经允许不得转载:木盒主机 » 拿到大厂前端offer的前端开发是怎么回答面试题的