94.link和@import的区别是什么?
答:
-
link标签在HTML文档的
<head>
部分使用,页面加载时同时加载外部CSS样式表,几乎可以兼容所有游览器,该标签引入的具有较高优先级,可以覆盖其他样式表。 -
@import规则在CSS文件内部使用,页面加载完毕后才加载外部CSS样式表,优先级低。
95.transition和animation的区别?
答:
- transition是过度属性,他的实现需要触发一个事件(比如鼠标移动上去,焦点,点击等)才执行动画。
- animation是动画属性,它的实现不需要触发事件,设定好时间之后可以自己执行,且可以循环一个动画。
96.display:none与visibility:hindden的区别?
答“”这两个属性都是让元素隐藏,不可见,区别如下:
- display:none会让元素完全从渲染树中消失,渲染时不会占据任何空间,且display:none是非继承属性,子孙节点会随着父节点从渲染树消失,通过修改子孙节点属性也无法显示。修改display会导致文档重排。
- visibility:hidden不会让元素从渲染树中消失,渲染的元素还会占据相应的空间,只是内容不可见。且是继承属性,可以通过修改子孙的visibility:visible让子孙显示出来。修改visibility属性会导致元素的重绘。
97.伪类和伪元素的区别和作用是什么?
答:
- 伪元素是在内容元素的前后插入额外的元素或样式,但是这些元素实际上并不在文档中生成。
p::before {content:"第一章:";}
p::after {content:"Hot!";}
p::first-line {background:red;}
p::first-letter {font-size:30px;}
- 伪类是将特殊的效果添加到特定的选择器上,它是改变已有元素的状态,不会产生新的元素。
a:hover {color: #FF00FF}
p:first-child {color: red}
98.对requestAnimationframe的理解?
答:window.requestAnimationFrame()告诉浏览器,你希望执行一个动画,并且要求游览器在下次重绘之前调用指定的回调函数更新动画,该方法需要传入一个回调函数作为参数,该回调函数会在游览器下一次重绘之前执行。requestAnimationFrame()在页面处于不可见状态时,该页面的屏幕刷新任务也会被系统暂停,有效节省cpu开销。且requestAnimationFrame会把每一帧中所有DOM操作集中起来,在一次重绘或回流中完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率,一般来说,这个频率为每秒60帧。
99.对盒模型的理解
- 标准盒模型(默认值)的width和height属性的范围只包含了content。设置
box-sizeing:content-box
- IE盒模型(怪异盒模型)的width和height属性的范围包含了border、padding和content。设置
box-sizeing:border-box
100.为什么有时候用tanslate来改变位置而不是定位?
答: 改变元素的tanslate不会触发重排或重绘,只会触发复合,使用元素改变元素定位的方式可能会触发重排,所以使用tanslate改变位置更加顺滑。
101.li与li之间有看不见的空白间隔是什么原因引起的?如何解决?
答:浏览器会把inline内联元素间空白字符(空格、换行、Tab等)渲染成一个空格,为了美观,通常是一个<li>
放在一行,这导致<li>
换行后产生换行字符,它就会变成一个空格,占用了一个字符的宽度。
解决办法:
- 为
<li>
设置float:left - 将所有
<li>
写在同一行 - 消除
<ul>
的字符间隔letter-spacing: -8px
102.CSS3中有哪些新特性?
答:
- 新增各种CSS选择器(:not(.input): 所有class不是"input"的结点)
- 圆角(border-radius:8px)
- 多列布局(multi-column layout)
- 阴影和反射(Shadow/reflection)
- 文字特效(text-shadow)
- 文字渲染(text-decoration)
- 线性渐变(gradient)
- 旋转(transform)
- 增加了旋转,缩放,定位,倾斜,动画,多背景
103.对CSS Sprites的理解?
答: CSS Sprites是一种优化网页加载速度和降低服务器负载的技术,它通过将多个小的图像合并成一张大的图像,然后使用CSS来定义每个小图像在页面中的位置和尺寸,这样一来,只需要加载一张大图像,而不是多个小图像,从而减少了HTTP请求的数量,提高了页面加载速度。
104.什么是物理像素、逻辑像素和像素密度?
答:
- 物理像素(Physical Pixel)是指显示设备的最小可见单元,它构成显示屏的物理元素。
- 逻辑像素(Logical Pixel)是指网页或应用程序中使用的抽象像素单位。它不直接映射到物理像素,而根据设备的屏幕密度和缩放比例进行转换。
- 像素密度(Pixel Density)是指在给定物理尺寸下,显示设备上每厘米的物理像素的数量。
105.margin和padding的使用场景有哪些?
答:
- 需要在border外侧添加空白,且空白处不需要背景色时,使用margin
- 需要在border内侧添加空白,且空白处不需要背景色时,使用padding
106.对line-height的理解及其赋值方式?
答: line-height(行高)是指文字行与行之间的垂直间距,也可以理解为行间距,他决定了文字在垂直方向上的排布方式。 line-height的赋值方式有
- 绝对长度值:line-height:20px
- 百分比: line-height: 120%
- 数字值:1.5
- normal:通常为1.2倍字体大小。
107.CSS优化和提高性能的方法有哪些?
答:
- 减少重绘和重排:避免频繁的修改DOM元素样式和结构,减少游览器的重绘和重排,提高性能。
- 避免使用@import:@import会导致CSS文件的阻塞和延迟加载,可以使用link代替。
- 使用CSS Sprites:将多个小图标合并成一个大的图片,通过background-position来显示不同的图标,减少http请求。
108.display:inline-block什么时候会显示间隙?
答:当使用display:inline-block时,如果在HTML结构中,元素之间存在空格,换行符等,这些空白字符会被解析成空间,导致在元素之间显示间隙。 解决办法:
- 去掉空白字符,将元素紧密放置在一起。
- 使用负的'margin'来消除间隙。
- 将元素的父容器的font-size设置为0;
109.如何写一个单行和多行文本溢出隐藏?
答:
- 单行文本溢出
overflow:hidden;
text-overflow:elliipsis;
white-space:nowrap;
- 多行文本溢出
overflow:hidden;
text-overflow:ellipsis;
display:-webkit-box;
-webkit-box-orient:vertical;
-webkit-line-clamp:3;
使用"-webkit-"前缀可以让开发者针对仅在WebKit浏览器中支持的特定CSS属性进行样式设置,以确保在这些浏览器中正确显示和渲染。
110.Sass、Less是什么?为什么要使用它们?
答:他们都是CSS预处理器,是CSS上的一种抽象层。Sass(Syntactically Awesome Stylesheets)和Less(Leaner Style Sheets)都允许开发者使用类似编程语言的语法来编写CSS样式。它们提供了一些功能和特性,如变量、嵌套规则、函数、混合(Mixins)等,使得CSS代码更易于编写、维护和扩展。
111.对媒体查询的理解?
答:媒体查询是指在Web开发中使用CSS样式表针对不同媒体设备或条件应用不同的样式,通过媒体查询,可以根据设备的屏幕大小、分辨率、浏览器类型来调整页面的布局和样式,以提供更好的用户体验。
<!-- link元素中的CSS媒体查询 -->
<link rel="stylesheet" media="(max-width: 800px)" href="example.css" />
<!-- 样式表中的CSS媒体查询 -->
<style>
@media (max-width: 600px) {
.facet_sidebar {
display: none;
}
}
</style>
112.如何判断元素是否到达可视区域?
- window.innerHeight是浏览器可视区的高度
- document.body.scrollTop || document.documentElement.scrollTop是浏览器滚动过的距离。
- imgs.offsetTop是元素顶部距离文档顶部的高度(包括滚动条的距离) 所以如果满足img.offsetTop < window.innerHeight + documnet.body.scrollTop的话,则表示元素是处于可视区域。
113.z-index属性在什么情况下会失效?
答:通常z-index的使用是在有两个重叠的标签,在一定的情况下控制其中一个在另一个的上方或者下方出现。 z-index值越大,就越在上层。z-index元素的position属性需要是relative、absolute或fixed。 z-index会在以下情况失效:
- 父元素position为relative时,子元素z-index失效。解决:父元素position设置为absolute
- 元素没有设置position属性,解决:设置该元素的position属性为relative、absolute或fixed中一种。
- 元素在设置z-index的同时还设置了float浮动。解决:去掉float。改为display:inline-block.
113.CSS3中的tansform有哪些属性?
答:Css3中的transform有以下几个属性:
- translate:用于平移元素,在水平和垂直方向上移动元素。
- rotate:用于旋转元素。
- scale:用于缩放元素。
- skew:用于倾斜元素。
- matrix:用于同时进行平移、旋转、缩放、和倾斜操作。
114.说一下px、em、rem的区别及使用场景?
答:
- px是固定的像素,一旦设置就无法因为适应页面大小而改变。主要用于只需要适配少量移动设备的场景。
- em是一个相对于父元素的字体的大小单位,主要用于相对于父元素调整尺寸的效果。
- rem是一个相对于页面根元素字体大小的单位,默认是1rem等于16px,主要用于适配分辨率差别较大的设备。
115.手写一下两栏布局的实现?
答: 原理: 一般两栏布局指的是左边一栏宽度固定,右边一栏宽度自适应,利用浮动,将左边元素宽度设置为200px,并且设置向左浮动,将右边元素的margin-left设置为200px(可以保证在同一行),宽度设置为auto(默认为auto,撑满整个父元素)
<style>
.outer {
height: 100px;
}
.left {
float: left;
width: 200px;
background: aqua;
}
.right {
margin-left: 200px;
width: auto;
background: brown;
}
</style>
116.手写一下三栏布局的实现?
答:原理: 利用浮动,左右两栏设置固定大小,并设置对应方向的浮动,中间一栏设置左右两个方向的margin值,注意:中间一栏必须放在最后
<style>
.outer {
height: 200px;
}
.left {
float: left;
width: 200px;
height: 200px;
background: brown;
}
.right {
float: right;
width: 200px;
height: 200px;
background: aqua;
}
.middle {
width: auto;
height: 200px;
margin-left: 200px;
margin-right: 200px;
background: blue;
}
</style>
117.手写一下水平垂直居中的实现?
答:
方法一:利用绝对定位,先将元素的左上角通过top:50%和left:50%定位到页面的中心,然后在通过translate来调整元素中心点到页面中心。
<style>
.parent {
position: relative;
height: 200px;
width: 100px;
background: aqua;
}
.child {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: brown;
}
</style>
方法二:使用flex布局,通过align-items:center
和justify-content:center
设置容器的垂直和水平方向为居中对齐,然后它的子元素也可以实现垂直和水平的居中。
<style>
.child {
display: flex;
height: 200px;
justify-content: center;
align-items: center;
background: aqua;
}
</style>
118.说说对Flex(弹性布局)布局的理解及其使用场景?
答:flex布局是CSS3新增的一种布局方式,可以通过将一个元素的display设置为flex,从而使它成为一个flex容器,它的所有子元素都会成为它的项目。一个容器默认有两条轴:一个是水平的主轴,一个是与主轴垂直的交叉轴,可以使用flex-direction来指定主轴的方向。可以使用justify-content来指定元素在主轴上的排列方式,使用align-items来指定元素在交叉轴上的排列方式,还可以使用flex-wrap来规定当一行排列不下时换行方式。
119.为什么需要清除浮动?清除浮动的方式有哪些?
答:清除浮动的主要原因是解决浮动元素导致父容器塌陷和布局混乱的问题,当一个元素设置了浮动属性后,它会脱离正常的文档流,导致父容器无法正确计算其高度,从而导致父容器塌陷。
- 给父级元素定义一个height属性。
.parent {
background: brown;
height: 300px;
}
.child {
float: right;
background: aqua;
height: 200px;
width: 100px;
}
- 最后一个浮动元素之后添加一个空的div标签,并添加
clear:both
样式
<div style="clear:both;"></div>
- 使用overflow属性清除浮动,将包含浮动元素的父容器设置为'overflow:auto'或'overflow:hidden',使其形成BFC(块级格式上下文),从而清除浮动。
<div style="overflow:auto;">
<!-- 浮动元素 -->
</div>
- 使用伪元素清除浮动:将包含浮动元素的父容器添加一个伪元素,并为伪元素设置样式'clear:both'
.clearfix::after {
content: "";
display: table;
clear: both;
}
120.对BFC的理解,如何创建BFC?
答:
概念: 块格式化上下文(Block Formatting Context,BFC)是一个独立的布局环境,可以理解为一个容器,在这个容器中按照一定规则进行物品的摆放,并且不会影响其他环境中的物品,如果一个元素符合触发BFC的条件,则BFC中的元素布局不受外部影响。
创建BFC的条件:
- 根元素:body
- 元素设置浮动:float除none以外的值
- 元素设置绝对定位:position(absolute、fixed)
- display值为:felx、inline-block、table-cell、table-caption等
- overflow值为:hidden、auto、scroll
BFC的作用:
- 解决margin的重叠问题(css中规定垂直方向上两个相邻元素都有margin的话,取两者中最大的一个margin作为间隙): 由于BFC是一个独立的区域,内部的元素和外部的元素互不影响,将两个元素变成两个BFC,就解决了margin重叠的问题。
- 解决高度塌陷的问题: 在对子元素设置浮动后,父元素会发生高度塌陷,也就是父元素的高度变成0,解决这个问题,只需要把父元素变成一个BFC,常用的方法是给父元素设置overflow:hidden
- 创建自适应两栏布局: 可以用来创建自适应两栏布局,左边的宽度固定,右边的宽度自适应。
.left{
width: 100px;
height: 200px;
background: red;
float: left;
}
.right{
height: 300px;
background: blue;
overflow: hidden;
}
<div class="left"></div>
<div class="right"></div>
121. 什么是margin重叠问题?如何解决?
答:在垂直方向上,两个块级元素的上外边距和下外边距可能会合并为一个外边距,通常大小会取其中外边距值大的那个,这种行为就是margin重叠。
解决办法:
- 使用BFC(块级格式上下文):将元素包裹在一个具有触发BFC条件的父级容器中,可以创建一个新的BFC环境,从而防止margin重叠,比如给父容器添加浮动,设置overflow除了visible以外的值,将元素设置为绝对定位等。
122.position的属性有哪些,区别是什么?(position、relative、fixed的区别有哪些?)
答:
- relative:元素的定位永远是相对元素自身位置的,和其他元素没有关系,也不会影响其他元素。
- fixed:元素的定位是相对于window边界的,和其他元素没有关系。但是他具有破坏性,会导致其他元素的位置变化。
- absolute: 元素的定位相对于前两者要复杂许多,如果为absolute设置了top、left,浏览器会根据什么区确定他的纵向和横向的偏移量呢?答案是浏览器会递归查找该元素的所有父元素,如果找到一个设置了position:relative/absolute/fixed的元素,就以该元素为基准定位,如果没有找到,就以浏览器边界定位。
123.display、float、position的关系?
答:他们之间类似于一个优先级的机制,position:absolute
和position:fixed
优先级最高,有它存在的时候,浮动不起作用,display
的值也需要调整,其次,元素的float
特征的值不是none的时候,或者它是根元素的时候,也会调整display的值,最后,只有是非根元素,并且是非浮动元素,并且是非绝对定位元素,display特征值同设置值。
124. absolute与fied的共同点和不同点是什么?
答:他们两都是CSS中常用的定位方式,且都脱离了正常的文档流,不占据原有元素的空间。不同点在于绝对定位(absolute)是相对于父元素定位的,位置随滚动改变,固定定位(fixed)相对于浏览器窗口定位,位置固定不变。
125.手写实现一个三角形?
原理:CSS绘制三角形主要用到的是border属性,也就是边框。
div {
width: 0;
height: 0;
border: 100px solid;
border-color: orange blue red green;
}
只需要用transparent隐藏其他边就可以实现三角形了。
div {
width: 0;
height: 0;
border-bottom: 50px solid red;
border-right: 50px solid transparent;
border-left: 50px solid transparent;
}
126.手写实现一个扇形?
原理:用CSS实现扇形的思路和三角形的基本一致,就是多了一个圆角的样式,实现一个90度的扇形。
div{
border: 100px solid transparent;
width: 0;
heigt: 0;
border-radius: 100px;
border-top-color: red;
}
127.实现一个宽度自适应地正方形?
原理:margin
和padding
的百分比值只是和父元素的宽度有关,而与高度无关。
.square {
width: 20%;
height: 0;
padding-top: 20%;
background: orange;
}
128.实现一条0.5px的线?
原理:对于一个具有1像素高度和100像素宽度的元素,应用 transform: scale(0.5, 0.5)
后,元素的新高度将为0.5像素,但依然是1像素的线条。这是因为浏览器会将0.5像素进行取整,最小渲染单位仍然是1像素。
同样的,此方法也可以用于近似实现0.5像素线的效果。但请记住,实际上并没有真正的0.5像素线。 原理:
<style>
.square {
width: 1000px;
height: 1px;
background: red;
transform: scale(1,0.5);
}
</style>
129.isNaN和Number.isNaN函数的区别?
答:
- 函数isNaN接收参数后,会尝试将这个参数转换为数值,任何不能被转换为数值的值都会返回true,因此非数字值传入也会返回true,会影响NaN的判断。
- 函数Number.isNaN会首先判断传入的参数是否是数字,如果是数字在判断是否为NaN,不会进行数据类型转换,这种方法对于NaN的判断更为准确。
130.几个特殊的转换规则?
答:
- Undefined类型转换为数字类型时的值转换为NaN。
- Null类型转换为数字类型时会转换为0
- String类型转换为数字类型时,如果是非数字的字符串是转换为NaN,如果是空字符串为0
- 转换为布尔值时,undefined、null、false、+0、-0和NaN、空字符串都是false
131.Object.is()与比较操作符"==="、"=="的区别?
答:
- 使用"=="进行相等判断时,如果两边的类型不一致,则会进行强制类型转换后再进行比较。
- 使用三等号"==="进行相等判断时,如果两边的类型不一致时,不会做强制类型转换,直接返回false。
- 使用Object.is()来进行相等判断时,一般情况下与三等号相同,处理了一些特殊情况,比如+0和-0不再相等,NaN和NaN是相等。
132.什么是javascript中的包装类型?
答:在javascript中,基本类型是没有属性和方法的,但是为了方便操作基本类型的值,在调用基本类型的属性和方法时,js会在后台,将基本类型转换成对象,比如:
const a = "abc";
a.length; // 3
a.toUpperCase(); // "ABC"
JavaScript也可以使用Object
函数显式地将基本类型转换为包装类型:
var a = 'abc'
Object(a) // String {"abc"}
133.object.assign和扩展运算法是深拷贝还是浅拷贝,两者的区别是什么?
答:两者都是浅拷贝。
- Object.assign()方法接收的第一个参数作为目标对象,后面的所有参数作为源对象,然后把所有的源对象合并到目标对象中,它会修改一个对象,因此会触发ES6 setter。
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = Object.assign({}, outObj)
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
- 扩展操作符(...)使用它时,数组或对象中的每一个值都会被拷贝到一个新的数组或对象中,它不复制继承的属性或类的属性,但是他会复制ES6的symbols属性。
let outObj = {
inObj: {a: 1, b: 2}
}
let newObj = {...outObj}
newObj.inObj.a = 2
console.log(outObj) // {inObj: {a: 2, b: 2}}
134.const对象的属性可以修改吗?
答:可以修改,const保证的并不是变量的值不能改动,而是变量指向的那个内存地址不能改动,对于基本类型的数据(数值,字符串,布尔类型),其值就保存在变量指向的那个内存地址,因此等同于常量。 但是对于引用类型(主要是对象和数组)而言,变量指向的数据的内存地址,保存的只是一个指针,const只能保证指针的固定不变,并不是保证数据结构不变。
135.如果new一个箭头函数会怎么样?
答:箭头函数是ES6提出来的,他没有prototype,也没有自己的this指向,更不能使用argument参数,所以不能new一个箭头函数。
new操作符的实现步骤: 1.创建一个空对象 2.将对象的__proto__指向构建函数的prototype属性,也就是指向原型对象。 3.执行构造函数中的代码,构造函数中的this指向该对象。 4.返回新对象。
136.箭头函数和普通函数的区别?
答:箭头函数比普通函数更加简洁,同时箭头函数没有自己的this,箭头函数this在定义时就已经确认,且永远不会改变,call()、apply()、bind()等方法也不能改变箭头函数中this指向,箭头函数不能作为构造函数使用,且没有自己的arguments和prototype,箭头函数不能用作Generator函数,不能使用yeild关键字。
137.箭头函数的this指向哪里?
答:箭头函数不同于传统的javascript函数,箭头函数并没有属于自己的this,它所谓的this是捕获其所在上下文的this值,作为自己this值。这个this在定义时确定,并且不会改变。
138.javascript有哪些内置对象?
答:js中的内置对象主要指的是在程序执行前存在全局作用域里的由js定义的一些全局值属性、函数和用来实例化其他对象的构造函数对象。一般用到的全局变量值NaN、undefined、全局函数如parseInt()、ParseFloat()用来实例化对象的构造函数如Date、Object等,数学计算的Math。
139.对JSON的理解?
答:JSON是一种基于文本的轻量级数据交换格式,它可以被任何的编程语言读取并作为数据格式来传递。JSON格式相比于js更加严格,属性值不能为函数、NaN等,故需要通过JSON.stringfy()函数将js对象转换成JSON,通过JSON.parse()函数解析。
140.javascript脚本延迟加载的方式有哪几种?
- 将脚本放在HTML文档底部,这样在页面加载完之后再加载脚本,从而不会阻塞页面的加载。
- 使用defer属性,相当于告诉浏览器立即异步下载脚本,但延迟执行。
- 使用async属性,相当于告诉浏览器立即异步下载脚本,并立即执行脚本
141.JavaScript类数组对象的定义?
答:一个拥有length属性和若干索引属性的对象就可以被称为类数组对象,类数组对象和数组类似,但是不能调用数组的方法。 类数组转换为数组的方法:
- 通过call调用数组的slice方法来实现转换。
Array.prototype.slice.call(arrayLike);
- 通过call调用数组的splice方法来实现
Array.prototype.splice.call(arrayLike, 0); //表示删除0个
- 通过call调用数组的concat方法来实现
Array.prototype.concat.apply([], arrayLike);
- 通过 Array.from 方法来实现转换
Array.from(arrayLike);
142.什么是DOM和BOM?
答:
- Dom(Document Object Model)是一个表示网页文档结构的对象,他将网页文档以一个树状结构表示,并提供一系列的方法和属性,可以让开发者对网页文档进行操作和访问。
- BOM(Browser Object Model)指的是浏览器对象模型,他指的是浏览器当做了一个对象来对待,这个对象定义了与浏览器进行交互的接口。BOM的核心是window,而window对象具有双重角色,既是js访问浏览器窗口的一个接口,又是一个全局对象。
143.encodeURI、encodeURIComponent 的区别
答:
- encodeURI用于对整个URL进行编码,但是会保留特殊字符。
- encodeURIComponent用于对URL的组件进行编码,对特殊字符进行编码。
144.对AJAX的理解,实现一个AJax请求?
Ajax指的是通过javascript的异步通信,从服务器获取XML文档从中提取数据,再更新当前网页的对应部分。 步骤:
- 创建一个XMLHttpRequest对象。
- 使用open方法创建一个HTTp请求
- 在发起请求前,添加一些信息和监听函数
- 最后调用send方法来向服务器发送请求。
const SERVER_URL = "/server";
let xhr = new XMLHttpRequest();
// 创建 Http 请求
xhr.open("GET", url, true);
// 设置状态监听函数
xhr.onreadystatechange = function() {
if (this.readyState !== 4) return;
// 当请求成功时
if (this.status === 200) {
handle(this.response);
} else {
console.error(this.statusText);
}
};
// 设置请求失败时的监听函数
xhr.onerror = function() {
console.error(this.statusText);
};
// 设置请求头信息
xhr.responseType = "json";
xhr.setRequestHeader("Accept", "application/json");
// 发送 Http 请求
xhr.send(null);
145.use strict是什么意思?使用它区别是什么?
答:use strict是一种ES5添加的严格模式,这种模式使得JS在更加严格的条件下运行,主要目的是:消除js语法的不合理、不严谨之处,消除代码运行的不安全之处。在严格模式下,禁止使用with语句、this禁止指向全局对象,对象不能有重名的属性。
146.如何判断一个对象是否属于某个类?
答:
- 第一种方式:通过instanceof运算符判断构造函数的原型是否出现在对象的原型链上
class Person {}
const p = new Person();
console.log(p instanceof Person); // true
- 第二种方法:判断对象的constructor属性获取对象的构造函数,与类的构造函数进行比较
class Person {}
const p = new Person();
console.log(p.constructor === Person); // true
- 第三种方法:通过Object.prototype.tostring()方法打印对象的[[class]]属性来进行判断
class Person {
// 重写 toString() 方法返回类名信息
toString() {
return "[object Person]";
}
}
const p = new Person();
console.log(p.toString()); // [object Person]
console.log(p.toString() === "[object Person]"); // true
147.强类型语言和弱类型语言的区别?
答:
- 强类型语言:强类型语言是一种强制类型定义的语言,要求变量的使用严格符合定义,所有变量都必须先定义后使用。java和c++都是。
- 弱类型语言:JS语言就是弱类型语言,简单理解就是一种变量类型可以被忽略的语言。比如字符串'12'和整数3相加,得到字符串'123',在相加的时候会进行强制类型转换。 对比:强类型语言在速度上可能比弱类型语言差,但是强类型语言带来的严谨性可以避免很多错误。
148.for...in和for...of的区别?
- for...of只可以遍历可迭代对象,比如数组、字符串、Set、Map、类数组等,不能遍历普通的对象。for...in可用于遍历普通对象,但是会遍历整个原型链,性能较差。
149.this是什么?
答:this是执行上下文中的一个属性,它指向最后一次调用这个方法的对象。this的指向可以通过四种调用模式来判断。
- 函数调用模式,当一个函数不是一个对象属性时,直接作为函数来调用,this指向全局对象
- 方法调用模式,函数作为对象的一个方法来调用,则this指向该对象。
- 构造器模式,使用new调用,函数执行会创建一个新对象,this指向该对象。
- apply、call、bind调用,this绑定传入的第一个参数。
使用构造器调用模式的优先级最高,然后是 apply、call 和 bind 调用模式,然后是方法调用模式,然后是函数调用模式。
150.call()和apply()的区别?
答:
- apply()接收两个参数,第一个参数是指定函数体内this的指向,第二个参数是一个带下标的集合,可以是数组和类数组,集合中的元素被作为参数传入给调用的函数。
- call()传入的参数数量是不固定的,第一个参数是函数图this的指向,第二个参数开始,每个参数依次传入给调用的函数。
专项提升系列
1.this指针指向专题
- 默认绑定: 非严格模式下
this
指向全局对象,严格模式下this
会绑定为undefined
- 隐式绑定: 满足
XXX.fn()
格式,fn
的this
指向XXX
。如果存在链式调用,this
永远指向最后调用它的那个对象 - 隐式绑定丢失:起函数别名,通过别名运行;函数作为参数会造成隐式绑定丢失。
- 显式绑定: 通过
call/apply/bind
修改this
指向 - new绑定: 通过
new
来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的this
。 - 箭头函数绑定: 箭头函数没有
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时
题目一:隐式绑定与隐式绑定丢失
var x = 1;
var obj = {
x: 3,
fun:function () {
var x = 5;
return this.x;
}
};
var fun = obj.fun;
console.log(obj.fun(), fun());
解析:
JavaScript
对于引用类型,其地址指针存放在栈内存中,真正的本体是存放在堆内存中的。fun = obj.fun
相当于将 obj.fun
指向得堆内存指针赋值给了 fun
,此后 fun
执行与 obj
不会有任何关系,发生隐式绑定丢失。
-
obj.fun()
: 隐式绑定,fun
里面的this
指向obj
,打印3
-
fun()
: 隐式绑定丢失:fun
默认绑定,非严格模式下,this
指向window
,打印1
题目二:隐式绑定丢失
var person = {
age: 18,
getAge: function() {
return this.age;
}
};
var getAge = person.getAge
console.log(getAge()) // undefined
题目三:隐式绑定丢失
var obj = {
name:"zhangsan",
sayName:function(){
console.log(this.name);
}
}
var wfunc = obj.sayName;
obj.sayName();
wfunc();
var name = "lisi";
obj.sayName();
wfunc();
答案:
zhangsan
undefined
zhangsan
lisi
题目四:new绑定
var a = 5;
function test() {
a = 0;
console.log(a);
console.log(this.a);
var a;
console.log(a);
}
new test();
输出:
0
undefined
0
解析:
使用new
来构建函数,会执行如下四步操作:
- 创建一个空的简单
JavaScript
对象(即{}
); - 为步骤1新创建的对象添加属性
__proto__
,将该属性链接至构造函数的原型对象 ; - 将步骤1新创建的对象作为
this
的上下文 ; - 如果该函数没有返回对象,则返回
this
。
通过 new
来调用构造函数,会生成一个新对象,并且把这个新对象绑定为调用函数的 this
。
console.log(a)
: 打印变量a
的值,当前testAO
中存在a
变量,打印0
console.log(this.a)
:new
绑定this
指向新的实例对象,当前题目没有给实例对象添加a
属性,打印undefined
console.log(a)
: 同第一个,打印0
题目五:箭头函数与显式绑定
function fun () {
return () => {
return () => {
return () => {
console.log(this.name)
}
}
}
}
var f = fun.call({name: 'foo'})
var t1 = f.call({name: 'bar'})()()
var t2 = f().call({name: 'baz'})()
var t3 = f()().call({name: 'qux'})
输出:
foo
foo
foo
解析:
- 箭头函数没有
this
,它的this
是通过作用域链查到外层作用域的this
,且指向函数定义时的this
而非执行时。 - 箭头函数,不能通过
call\apply\bind
来修改this
指向,但可以通过修改外层作用域的this
来达成间接修改。 JavaScript
是静态作用域,即函数的作用域在函数定义的时候就决定了,而箭头函数的this
是通过作用域链查到的,因此箭头函数定义后,它的作用域链就定死了。
f = fun.call({name: 'foo'})
: 将fun
函数的this
指向{name: 'foo'}
,并返回一个箭头函数,因此箭头函数的this
也指向{name: 'foo'}
t1 = f.call({name: 'bar'})()()
: 对第一层箭头函数执行call
操作,无效,当前this
仍指向{name: 'foo'}
,第二层、第三层都是箭头函数,第三层的this
也指向{name: 'foo'}
,打印foo
- 后续
t2 t3
分别对第二层、第三层箭头函数使用call
,无效,最终都打印foo
。
题目六:箭头函数
let obj1 = {
a: 1,
foo: () => {
console.log(this.a)
}
}
// log1
console.log(obj1.foo())
const obj2 = obj1.foo
// log2
console.log(obj2())
//undefined
//undefined
解析:
obj1.foo
为箭头函数,obj1
为对象,无法提供外层作用域,因此obj.foo
里面的this
指向window
obj1.foo()
: 箭头函数,this
指向window
,打印undefined
obj2
隐式绑定丢失: 打印undefined
题目七:综合题
var name = 'global';
var obj = {
name: 'local',
foo: function(){
this.name = 'foo';
console.log(this.name);
}.bind(window)
};
var bar = new obj.foo();
setTimeout(function() {
console.log(window.name);
}, 0);
console.log(bar.name);
var bar3 = bar2 = bar;
bar2.name = 'foo2';
console.log(bar3.name);
这个题的整体出题质量还是挺高的,首先咱们来把涉及到的知识罗列一下:
bind
是显式绑定,会修改this
指向,但bind()
函数不会立即执行函数,会返回一个新函数setTimeout
是异步任务,同步任务执行完毕后才会执行异步任务- 绑定优先级: new绑定 > 显式绑定 > 隐式绑定 > 默认绑定
解析
obj.foo
将它的this
通过bind
显式的绑定为window
,但bind
不会立即执行var bar = new obj.foo()
:new
绑定优先级大于bind
,因此bind
失效了,此时this
指向new
实例,因此obj.foo
内部的console
打印foo
bar
是new obj.foo()
的实例,console.log(bar.name)
打印foo
setTimeout
异步任务,等到同步执行完毕再来调用它的回调bar3 = bar2 = bar
,将bar2,bar3,bar
的地址都指向bar
所指向的空间。bar2.name = 'foo2'
,修改地址指向堆内存的值console.log(bar3.name)
: 由于三个变量指向同一块地址,bar3
修改了name
,bar3
也随之改变,打印foo2
setTimeout
的回调执行,打印global
2.变量提升问题专题
- 什么是变量提升?
定义:变量提升是当栈内存作用域形成时,JS代码执行前,浏览器会将带有var,function关键字的变量提前进行声明(值默认undefined),这种预先处理的机制就叫做变量提升机制。带 var
的只声明还没有被定义,带 function
的已经声明和定义。
- 带var和不带var的区别
注意: var a=b=12相当于var a=12; b=12(b是没有var的) 分两种情况: 全局作用域中,带不带var都可以,自动属于window对象一个属性。 私有作用域(函数作用域),带有var的私有变量,不带的var的会向上级作用域查找,一直找到window为止,这个查找过程叫作用域链。 例子:
// 1
console.log(a, b)
var a =12, b ='林一一'
function foo(){
// 2
console.log(a, b)
// 3
var a = b =13
console.log(a, b)
}
foo()
console.log(a, b)
/* 输出:
undefined undefined
undefined "林一一"
13 13
12 13
*/
思路:1处的 a, b 其实就是 window下面的属性为 undefined。在函数内部由于变量提升机制 a
带 var
一开始就是 undefined,b
不带var
将向上级作用域查找,找到全局作用域下的林一一
所以2处打印出来的就是 undefined "林一一"
。随后 a =13,window.b =13
,即原来 b='林一一'
变成了 b=13
,打印出13, 13
,最后第4处打印出12, 13
。
例二:
a = 2
function foo(){
var a =12;
b = '林一一'
console.log('b' in window)
console.log(a, b)
}
foo()
console.log(b)
console.log(a)
/* 输出
true
12 "林一一"
林一一
2
/
思路:这是比较简单的一道题,需要注意的是函数内的 b 没有带 var
,b 会一直向上查找到 window 下,发现 window 下也没有就直接给 window 设置了一个属性 window.b = '林一一'
,同理全局下的 a
也一样。
- 等号左边下的变量提升
普通函数下变量提升例子:
print()
function print(){
console.log('林一一')
}
print()
匿名函数下的带=的变量提升
print()
var print = function() {
console.log('林一一')
}
print()
/*输出
Uncaught TypeError: print is not a function
/
思路:同样由于变量提升机制带var的print是一开始值是undefined,所以print()这时还不是一个函数,所以报出类型错误。
- 条件判断下的变量提升
if else判断下的变量提升,不管条件是否成立,都会进行变量提升
console.log(a)
if(false){
var a = '林一一'
}
console.log(a)
/* 输出
undefined
undefined
/
if中()内的表达式不会变量提升
var y = 1
if(function f(){}){
console.log(typeof f) // undefined
y = y + typeof f
}
console.log(y) // 1undefined
理解:判断的条件没有提升,所以条件内部的f是未定义的。
- 重名问题下的变量提升
带var和带function重名条件下的变量提升优先级,函数先执行。(js中函数是一等公民)
console.log(a);
var a=1;
function a(){
console.log(1);
}
// 或
console.log(a);
function a(){
console.log(1);
}
var a=1;
// 输出都是: ƒ a(){ console.log(1);}
理解:在 var 和 function
同名的变量提升的条件下,函数会先执行。所以输出的结果都是一样的。换一句话说,var 和 function
的变量同名 var
会先进行变量提升,但是在变量提升阶段,函数声明的变量会覆盖 var
的变量提升,所以直接结果总是函数先执行优先。
**函数名和 var
声明的变量重名 **
var fn = 12
function fn() {
console.log('林一一')
}
console.log(window.fn)
fn()
/* 输出
* 12
* Uncaught TypeError: fn is not a function
/
理解:带var声明的和带function声明的其实都是在window下的属性,也就是重名了,根据变量提升的机制,fn属于函数,函数会先执行,随着js代码自上而下执行时,此时fn是fn=12,输出window.fn=12,所以fn()==>12(),又是一个类型错误TypeError
变量重名在变量提升阶段会重新定义也就是重新赋值
console.log('1',fn())
function fn(){
console.log(1)
}
console.log('2',fn())
function fn(){
console.log(2)
}
console.log('3',fn())
var fn = '林一一'
console.log('4',fn())
function fn(){
console.log(3)
}
/* 输出
* 3
* 1 undefined //为什么是undefined,这里打印的是函数返回值,但是函数没有返回值
* 3
* 2 undefined
* 3
* 3 undefined
* Uncaught TypeError: fn is not a function
/
思路:同样由于变量提升机制,fn
会被多次重新赋值最后赋值的地址值(假设为oxfffee)为最后一个函数,所以调用 fn
都只是在调用最后一个函数输出都是 3
, 代码执行到var fn = '林一一'
,所以 fn() 其实 == 林一一()
导致类型错误 TypeError
- 函数形参的变量提升
函数的形参也会进行一次变量提升
function a(b){
console.log(b);
}
a(45);
// 等价于
// function a(b) {
// var b = undefined;
// b = 45;
// }
function foo(a) {
console.log(a)
var a
console.log(a)
}
foo(a);
// 输出 1 1
- 非匿名自执行函数的变量提升
匿名执行函数和非匿名自执行函数在全局环境下不具备变量提升的机制。
var a = 10;
(function c(){
})()
console.log(c)
// Uncaught ReferenceError: c is not defined
匿名自执行函数在自己的作用域内存在正常的变量提升
var a = 10;
(function(){
console.log(a)
a = 20
console.log(a)
})()
console.log(a)
// 10, 20, 20
非匿名自执行函数的函数名在自己的作用域内变量提升,且修改函数名的值无效,这是非匿名函数和普通函数的差别
var a = 10;
(function a(){
console.log(a)
a = 20
console.log(a)
})()
// ƒ a(){a = 20 console.log(a)} ƒ a(){a = 20 console.log(a)}
理解:首先在全局环境下,var 声明的变量 a 会变量提升,但是非匿名函数不会在全局环境下变量提升因为具备自己的作用域了,而且上面的函数名 a 同样变量提升了,值就是函数 a 的应用地址值,输出的结果就是a(){a = 20 console.log(a)}
。而且非匿名自执行函数名是不可以修改的,即使修改了也不会有任何作用,严格模式下还会报错,所以最后输出的 a 还是 a(){a = 20 console.log(a)}
3.原型以及原型链专题
原型概念: JS在每个函数创建的时候,都会生成一个属性prototype,这个属性指向一个对象,这个对象就是此函数的原型对象。该原型对象有个constructor,指向该函数。
原型链概念: 当访问一个对象的某个属性时,会先在这个对象本身属性上查找,如果没有找到,则会通过它的__proto__隐式属性,找到它的构造函数的原型对象,如果还没有找到,就会再其构造函数的prototype的__proto__中查找,这样一层一层向上查找就会形成一个链式结构,我们称为原型链。
4.javascript中如何进行隐式类型转换专题
ToPrimitive:
如果值已经是原始类型,则返回它本身。否则,如果值有 valueOf() 方法,如果返回值为原始类型则返回 valueOf() 的结果。否则,如果值有 toString() 方法,如果返回值为原始类型则返回 toString() 的结果。否则,抛出 TypeError。
ToNumber:
如果值已经是数字,则返回它本身。否则,如果值是一个对象,则尝试调用 valueOf() 方法,并将其结果转换为数字。否则,如果值是字符串,则尝试将其解析为数字,并返回解析的结果。否则,返回 NaN。
- 情况一:
+操作符
,两边至少有一个string类型变量时,两边的变量会被隐式转换为字符串,其他情况两边变量都会被转换为数字。
1 + '23' // '123'
1 + false // 1
1 + Symbol() // Uncaught TypeError: Cannot convert a Symbol value to a number
'1' + false // '1false'
false + true // 1
- 情况二:
-
、*
、`` 操作符均转换成数字进行计算
1 * '23' // 23
1 * false // 0
1 / 'aa' // NaN
- 情况三:对于
==操作符
3 == true // false, 3 转为number为3,true转为number为1
'0' == false //true, '0'转为number为0,false转为number为0
'0' == 0 // '0'转为number为0
- 4.情况四:对于
<
和>
比较符 如果两边都是字符串,则比较字母表顺序
'ca' < 'bd' // false
'a' < 'b' // true
其他情况都转换为数字再进行比较:
'12' < 13 // true
false > -1 // true
5.情况五:以上都是基本类型,如果涉及对象的话,则用ToPrimitive转换为基本类型再进行转换。
var a = {}
a > 2 // false
解析:
a.valueOf() // {}, 上面提到过,ToPrimitive默认type为number,所以先valueOf,结果还是个对象,下一步
a.toString() // "[object Object]",现在是一个字符串了
Number(a.toString()) // NaN,根据上面 < 和 > 操作符的规则,要转换成数字
NaN > 2 //false,得出比较结果
5.javascript执行上下文专题
执行上下文概念: JS执行上下文是javascript代码运行时的环境,他决定了变量的作用域,函数的调用和对象的访问。分为全局执行上下文和函数执行上下文。
全局执行上下文的概念: 全局执行上下文只有一个,在客户端中一般由浏览器的js引擎创建,所有不在函数内部的js代码,都会在全局执行上下文中执行。
函数执行上下文的概念: 函数执行上下文理论上可能存在无数个,每当一个函数被调用时都会创建一个函数执行上下文,同一个函数被多次调用,都会创建一个新的上下文。
变量对象(VO)的概念: 变量对象(Variable Object)是javascript中的一个概念,它是在执行上下文(Execution Context)创建阶段被创建的一个特殊对象,用于存储变量和函数的定义。它包含以下内容:
- 函数参数:如果当前执行上下文是一个函数的执行上下文,则函数的参数被作为变量对象的属性存储。
- 函数声明:所有在当前执行上下文中定义的函数,无论是函数声明还是函数表达式,都被作为变量对象的属性存储,这允许在函数声明之前可以访问到这些函数。
- 变量声明: 对于使用var声明的变量,在执行上下文创建阶段会被初始化为undefined并添加到变量中。
活动对象(AO)的概念: 活动对象(activation object)是指函数进入执行阶段时,原本不能访问的变量对象被激活成为了一个活动对象,我们可以访问其中的各种属性,其实变量对象和活动对象时一个东西,只是处于不同的状态和阶段。
作用域链的概念: 作用域规定了如何查找对象,也就是确定当前执行代码对变量的访问权限。当查找变量的时候,会先从当前上下文的变量对象中查找,如果没有找到,就会从父级执行上下文的变量对象中查找,一直查找到全局上下文的变量对象,也就是全局对象。这样由多个执行上下文的变量对象构成的链表叫做作用域链
。
当前可执行代码块的调用者(this value): 如果当前函数被作为对象方法调用或使用bind、call、apply等api进行委托调用,则将当前代码快的调用者信息(this value)存入当前执行上下文,否则默认为全局对象调用。
执行上下文数据结构模拟:
executionContext:{
[variable object | activation object]:{
arguments,
variables: [...],
funcions: [...]
},
scope chain: variable object + all parents scopes
thisValue: context object
}
执行上下文的生命周期:
执行上下文的生命周期,分为三个阶段,分别是: 创建阶段、执行阶段、销毁阶段
-
创建阶段(发生在函数调用时且执行函数体内的具体代码之前)
- 用当前函数的参数列表(arguments)初始化一个变量对象,并将当前执行上下文与之关联,函数代码中声明的变量和函数将作为属性添加到这个变量对象上。在这一阶段,会进行变量和函数的初始化声明,变量统一定义为undefined,需要等到赋值时才会有确切值,而函数则会直接定义。
- 构建作用域链
- 确定this的值
-
执行阶段
- 执行阶段,JS代码开始逐条执行,在这个阶段,JS引擎开始对定义的变量赋值、开始顺着作用域链访问变量,如果内部有函数调用就创建一个新的执行上下文压入执行栈并把控制权交出。
-
销毁阶段
- 一般来讲,当函数执行完后,当前执行上下文会被弹出执行上下文栈并且销毁,控制权被重新交给执行栈上一层的执行上下文。
注意:闭包的情况下,由于闭包的作用域链仍然在引用父函数的变量对象,导致了父函数的变量对象会一直驻存于内存中,无法销毁,除非闭包的引用被销毁,闭包不再引用父函数的变量对象,这块内存才能释放掉。过度使用闭包会造成内存泄漏。
ES3执行上下文总结:
对于 ES3
中的执行上下文,我们可以用下面这个列表来概括程序执行的整个过程:
-
函数被调用
-
在执行具体的函数代码之前,创建了执行上下文
-
进入执行上下文的创建阶段:
-
初始化作用域链
-
创建
arguments object
检查上下文中的参数,初始化名称和值并创建引用副本 -
扫描上下文找到所有函数声明:
- 对于每个找到的函数,用它们的原生函数名,在变量对象中创建一个属性,该属性里存放的是一个指向实际内存地址的指针
- 如果函数名称已经存在了,属性的引用指针将会被覆盖
-
扫描上下文找到所有
var
的变量声明:- 对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用
undefined
来初始化 - 如果变量名作为属性在变量对象中已存在,则不做任何处理并接着扫描
- 对于每个找到的变量声明,用它们的原生变量名,在变量对象中创建一个属性,并且使用
-
确定
this
值
-
-
进入执行上下文的执行阶段:
-
在上下文中运行/解释函数代码,并在代码逐行执行时分配变量值。
-
执行上下文栈的概念:
执行上下文栈是用于管理javascript执行上下文的数据结构,当JS引擎开始解析脚本代码时,会首先创建一个全局执行上下文,压入栈底(这个全局执行上下文从创建一直到程序销毁,都会存在于栈的底部),每当引擎发现一处函数调用,就会创建一个新的函数执行上下文压入栈内,并将控制权交给该上下文,待函数执行完成后,即将该执行上下文从栈内弹出销毁,将控制权重新给栈内上一个执行上下文。
例题:
第一题
var foo = function () {
console.log('foo1');
}
foo();
var foo = function () {
console.log('foo2');
}
foo();
// foo1
// foo2
理解:函数表达式的形式的定义,应该作为变量来对待。而不是作为函数。
第二题:
foo();
var foo = function foo() {
console.log('foo1');
}
function foo() {
console.log('foo2');
}
foo();
// foo2
// foo1
理解:由于函数声明优先级更高,所以函数声明在前,且如果var定义变量时发现已有同名的函数定义,则跳过变量定义。
第三题:
var foo = 1;
function bar () {
console.log(foo);
var foo = 10;
console.log(foo);
}
bar();
// undefined
// 10
理解:foo带有var,所以本地作用域重新定义foo,故变量提升,输出undefined和10
第四题:
var foo = 1;
function bar () {
console.log(foo);
foo = 2;
}
bar();
console.log(foo);
//1
//2
理解:由于foo不带var,故直接从上层作用域找foo,所以第一个输出1, foo=2直接修改了上层作用域的值,故最后输出2
第五题:
var foo = 1;
function bar (foo) {
console.log(foo);
foo = 234;
}
bar(123);
console.log(foo);
//123
//1
理解:存在参数,参数可以转换为var foo = undefined; foo = 123;故先输出123,foo=234修改的是本地作用域。所以外层输出的1
第六题:
var a = 1;
function foo () {
var a = 2;
return function () {
console.log(a);
}
}
var bar = foo();
bar();
// 2
理解:函数能够访问到的上层作用域,是在函数声明时候就已经确定了的,函数声明在哪里,上层作用域就在哪里,和拿到哪里执行没有关系。
第七题:
"use strict";
var a = 1;
function foo () {
var a = 2;
return function () {
console.log(this.a);
}
}
var bar = foo().bind(this);
bar();
理解:这题考察的是执行环境中的 this
指向的问题,由于闭包内明确指定访问 this
中的 a
属性,并且闭包被 bind
绑定在全局环境下运行,所以打印出的是全局对象中的 a
。
总结:
-
当函数运行的时候,会生成一个叫做 “执行上下文” 的东西,也可以叫做执行环境,它用于保存函数运行时需要的一些信息。
-
所有的执行上下文都会被交给系统的 “执行上下文栈” 来管理,它是一个栈结构数据,全局上下文永远在该栈的最底部,每当一个函数执行生成了新的上下文,该上下文对象就会被压入栈,但是上下文栈有容量限制,如果超出容量就会栈溢出。
-
执行上下文内部存储了包括:变量对象、作用域链、this 指向 这些函数运行时的必须数据。
-
变量对象构建的过程中会触发变量和函数的声明提升。
-
函数内部代码执行时,会先访问本地的变量对象去尝试获取变量,找不到的话就会攀爬作用域链层层寻找,找到目标变量则返回,找不到则
undefined
。 -
一个函数能够访问到的上层作用域,在函数创建的时候就已经被确定且保存在函数的
[[scope]]
属性里,和函数拿到哪里去执行没有关系。 -
一个函数调用时的
this
指向,取决于它的调用者,通常有以下几种方式可以改变函数的this
值:对象调用、call
、bind
、apply
。
6.关于闭包的专题
闭包概念:能够访问其他函数内部的变量的函数,称为闭包。
闭包的应用场景
- 单例模式
单例模式是一种常见的涉及模式,它保证了一个类只有一个实例。实现方法一般是先判断实例是否存在,如果存在就直接返回,否则就创建了再返回。单例模式的好处就是避免了重复实例化带来的内存开销:
// 单例模式
function Singleton(){
this.data = 'singleton';
}
Singleton.getInstance = (function () {
var instance;
return function(){
if (instance) {
return instance;
} else {
instance = new Singleton();
return instance;
}
}
})();
var sa = Singleton.getInstance();
var sb = Singleton.getInstance();
console.log(sa === sb); // true
console.log(sa.data); // 'singleton'
- 模拟私有属性
javascript
没有java
中那种public
private
的访问权限控制,对象中的所用方法和属性均可以访问,这就造成了安全隐患,内部的属性任何开发者都可以随意修改。虽然语言层面不支持私有属性的创建,但是我们可以用闭包的手段来模拟出私有属性:
// 模拟私有属性
function getGeneratorFunc () {
var _name = 'John';
var _age = 22;
return function () {
return {
getName: function () {return _name;},
getAge: function() {return _age;}
};
};
}
var obj = getGeneratorFunc()();
obj.getName(); // John
obj.getAge(); // 22
obj._age; // undefined
- 柯里化
柯里化概念:柯里化(currying)是一种将带有多个参数的函数转换成一系列嵌套、只接受单一参数的函数的技术。
function add(x, y, z) {
return x + y + z;
}
// 使用柯里化转换函数
function curry(fn) {
return function curried(...args) {
if (args.length >= fn.length) {
return fn.apply(this, args);
} else {
return function(...restArgs) {
return curried.apply(this, args.concat(restArgs));
};
}
};
}
const curriedAdd = curry(add);
console.log(curriedAdd(1)(2)(3)); // 输出:6
console.log(curriedAdd(1, 2)(3)); // 输出:6
console.log(curriedAdd(1)(2, 3)); // 输出:6
闭包的问题
function foo() {
var a = 2;
function bar() {
console.log( a );
}
return bar;
}
var baz = foo();
baz(); // 这就形成了一个闭包
理解:javascript内部的垃圾回收机制用的是引用技术收集,即当内存中的一个变量被引用一次,计数就加一。垃圾回收机制会以固定的时间轮询这些变量,将计数为0的变量标记为失效变量并回收。 但是上述代码中,foo函数作用域隔绝了外部环境,所有变量引用都在函数内部完成,foo运行完成后,内部的变量就应该被销毁,内存被回收。然后闭包导致了全局作用域始终存在一个baz的变量在引用这foo内部的bar函数,这就意味着foo内部定义bar函数引用数始终为1,垃圾运行机制无法把它销毁。导致内存泄漏。
内存泄漏
概念:内存泄漏是指一块内存不在被应用程序使用的时候,由于某种原因,这块内存没有返还给操作系统或者内存池的现象。内存泄漏可能会导致应用程序卡顿或者崩溃。全局变量的无意创建、闭包过度使用、Dom的事件绑定未移除等都会导致。
内存泄漏的解决方案
1.使用严格模式,避免不经意间的全局变量泄漏
"use strict";
function foo () {
b = 2;
}
foo(); // ReferenceError: b is not defined
2.关注 DOM
生命周期,在销毁阶段记得解绑相关事件:
const wrapDOM = document.getElementById('wrap');
wrapDOM.onclick = function (e) {console.log(e);};
// some codes ...
// remove wrapDOM
wrapDOM.onclick = null;
wrapDOM.parentNode.removeChild(wrapDOM);
3.避免过度使用闭包。
7.事件循环专题
事件循环概念: 浏览器中的事件循环是一种机制,用于控制和调度javascript代码在浏览器中的执行的顺序,事件循环确保所有javascript代码的执行都是异步和非阻塞的,以确保用户界面的响应性。事件循环将任务分为两类,宏任务和微任务。
- 宏任务包括script(整体代码)、setTimeout、setInterval、setImmediate、I/O、UI Render
- 微任务包括process.nextTick、Promise(then\catch\finally)、Async/Await(await会把他后面的代码注册到微任务中,不包括他自己那行)、MutationObserver(html5新特性)
总结:执行一个宏任务,然后执行目前微任务队列中全部微任务,再执行一个宏任务,以此反复执行。
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// 旧版输出如下,但是请继续看完本文下面的注意那里,新版有改动
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
分析:
setTimeout(() => {
task()
},3000)
sleep(10000000)
-
task()进入Event Table并注册,计时开始。
-
执行sleep函数,很慢,非常慢,计时仍在继续。
-
3秒到了,计时事件timeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。
-
sleep终于执行完了,task()终于从Event Queue进入了主线程执行。
8.异步编程专题
1.异步编程的实现方式?
答:
- 回调函数:将函数作为参数传递给其他函数,当操作完成时调用回调函数,会造成回调地狱。
function doSomethingAsync(callback) {
setTimeout(function() {
callback("操作完成");
}, 1000);
}
doSomethingAsync(function(result) {
console.log(result);
});
- Promise方式:使用Promise的方式可以将嵌套的回调函数作为链式调用。
function doSomethingAsync() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("操作完成");
}, 1000);
});
}
doSomethingAsync().then(function(result) {
console.log(result);
});
- generator的方式:生成器是一个特殊的函数,可以暂停其执行,并在需要时重新开始执行。
function* doSomethingAsync() {
yield new Promise(function(resolve, reject) {
setTimeout(function() {
resolve("操作完成");
}, 1000);
});
}
const generator = doSomethingAsync();
generator.next().value.then(function(result) {
console.log(result);
});
- async 函数 的方式:async函数是generator和promise实现的一个自动执行的语法糖,它内部自带执行器,当函数内部运行到一个await语句的时候,如果语句返回一个promise对象,那么函数将会等待promise对象的状态变成resolve后再继续向下执行。
2.setTimeout、Promise、Async/Await的区别?
答:setTimeout是一个异步api,用于在指定时间后执行回调函数,Promise是ES6引入的一种处理异步操作的对象,Promise对象有三种状态:pending(进行中)、fulfilled(已完成)和rejected(已拒绝),可以使用then()方法处理异步操作的结果,并用catch()方法处理错误。Async/Await是ES8引入的一种处理异步操作的语法,他是基于Promise对象的语法糖,通过async关键字定义一个异步函数,使用await关键字等待一个Promise对象解决,并以同步的形式处理异步操作。
3.介绍一下Promise是什么?
答: Promise对象是异步编程的一种解决方案,Promise是一个构造函数,接收一个函数作为参数,返回一个Promise实例。一个Promise实例有三种状态,分别是pending、resolved和rejected,分别代表了进行中,已成功和已失败。实例的状态只能由pending转变resolved和rejected状态,并且状态一经改变,就无法改变。同时状态的改变是通过resolve()和reject()函数来实现的,可以在异步操作结束后调用这两个函数改变Promise实例的状态,它的原型上定义了一个then方法,使用这个then方法可以为两个状态的改变注册回调函数,这个回调函数属于微任务。
4.Promise常见的方法有哪些?
- then()方法可以接受两个回调函数作为参数,第一个回调函数是Promise对象的状态变为resolved时调用,第二个回调函数是Promise对象状态变为rejected时调用,其中第二个参数可以省略。
- catch()当Promise对象的状态变为rejected时调用。
- all()方法可以完成并行任务,它接收一个数组,数组的每一项都是一个promise对象,当数组中所有的promise的状态都达成resolved的时候,all方法的状态会变成resolved,如果有一个状态变成了rejected,那么all方法的状态就会变成rejected。
- race()接受的参数是一个每项都是promise的数组,当最先执行完的事件执行完之后,就会直接返回该promise对象的值,如果第一个完成promise对象状态变成resolved,那自身的状态变成resolved。反之,第一个promise变成rejected,那么自身状态就变成rejected。 5.finally()方法用于不管Promise对象最后状态如何,都会执行的操作。
5.Promise解决了什么问题?
答:解决了以下几个问题:
- 回调地狱:通过Promise的链式调用,可以避免多层嵌套的回调函数,使代码结构更加清晰简洁。
- 异步操作结果的处理:Promise提供了then()方法,用于处理异步操作的结果,并将结果传递给下一个then()方法,使得处理异步操作的结果更加灵活和方便。
- 异步操作错误的处理:promise提供了catch()方法,用于捕获和处理异步操作产生的错误。
6.对async/await的理解?
答:async函数是一个特殊的函数,它会返回一个promise对象,通过在函数前面加上async关键词,函数内部的代码就可以使用await关键字来等待一个异步操作的完成。在使用await关键字时,函数会暂停执行,直到等待的异步操作完成并返回结果,这样可以使得异步操作的写法看起来像同步操作,而不需要使用回调函数或者链式调用。
7.await到底在等待什么?
答:await表达式的运算结果取决于它等的是什么,如果它等到的不是一个promise对象,那await表达式的运算结果就是它等到的东西,如果它等到的是一个Promise对象,await就会阻塞后面的代码,等着Promise对象状态变为resolved,它返回的结果作为await表达式的值。
async/await对比Promise的优势是什么?
答:
-
代码看起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调用也会带来额外的阅读负担。
-
Promise传递中间值比较复杂,而async和await几乎就是同步的写法,比较简单。
-
Promise需要使用then、catch链式的捕获错误,但是async/await可以使用try..catch,很容易。
垃圾回收机制专题
1.介绍一下js的垃圾回收机制GC(什么是垃圾回收机制)?
答:GC即Garbage collection,程序工作过程中会产生很多垃圾,这些垃圾是程序不用的内存或者是之前用过了,以后不会再用的内存空间,而GC就是负责回收垃圾的,这一套引擎执行则称为垃圾回收机制。
2. 为什么要进行垃圾回收?
答:程序的运行需要内存,只要程序提出要求,操作系统或者运行时就必须提供内存,那么对于持续运行的服务进程,必须要及时释放内存,否则,内存占用越来越高,轻则影响系统性能,重则就会导致进程崩溃。
3.垃圾回收策略有哪些?
主要有两种垃圾回收策略,第一种是标记清除算法,第二种是引用计数算法
标记清除算法执行过程如下: 垃圾收集器在运行时会给内存中所有变量加上一个标记,假设内存中所有对象都是垃圾,全标记为0; 然后从各个根对象开始遍历,把不是垃圾的结点改为1; 清理所有标记为0的垃圾,销毁并回收他们所占用的内存空间; 最后,把所有内存中对象标记修改为0,等待下一轮垃圾回收。
优点: 实现比较简单,打标记只有打和不打两种情况,这使得一位二进制位(0/1)就可以为其标记,非常简单。
缺点: 标记清除算法在清楚之后,剩余的内存位置是不变的,也会导致空闲内存空间是不连续的,出现了内存碎片。
引用计数算法执行过程如下: 当声明了一个变量并且将一个引用类型赋值给该变量的时候这个值得引用次数为1 如果同一个值又被赋给另一个变量,那么引用数加1 如果该变量的值被其他的值覆盖了,则 引用次数减1 当这个值得引用次数为0时,说明没有变量在使用,这个值没法被访问了,垃圾回收器会在运行的时候清理掉有引用次数为0的值占用的内存。
优点: 引用计数在引用值为0时,也就是在变成垃圾的那一刻就会被回收,所以它可以立即回收垃圾。
缺点: 无法解决循环引用无法回收的问题
function test(){
let A = new Object()
let B = new Object()
A.b = B
B.a = A
}
分代式垃圾回收 执行过程: 分代式机制把一些新、小、存活时间短的对象作为新生代,采用一小块内存频率较高的快速清理,而一些大、老、存活时间长的对象作为老生代,使其很少接手检查,新老生代的回收机制及频率是不同的,可以说此机制的出现提高了垃圾回收机制的效率。
4.如何减少垃圾回收?
答:
- 对数组进行优化:在清空一个数组时,最简单的方式是将数组length设置为0,以此达到清空数组的目的。
- 对object进行优化:对象尽量复用,对于不再使用的对象,就将其设置为null,尽快被回收。
5.哪些情况会导致内存泄漏?
答:以下四种情况会造成内存泄漏:
- 意外的全局变量: 由于使用未声明的变量,而意外创建了一个全局变量,而使这个变量一直留在内存中无法回收。
- 被遗忘的计数器或回调函数: 设置了setInteval定时器,忘记取消他,如果循环函数有对外部变量的引用的话,那么这个变量会被一直留在内存中,而无法被回收。
- 脱离DOM的引用: 获取一个DOM元素的引用,而后面这个元素被删除,由于一直保留了这个元素的引用,所以它也无法被回收。
- 闭包: 不合理的使用闭包,从而导致某些变量一直都被留内存当中。