赞
踩
一、html和css部分
1、如何理解CSS的盒子模型?
标准盒子模型:宽度=内容的宽度(content)+ border + padding
低版本IE盒子模型:宽度=内容宽度(content+border+padding)
2、BFC?
什么是 BFC
BFC(Block Formatting Context)格式化上下文,是 Web 页面中盒模型布局的 CSS 渲染模式,指一个独立的渲染区域或者说是一个隔离的独立容器。
形成 BFC 的条件
1) 浮动元素,float 除 none 以外的值
2) 定位元素,position(absolute,fixed)
3) display 为以下其中之一的值 inline-block,table-cell,table-caption
4) overflow 除了 visible 以外的值(hidden,auto,scroll)
BFC 的特性
1) 内部的 Box 会在垂直方向上一个接一个的放置。
2) 垂直方向上的距离由 margin 决定
3) bfc 的区域不会与 float 的元素区域重叠。
4) 计算 bfc 的高度时,浮动元素也参与计算
5) bfc 就是页面上的一个独立容器,容器里面的子元素不会影响外面素。
3、如何清除浮动?
不清除浮动会发生高度塌陷:
浮动元素父元素高度自适应(父元素不写高度时,子元素写了浮动后,父元素会发生高度塌陷)
1) clear清除浮动(添加空div法)在浮动元素下方添加空div,并给该元素写css样式: {clear:both;height:0;overflow:hidden;}
2) 给浮动元素父级设置高度
3) 父级同时浮动(需要给父级同级元素添加浮动)
4) 父级设置成inline-block,其margin: 0 auto居中方式失效
5) 给父级添加overflow:hidden 清除浮动方法
6) 万能清除法 after伪类 清浮动(现在主流方法,推荐使用)
.float_div:after{
content:".";
clear:both;
display:block;
height:0;
overflow:hidden;
visibility:hidden;
}
.float_div{
zoom:1
}
4、用纯CSS创建一个三角形的原理是什么?
span {
width: 0;
height: 0;
border-top: 40px solid transparent;
border-left: 40px solid transparent;
border-right: 40px solid transparent;
border-bottom: 40px solid #ff0000;
}
5、css3实现0.5px的细线?
/* css */
.line {
position: relative;
}
.line:after {
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
background-color: #000000;
-webkit-transform: scaleY(.5);
transform: scaleY(.5);
}
/* html */
<div class="line"></div>
6、css实现三栏布局
左右固定,中间自适应的情况下。
1) flex方式
<style>
.box {
display: flex;
justify-content: center;
height: 200px;
}
.left {
width: 200px;
background-color: red;
height: 100%;
}
.content {
background-color: yellow;
flex: 1;
}
.right {
width: 200px;
background-color: green;
}
</style>
</head>
<body>
<div class="box">
<div class="left"></div>
<div class="content"></div>
<div class="right"></div>
</div>
</body>
</html>
2)绝对定位方式
<style>
.box {
position: relative;
height: 200px;
}
.left {
width: 200px;
background-color: red;
left: 0;
height: 100%;
position: absolute;
}
.content {
background-color: yellow;
left: 200px;
right: 200px;
height: 100%;
position: absolute;
}
.right {
width: 200px;
background-color: green;
right: 0;
height: 100%;
position: absolute;
}
</style>
</head>
<body>
<div class="box">
<div class="left"></div>
<div class="content"></div>
<div class="right"></div>
</div>
</body>
</html>
3)浮动方式
<style>
.box {
height: 200px;
}
.left {
width: 200px;
background-color: red;
float: left;
height: 100%;
}
.content {
background-color: yellow;
height: 100%;
}
.right {
width: 200px;
background-color: green;
float: right;
height: 100%;
}
</style>
</head>
<body>
<div class="box">
<div class="left"></div>
<div class="right"></div>
<div class="content"></div>
</div>
</body>
</html>
7、 让一个div垂直居中
1)宽度和高度已知的 margin负值法
<style>
.box {
width: 400px;
height: 200px;
position: relative;
background: red;
}
.content {
width: 200px;
height: 100px;
position: absolute;
top: 50%;
left: 50%;
margin-left: -100px;
margin-top: -50px;
background: green;
}
</style>
</head>
<body>
<div class="box">
<div class="content"></div>
</div>
</body>
</html>
2)宽度和高度未知transform
<style>
.box {
width: 400px;
height: 200px;
position: relative;
background: red;
}
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: green;
}
</style>
</head>
<body>
<div class="box">
<div class="content"></div>
</div>
</body>
</html>
3)flex布局
<style>
.box {
width: 400px;
height: 200px;
background: red;
display: flex;
justify-content: center;
align-items: center;
}
.content {
width: 200px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div class="box">
<div class="content"></div>
</div>
</body>
</html>
另外还有其他的写法,以下写法不用关注html结构,只关注css些法即可
4)margin:auto法
<style>
.box {
width: 400px;
height: 200px;
background: red;
display: flex;
justify-content: center;
align-items: center;
}
.content {
width: 200px;
height: 100px;
background: green;
}
</style>
</head>
<body>
<div class="box">
<div class="content"></div>
</div>
</body>
</html>
5)table-cell(未脱离文本流)
div{
width: 300px;
height: 300px;
border: 3px solid #555;
display: table-cell;
vertical-align: middle;
text-align: center;
}
img{
vertical-align: middle;
}
8、Doctype作用?严格模式与混杂模式
Doctype声明于文档最前面,告诉浏览器以何种方式来渲染页面,这里有两种模式,严格模式和混杂模式。
严格模式的排版和 JS 运作模式是 以该浏览器支持的最高标准运行。
混杂模式,向后兼容,模拟老式浏览器,防止浏览器无法兼容页面。
9、link标签和import标签的区别
1) link属于html标签,而@import是css提供的
2) 页面被加载时,link会同时被加载,而@import引用的css会等到页面加载结束后加载。
3) link是html标签,因此没有兼容性,而@import只有IE5以上才能识别。
4) link方式样式的权重高于@import的。
5) link可以使用 js 动态引入,@import不行
10、flex布局
Flex是Flexible Box的缩写,意为"弹性布局",用来为盒状模型提供最大的灵活性。 布局的传统解决方案,基于盒状模型,依赖 display属性 + position属性 + float属性。它对于那些特殊布局非常不方便,比如,垂直居中就不容易实现。
(一).任何一个容器都可以指定为Flex 布局。
.box{
display:flex;
}
行内元素也可以使用Flex布局。
.box{
display:inline-flex;
}
webkit内核的浏览器,必需加上-webkit前缀
1
2
3
4 .box{
display:-webkit-flex;
display:flex;
}
需要注意的是,设为flex布局以后,子元素的float、clear和vertical-align属性将失效
(二)、基本概念
采用Flex布局的元素,称为Flex容器(flex container),简称“容器”。它的所有子元素自动成为容器成员,成为flex项目(flex item),简称“项目”。
容器默认存在两根轴:水平的主轴(main axis)和垂直的交叉轴(cross axis)。主轴的开始位置(与边框 的交叉点)叫做main start,结束位置叫做main end;交叉轴的开始位置叫做cross start,结束位置叫做cross end。
项目默认沿主轴排列。单个项目占据的主轴空间叫做main size,占据的交叉轴空间叫做cross size。
(三)、容器的属性
以下6个属性设置在容器上。
• flex-direction
• flex-wrap
• flex-flow
• justify-content
• align-items
• align-content
flex-direction属性
flex-direction属性决定主轴的方向(即项目的排列方向)。
.box{
flex-direction:row | row-reverse | column | column-reverse;
}
它可能有四个值
• row(默认值):主轴为水平方向,起点在左端
• row-reverse:主轴为水平方向,起点在右端
• column:主轴为垂直方向,起点在上沿
• column-reverse:主轴为垂直方向,起点在下沿
flex-wrap属性
默认情况下,项目都排在一条线(又称“轴线”)上。flex-wrap属性定义,如果一条轴线 排不下,如何换行?
.box{
flex-wrap: nowrap | wrap | wrap-reverse;
}
它可能去三个值。
(1)nowrap(默认):不换行
(2)wrap:换行,第一行在上方
(3)wrap-reverse:换行,在第一行的下方
flex-flow
flex-flow属性是flex-direction属性和flex-wrap属性的简写形式,默认 row nowrap。
.box{
flex-flow:<flex-direction> || <flex-wrap>;
}
justify-content属性
justify-content属性定义了项目在主轴上的对齐方式
.box{
justify-content:flex-start | flex-end | center | space-between |space-around;
}
它可能取5个值,具体对齐方式与轴的方向有关。下面假设主轴为从左到右。
• flex-start(默认值):左对齐
• flex-end:右对齐
• center:居中
• space-between:两端对齐,项目之间的间隔都相等
• space-around:每个项目两侧的间隔相等。所以,项目之间的间隔比项目与边框的间隔大一倍。
align-items属性
align-items属性定义项目在交叉轴上如何对齐。
.box{
align-items:flex-start | flex-end | center |baseline | stretch;
}
它可能取5个值。具体的对齐方式与交叉轴的方向有关,下面假设交叉轴从上之下。
• flex-start:交叉轴的起点对齐
• flex-end:交叉轴的终点对齐
• center:交叉轴的中点对齐
• baseline:项目的第一行文字的基线对齐。
• stretch(默认值):如果项目未设置高度或设为auto,将占满整个容器的高度。
align-content属性
align-content属性定义了多根轴线的对齐方式。如果项目只有一根轴线,该属性不起作用。
.box{
align-content:flex-start | flex-end | center | spance-between | space-around |stretch;
}
该属性可能取6个值。
• flex-start:与交叉轴的起点对齐。
• flex-end:与交叉轴的终点对齐。
• center:与交叉轴的中点对齐。
• space-between:与交叉轴两端对齐,轴线之间的间隔平均分布。
• space-around:每根轴线两侧的间隔都相等。所以,轴线之间的间隔比轴线与边框的间隔大一倍。
• stretch(默认值):轴线占满整个交叉轴。
11、多行元素的文本省略号
display: -webkit-box
-webkit-box-orient:vertical
-web-line-clamp:3
overflow:hidden
12、双边距重叠问题
多个相邻(兄弟或者父子关系)普通流的块元素垂直方向marigin会重叠
折叠的结果为:
两个相邻的外边距都是正数时,折叠结果是它们两者之间较大的值。 两个相邻的外边距都是负数时,折叠结果是两者绝对值的较大值。 两个外边距一正一负时,折叠结果是两者的相加的和。
二、JS
1、闭包
闭包概念
能够读取其他函数内部变量的函数。
或简单理解为定义在一个函数内部的函数,内部函数持有外部函数内变量的引用。
闭包用途
读取函数内部的变量
让这些变量的值始终保持在内存中。不会再f1调用后被自动清除。
方便调用上下文的局部变量。利于代码封装。
原因:
f1是f2的父函数,f2被赋给了一个全局变量,f2始终存在内存中,f2的存在依赖f1,因此f1也始终存在内存中,不会在调用结束后,被垃圾回收机制回收。
闭包缺点
1) 由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题,在IE中可能导致内存泄露。解决方法是:在退出函数之前,将不使用的局部变量全部删除。
2)闭包会在父函数外部,改变父函数内部变量的值。所以,如果你把父函数当作对象(object)使用,把闭包当作它的公用方法(Public Method),把内部变量当作它的私有属性(private value),这时一定要小心,不要随便改变父函数内部变量的值。
产生一个闭包:
创建闭包最常见方式,就是在一个函数内部创建另一个函数。下面例子中的 closure 就是一个闭包:
function func(){
var a = 1,b = 2;
function closure(){
return a+b;
}
return closure;
}
闭包的作用域链包含着它自己的作用域,以及包含它的函数的作用域和全局作用域。
闭包的注意事项:
通常,函数的作用域及其所有变量都会在函数执行结束后被销毁。但是,在创建了一个闭包以后,这个函数的作用域就会一直保存到闭包不存在为止。
function makeAdder(x) {
return function(y) {
return x + y;
};
}
var add5 = makeAdder(5);
var add10 = makeAdder(10);
console.log(add5(2)); // 7
console.log(add10(2)); // 12
// 释放对闭包的引用
add5 = null;
add10 = null;
add5 和 add10 都是闭包。它们共享相同的函数定义,但是保存了不同的环境。在 add5 的环境中,x 为 5。而在 add10 中,x 则为 10。最后通过 null 释放了 add5 和 add10 对闭包的引用。
在javascript中,如果一个对象不再被引用,那么这个对象就会被垃圾回收机制回收;
如果两个对象互相引用,而不再被第3者所引用,那么这两个互相引用的对象也会被回收。
闭包只能取得包含函数中任何变量的最后一个值,这是因为闭包所保存是整个变量对象,而不是某个特殊的变量。
function test(){
var arr = [];
for(var i = 0;i < 10;i++){
arr[i] = function(){
return i;
};
}
for(var a = 0;a < 10;a++){
console.log(arr[a]());
}
}
test(); // 连续打印 10 个 10
对于上面的情况,如果我们改变代码如下:
function test(){
var arr = [];
for(let i = 0;i < 10;i++){ // 仅在这里作出了改动
arr[i] = function(){
return i;
};
}
for(var a = 0;a < 10;a++){
console.log(arr[a]());
}
}
test(); // 打印 0 到 9
this的使用:
var name = "The Window";
var obj = {
name: "My Object",
getName: function(){
return function(){
return this.name;
};
}
};
console.log(obj.getName()()); // The Window
obj.getName()()实际上是在全局作用域中调用了匿名函数,this指向了window。这里要理解函数名与函数功能(或者称函数值)是分割开的,不要认为函数在哪里,其内部的this就指向哪里。匿名函数的执行环境具有全局性,因此其 this 对象通常指向 window。
var name = "The Window";
var obj = {
name: "My Object",
getName: function(){
var that = this;
return function(){
return that.name;
};
}
};
console.log(obj.getName()()); // My Object
闭包的应用
应用闭包的主要场合是:
设计私有的方法和变量。
任何在函数中定义的变量,都可以认为是私有变量,因为不能在函数外部访问这些变量。私有变量包括函数的参数、局部变量和函数内定义的其他函数。
把有权访问私有变量的公有方法称为特权方法(privileged method)。
function Animal(){
// 私有变量
var series = "哺乳动物";
function run(){
console.log("Run!!!");
}
// 特权方法
this.getSeries = function(){
return series;
};
}
模块模式(The Module Pattern):为单例创建私有变量和方法。
单例(singleton):指的是只有一个实例的对象。JavaScript 一般以对象字面量的方式来创建一个单例对象。
var singleton = {
name: "percy",
speak:function(){
console.log("speaking!!!");
},
getName: function(){
return this.name;
}
};
上面是普通模式创建的单例,下面使用模块模式创建单例:
var singleton = (function(){
// 私有变量
var age = 22;
var speak = function(){
console.log("speaking!!!");
};
// 特权(或公有)属性和方法
return {
name: "percy",
getAge: function(){
return age;
}
};
})();
匿名函数最大的用途是创建闭包,并且还可以构建命名空间,以减少全局变量的使用。从而使用闭包模块化代码,减少全局变量的污染。
var objEvent = objEvent || {};
(function(){
var addEvent = function(){
// some code
};
function removeEvent(){
// some code
}
objEvent.addEvent = addEvent;
objEvent.removeEvent = removeEvent;
})();
在这段代码中函数 addEvent 和 removeEvent 都是局部变量,但我们可以通过全局变量 objEvent 使用它,这就大大减少了全局变量的使用,增强了网页的安全性。
一个闭包计数器
var countNumber = (function(){
var num = 0;
return function(){
return ++num;
};
})();
闭包的缺陷:
闭包的缺点就是常驻内存会增大内存使用量,并且使用不当很容易造成内存泄露。
如果不是因为某些特殊任务而需要闭包,在没有必要的情况下,在其它函数中创建函数是不明智的,因为闭包对脚本性能具有负面影响,包括处理速度和内存消耗。
最后再来一些有关闭包的面试题
下面代码中,标记 ? 的地方输出分别是什么?
function fun(n,o){
console.log(o);
return {
fun: function(m){
return fun(m,n);
}
};
}
var a = fun(0); // ?
a.fun(1); // ?
a.fun(2); // ?
a.fun(3); // ?
var b = fun(0).fun(1).fun(2).fun(3); // ?
var c = fun(0).fun(1); // ?
c.fun(2); // ?
c.fun(3); // ?
undefined
0
0
0
undefined, 0, 1, 2
undefined, 0
1
1
2、js中函数执行
在 ES5.1 里面函数是这样执行的(不讨论use strict和一些特殊情况),按如下顺序执行:
1) 确定“this”的值 (确切的来说,this在JS里面不是一个变量名而是一个关键字)
2) 创建一个新的作用域
3) 处理形参/实参(没有定义过才声明,无论如何都重新赋值,没有对应实参则赋值为"undefined"):
对于每一个传入的实参,按照从左往右的顺序依次执行:如果对应的形参在本作用域中还没有定义,则在本作用域中声明形参,并赋值。如果已经定义过了,则重新给其赋值。(没有对应实参则赋值为"undefined")(没有定义:就是“没有声明”的意思)
4) 处理函数定义(没有定义过才声明,无论如何都重新赋值):
对该函数中所有的定义的函数,按照代码写的顺序依次执行:如果这个变量名在本作用域中还没有定义,则在本作用域中声明这个函数名,并且赋值为对应的函数,如果定义了这个变量,在可写的情况下重新给这个变量赋值为这个函数,否则抛出异常。
5)处理 "arguments"(没有定义过才声明和赋值):
如果在本作用域中没有定义 arguments,则在本作用域中声明arguments并给其赋值。
6)处理变量声明(没有定义过才声明,不赋值):
对于所有变量声明,按照代码写的顺序依次执行:如果在本作用域中没有定义这个变量,则在本作用域中声明这个变量,赋值为undefined
7)然后执行函数代码。(当然是去变量定义里面的 var 执行)
3、new一个对象的过程中发生了什么嘛
1. 创建一个空对象,构造函数中的this指向这个空对象
2. 这个新对象被执行 [[原型]] 连接
3. 执行构造函数方法,属性和方法被添加到this引用的对象中
4. 如果构造函数中没有返回其它对象,那么返回this,即创建的这个的新对象,否则,返回构造函数中返回的对象。
function _new(){
// 创建新对象
let target = {}
//第一个参数是构造函数
let [constructor, ...args] = [...arguments];
//执行[[原型]]连接;target是constructor的实例
target.__proto__ = constructor.prototype;
//执行构造函数,将属性或者方法添加到创建的空对象上
let result = constructor.apply(target,args);
if (result && (typeof (result) == "object" || typeof (result) == "function")){
//如果构造函数执行的结果返回的是一个对象,那么返回这个对象
return result;
}
//如果构造函数执行的结果返回的不是一个对象,那么返回创建的新对象
return target;
}
new 的作用:
我们先来通过两个例子来了解 new 的作用
function Test(name) {
this.name = name
}
Test.prototype.sayName = function () {
console.log(this.name)
}
const t = new Test('yck')
console.log(t.name) // 'yck'
t.sayName() // 'yck'
从上面一个例子中我们可以得出这些结论:
new 通过构造函数 Test 创建出来的实例可以访问到构造函数中的属性
new 通过构造函数 Test 创建出来的实例可以访问到构造函数原型链中的属性,也就是说通过 new 操作符,实例与构造函数通过原型链连接了起来
但是当下的构造函数 Test 并没有显式 return 任何值(默认返回 undefined),如果我们让它返回值会发生什么事情呢?
function Test(name) {
this.name = name
return 1
}
const t = new Test('yck')
console.log(t.name) // 'yck'
虽然上述例子中的构造函数中返回了 1,但是这个返回值并没有任何的用处,得到的结果还是和之前的例子完全一样。
那么通过这个例子,我们又可以得出一个结论:
构造函数如果返回原始值(虽然例子中只有返回了 1,但是你可以试试其他的原始值,结果还是一样的),那么这个返回值毫无意义
试完了返回原始值,我们再来试试返回对象会发生什么事情吧
function Test(name) {
this.name = name
console.log(this) // Test { name: 'yck' }
return { age: 26 }
}
const t = new Test('yck')
console.log(t) // { age: 26 }
console.log(t.name) // 'undefined'
通过这个例子我们可以发现,虽然构造函数内部的 this 还是依旧正常工作的,但是当返回值为对象时,这个返回值就会被正常的返回出去。
那么通过这个例子,我们再次得出了一个结论:
构造函数如果返回值为对象,那么这个返回值会被正常使用
这两个例子告诉了我们一点,构造函数尽量不要返回值。因为返回原始值不会生效,返回对象会导致 new 操作符没有作用。
通过以上几个例子,相信大家也大致了解了 new 操作符的作用了,接下来我们就来尝试自己实现 new 操作符。
自己实现 new 操作符:
首先我们再来回顾下 new 操作符的几个作用
new 操作符会返回一个对象,所以我们需要在内部创建一个对象
这个对象,也就是构造函数中的 this,可以访问到挂载在 this 上的任意属性
这个对象可以访问到构造函数原型上的属性,所以需要将对象与构造函数链接起来
返回原始值需要忽略,返回对象需要正常处理
回顾了这些作用,我们就可以着手来实现功能了
function create(Con, ...args) {
let obj = {}
Object.setPrototypeOf(obj, Con.prototype)
let result = Con.apply(obj, args)
return result instanceof Object ? result : obj
}
这就是一个完整的实现代码,我们通过以下几个步骤实现了它:
首先函数接受不定量的参数,第一个参数为构造函数,接下来的参数被构造函数使用
然后内部创建一个空对象 obj
因为 obj 对象需要访问到构造函数原型链上的属性,所以我们通过 setPrototypeOf 将两者联系起来。这段代码等同于 obj.__proto__ = Con.prototype
将 obj 绑定到构造函数上,并且传入剩余的参数
判断构造函数返回值是否为对象,如果为对象就使用构造函数返回的值,否则使用 obj,这样就实现了忽略构造函数返回的原始值
接下来我们来使用下该函数,看看行为是否和 new 操作符一致
function Test(name, age) {
this.name = name
this.age = age
}
Test.prototype.sayName = function () {
console.log(this.name)
}
const a = create(Test, 'yck', 26)
console.log(a.name) // 'yck'
console.log(a.age) // 26
a.sayName() // 'yck'
4、宏任务跟微任务
• macro-task(宏任务):包括整体代码script,setTimeout,setInterval
• micro-task(微任务):Promise,process.nextTick
5、作用域
原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。
• 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined;
• 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。
6、数组的常用方法
改变原数组的方法
1)splice() 添加/删除数组元素
语法:arrayObject.splice(index,howmany,item1,.....,itemX)
参数:
1.index:必需。整数,规定添加/删除项目的位置,使用负数可从数组结尾处规定位置。
2.howmany:可选。要删除的项目数量。如果设置为 0,则不会删除项目。
3.item1, ..., itemX: 可选。向数组添加的新项目。
返回值: 如果有元素被删除,返回包含被删除项目的新数组。
2) sort() 数组排序
语法:arrayObject.sort(sortby)
参数:
1.sortby 可选。规定排序顺序。必须是函数。。
返回值: 返回包排序后的新数组。
3)pop() 删除一个数组中的最后的一个元素
语法:arrayObject.pop()
参数:无
返回值: 返回被删除的元素。
4) shift() 删除数组的第一个元素
语法:arrayObject.shift()
参数:无
返回值: 返回被删除的元素。
5)push() 向数组的末尾添加元素
语法:arrayObject.push(newelement1,newelement2,....,newelementX)
参数:
1.newelement1 必需。要添加到数组的第一个元素。
2.newelement2 可选。要添加到数组的第二个元素。
3.newelementX 可选。可添加若干个元素。
返回值: arrayObject 的新长度。
6) unshift() 向数组的开头添加一个或更多元素
语法:arrayObject.unshift(newelement1,newelement2,....,newelementX)
参数:
1.newelement1 必需。要添加到数组的第一个元素。
2.newelement2 可选。要添加到数组的第二个元素。
3.newelementX 可选。可添加若干个元素。
返回值: arrayObject 的新长度。
7) reverse() 颠倒数组中元素的顺序
语法:arrayObject.reverse()
参数:无
返回值: 颠倒后的新数组。
8)copyWithin() 指定位置的成员复制到其他位置
语法: array.copyWithin(target, start = 0, end = this.length)
参数:
1.target(必需):从该位置开始替换数据。如果为负值,表示倒数。
2.start(可选):从该位置开始读取数据,默认为 0。如果为负值,表示倒数。
3.end(可选):到该位置前停止读取数据,默认等于数组长度。如果为负值,表示倒数。
返回值: 返回当前数组。
9) fill() 填充数组
语法: array.fill(value, start, end)
参数:
1.value 必需。填充的值。
2.start 可选。开始填充位置。
3.end 可选。停止填充位置 (默认为 array.length)
返回值: 返回当前数组。
________________________________________
不改变原数组的方法
1) slice() 浅拷贝数组的元素
语法: array.slice(begin, end);
参数:
1.begin(可选): 索引数值,接受负值,从该索引处开始提取原数组中的元素,默认值为0。
2.end(可选):索引数值(不包括),接受负值,在该索引处前结束提取原数组元素,默认值为数组末尾(包括最后一个元素)。
返回值: 返回一个从开始到结束(不包括结束)选择的数组的一部分浅拷贝到一个新数组对象,且原数组不会被修改。
2) join() 数组转字符串
语法:array.join(str)
参数:
1.str(可选): 指定要使用的分隔符,默认使用逗号作为分隔符。
返回值: 返回生成的字符串。
3) concat() 合并两个或多个数组
语法: var newArr =oldArray.concat(arrayX,arrayX,......,arrayX)
参数:
1.arrayX(必须):该参数可以是具体的值,也可以是数组对象。可以是任意多个。
返回值: 返回返回合并后的新数组。
4) indexOf() 查找数组是否存在某个元素
语法:array.indexOf(searchElement,fromIndex)
参数:
1.searchElement(必须):被查找的元素
2.fromIndex(可选):开始查找的位置(不能大于等于数组的长度,返回-1),接受负值,默认值为0。
返回值: 返回下标
5) lastIndexOf() 查找指定元素在数组中的最后一个位置
语法:arr.lastIndexOf(searchElement,fromIndex)
参数:
1.searchElement(必须): 被查找的元素
2.fromIndex(可选): 逆向查找开始位置,默认值数组的长度-1,即查找整个数组。
返回值: 方法返回指定元素,在数组中的最后一个的索引,如果不存在则返回 -1。(从数组后面往前查找)
6)includes() 查找数组是否包含某个元素
语法: array.includes(searchElement,fromIndex=0)
参数:
1.searchElement(必须):被查找的元素
2.fromIndex(可选):默认值为0,参数表示搜索的起始位置,接受负值。正值超过数组长度,数组不会被搜索,返回false。负值绝对值超过长数组度,重置从0开始搜索。
返回值: 返回布尔
7、立即执行函数
声明一个匿名函数,马上调用这个匿名函数。目的是保护内部变量不受污染。
(function(n1, n2) {
console.log("这是匿名函数的自执行的第一种写法,结果为:" + (n1 + n2))
})(10, 100);
(function start(n1, n2) {
console.log("这是函数声明方式的自执行的第一种写法,结果为:" + (n1 + n2))
})(10, 100);
(function(n1, n2) {
console.log("这是匿名函数的自执行的第二种写法,结果为:" + (n1 + n2))
}(10, 100));
(function start(n1, n2) {
console.log("这是函数声明方式的自执行的第二种写法,结果为:" + (n1 + n2))
}(10, 100));
复制代码
8、js原型和原型链
原型链是由原型对象组成,每个对象都有 __proto__ 属性,指向了创建该对象的构造函数的原型,__proto__ 将对象连接起来组成了原型链。是一个用来实现继承和共享属性的有限的对象链。
• 属性查找机制: 当查找对象的属性时,如果实例对象自身不存在该属性,则沿着原型链往上一级查找,找到时则输出,不存在时,则继续沿着原型链往上一级查找,直至最顶级的原型对象Object.prototype,如还是没找到,则输出undefined;
• 属性修改机制: 只会修改实例对象本身的属性,如果不存在,则进行添加该属性,如果需要修改原型的属性时,则可以用: b.prototype.x = 2;但是这样会造成所有继承于该对象的实例的属性发生改变。
每个对象都会在其内部初始化一个属性,就是prototype(原型),当我们访问一个对象的属性时,如果这个对象内部不存在这个属性,那么他就会去prototype里找这个属性,这个prototype又会有自己的prototype,于是就这样一直找下去,也就是我们平时所说的原型链的概念。
关系:instance.constructor.prototype = instance.proto
特点:JavaScript对象是通过引用来传递的,我们创建的每个新对象实体中并没有一份属于自己的原型副本,当我们修改原型时,与之相关的对象也会继承这一改变。 当我们需要一个属性时,JavaScript引擎会先看当前对象中是否有这个属性,如果没有的话,就会查找它的prototype对象是否有这个属性,如此递推下去,一致检索到Object内建对象。
function Func(){}
Func.prototype.name = "汪某";
Func.prototype.getInfo = function() {
return this.name;
}
var person = new Func();
console.log(person.getInfo());//"汪某"
console.log(Func.prototype);//Func { name = "汪某", getInfo = function() }
复制代码
9、如何解决异步回调地狱
promise、generator、async/await
10、Promise
一句话概括Promise:Promise对象用于异步操作,它表示一个尚未完成且预计在未来完成的异步操作。
promise是用来解决两个问题的:
• 回调地狱,代码难以维护,常常第一个的函数的输出是第二个函数的输入这种现象
• promise可以支持多个并发的请求,获取并发请求中的数据
这个promise可以解决异步的问题,本身不能说promise是异步的
/*Promise 的简单实现*/
class MyPromise {
constructor(fn) {
this.resolvedCallbacks = [];
this.rejectedCallbacks = [];
this.state = "PADDING";
this.value = "";
fn(this.resolve.bind(this), this.reject.bind(this));
}
resolve(value) {
if (this.state === "PADDING") {
this.state = "RESOLVED";
this.value = value;
this.resolvedCallbacks.forEach(cb => cb());
}
}
reject(value) {
if (this.state === "PADDING") {
this.state = "REJECTED";
this.value = value;
this.rejectedCallbacks.forEach(cb => cb());
}
}
then(resolve = function() {}, reject = function() {}) {
if (this.state === "PADDING") {
this.resolvedCallbacks.push(resolve);
this.rejectedCallbacks.push(reject);
}
if (this.state === "RESOLVED") {
resolve(this.value);
}
if (this.state === "REJECTED") {
reject(this.value);
}
}
}
复制代码
11、async/await
如何使用 Async 函数
async function timeout(ms) {
await new Promise((resolve) => {
setTimeout(resolve, ms);
});
}
async function asyncPrint(value, ms) {
await timeout(ms);
console.log(value);
}
asyncPrint('hello world', 50);
复制代码
上面代码指定50毫秒以后,输出hello world。 进一步说,async函数完全可以看作多个异步操作,包装成的一个 Promise 对象,而await命令就是内部then命令的语法糖。
待补充。。。
12、深拷贝、浅拷贝
浅拷贝和深拷贝都只针对于引用数据类型,浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存;但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象;
区别:
浅拷贝: 只复制对象的第一层属性
深拷贝: 可以对对象的属性进行递归复制;
*浅拷贝的实现方式
1.自定义函数
function simpleCopy (initalObj) {
var obj = {};
for ( var i in initalObj) {
obj[i] = initalObj[i];
}
return obj;
}
2.ES6 的 Object.assign()
let newObj = Object.assign({}, obj);
3.ES6 的对象扩展
let newObj = {...obj};
可以看出浅拷贝只最第一层属性进行了拷贝,当第一层的属性值是基本数据类型时,新的对象和原对象互不影响,但是如果第一层的属性值是复杂数据类型,那么新对象和原对象的属性值其指向的是同一块内存地址。
*深拷贝的实现方式
1.JSON.stringify 和 JSON.parse
用 JSON.stringify 把对象转换成字符串,再用 JSON.parse 把字符串转换成新的对象。
let newObj = JSON.parse(JSON.stringify(obj));
2.lodash
用 lodash 函数库提供的 _.cloneDeep 方法实现深拷贝。
var _ = require('lodash');
var newObj = _.cloneDeep(obj);
3.自己封装
1. 如果是基本数据类型,直接返回
2. 如果是 RegExp 或者 Date 类型,返回对应类型
3. 如果是复杂数据类型,递归。
4. 考虑循环引用的问题
function deepClone(obj) {
let objClone = Array.isArray(obj) ? [] : {};
if (obj && typeof obj === "object") {
// for...in 会把继承的属性一起遍历
for (let key in obj) {
// 判断是不是自有属性,而不是继承属性
if (obj.hasOwnProperty(key)) {
//判断ojb子元素是否为对象或数组,如果是,递归复制
if (obj[key] && typeof obj[key] === "object") {
objClone[key] = this.deepClone(obj[key]);
} else {
//如果不是,简单复制
objClone[key] = obj[key];
}
}
}
}
return objClone;
}
13、跨域
跨域需要针对浏览器的同源策略来理解,同源策略指的是请求必须是同一个端口,同一个协议,同一个域名,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。
受浏览器同源策略的影响,不是同源的脚本不能操作其他源下面的对象。想要操作另一个源下的对象是就需要跨域。
CORS 跨域
nginx反向代理
WebSockets
JSONP 只支持GET请求
hash + iframe 只支持GET请求
postMessage 只支持GET请求
document.domain
14、for in 和 for of
• for in
1.一般用于遍历对象的可枚举属性。以及对象从构造函数原型中继承的属性。对于每个不同的属性,语句都会被执行。
2.不建议使用for in 遍历数组,因为输出的顺序是不固定的。
3.如果迭代的对象的变量值是null或者undefined, for in不执行循环体,建议在使用for in循环之前,先检查该对象的值是不是null或者undefined
复制代码
• for of
1.for…of 语句在可迭代对象(包括 Array,Map,Set,String,TypedArray,arguments 对象等等)上创建一个迭代循环,调用自定义迭代钩子,并为每个不同属性的值执行语句
复制代码
遍历对象
var s = {
a: 1,
b: 2,
c: 3
}
var s1 = Object.create(s);
for (var prop in s1) {
console.log(prop); //a b c
console.log(s1[prop]); //1 2 3
}
for (let prop of s1) {
console.log(prop); //报错如下 Uncaught TypeError: s1 is not iterable
}
for (let prop of Object.keys(s1)) {
console.log(prop); // a b c
console.log(s1[prop]); //1 2 3
}
15、前端中的事件流?
什么是事件流:事件流描述的是从页面中接收事件的顺序,DOM2级事件流包括下面几个阶段。
• 事件捕获阶段
• 处于目标阶段
• 事件冒泡阶段
addEventListener:addEventListener 是DOM2 级事件新增的指定事件处理程序的操作,这个方法接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。
最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。
IE只支持事件冒泡。
奇淫技巧:捕母猫(捕获-目标-冒泡)
16、如何阻止冒泡?
冒泡型事件:事件按照从最特定的事件目标到最不特定的事件目标(document对象)的顺序触发。
w3c的方法是e.stopPropagation(),IE则是使用e.cancelBubble = true。
//阻止冒泡行为
function stopBubble(e) {
//如果提供了事件对象,则这是一个非IE浏览器
if ( e && e.stopPropagation )
//因此它支持W3C的stopPropagation()方法
e.stopPropagation();
else
//否则,我们需要使用IE的方式来取消事件冒泡
window.event.cancelBubble = true;
}
17、如何阻止默认事件?
w3c的方法是e.preventDefault(),IE则是使用e.returnValue = false
//阻止浏览器的默认行为
function stopDefault( e ) {
//阻止默认浏览器动作(W3C)
if ( e && e.preventDefault )
e.preventDefault();
//IE中阻止函数器默认动作的方式
else
window.event.returnValue = false;
return false;
}
18、事件委托?
• 简介:事件委托指的是,不在事件的发生地(直接dom)上设置监听函数,而是在其父元素上设置监听函数,通过事件冒泡,父元素可以监听到子元素上事件的触发,通过判断事件发生元素DOM的类型,来做出不同的响应。
• 举例:最经典的就是ul和li标签的事件监听,比如我们在添加事件时候,采用事件委托机制,不会在li标签上直接添加,而是在ul父元素上添加。
好处:
比较合适动态元素的绑定,新添加的子元素也会有监听函数,也可以有事件触发机制。
• 添加到页面上的事件数量会影响页面的运行性能,如果添加的事件过多,会导致网页的性能下降。采用事件代理的方式,可以大大减少注册事件的个数。
• 事件代理的当时,某个子孙元素是动态增加的,不需要再次对其进行事件绑定。
• 不用担心某个注册了事件的DOM元素被移除后,可能无法回收其事件处理程序,我们只要把事件处理程序委托给更高层级的元素,就可以避免此问题。
• 允许给一个事件注册多个监听。
• 提供了一种更精细的手段控制 listener 的触发阶段(可以选择捕获或者是冒泡)。
• 对任何 DOM 元素都是有效的,而不仅仅是对 HTML 元素有效。
19、var,let,const
//变量提升
console.log(a); // undefined
console.log(b); // 报错
console.log(c); // 报错
var a = 1;
let b = 2;
const c = 3;
// 全局声明
console.log(window.a) // 1
// 重复声明
let b = 200;//报错
复制代码
其实这里很容易理解,var是可以变量提升的。而let和const是必须声明后才能调用的。 对于let和const来说,这里就是暂缓性死区。
20、Class
es6新增的Class其实也是语法糖,js底层其实没有class的概念的,其实也是原型继承的封装。
class People {
constructor(props) {
this.props = props;
this.name = '汪某';
}
callMyName() {
console.log(this.name);
}
}
class Name extends People { // extends 其实就是继承了哪个类
constructor(props) {
// super相当于 把类的原型拿过来
// People.call(this, props)
super(props)
}
callMyApple() {
console.log('我是汪某!')
}
}
let a = new Name('啊啊啊')
a.callMyName(); //汪某
a.callMyApple(); // 我是汪某!
复制代码
21、Set
Set数据结构类似数组,但所有成员的值唯一。
let a = new Set();
[1,2,2,1,3,4,5,4,5].forEach(x=>a.add(x));
for(let k of a){
console.log(k)
};
// 1 2 3 4 5
复制代码
基本使用
let a = new Set([1,2,3,3,4]);
[...a]; // [1,2,3,4]
a.size; // 4
// 数组去重
[...new Set([1,2,3,4,4,4])];// [1,2,3,4]
复制代码
方法
• add(value):添加某个值,返回 Set 结构本身。
• delete(value):删除某个值,返回一个布尔值,表示删除是否成功。
• has(value):返回一个布尔值,表示该值是否为Set的成员。
• clear():清除所有成员,没有返回值。
let a = new Set();
a.add(1).add(2); // a => Set(2) {1, 2}
a.has(2); // true
a.has(3); // false
a.delete(2); // true a => Set(1) {1}
a.clear(); // a => Set(0) {}
复制代码
22、Map
Map结构提供了“值—值”的对应,是一种更完善的 Hash 结构实现。
let a = new Map();
let b = {name: 'leo' };
a.set(b,'my name'); // 添加值
a.get(b); // 获取值
a.size; // 获取总数
a.has(b); // 查询是否存在
a.delete(b); // 删除一个值
a.clear(); // 清空所有成员 无返回
复制代码
基本使用
• 传入数组作为参数,指定键值对的数组。
let a = new Map([
['name','wzx'],
['age',23]
])
• 如果对同一个键多次赋值,后面的值将覆盖前面的值。
let a = new Map();
a.set(1,'aaa').set(1,'bbb');
a.get(1); // 'bbb'
• 如果读取一个未知的键,则返回undefined。
new Map().get('asdsad'); // undefined
• 同样的值的两个实例,在 Map 结构中被视为两个键。
let a = new Map();
let a1 = ['aaa'];
let a2 = ['aaa'];
a.set(a1,111).set(a2,222);
a.get(a1); // 111
a.get(a2); // 222
方法
• keys():返回键名的遍历器。
• values():返回键值的遍历器。
• entries():返回所有成员的遍历器。
• forEach():遍历 Map 的所有成员。
let a = new Map([
['name', 'leo'],
['age', 18]
])
for (let i of a.keys()) {
console.log(i)
};
//name
//age
for (let i of a.values()) {
console.log(i)
};
//leo
//18
for (let i of a.entries()) {
console.log(i)
};
//["name", "leo"]
a.forEach((v, k, m) => {
console.log(`key:${k},value:${v},map:${m}`)
})
//["age", 18]
23、setTimeout倒计时为什么会出现误差
setTimeout 只能保证延时或间隔不小于设定的时间。因为它实际上只是将回调添加到了宏任务队列中 ,但是如果主线程上有任务还没有执行完成,它必须要等待。
如果你对前面这句话不是非常理解,那么有必要了解一下 JS的运行机制。
JS的运行机制
(1)所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。
(2)主线程之外,还存在"任务队列"(task queue)。
(3)一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,于是结束等待状态,进入执行栈,开始执行。
(4)主线程不断重复上面的第三步。
如 setTimeout(()=>{callback();}, 1000) ,即表示在1s之后将 callback 放到宏任务队列中,当1s的时间到达时,如果主线程上有其它任务在执行,那么 callback 就必须要等待,另外 callback 的执行也需要时间,因此 setTimeout 的时间间隔是有误差的,它只能保证延时不小于设置的时间。
如何减少 setTimeout 的误差
我们只能减少执行多次的 setTimeout 的误差,例如倒计时功能。
倒计时的时间通常都是从服务端获取的。
造成误差的原因:
1.没有考虑误差时间(函数执行的时间/其它代码的阻塞)
2.没有考虑浏览器的“休眠”
完全消除 setTimeout的误差是不可能的,但是我们减少 setTimeout 的误差。通过对下一次任务的调用时间进行修正,来减少误差。
24、js对象的简单介绍
JS有哪些内置对象:
数据封装类对象: String Number Boolean Array Object
其他对象: Function Math Date Error Arguments RegExp
宿主对象和原生对象的区别:
• 原生对象(native object)是由ECMAScript规范定义的JavaScript内置对象,如 String Number Boolean Array Object Function Math Date Error Arguments RegExp等。
• 宿主对象(host object)是由运行时的环境(浏览器或node)决定的,如window、XMLHTTPRequest等。
25、js的类型转换
• -、*、/、% :一律转换成数值后计算
• +:
• 数字 + 字符串 = 字符串, 运算顺序是从左到右
• 数字 + 对象, 优先调用对象的valueOf -> toString
• 数字 + boolean/null -> 数字
• 数字 + undefined -> NaN
• [1].toString() === '1'
• {}.toString() === '[object object]'
• NaN !== NaN
• +undefined 为 NaN
26、Attribute和property的区别
"Attribute"是在HTML中定义的,而"property"是在DOM上定义的。为了说明区别,假设我们在HTML中有一个文本框:
<input type="text" value="Hello">
const input = document.querySelector('input');
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello
但是在文本框中键入“ World!”后:
console.log(input.getAttribute('value')); // Hello
console.log(input.value); // Hello World!
27、判断两个对象相等
需要考虑三个问题:
1. 如果对象的属性值之一本身就是一个对象,需要再次遍历
2. 如果属性值中的一个是NaN(在JavaScript中,是不是等于自己唯一的价值?),肯定不相等
3. 如果一个属性的值为undefined,而另一个对象没有这个属性(因而计算结果为不确定?),不相等
检查对象的“值相等”的一个强大的方法,最好是依靠完善的测试库,涵盖了各种边界情况。
Underscore和Lo-Dash有一个名为_.isEqual()方法,用来比较好的处理深度对象的比较。您可以使用它们像这样:
// Outputs: true
console.log(_.isEqual(obj1, obj2));
28、严格模式的优缺点
'use strict' 是用于对整个脚本或单个函数启用严格模式的语句。严格模式是可选择的一个限制 JavaScript 的变体一种方式 。
优点:
• 无法再意外创建全局变量。
• 会使引起静默失败(silently fail,即:不报错也没有任何效果)的赋值操抛出异常。
• 试图删除不可删除的属性时会抛出异常(之前这种操作不会产生任何效果)。
• 要求函数的参数名唯一。
• 全局作用域下,this的值为undefined。
• 捕获了一些常见的编码错误,并抛出异常。
• 禁用令人困惑或欠佳的功能。
缺点:
• 缺失许多开发人员已经习惯的功能。
• 无法访问function.caller和function.arguments。
• 以不同严格模式编写的脚本合并后可能导致问题。
总的来说,我认为利大于弊,我从来不使用严格模式禁用的功能,因此我推荐使用严格模式。
29、如何判断是否为空数组
var arr = [];
if (Array.isArray(arr) && arr.length === 0) {
console.log('是空数组');
}
// Array.isArray是ES5提供的,如果不支持。用下面的方案。
if (!Array.isArray) {
Array.isArray = function(arg) {
return Object.prototype.toString.call(arg) === '[object Array]';
};
}
30、数组的拆解和扁平化
// flat: [1,[2,3]] --> [1, 2, 3]
Array.prototype.flat = function() {
return this.toString().split(',').map(item => +item )
}
31、push,pop,unshift,shift
push:添加新元素到数组末尾,返回数组长度
pop:移除数组最后一个元素,返回元素本身
unshift:添加新元素到数组开头,返回数组长度
shift:移除数组首个元素,返回元素本身
以上4种操作均会改变数组本身
32、如何将arguments转换为数组
1. Array.prototype.slice.call(arguments)
2. [].slice.call(arguments)
33、对象的遍历方法
for循环
for (let property in obj) {
console.log(property);
}
但是,这还会遍历到它的继承属性,在使用之前,你需要加入obj.hasOwnProperty(property)检查。
Object.keys()
Object.keys(obj).forEach((property) => { ... })
Object.keys()方法会返回一个由一个给定对象的自身可枚举属性组成的数组。
Object.getOwnPropertyNames()
Object.getOwnPropertyNames(obj).forEach((property) => { ... })
Object.getOwnPropertyNames()方法返回一个由指定对象的所有自身属性的属性名(包括不可枚举属性但不包括 Symbol 值作为名称的属性)组成的数组。
34、数组的遍历方法
for loops
for (let i = 0; i < arr.length; i++) { ... }
forEach
arr.forEach((item, index, arr) { ... })
map
arr.map((item, index, arr) => { ... })
35、数组类型的判断方式
var bool = true
var num = 1
var str = 'abc'
var und = undefined
var nul = null
var arr = [1, 2, 3]
var obj = {a: 'aa', b: 'bb'}
var fun = function() {console.log('I am a function')}
typeof: 适合基本的数据类型和函数
console.log(typeof null); // object
console.log(typeof arr); // object
console.log(typeof obj); // object
console.log(typeof fun); // function
由结果可知typeof可以测试出number、string、boolean、undefined及function,而对于null及数组、对象,typeof均检测出为object,不能进一步判断它们的类型。
instanceof: 判断对象类型,基于原型链去判断
obj instanceof Object: 左操作数是一个对象,右操作数是一个函数构造器或者函数对象,判断左边的操作数的原型链_proto_属性是否有右边这个函数对象的proptotype属性。
console.log(bool instanceof Boolean);// false
console.log(und instanceof Object); // false
console.log(arr instanceof Array); // true
console.log(nul instanceof Object); // false
console.log(obj instanceof Object); // true
console.log(fun instanceof Function);// true
var bool2 = new Boolean()
console.log(bool2 instanceof Boolean);// true
var str2 = new String()
console.log(str2 instanceof String);// true
function Animation(){}
var ani = new Animation()
console.log(ani instanceof Animation);// true
function Dog(){}
Dog.prototype = new Animation()
var dog = new Dog()
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animation);// true
console.log(dog instanceof Object); // true
从结果中看出instanceof不能区别undefined和null,而且对于基本类型如果不是用new声明的则也测试不出来,对于是使用new声明的类型,它还可以检测出多层继承关系。
constructor: 返回对创建此对象的函数的引用
console.log(bool.constructor === Boolean); // true
console.log(num.constructor === Number); // true
console.log(str.constructor === String); // true
console.log(arr.constructor === Array); // true
console.log(obj.constructor === Object); // true
console.log(fun.constructor === Function); // true
console.log(ani.constructor === Animation); // true
console.log(dog.constructor === Dog); // false
console.log(dog.constructor === Animation);// true
null 和 undefined 是无效的对象,因此是不会有 constructor 存在的,这两种类型的数据需要通过其他方式来判断。
函数的 constructor 是不稳定的,这个主要体现在自定义对象上,当开发者重写 prototype 后,原有的 constructor 引用会丢失。所以dog.constructor === Animation 而不是 Dog
Object.prototype.toString.call
console.log(Object.prototype.toString.call(bool)); //[object Boolean]
console.log(Object.prototype.toString.call(num)); //[object Number]
console.log(Object.prototype.toString.call(str)); //[object String]
console.log(Object.prototype.toString.call(und)); //[object Undefined]
console.log(Object.prototype.toString.call(nul)); //[object Null]
console.log(Object.prototype.toString.call(arr)); //[object Array]
console.log(Object.prototype.toString.call(obj)); //[object Object]
console.log(Object.prototype.toString.call(fun)); //[object Function]
console.log(Object.prototype.toString.call(dog)); //[object Object]
原理(摘自高级程序设计3):在任何值上调用 Object 原生的 toString() 方法,都会返回一个 [object NativeConstructorName] 格式的字符串。每个类在内部都有一个 [[Class]] 属性,这个属性中就指定了上述字符串中的构造函数名。
但是它不能检测非原生构造函数的构造函数名。
使用jquery中的$.type
$.type()内部原理就是用的Object.prototype.toString.call()
36、内存泄露
• 意外的全局变量: 无法被回收
• 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
• 事件监听: 没有正确销毁 (低版本浏览器可能出现)
• 闭包: 会导致父级中的变量无法被释放
• dom 引用: dom 元素被删除时,内存中的引用未被正确清空
可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变化情况,找出异常点。
37、this的指向
如果用一句话说明 this 的指向,那么即是: 谁调用它,this 就指向谁。
但是仅通过这句话,我们很多时候并不能准确判断 this 的指向。因此我们需要借助一些规则去帮助自己:
全局环境中的 this:
浏览器环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部)this 都指向全局对象 window;
node 环境:无论是否在严格模式下,在全局执行环境中(在任何函数体外部),this 都是空对象 {};
是否是 new 绑定:
如果是 new 绑定,并且构造函数中没有返回 function 或者是 object,那么 this 指向这个新对象。如下:
(1)构造函数返回值不是 function 或 object。new Super() 返回的是 this 对象。
Super是构造函数,它里面没有返回一个function 或 object,所以它实际上返回的是函数里面的this对象,即{age:”age”},所以new Super(“26”)实际上返回一个对象{age:26}
(2)构造函数返回值是 function 或 object,new Super()是返回的是Super中返回的对象。
38、异步加载js脚本的方法有哪些
(1) <script> 标签中增加 async(html5) 或者 defer(html4) 属性,脚本就会异步加载。
<script src="../XXX.js" defer></script>
defer 和 async 的区别在于:
• defer 要等到整个页面在内存中正常渲染结束(DOM 结构完全生成,以及其他脚本执行完成),在window.onload 之前执行;
• async 一旦下载完,渲染引擎就会中断渲染,执行这个脚本以后,再继续渲染。
• 如果有多个 defer 脚本,会按照它们在页面出现的顺序加载
• 多个 async 脚本不能保证加载顺序
(2) 动态创建 script 标签
动态创建的 script ,设置 src 并不会开始下载,而是要添加到文档中,JS文件才会开始下载。
(3) XHR 异步加载JS
三、手撸代码
1、instanceof原理
能在实例的 原型对象链 中找到该构造函数的prototype属性所指向的 原型对象,就返回true。即:
// __proto__: 代表原型对象链
instance.[__proto__...] === instance.constructor.prototype
// return true
2、实现一个call或 apply
通过apply和call改变函数的this指向,他们两个函数的第一个参数都是一样的表示要改变指向的那个对象,第二个参数,apply是数组,而call则是arg1,arg2...这种形式。
通过bind改变this作用域会返回一个新的函数,这个函数不会马上执行。
(1)call
• 在 call 方法中获取调用call()函数
• 如果第一个参数没有传入,那么默认指向 window / global(非严格模式)
• 传入 call 的第一个参数是 this 指向的对象,根据隐式绑定的规则,我们知道 obj.foo(), foo() 中的 this 指向 obj;因此我们可以这样调用函数 thisArgs.func(...args)
• 返回执行结果
Function.prototype.call = function () {
// thisArg表示this绑定的对象,args表示参数
let [thisArg,...args] = [...arguments];
if(!thisArg) {
//如果绑定的对象为null或者undefined
thisArg = typeof window === 'undefined' ? global : window;
}
//this(要执行的函数)的指向是当前函数 func (func.call)
thisArg.func = this;
//执行函数
let result = thisArg.func(...args);
delete thisArg.func;//thisArg上并没有 func 属性,因此要移除
return result;
}
(2)apply
Function.prototype.applay = function (thisArg,rest) {
// 函数返回结果
let result;
if(!thisArg) {
//如果绑定的对象为null或者undefined
thisArg = typeof window === 'undefined' ? global : window;
}
//this(要执行的函数)的指向是当前函数 func (func.call)
thisArg.func = this;
if(!rest) {
//第二个参数为null,undefined
result = thisArg.func();
}else {
//把参数进行展开为数组的形式
result = thisArg.func(...rest);
}
//thisArg上并没有 func 属性,因此要移除
delete thisArg.func;
return result;
}
(3)bind
(1)初始版本
Function.prototype.bind=function(obj,arg){
//取传进来的除obj外的参数
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
return function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
}
(2) 考虑到原型链
为什么要考虑?因为在new 一个bind过生成的新函数的时候,必须的条件是要继承原函数的原型
Function.prototype.bind=function(obj,arg){
var arg=Array.prototype.slice.call(arguments,1);
var context=this;
var bound=function(newArg){
arg=arg.concat(Array.prototype.slice.call(newArg));
return context.apply(obj,arg);
}
var F=function(){}
//这里需要一个寄生组合继承
F.prototype=context.prototype;
bound.prototype=new F();
return bound;
}
3、实现一个Function.bind
Function.prototype.bind2 = function (context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function () {};
var fbound = function () {
self.apply(this instanceof self ? this : context, args.concat(Array.prototype.slice.call(arguments)));
}
fNOP.prototype = this.prototype;
fbound.prototype = new fNOP();
return fbound;
}
复制代码
参考:JavaScript深入之bind的模拟实现
4、类的创建和继承
(1)类的创建(es5):new一个function,在这个function的prototype里面增加属性和方法。
下面来创建一个Animal类:
// 定义一个动物类
function Animal (name) {
// 属性
this.name = name || 'Animal';
// 实例方法
this.sleep = function(){
console.log(this.name + '正在睡觉!');
}
}
// 原型方法
Animal.prototype.eat = function(food) {
console.log(this.name + '正在吃:' + food);
};
这样就生成了一个Animal类,实力化生成对象后,有方法和属性。
(2)类的继承——原型链继承
--原型链继承
function Cat(){ }
Cat.prototype = new Animal();
Cat.prototype.name = 'cat';
//测试代码
var cat = new Cat();
console.log(cat.name);// cat
console.log(cat.eat('fish'));// cat正在吃:fish
console.log(cat.sleep());//cat正在睡觉!
console.log(cat instanceof Animal); //true
console.log(cat instanceof Cat); //true
• 介绍:在这里我们可以看到new了一个空对象,这个空对象指向Animal并且Cat.prototype指向了这个空对象,这种就是基于原型链的继承。
• 特点:基于原型链,既是父类的实例,也是子类的实例
• 缺点:无法实现多继承
(3)构造继承:使用父类的构造函数来增强子类实例,等于是复制父类的实例属性给子类(没用到原型)
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
// 测试代码
var cat = new Cat();
console.log(cat.name);// Tom
console.log(cat.sleep());//Tom正在睡觉
console.log(cat instanceof Animal); // false
console.log(cat instanceof Cat); // true
• 特点:可以实现多继承
• 缺点:只能继承父类实例的属性和方法,不能继承原型上的属性和方法。
(4)实例继承和拷贝继承
实例继承:为父类实例添加新特性,作为子类实例返回
拷贝继承:拷贝父类元素上的属性和方法
上述两个实用性不强,不一一举例。
(5)组合继承:
相当于构造继承和原型链继承的组合体。通过调用父类构造,继承父类的属性并保留传参的优点,然后通过将父类实例作为子类原型,实现函数复用
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
Cat.prototype = new Animal();
Cat.prototype.constructor = Cat;
// 测试代码
var cat = new Cat();
console.log(cat.name);//tom
console.log(cat.sleep());//Tom正在睡觉!
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); // true
• 特点:可以继承实例属性/方法,也可以继承原型属性/方法
• 缺点:调用了两次父类构造函数,生成了两份实例
(6)寄生组合继承:
通过寄生方式,砍掉父类的实例属性,这样,在调用两次父类的构造的时候,就不会初始化两次实例方法/属性
function Cat(name){
Animal.call(this);
this.name = name || 'Tom';
}
(function(){
// 创建一个没有实例方法的类
var Super = function(){};
Super.prototype = Animal.prototype;
//将实例作为子类的原型
Cat.prototype = new Super();
})();
//测试代码
var cat = new Cat();
console.log(cat.name);
console.log(cat.sleep());
console.log(cat instanceof Animal); // true
console.log(cat instanceof Cat); //true
• 较为推荐
5、手写一个Promise
面试够用版
function myPromise(constructor) {
let self = this;
self.status = "pending"
//定义状态改变前的初始状态
self.value = undefined;
//定义状态为resolved的时候的状态
self.reason = undefined;
//定义状态为rejected的时候的状态
function resolve(value) {
//两个==="pending",保证了状态的改变是不可逆的
if (self.status === "pending") {
self.value = value;
self.status = "resolved";
}
}
function reject(reason) {
//两个==="pending",保证了状态的改变是不可逆的
if (self.status === "pending") {
self.reason = reason;
self.status = "rejected";
}
}
//捕获构造异常
try {
constructor(resolve, reject);
} catch (e) {
reject(e);
}
}
//同时,需要在 myPromise的原型上定义链式调用的 then方法:
myPromise.prototype.then = function(onFullfilled, onRejected) {
let self = this;
switch (self.status) {
case "resolved":
onFullfilled(self.value);
break;
case "rejected":
onRejected(self.reason);
break;
default:
}
}
//测试一下:
var p = new myPromise(function(resolve, reject) {
resolve(1)
});
p.then(function(x) {
console.log(x)
})
复制代码
高级版请参考:史上最最最详细的手写Promise教程
6、手写防抖(Debouncing)和节流(Throttling)
• 防抖 (debounce): 将多次高频操作优化为只在最后一次执行,通常使用的场景是:用户输入,只需再输入完成后做一次输入校验即可。
防抖的应用场景:
1. 搜索框输入查询,如果用户一直在输入中,没有必要不停地调用去请求服务端接口,等用户停止输入的时候,再调用,设置一个合适的时间间隔,有效减轻服务端压力。
2. 表单验证
3. 按钮提交事件。
4. 浏览器窗口缩放,resize事件(如窗口停止改变大小之后重新计算布局)等。
• 节流(throttle): 每隔一段时间后执行一次,也就是降低频率,将高频操作优化成低频操作,通常使用场景: 滚动条事件 或者 resize 事件,通常每隔 100~500 ms执行一次即可。
节流的应用场景
1. 按钮点击事件
2. 拖拽事件
3. onScoll
4. 计算鼠标移动的距离(mousemove)
// 防抖函数
function debounce(fn, wait) {
let timer;
return function() {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
}, wait)
}
}
// 节流函数
function throttle(fn, wait) {
let prev = new Date();
return function() {
const args = arguments;
const now = new Date();
if (now - prev > wait) {
fn.apply(this, args);
prev = new Date();
}
}
}
7、手写一个JS深拷贝
面试版
function deepCopy(obj) {
//判断是否是简单数据类型,
if (typeof obj == "object") {
//复杂数据类型
var result = obj.constructor == Array ? [] : {};
for (let i in obj) {
result[i] = typeof obj[i] == "object" ? deepCopy(obj[i]) : obj[i];
}
} else {
//简单数据类型 直接 == 赋值
var result = obj;
}
return result;
}
8、柯里化函数的实现
概念:
函数柯里化是把接收多个参数的函数变换成接收一个单一参数(最初函数的第一个参数)的函数,并且返回接收余下的参数而且返回结果的新函数的技术。
函数柯里化的主要作用:
• 参数复用
• 提前返回 – 返回接受余下的参数且返回结果的新函数
• 延迟执行 – 返回新函数,等待执行
const curry = (fn,...args) =>{
args.length < fn.length ?
//参数长度不足时,重新柯里化该函数,等待接收新参数
(...arguments) =>{
curry(fn,...args,...arguments)
}:
// 参数足够时,直接执行该函数
fn(...args)
}
function sumFn(a,b,c) {
return a + b + c;
}
var sum = curry(sumFn);
console.log(sum(2)(3)(5));
console.log(sum(2,3,5));
console.log(sum(2),(3,5));
console.log(sum(2,3)(5));
9、几大排序算法
基础排序算法:
• 冒泡排序: 两两比较
function bubleSort(arr) {
var len = arr.length;
for (let outer = len ; outer >= 2; outer--) {
for(let inner = 0; inner <=outer - 1; inner++) {
if(arr[inner] > arr[inner + 1]) {
[arr[inner],arr[inner+1]] = [arr[inner+1],arr[inner]]
}
}
}
return arr;
}
• 选择排序: 遍历自身以后的元素,最小的元素跟自己调换位置
function selectSort(arr) {
var len = arr.length;
for(let i = 0 ;i < len - 1; i++) {
for(let j = i ; j<len; j++) {
if(arr[j] < arr[i]) {
[arr[i],arr[j]] = [arr[j],arr[i]];
}
}
}
return arr
}
• 插入排序: 即将元素插入到已排序好的数组中
function insertSort(arr) {
for(let i = 1; i < arr.length; i++) { //外循环从1开始,默认arr[0]是有序段
for(let j = i; j > 0; j--) { //j = i,将arr[j]依次插入有序段中
if(arr[j] < arr[j-1]) {
[arr[j],arr[j-1]] = [arr[j-1],arr[j]];
} else {
break;
}
}
}
return arr;
}
高级排序算法:
• 快速排序
o 选择基准值(base),原数组长度减一(基准值),使用 splice
o 循环原数组,小的放左边(left数组),大的放右边(right数组);
o concat(left, base, right)
o 递归继续排序 left 与 right
function quickSort(arr) {
if(arr.length <= 1) {
return arr; //递归出口
}
var left = [],
right = [],
current = arr.splice(0,1);
for(let i = 0; i < arr.length; i++) {
if(arr[i] < current) {
left.push(arr[i]) //放在左边
} else {
right.push(arr[i]) //放在右边
}
}
return quickSort(left).concat(current,quickSort(right));
}
• 希尔排序:不定步数的插入排序,插入排序
• 口诀: 插冒归基稳定,快选堆希不稳定
四、VUE
1、Vue2.0的双向数据绑定原理是什么?
vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
//vue实现数据双向绑定的原理就是用Object.defineproperty()重新定义(set方法)对象设置属性值和(get方法)获取属性值的操纵来实现的。
//Object.property()方法的解释:Object.property(参数1,参数2,参数3) 返回值为该对象obj
//其中参数1为该对象(obj),参数2为要定义或修改的对象的属性名,参数3为属性描述符,属性描述符是一个对象,主要有两种形式:数据描述符和存取描述符。这两种对象只能选择一种使用,不能混合使用。而get和set属于存取描述符对象的属性。
//这个方法会直接在一个对象上定义一个新属性或者修改对象上的现有属性,并返回该对象。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<div id="myapp">
<input v-model="message" /><br>
<span v-bind="message"></span>
</div>
<script type="text/javascript">
var model = {
message: ""
};
var models = myapp.querySelectorAll("[v-model=message]");
for (var i = 0; i < models.length; i++) {
models[i].onkeyup = function() {
model[this.getAttribute("v-model")] = this.value;
}
}
// 观察者模式 / 钩子函数
// defineProperty 来定义一个对象的某个属性
Object.defineProperty(model, "message", {
set: function(newValue) {
var binds = myapp.querySelectorAll("[v-bind=message]");
for (var i = 0; i < binds.length; i++) {
binds[i].innerHTML = newValue;
};
var models = myapp.querySelectorAll("[v-model=message]");
for (var i = 0; i < models.length; i++) {
models[i].value = newValue;
};
this.value = newValue;
},
get: function() {
return this.value;
}
})
</script>
</body>
</html>
Vue3.0将用原生Proxy替换Object.defineProperty
为什么要替换Object.defineProperty?
• 在Vue中,Object.defineProperty无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,不能实时响应。
• Object.defineProperty不能深度监听,只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。
什么是Proxy
• Proxy是 ES6 中新增的一个特性,翻译过来意思是"代理",用在这里表示由它来“代理”某些操作。 Proxy 让我们能够以简洁易懂的方式控制外部对对象的访问。其功能非常类似于设计模式中的代理模式。
• Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。
• 使用 Proxy 的核心优点是可以交由它来处理一些非核心逻辑(如:读取或设置对象的某些属性前记录日志;设置对象的某些属性值前,需要验证;某些属性的访问控制等)。 从而可以让对象只需关注于核心逻辑,达到关注点分离,降低对象复杂度等目的。
2、请详细说下你对vue生命周期的理解?
总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后
• beforeCreate 创建前执行(vue实例的挂载元素$el和数据对象data都为undefined,还未初始化)
• created 完成创建 (完成了data数据初始化,el还未初始化)
• beforeMount 载入前(vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。)
• mounted 载入后html已经渲染(vue实例挂载完成,data.message成功渲染。)
• beforeUpdate 更新前状态(view层的数据变化前,不是data中的数据改变前)
• updated 更新状态后
• beforeDestroy 销毁前
• destroyed 销毁后 (在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在)
说一下每一个阶段可以做的事情
• beforeCreate:可以在这里加一个loading事件,在加载实例时触发。
• created:初始化完成时的事件写这里,如果这里结束了loading事件,异步请求也在这里调用。
• mounted:挂在元素,获取到DOM节点
• updated:对数据进行处理的函数写这里。
• beforeDestroy:可以写一个确认停止事件的确认框。
附上一张中文解析图
3、动态路由定义和获取
在 router 目录下的 index.js 文件中,对 path 属性加上 /:id。
使用 router 对象的 params.id 获取
4、vue-router 有哪几种导航钩子?
三种
1. 全局导航钩子(跳转前进行判断拦截)
o router.beforeEach(to, from, next),
o router.beforeResolve(to, from, next),
o router.afterEach(to, from ,next)
2. 组件内钩子
o beforeRouteEnter
o beforeRouteUpdate
o beforeRouteLeave
3. 单独路由独享组件
o beforeEnter
5、组件之间的传值通信?
• 父组件向子组件传值:
o 子组件在props中创建一个属性,用来接收父组件传过来的值;
o 在父组件中注册子组件;
o 在子组件标签中添加子组件props中创建的属性;
o 把需要传给子组件的值赋给该属性
• 子组件向父组件传值:
o 子组件中需要以某种方式(如点击事件)的方法来触发一个自定义的事件;
o 将需要传的值作为$emit的第二个参数,该值将作为实参传给响应事件的方法;
o 在父组件中注册子组件并在子组件标签上绑定自定义事件的监听。
6、vuex
是一个能方便vue实例及其组件传输数据的插件 方便传输数据,作为公共存储数据的一个库
state: 状态中心
mutations: 更改状态,同步的
actions: 异步更改状态
getters: 获取状态
modules: 将state分成多个modules,便于管理
应用场景:单页应用中,组件之间的状态。音乐播放、登录状态、加入购物车。
网上找的一个通俗易懂的了解vuex的例子
公司有个仓库
1.State(公司的仓库)
2.Getter(只能取出物品,包装一下,不能改变物品任何属性)
3.Muitation(仓库管理员,只有他可以直接存储到仓库)
4.Action(公司的物料采购员,负责从外面买东西和接货, 要往仓库存东西,告诉仓库管理员要存什么)
非常要注意的地方:只要刷新或者退出浏览器,仓库清空。
7、Vue hash 路由和 history 路由的区别
hash模式url里面永远带着#号,我们在开发当中默认使用这个模式。
那么什么时候要用history模式呢?
如果用户考虑url的规范那么就需要使用history模式,因为history模式没有#号,是个正常的url适合推广宣传。当然其功能也有区别,比如我们在开发app的时候有分享页面,那么这个分享出去的页面就是用vue或是react做的,咱们把这个页面分享到第三方的app里,有的app里面url是不允许带有#号的,所以要将#号去除那么就要使用history模式,但是使用history模式还有一个问题就是,在访问二级页面的时候,做刷新操作,会出现404错误,那么就需要和后端人配合让他配置一下apache或是nginx的url重定向,重定向到你的首页路由上就ok啦。
router有两种模式:
hash模式(默认)、history模式(需配置mode: 'history')
hash history
url显示 有#,很Low 无#,好看
回车刷新 可以加载到hash值对应页面 一般就是404掉了
支持版本 支持低版本浏览器和IE浏览器 支持低版本浏览器和IE浏览器
8、virtual dom的实现
• 创建 dom 树
• 树的diff,同层对比,输出patchs(listDiff/diffChildren/diffProps)
o 没有新的节点,返回
o 新的节点tagName与key不变, 对比props,继续递归遍历子树
对比属性(对比新旧属性列表):
旧属性是否存在与新属性列表中
都存在的是否有变化
是否出现旧列表中没有的新属性
o tagName和key值变化了,则直接替换成新节点
• 渲染差异
o 遍历patchs, 把需要更改的节点取出来
o 局部更新dom
// diff算法的实现
function diff(oldTree, newTree) {
// 差异收集
let pathchs = {}
dfs(oldTree, newTree, 0, pathchs)
return pathchs
}
function dfs(oldNode, newNode, index, pathchs) {
let curPathchs = []
if (newNode) {
// 当新旧节点的 tagName 和 key 值完全一致时
if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// 继续比对属性差异
let props = diffProps(oldNode.props, newNode.props)
curPathchs.push({ type: 'changeProps', props })
// 递归进入下一层级的比较
diffChildrens(oldNode.children, newNode.children, index, pathchs)
} else {
// 当 tagName 或者 key 修改了后,表示已经是全新节点,无需再比
curPathchs.push({ type: 'replaceNode', node: newNode })
}
}
// 构建出整颗差异树
if (curPathchs.length) {
if(pathchs[index]){
pathchs[index] = pathchs[index].concat(curPathchs)
} else {
pathchs[index] = curPathchs
}
}
}
// 属性对比实现
function diffProps(oldProps, newProps) {
let propsPathchs = []
// 遍历新旧属性列表
// 查找删除项
// 查找修改项
// 查找新增项
forin(olaProps, (k, v) => {
if (!newProps.hasOwnProperty(k)) {
propsPathchs.push({ type: 'remove', prop: k })
} else {
if (v !== newProps[k]) {
propsPathchs.push({ type: 'change', prop: k , value: newProps[k] })
}
}
})
forin(newProps, (k, v) => {
if (!oldProps.hasOwnProperty(k)) {
propsPathchs.push({ type: 'add', prop: k, value: v })
}
})
return propsPathchs
}
// 对比子级差异
function diffChildrens(oldChild, newChild, index, pathchs) {
// 标记子级的删除/新增/移动
let { change, list } = diffList(oldChild, newChild, index, pathchs)
if (change.length) {
if (pathchs[index]) {
pathchs[index] = pathchs[index].concat(change)
} else {
pathchs[index] = change
}
}
// 根据 key 获取原本匹配的节点,进一步递归从头开始对比
oldChild.map((item, i) => {
let keyIndex = list.indexOf(item.key)
if (keyIndex) {
let node = newChild[keyIndex]
// 进一步递归对比
dfs(item, node, index, pathchs)
}
})
}
// 列表对比,主要也是根据 key 值查找匹配项
// 对比出新旧列表的新增/删除/移动
function diffList(oldList, newList, index, pathchs) {
let change = []
let list = []
const newKeys = getKey(newList)
oldList.map(v => {
if (newKeys.indexOf(v.key) > -1) {
list.push(v.key)
} else {
list.push(null)
}
})
// 标记删除
for (let i = list.length - 1; i>= 0; i--) {
if (!list[i]) {
list.splice(i, 1)
change.push({ type: 'remove', index: i })
}
}
// 标记新增和移动
newList.map((item, i) => {
const key = item.key
const index = list.indexOf(key)
if (index === -1 || key == null) {
// 新增
change.push({ type: 'add', node: item, index: i })
list.splice(i, 0, key)
} else {
// 移动
if (index !== i) {
change.push({
type: 'move',
form: index,
to: i,
})
move(list, index, i)
}
}
})
return { change, list }
}
五、http
1、DNS 解析的详细过程
当我们在浏览器中输入一个URL,例如”www.google.com”时,这个地址并不是谷歌网站真正意义上的地址。互联网上每一台计算机的唯一标识是它的IP地址,因此我们输入的网址首先需要先解析为IP地址,这一过程叫做DNS解析。
DNS解析是一个递归查询的过程。例如,我们需要解析”www.google.com”时,会经历以下步骤:
• 在本地域名服务器中查询IP地址,未找到域名;
• 本地域名服务器回向根域名服务器发送请求,未找到域名;
• 本地域名服务器向.com顶级域名服务器发送请求,未找到域名;
• 本地域名服务器向.google.com域名服务器发送请求,找到该域名,将对应的IP返回给本地域名服务器。
2、简述三次握手
HTTP协议是使用TCP协议作为其传输层协议的,在拿到服务器的IP地址后,浏览器客户端会与服务器建立TCP连接。该过程包括三次握手:
• 第一次握手:建立连接时,客户端向服务端发送请求报文
• 第二次握手:服务器收到请求报文后,如同意连接,则向客户端发送确认报文
• 第三次握手:客户端收到服务器的确认后,再次向服务器给出确认报文,完成连接。
* 三次握手主要是为了防止已经失效的请求报文字段发送给服务器,浪费资源。
3、四次挥手
客户端与服务器四次挥手,断开tcp连接。
• 第一次挥手:客户端想分手,发送消息给服务器
• 第二次挥手:服务器通知客户端已经接受到分手请求,但还没做好分手准备
• 第三次挥手:服务器已经做好分手准备,通知客户端
• 第四次挥手:客户端发送消息给服务器,确定分手,服务器关闭连接
4、get请求传参长度的误区
误区:我们经常说get请求参数的大小存在限制,而post请求的参数大小是无限制的。
实际上HTTP 协议从未规定 GET/POST 的请求长度限制是多少。对get请求参数的限制是来源与浏览器或web服务器,浏览器或web服务器限制了url的长度。为了明确这个概念,我们必须再次强调下面几点:
• HTTP 协议 未规定 GET 和POST的长度限制
• GET的最大长度显示是因为 浏览器和 web服务器限制了 URI的长度
• 不同的浏览器和WEB服务器,限制的最大长度不一样
• 要支持IE,则最大长度为2083byte,若只支持Chrome,则最大长度 8182byte
5、补充get和post请求在缓存方面的区别
补充补充一个get和post在缓存方面的区别:
• get请求类似于查找的过程,用户获取数据,可以不用每次都与数据库连接,所以可以使用缓存。
• post不同,post做的一般是修改和删除的工作,所以必须与数据库交互,所以不能使用缓存。因此get请求适合于请求缓存。
6、HTTP2.0
http协议 简要概括:http2.0是基于1999年发布的http1.0之后的首次更新。
• 提升访问速度(可以对于,请求资源所需时间更少,访问速度更快,相比http1.0)
• 允许多路复用:多路复用允许同时通过单一的HTTP/2连接发送多重请求-响应信息。改善了:在http1.1中,浏览器客户端在同一时间,针对同一域名下的请求有一定数量限制(连接数量),超过限制会被阻塞。
• 二进制分帧:HTTP2.0会将所有的传输信息分割为更小的信息或者帧,并对他们进行二进制编码
• 首部压缩
• 服务器端推送
6、http和https的介绍
(1)http和https的基本概念
http: 超文本传输协议,是互联网上应用最为广泛的一种网络协议,是一个客户端和服务器端请求和应答的标准(TCP),用于从WWW服务器传输超文本到本地浏览器的传输协议,它可以使浏览器更加高效,使网络传输减少。
https: 是以安全为目标的HTTP通道,简单讲是HTTP的安全版,即HTTP下加入SSL层,HTTPS的安全基础是SSL,因此加密的详细内容就需要SSL。
https协议的主要作用是:建立一个信息安全通道,来确保数组的传输,确保网站的真实性。
(2)http和https的区别?
http传输的数据都是未加密的,也就是明文的,网景公司设置了SSL协议来对http协议传输的数据进行加密处理,简单来说https协议是由http和ssl协议构建的可进行加密传输和身份认证的网络协议,比http协议的安全性更高。 主要的区别如下:
• Https协议需要ca证书,费用较高。
• http是超文本传输协议,信息是明文传输,https则是具有安全性的ssl加密传输协议。
• 使用不同的链接方式,端口也不同,一般而言,http协议的端口为80,https的端口为443
• http的连接很简单,是无状态的;HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,比http协议安全。
(3)https协议的工作原理
客户端在使用HTTPS方式与Web服务器通信时有以下几个步骤,如图所示。
• 客户使用https url访问服务器,则要求web 服务器建立ssl链接。
• web服务器接收到客户端的请求之后,会将网站的证书(证书中包含了公钥),返回或者说传输给客户端。
• 客户端和web服务器端开始协商SSL链接的安全等级,也就是加密等级。
• 客户端浏览器通过双方协商一致的安全等级,建立会话密钥,然后通过网站的公钥来加密会话密钥,并传送给网站。
• web服务器通过自己的私钥解密出会话密钥。
• web服务器通过会话密钥加密与客户端之间的通信。
(4)https协议的优点
• 使用HTTPS协议可认证用户和服务器,确保数据发送到正确的客户机和服务器;
• HTTPS协议是由SSL+HTTP协议构建的可进行加密传输、身份认证的网络协议,要比http协议安全,可防止数据在传输过程中不被窃取、改变,确保数据的完整性。
• HTTPS是现行架构下最安全的解决方案,虽然不是绝对安全,但它大幅增加了中间人攻击的成本。
• 谷歌曾在2014年8月份调整搜索引擎算法,并称“比起同等HTTP网站,采用HTTPS加密的网站在搜索结果中的排名将会更高”。
(5)https协议的缺点
• https握手阶段比较费时,会使页面加载时间延长50%,增加10%~20%的耗电。
• https缓存不如http高效,会增加数据开销。
• SSL证书也需要钱,功能越强大的证书费用越高。
• SSL证书需要绑定IP,不能再同一个ip上绑定多个域名,ipv4资源支持不了这种消耗。
奇淫技巧: https的SSL加密是在传输层实现的。
7、http状态码
常见的状态码:
• 1xx: 接受,继续处理
• 200: 成功,并返回数据
• 201: 已创建
• 202: 已接受
• 203: 成为,但未授权
• 204: 成功,无内容
• 205: 成功,重置内容
• 206: 成功,部分内容
• 301: 永久移动,重定向
• 302: 临时移动,可使用原有URI
• 304: 资源未修改,可使用缓存
• 305: 需代理访问
• 400: 请求语法错误
• 401: 要求身份认证
• 403: 拒绝请求
• 404: 资源不存在
• 500: 服务器错误
(1)400状态码:请求无效
产生原因:
• 前端提交数据的字段名称和字段类型与后台的实体没有保持一致
• 前端提交到后台的数据应该是json字符串类型,但是前端没有将对象JSON.stringify转化成字符串。
解决方法:
• 对照字段的名称,保持一致性
• 将obj对象通过JSON.stringify实现序列化
(2)401状态码:当前请求需要用户验证
(3)403状态码:服务器已经得到请求,但是拒绝执行
8、TCP和UDP的区别
(1)TCP是面向连接的,udp是无连接的即发送数据前不需要先建立链接。
(2)TCP提供可靠的服务。也就是说,通过TCP连接传送的数据,无差错,不丢失,不重复,且按序到达;UDP尽最大努力交付,即不保证可靠交付。 并且因为tcp可靠,面向连接,不会丢失数据因此适合大数据量的交换。
(3)TCP是面向字节流,UDP面向报文,并且网络出现拥塞不会使得发送速率降低(因此会出现丢包,对实时的应用比如IP电话和视频会议等)。
(4)TCP只能是1对1的,UDP支持1对1,1对多。
(5)TCP的首部较大为20字节,而UDP只有8字节。
7、WebSocket的实现和应用
WebSocket是HTML5中的协议,支持持久连续,http协议不支持持久性连接。Http1.0和HTTP1.1都不支持持久性的链接,HTTP1.1中的keep-alive,将多个http请求合并为1个
8、Head请求的方式
• head:类似于get请求,只不过返回的响应中没有具体的内容,用户获取报头
• options:允许客户端查看服务器的性能,比如说服务器支持的请求方式等等。
9、浏览器BOM对象
(1)location对象
location.href-- 返回或设置当前文档的URL
location.search -- 返回URL中的查询字符串部分。例如: http://www.dreamdu.com/dreamdu.php?id=5&name=dreamdu 返回包括(?)后面的内容?id=5&name=dreamdu
location.hash -- 返回URL#后面的内容,如果没有#,返回空
location.host -- 返回URL中的域名部分,例如www.dreamdu.com
location.hostname -- 返回URL中的主域名部分,例如dreamdu.com
location.pathname -- 返回URL的域名后的部分。例如 http://www.dreamdu.com/xhtml/ 返回/xhtml/
location.protocol -- 返回URL中的协议部分。例如 http://www.dreamdu.com:8080/xhtml/ 返回(//)前面的内容http:
location.assign -- 设置当前文档的URL
location.replace(url); location.reload() -- 重载当前页面
(2)history对象
history.go() -- 前进或后退指定的页面数
history.go(num); history.back() -- 后退一页
history.forward() -- 前进一页
(3)Navigator对象
navigator.userAgent -- 返回用户代理头的字符串表示(就是包括浏览器版本信息等的字符串)
navigator.cookieEnabled -- 返回浏览器是否支持(启用)cookie
10、fetch发送2次请求的原因
fetch发送post请求的时候,总是发送2次,第一次状态码是204,第二次才成功?
原因很简单,因为你用fetch的post请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。
11、cookie,localstorage,sessionstorage
Cookie的作用:
HTTP是一个无状态协议,因此Cookie的最大的作用就是存储sessionId用来唯一标识用户
共同点:都是保存在浏览器端,并且是同源的
• Cookie:cookie数据始终在同源的http请求中携带(即使不需要),即cookie在浏览器和服务器间来回传递。而sessionStorage和localStorage不会自动把数据发给服务器,仅在本地保存。cookie数据还有路径(path)的概念,可以限制cookie只属于某个路径下,存储的大小很小只有4K左右。 (key:可以在浏览器和服务器端来回传递,存储容量小,只有大约4K左右)
• sessionStorage:仅在当前浏览器窗口关闭前有效,自然也就不可能持久保持,localStorage:始终有效,窗口或浏览器关闭也一直保存,因此用作持久数据;cookie只在设置的cookie过期时间之前一直有效,即使窗口或浏览器关闭。(key:本身就是一个回话过程,关闭浏览器后消失,session为一个回话,当页面不同即使是同一页面打开两次,也被视为同一次回话)
• localStorage:localStorage 在所有同源窗口中都是共享的;cookie也是在所有同源窗口中都是共享的。(key:同源窗口都会共享,并且不会失效,不管窗口或者浏览器关闭与否都会始终生效)
六、浏览器
1、iframe是什么?有什么缺点
定义:iframe元素会创建包含另一个文档的内联框架 (提示:可以将提示文字放在之间,来提示某些不支持iframe的浏览器)
缺点:
• 会阻塞主页面的onload事件
• 搜索引擎无法解读这种页面,不利于SEO
• iframe和主页面共享连接池,而浏览器对相同区域有限制所以会影响性能。
2、浏览器下事件循环
事件循环是指: 执行一个宏任务,然后执行清空微任务列表,循环再执行宏任务,再清微任务列表
• 微任务 microtask(jobs): promise / ajax / Object.observe(该方法已废弃)
• 宏任务 macrotask(task): setTimout / setInterval / script / IO / UI Rendering
3、浏览器从输入url到展示过程
• DNS 解析
• TCP 三次握手
• 发送请求,分析 url,设置请求报文(头,主体)
• 服务器返回请求的文件 (html)
• 浏览器渲染
• HTML parser --> DOM Tree
o 标记化算法,进行元素状态的标记
o dom 树构建
• CSS parser --> Style Tree
o 解析 css 代码,生成样式树
• attachment --> Render Tree
o 结合 dom树 与 style树,生成渲染树
• layout: 布局
• GPU painting: 像素绘制页面
4、本地存储
在H5出现之前,数据都是存储在cookie中的。为了解决cookie的局限性引入了Web存储,indexedDB用于客户端存储大量结构化数据(包括, 文件/ blobs)。
共同点:都是保存在浏览器端、且同源的
区别:
Cookie localStorage sessionStorage indexedDB
容量大小 4kb左右 5M左右 5M左右 无限容量
过期时间 只在设置的过期时间之前一直有效,
即使窗口或者浏览器关闭 始终有效 当前浏览器窗口关闭前有效 始终有效
存储方式 浏览器和服务器间来回传递 本地保存 本地保存 本地保存
作用域 在同源窗口中共享 在同源窗口中共享 在同源窗口并且同一窗口中共享 在同源窗口中共享
5、重绘和回流
当元素的样式发生变化时,浏览器需要触发更新,重新绘制元素。这个过程中,有两种类型的操作,即重绘与回流。
• 重绘(repaint): 当元素样式的改变不影响布局时,浏览器将使用重绘对元素进行更新,此时由于只需要UI层面的重新像素绘制,因此 损耗较少
• 回流(reflow): 当元素的尺寸、结构或触发某些属性时,浏览器会重新渲染页面,称为回流。此时,浏览器需要重新经过计算,计算后还需要重新页面布局,因此是较重的操作。会触发回流的操作:
o 页面初次渲染
o 浏览器窗口大小改变
o 元素尺寸、位置、内容发生改变
o 元素字体大小变化
o 添加或者删除可见的 dom 元素
o 激活 CSS 伪类(例如::hover)
o 查询某些属性或调用某些方法
clientWidth、clientHeight、clientTop、clientLeft
offsetWidth、offsetHeight、offsetTop、offsetLeft
scrollWidth、scrollHeight、scrollTop、scrollLeft
getComputedStyle()
getBoundingClientRect()
scrollTo()
回流必定触发重绘,重绘不一定触发回流。重绘的开销较小,回流的代价较高。
6、V8的垃圾回收机制
垃圾回收: 将内存中不再使用的数据进行清理,释放出内存空间。V8 将内存分成 新生代空间 和 老生代空间。
• 新生代空间: 用于存活较短的对象
o 又分成两个空间: from 空间 与 to 空间
o Scavenge GC算法: 当 from 空间被占满时,启动 GC 算法
存活的对象从 from space 转移到 to space
清空 from space
from space 与 to space 互换
完成一次新生代GC
• 老生代空间: 用于存活时间较长的对象
o 从 新生代空间 转移到 老生代空间 的条件
经历过一次以上 Scavenge GC 的对象
当 to space 体积超过25%
o 标记清除算法: 标记存活的对象,未被标记的则被释放
增量标记: 小模块标记,在代码执行间隙执,GC 会影响性能
并发标记(最新技术): 不阻塞 js 执行
o 压缩算法: 将内存中清除后导致的碎片化对象往内存堆的一端移动,解决 内存的碎片化
7、内存泄露
• 意外的全局变量: 无法被回收
• 定时器: 未被正确关闭,导致所引用的外部变量无法被释放
• 事件监听: 没有正确销毁 (低版本浏览器可能出现)
• 闭包: 会导致父级中的变量无法被释放
• dom 引用: dom 元素被删除时,内存中的引用未被正确清空
可用 chrome 中的 timeline 进行内存标记,可视化查看内存的变化情况,找出异常点。
8、缓存策略
缓存策略: 可分为 强缓存 和 协商缓存
• Cache-Control/Expires: 浏览器判断缓存是否过期,未过期时,直接使用强缓存,Cache-Control的 max-age 优先级高于 Expires
• 当缓存已经过期时,使用协商缓存
o 唯一标识方案: Etag(response 携带) & If-None-Match(request携带,上一次返回的 Etag): 服务器判断资源是否被修改,
o 最后一次修改时间: Last-Modified(response) & If-Modified-Since (request,上一次返回的Last-Modified)
如果一致,则直接返回 304 通知浏览器使用缓存
如不一致,则服务端返回新的资源
• Last-Modified 缺点:
o 周期性修改,但内容未变时,会导致缓存失效
o 最小粒度只到 s, s 以内的改动无法检测到
• Etag 的优先级高于 Last-Modified
七、性能优化
1、图片懒加载和预加载
• 预加载:提前加载图片,当用户需要查看时可直接从本地缓存中渲染。
• 懒加载:懒加载的主要目的是作为服务器前端的优化,减少请求数或延迟请求数。
两种技术的本质:两者的行为是相反的,一个是提前加载,一个是迟缓甚至不加载。 懒加载对服务器前端有一定的缓解压力作用,预加载则会增加服务器前端压力。
八、安全
1、cookie如何防御xss攻击
XSS(跨站脚本攻击)是指攻击者在返回的HTML中嵌入javascript脚本,为了减轻这些攻击,需要在HTTP头部配上,set-cookie:
• httponly-这个属性可以防止XSS,它会禁止javascript脚本来访问cookie。
• secure - 这个属性告诉浏览器仅在请求为https的时候发送cookie。
结果应该是这样的:Set-Cookie=.....
2、什么是xss攻击,可以分为几类,如何防御
1. XSS攻击
XSS(Cross-Site Scripting,跨站脚本攻击)是一种代码注入攻击。攻击者在目标网站上注入恶意代码,当被攻击者登陆网站时就会执行这些恶意代码,这些脚本可以读取 cookie,session tokens,或者其它敏感的网站信息,对用户进行钓鱼欺诈,甚至发起蠕虫攻击等。
XSS 的本质是:
恶意代码未经过滤,与网站正常的代码混在一起;浏览器无法分辨哪些脚本是可信的,导致恶意脚本被执行。由于直接在用户的终端执行,恶意代码能够直接获取用户的信息,利用这些信息冒充用户向网站发起攻击者定义的请求。
XSS分类
根据攻击的来源,XSS攻击可以分为存储型(持久性)、反射型(非持久型)和DOM型三种。下面我们来详细了解一下这三种XSS攻击:
1.1 反射型XSS
当用户点击一个恶意链接,或者提交一个表单,或者进入一个恶意网站时,注入脚本进入被攻击者的网站。Web服务器将注入脚本,比如一个错误信息,搜索结果等,未进行过滤直接返回到用户的浏览器上。
反射型 XSS 的攻击步骤:
攻击者构造出特殊的 URL,其中包含恶意代码。
用户打开带有恶意代码的 URL 时,网站服务端将恶意代码从 URL 中取出,拼接在 HTML 中返回给浏览器。
用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
反射型 XSS 漏洞常见于通过 URL 传递参数的功能,如网站搜索、跳转等。由于需要用户主动打开恶意的 URL 才能生效,攻击者往往会结合多种手段诱导用户点击。
POST 的内容也可以触发反射型 XSS,只不过其触发条件比较苛刻(需要构造表单提交页面,并引导用户点击),所以非常少见。
如果不希望被前端拿到cookie,后端可以设置 httpOnly (不过这不是 XSS攻击 的解决方案,只能降低受损范围)
如何防范反射型XSS攻击
对字符串进行编码。
对url的查询参数进行转义后再输出到页面。
app.get('/welcome', function(req, res) {
//对查询参数进行编码,避免反射型 XSS攻击
res.send(`${encodeURIComponent(req.query.type)}`);
});
1.2 DOM 型 XSS
DOM 型 XSS 攻击,实际上就是前端 JavaScript 代码不够严谨,把不可信的内容插入到了页面。在使用 .innerHTML、.outerHTML、.appendChild、document.write()等API时要特别小心,不要把不可信的数据作为 HTML 插到页面上,尽量使用 .innerText、.textContent、.setAttribute() 等。
DOM 型 XSS 的攻击步骤:
攻击者构造出特殊数据,其中包含恶意代码。
用户浏览器执行了恶意代码。
恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
如何防范 DOM 型 XSS 攻击
防范 DOM 型 XSS 攻击的核心就是对输入内容进行转义(DOM 中的内联事件监听器和链接跳转都能把字符串作为代码运行,需要对其内容进行检查)。
1.对于url链接(例如图片的src属性),那么直接使用 encodeURIComponent 来转义。
2.非url,我们可以这样进行编码:
function encodeHtml(str) {
return str.replace(/"/g, '"')
.replace(/'/g, ''')
.replace(/</g, '<')
.replace(/>/g, '>');
}
DOM 型 XSS 攻击中,取出和执行恶意代码由浏览器端完成,属于前端 JavaScript 自身的安全漏洞。
1.3 存储型XSS
恶意脚本永久存储在目标服务器上。当浏览器请求数据时,脚本从服务器传回并执行,影响范围比反射型和DOM型XSS更大。存储型XSS攻击的原因仍然是没有做好数据过滤:前端提交数据至服务端时,没有做好过滤;服务端在接受到数据时,在存储之前,没有做过滤;前端从服务端请求到数据,没有过滤输出。
存储型 XSS 的攻击步骤:
攻击者将恶意代码提交到目标网站的数据库中。
用户打开目标网站时,网站服务端将恶意代码从数据库取出,拼接在 HTML 中返回给浏览器。
用户浏览器接收到响应后解析执行,混在其中的恶意代码也被执行。
恶意代码窃取用户数据并发送到攻击者的网站,或者冒充用户的行为,调用目标网站接口执行攻击者指定的操作。
这种攻击常见于带有用户保存数据的网站功能,如论坛发帖、商品评论、用户私信等。
如何防范存储型XSS攻击:
前端数据传递给服务器之前,先转义/过滤(防范不了抓包修改数据的情况)
服务器接收到数据,在存储到数据库之前,进行转义/过滤
前端接收到服务器传递过来的数据,在展示到页面前,先进行转义/过滤
除了谨慎的转义,我们还需要其他一些手段来防范XSS攻击:
1.Content Security Policy
在服务端使用 HTTP的 Content-Security-Policy 头部来指定策略,或者在前端设置 meta 标签。
例如下面的配置只允许加载同域下的资源:
Content-Security-Policy: default-src 'self'
<meta http-equiv="Content-Security-Policy" content="form-action 'self';">
前端和服务端设置 CSP 的效果相同,但是meta无法使用report
严格的 CSP 在 XSS 的防范中可以起到以下的作用:
禁止加载外域代码,防止复杂的攻击逻辑。
禁止外域提交,网站被攻击后,用户的数据不会泄露到外域。
禁止内联脚本执行(规则较严格,目前发现 GitHub 使用)。
禁止未授权的脚本执行(新特性,Google Map 移动版在使用)。
合理使用上报可以及时发现 XSS,利于尽快修复问题。
2.输入内容长度控制
对于不受信任的输入,都应该限定一个合理的长度。虽然无法完全防止 XSS 发生,但可以增加 XSS 攻击的难度。
3.输入内容限制
对于部分输入,可以限定不能包含特殊字符或者仅能输入数字等。
4.其他安全措施
HTTP-only Cookie: 禁止 JavaScript 读取某些敏感 Cookie,攻击者完成 XSS 注入后也无法窃取此 Cookie。
验证码:防止脚本冒充用户提交危险操作。
3.CSRF
跨站请求伪造,防护:
get 不修改数据
不被第三方网站访问到用户的 cookie
设置白名单,不被第三方网站请求
请求校验
九、兼容性
1、click在ios上有300ms延迟,原因及如何处理
(1)粗暴型,禁用缩放
<meta name="viewport" content="width=device-width, user-scalable=no">
(2)利用FastClick,其原理是:
检测到touchend事件后,立刻触发模拟click事件,并且把浏览器300毫秒之后真正出发的事件给阻断掉
十、前端常见题目
1、变量提升
function sayHi() {
console.log(name);
console.log(age);
var name = "Lydia";
let age = 21;
}
sayHi();
打印的结果是:undefined 和 ReferenceError
解析:
在函数中,我们首先使用var关键字声明了name变量。 这意味着变量在创建阶段会被提升(JavaScript会在创建变量创建阶段为其分配内存空间),默认值为undefined,直到我们实际执行到使用该变量的行。 我们还没有为name变量赋值,所以它仍然保持undefined的值。
使用let关键字(和const)声明的变量也会存在变量提升,但与var不同,初始化没有被提升。 在我们声明(初始化)它们之前,它们是不可访问的。 这被称为“暂时死区”。 当我们在声明变量之前尝试访问变量时,JavaScript会抛出一个ReferenceError。
变量的赋值可以分为三个阶段:
• 创建变量,在内存中开辟空间
• 初始化变量,将变量初始化为undefined
• 真正赋值
关于let、var和function:
• let 的「创建」过程被提升了,但是初始化没有提升。
• var 的「创建」和「初始化」都被提升了。
• function 的「创建」「初始化」和「赋值」都被提升了。
2、块级作用域
for (var i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1);
}
for (let i = 0; i < 3; i++) {
setTimeout(() => console.log(i), 1);
}
上面两个式子分别打印什么:3 3 3 and 0 1 2
解析:
由于JavaScript中的事件执行机制,setTimeout函数真正被执行时,循环已经走完。 由于第一个循环中的变量i是使用var关键字声明的,因此该值是全局的。 在循环期间,我们每次使用一元运算符++都会将i的值增加1。 因此在第一个例子中,当调用setTimeout函数时,i已经被赋值为3。
在第二个循环中,使用let关键字声明变量i:使用let(和const)关键字声明的变量是具有块作用域的(块是{}之间的任何东西)。 在每次迭代期间,i将被创建为一个新值,并且每个值都会存在于循环内的块级作用域。
执行过程:每次执行时,都会往{}里面丢一个i存起来,第1个i在第1个{}的值为1, 第2个i在第2个{}的值为2, 第3个i在第3个{}的值为3,等三个值都存好后,才开始执行{}里面的代码。Var声明时:i是全局的,所以会找它的最新值3,所以是3,3,3。let声明:每个块里面的值不相互影响,所以只能取存在当前块的值,分别是0,1,2
3、this的指向,箭头函数
const shape = {
radius: 10,
diameter() {
return this.radius * 2;
},
perimeter: () => 2 * Math.PI * this.radius
};
shape.diameter();
shape.perimeter();
分别打印:20 and NaN
解析:请注意,diameter是普通函数,而perimeter是箭头函数。
对于箭头函数,this关键字指向是它所在上下文(定义时的位置)的环境,与普通函数不同! 这意味着当我们调用perimeter时,它不是指向shape对象,而是指其定义时的环境(window)。没有值radius属性,返回undefined。
4、给构造函数的原型加属性
function Person(firstName, lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
const member = new Person("Lydia", "Hallie");
Person.getFullName = () => this.firstName + this.lastName;
console.log(member.getFullName());
打印:TypeError
解析:您不能像使用常规对象那样向构造函数添加属性。 如果要一次向所有对象添加功能,则必须使用原型。 所以在这种情况下应该这样写:
Person.prototype.getFullName = function () {
return `${this.firstName} ${this.lastName}`;
}
这样会使member.getFullName()是可用的,为什么样做是对的? 假设我们将此方法添加到构造函数本身。 也许不是每个Person实例都需要这种方法。这会浪费大量内存空间,因为它们仍然具有该属性,这占用了每个实例的内存空间。 相反,如果我们只将它添加到原型中,我们只需将它放在内存中的一个位置,但它们都可以访问它!
5、++n和n++
let number = 0;
console.log(number++);
console.log(++number);
console.log(number);
打印:0 2 2
解析:后缀一元运算符++:
1. 返回值(返回0)
2. 增加值(数字现在是1)
前缀一元运算符++:
1. 增加值(数字现在是2)
2. 返回值(返回2)
所以返回0 2 2。
奇淫技巧:+在前,自加,表(表达式)也加
+在后,自加,表不加
人话: 家在前,自己家,表弟家
家在后,自己家,表弟没家
6、模板字符串
function getPersonInfo(one, two, three) {
console.log(one);
console.log(two);
console.log(three);
}
const person = "Lydia";
const age = 21;
getPersonInfo`${person} is ${age} years old`;
打印:["", "is", "years old"] Lydia 21
解析:如果使用标记的模板字符串,则第一个参数的值始终是字符串(注意:age表示的不是字符串)值的数组。 其余参数获取传递到模板字符串中的表达式的值!
7、对象相等
Console.log({ age: 18 }=={ age: 18 })//false
Console.log({ age: 18 }==={ age: 18 })//false
解析:对象在内存中位于不同位置,所以它们的引用是不同的。
8、…扩展符
function getAge(...args) {
console.log(typeof args);
}
getAge(21);
打印:object
解析: 扩展运算符(... args)返回一个带参数的数组。 数组是一个对象,因此typeof args返回object。
9、严格模式下的变量声明
function getAge() {
"use strict";
age = 21;
console.log(age);
}
getAge();
打印:ReferenceError
解析: 使用“use strict”,可以确保不会意外地声明全局变量。 我们从未声明变量age,因为我们使用``use strict',它会引发一个ReferenceError。 如果我们不使用“use strict”,它就会起作用,因为属性age`会被添加到全局对象中。
10、对象的键存储格式
const obj = { 1: "a", 2: "b", 3: "c" };
const set = new Set([1, 2, 3, 4, 5]);
obj.hasOwnProperty("1");//true
obj.hasOwnProperty(1); //true
set.has("1");//false
set.has(1); //true
解析:所有对象键(不包括Symbols)都会被存储为字符串,即使你没有给定字符串类型的键。 这就是为什么obj.hasOwnProperty('1')也返回true。
上面的说法不适用于Set。 在我们的Set中没有“1”:set.has('1')返回false。 它有数字类型1,set.has(1)返回true。
11、对象的重复属性
const obj = { a: "one", b: "two", a: "three" };
console.log(obj); // { a: "three", b: "two" }
解析:如果对象有两个具有相同名称的键,则将替前面的键。它仍将处于第一个位置,但具有最后指定的值。
12、对象键自动转换为字符串
const a = {};
const b = { key: "b" };
const c = { key: "c" };
a[b] = 123;
a[c] = 456;
console.log(a[b]); // 456
解析:对象键自动转换为字符串。我们试图将一个对象设置为对象a的键,其值为123。
但是,当对象自动转换为字符串化时,它变成了[Object object]。 所以我们在这里说的是a["Object object"] = 123。 然后,我们可以尝试再次做同样的事情。 c对象同样会发生隐式类型转换。那么,a["Object object"] = 456。
然后,我们打印a[b],它实际上是a["Object object"]。 我们将其设置为456,因此返回456。
13、自执行函数的返回
function sayHi() {
return (() => 0)();
}
typeof sayHi();//number
解析:sayHi函数返回立即调用的函数(IIFE)的返回值。 该函数返回0,类型为数字。
14、假值
0;
new Number(0);
("");
(" ");
new Boolean(false);
undefined;
假值为:0, '', undefined
解析:JavaScript中只有6个假值:undefined null NaN 0 '' (empty string) false 。
函数构造函数,如new Number和new Boolean都是真值。
15、数组溢出
const numbers = [1, 2, 3];
numbers[10] = 11;
console.log(numbers); //[1, 2, 3, 7 x empty, 11]
16、reduce的使用
[[0, 1], [2, 3]].reduce(
(acc, cur) => {
return acc.concat(cur);
},
[1, 2]
);
打印:[1, 2, 0, 1, 2, 3]
解析:[1,2]是我们的初始值。 这是我们开始执行reduce函数的初始值,以及第一个acc的值。 在第一轮中,acc是[1,2],cur是[0,1]。 我们将它们连接起来,结果是[1,2,0,1]。
然后,acc的值为[1,2,0,1],cur的值为[2,3]。 我们将它们连接起来,得到[1,2,0,1,2,3]。
17、setInterval的返回
setInterval(() => console.log("Hi"), 1000);
上面的式子返回一个唯一的id, 此id可用于使用clearInterval()函数清除该定时器。
18、单页面SPA的优缺点是什么?如何使其对SEO友好
etInterval(()单页Web应用(single page web application,SPA),就是只有一张Web页面的应用,是加载单个HTML 页面并在用户与应用程序交互时动态更新该页面的Web应用程序。
单页应用SPA 多页应用MPA
组成 一个外壳页面和多个页面片段组成 多个完整页面构成
资源共用(css,js) 共用,只需在外壳部分加载 不共用,每个页面都需要加载
刷新方式 页面局部刷新或更改 整页刷新
url 模式 a.com/#/pageone
a.com/#/pagetwo a.com/pageone.html
a.com/pagetwo.html
用户体验 页面片段间的切换快,用户体验良好
由于要一次加载所有的资源(html/js),故首屏加载慢 页面切换加载缓慢,流畅度不够,用户体验比较差
首屏加载很快
转场动画 容易实现 无法实现
数据传递 容易 依赖 url传参、或者cookie 、localStorage等
搜索引擎优化(SEO) 需要单独方案、实现较为困难、不利于SEO检索。
Prerender预渲染优化SEO
实现方法简易
试用范围 高要求的体验度、追求界面流畅的应用 适用于追求高度支持搜索引擎的应用
开发成本 较高,常需借助专业的框架 较低,但页面重复代码多
维护成本 相对容易 相对复杂
十一、模块化
模块化开发在现代开发中已是必不可少的一部分,它大大提高了项目的可维护、可拓展和可协作性。通常,我们 在浏览器中使用 ES6 的模块化支持,在 Node 中使用 commonjs 的模块化支持
分类:
• es6: import / export
• commonjs: require / module.exports / exports
• amd: require / defined
require与import的区别
• require支持 动态导入,import不支持,正在提案 (babel 下可支持)
• require是 同步 导入,import属于 异步 导入
• require是 值拷贝,导出值变化不会影响导入值;import指向 内存地址,导入值会随导出值而变化
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。