面试图谱未攻略内容

面试图谱未攻略内容

  最近发现一个大佬总结的前端面试百科全书网站:面试图谱,感觉挺不错的,现在正在看,但现在水平不高肯定有些地方理解不了,在这里记录一下以便日后回顾。

未攻略内容

JS部分

  模块化、防抖、节流、Promise实现、Generator实现

需反复巩固的内容

  原型、模拟实现call,apply,bind

其它部分

JS辨别数据类型

首先要了解JS判断语句中的假值

  值     类型
  0      Number
  NaN    Number
 ‘’(空字符串) String
  false    Boolean
  null     Object
  undefined  Undefined

怎么辨别数组,null和对象

typeof

  typeof不能判断出数组和null,而且对于通过new操作符生成的对象,只会返回’object’

1
2
3
alert(typeof null); //'object'
alert(typeof []); //'object'
alert(typeof {}); //'object'

  但Object.prototype.toString对任何变量会永远返回这样一个字符串”[object class]”,而这个class就是JavaScript内嵌对象的构造函数的名字。至于用户自定义的变量,则class等于object。

1
2
3
alert(Object.prototype.toString.call(null));    //'[object Null]'
alert(Object.prototype.toString.call([])); //'[object Array]'
alert(Object.prototype.toString.call({})); //'[object Object]'

①因此通过Object.prototype.toString.call(obj)可以准确的获取变量数据类型。

  通过Object.prototype.toString可以获得的数据类型包括:Date, Object, String, Number, Boolean, Regexp, Function, undefined, null等。

②因为null是假值,所以辨别null与object或数组的关系可以用如下

1
2
3
4
//var value=null;
if(value && typeof value==='object'){
alert('这是个对象或数组');
}

判断NaN值

  NaN值一般在试图把非数字形式的字符串转换为数字时产生。

  上面的typeof和Object.prototype.toString.call(obj);都不能判断NaN值,而且NaN也不等同于它自己。

1
2
3
alert(typeof NaN);	//'number'
alert(Object.prototype.toString.call(NaN)); //'[object Number]'
alert(NaN===NaN); //false

好在JS提供了isNaN函数,可以辨别数字与NaN

1
2
alert(isNaN('0'));	//false
alert(isNaN('oops')); //true

其它问题记录

  undefined和null与任何有意义的值比较返回的都是false,但是null与undefined之间互相比较返回的是true。

  一般typeof正则表达式都会返回’object’,但在Safari3.x版本中,返回的是’function’

1
2
alert(undefined==null);    //true
alert(typeof /a/); //'object'

  严格相等运算符(===)有两个需要注意的例子:除了NaN===NaN返回false以外,+0===-0却返回true。所以ES6提出了Object.is()方法来比较两个值是否严格相等。

JS中的数值运算

虽然有不少坑但是比较可靠的parseInt

Number.parseInt(string,radix) 默认接收两个参数

  第一个参数是默认是 string 类型值,如果不是,会通过抽象的 ToString 强制转化成 string 类型的值。这其中就会有强制类型转换过程中的各种坑

  第二个参数是 number 类型的进制,如果不是,会通过抽象的 ToNumber 强制转化成 number 类型的值,范围是 2-36,通过强制类型转换后如果是其他值会返回 NaN。在 ES5 之前如果没有传入这个参数,会根据第一个参数的开头来判断进制,0 开头的字符串会判断成八进制,也就是很多人提到的老黄历坑。ES5 之后已经解决,不传这个参数默认十进制。但是这个参数容易被忽略,尤其是在和 map 之类的也容易忽略后续可选参数的函数搭配使用的时候,比如

1
2
["1", "2", "3"].map(parseInt) // 结果是 [1, NaN, NaN]
["1", "2", "3"].map(x=>parseInt(x,10)) //正确的做法

这里第一个结果是[1, NaN, NaN]的原因为:

  map方法在调用callback函数时,会给它传递三个参数:当前正在遍历的元素, 元素索引, 原数组本身.

  第三个参数parseInt会忽视, 但第二个参数不会,也就是说,parseInt把传过来的索引值当成进制数来使用.从而返回了NaN。

  即实际是下面这样:

1
2
3
parseInt("1", 0); // 这是特例, 按照 0 进制转成数, 直接得本身
parseInt("2", 1); // 直接NaN, 因为计数的进制至少也是 2 进制
parseInt("3", 2); // 也是NaN, 因为二进制只有0,1

  所以如果只是用 parseInt 来 “取整”,一个良好的习惯是永远记得设置第二个参数为 10

  最后提一点,在ES6中将全局对象的parseInt()和parseFloat()移植到了Number对象上面,行为完全不变。但Number.isFinite()和Number.isNaN()则是在内层借用了全局的相应方法,所以只能说ES6在Number对象上提供了isFinite()和isNaN()方法。

1
2
3
alert(isFinite===Number.isFinite);  //false
alert(isNaN===Number.isNaN); //false
alert(parseInt===Number.parseInt); //true

巧妙使用位操作符

  JavaScript 中的 number 类型的值都是使用 IEEE 754 标准的 64 位双精度浮点型存储,即 1 位符号位 + 11 位指数部分 + 52 位尾数部分 。用来表示整数时,安全的范围是 53 位,超出这个返回可能会造成精度丢失

  1. 按位或| 向下取整(数值不够大才能用)
1
2
3
4
5
6
7
var num=-9999999.1234;
var int=num|0;
console.log(int); //-9999999

var num=-9999999999.1234;
var int=num|0;
console.log(int); //-1410065407

关于其它的取整方法可以见博客中:“JS中的取整”

  1. 按位非~(按位取反)任意数值x相当于-(x+1)
1
2
3
4
5
6
console.log(~1);    //-2
console.log(~0); //-1
console.log(~-1); //0

console.log(~NaN); //-1
console.log(~[]); //-1

  我们可以发现:-1 是唯一一个经过 ~ 运算返回假值0的值(包括其他那些特殊的值比如 NaN、{}、[] 等都不会返回假值)

  所以字符串和数组的 indexOf 函数查找失败会返回 -1,这时候就可以用:

1
if(~str.indexOf('str')) // 来表示查找失败

比判断 >= 0 或者 != -1 更优雅,跟用 !! 来判断非假值有异曲同工之妙

  1. 按位与&取最后一位数字

  eg:取任意数字的任意二进制位上的值(不是1就是0)
eg:128的第一到第7位是0,第八位是1(128=10000000)

1
2
3
4
5
6
7
8
9
10
/*
主要方法是和1按位与,
如10000
&
00001,这样就能取到10000的最后一位数字
*/
function valueAtBit(num,bit){
return (num>>(bit-1)) & 1;
}
console.log(valueAtBit(128,8)); //1

其它的骚操作

  JS取0~9的一个随机数可以用:

1
(Math.random()+"").slice(-1);   //eg从0.984652315464中复制最后一个数字4

JS中new运算符详解

JS中new运算符和原型链继承

  看下面的这些东西之前,先好好看看高程的第六章面向对象部分!里面提到了很多基本概念,比如:

  1. 只要创建了一个新函数,就会根据一组特定的规则为该函数创建一个prototype属性,这个属性指向函数的原型对象。

  2. 对原型所做的修改,能够立即在所有实例中得到反映。

  因为new运算符会返回一个指向构造器函数原型的对象,所以通常情况下this就指向返回的这个对象。

  其中new运算符创建对象的过程,实际上就是:

  1. 先克隆Object.prototype对象,得到一个空对象,并把它的prototype属性指向构造器函数的原型对象
  2. 将构造函数的作用域赋给新对象(因此this就指向了这个新对象)
  3. 执行构造函数中的代码
  4. 返回新对象

  在Chrome和Firefox等向外暴露了对象proto属性的浏览器下,我们可以通过下面这段代码理解new运算的过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Person(name){
this.name=name;
};

Person.prototype.getName=function(){
return this.name;
};
var objectFactory=function(){
var obj=new Object(), //1. 先克隆Object.prototype对象,得到一个空对象
Constructor=[].shift.call(arguments); //把第一个参数赋给Constructor变量
obj.__proto__=Constructor.prototype; //1.指向正确的原型
var ret=Constructor.apply(obj,arguments); //2. 将构造函数的作用域赋给新对象+3. 执行构造函数中的代码
return typeof ret === "object" ? ret : obj; //4. 返回新对象
};

var a=objectFactory(Person,"sven");

console.log(a.name);//sven
console.log(a.getName());//sven
console.log(Object.getPrototypeOf(a) === Person.prototype);//true

  然后我们就能理解构造器模式是如何创建对象的了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Point(x=10,y=6){
this.x=x;
this.y=y;
}
Point.prototype.z=20;
Point.prototype.getX=function(){
alert(this.x);//不要忘了这里的this也是动态绑定的
}

var p=new Point();//此时Point函数里的this动态绑定到p对象上,都是给p加的x,y属性

p.getX(); //10 this绑定到p上,相当于alert(p.x)
alert(p.__proto__.x); //undefined
p.__proto__.getX() //undefiend this绑定到p的原型对象上也就是构造器原型对象上,当然没有x啦
alert(p.z); //20 虽然p对象本身没有z属性,但如果对象无法响应某个请求,它会把这个请求委托给它的构造器的原型

相关理解图示:原型继承示意图

实现slideUp/Down效果

实现slideUp/Down效果

  最近做网页时,想做一个点击标题滑动出内容的模块,然后看了下参考的源码,结果发现有好几种做法。

第一种最简单,就是使用JQuery的slideUp/Down方法,一步搞定

第二种使用JS配合CSS3的transition属性,但是在该例子中transition需要处理与display的关系,两者貌似搭配起来不会产生效果

  解决方法是使用setTimeout延时display: none;的操作,因为我最终没有采用这种方法,留待以后检测。

第三种使用纯JS实现,即设置setTimeout来控制层的高度实现滑入滑出效果

Git多人协作

Git多人协作

  首先肯定需要对Git的基本操作有一定的认识,这里肯定要放上浅显易懂的:廖雪峰老师的Git教程

  看完后我们就应该对Git有个大体的认识了,然后一般用Git的人中我们分两类:一类就是自己在用着玩,那看完上面的教程差不多就可以自己去摸索了;第二类就是想要多人协作,进行一个项目开发或协同作业,所以我在这里就记一下这几天搞多人协作这块儿踩过的坑。

  首先干一件事肯定是有个目标,我最近正在和同班同学搞一个网站的小项目,我们前端部分用的VSCode码代码,然后发现里面内置了Git,于是我们就兴冲冲的准备使用,经过一段时间摸索,自己add、commit、push到自己的GitHub时一点问题都没有,于是我们就准备多人协作了,在疯狂网上查找后,我们试出了三种方法。

第一种:fork别人的库成为自己的,然后clone到自己电脑上操作,操作完push到自己的GitHub上,再去自己的GitHub上发起pull request发给项目原作者,经原作者审核后merge

  具体操作可见:GitHub上如何进行PR(Pull Request)操作

  这样可行,但各位仁兄不觉得太过麻烦了吗……而且还需要审核,对于那种开源项目来说肯定是有必要的,但如果就是想四五个人协同作业的话,可以选用下面两种方法。

第二种:进入需要多人协作的项目中配置其它成员的公钥SSH key,然后其他成员即可拥有同等的权限,无需审核

  如下图:多人协作方法2

  这个方法有一个缺点,那就是需要其他成员把自己的SSH与自己的Github远程库解绑,才能被项目拥有者绑定到这个项目上,所以这就不方便其他成员操作自己的GitHub了。

第三种:添加其他成员为项目协作者collaborators,其他成员也可拥有同等的权限,无需审核

  如下图:多人协作方法3

  综上看如果你是个小项目想多人协作的话,这是最简便的方法,不过这里需要注意一点:项目拥有者发送邀请后,被邀请协作的成员需要去自己的GitHub注册邮箱里面找一封确认信!!!可能被标记为垃圾邮件了!一定要确认,否则无法完成!

  最后多人协作肯定会有冲突发生,以后再更新一下解决冲突的方法。

JS中this的指向

JS中this的指向

JavaScript中的this总是指向一个对象,而具体指向哪个对象是在**运行时**基于函数的执行环境动态绑定的。

有位人说A.apply(x,array);等价于(A.bind(x))(array),现在看着挺对的,留待以后验证。

方法调用模式

  当函数作为对象的方法被调用时,this指向该对象。

普通函数调用模式

  this总是指向全局对象,在浏览器中,这个全局对象就是window对象。

但在使用**严格模式**"use strict";时,this在函数调用模式不会指向全局对象window,而是undefined。

构造器调用模式

  因为new运算符会返回一个指向构造器函数原型的对象,所以通常情况下this就指向返回的这个对象。

  关于new运算符的内部具体操作请见我博客中的“JS中new运算符详解”!

  但还有特殊的情况,比如构造器显式的返回了一个object类型的对象时,new调用构造器则会最终返回这个对象,而不是我们期待的this。

Function.prototype.apply调用模式

  详情见我博客中的“JS中的apply()使用详解”一文,它的第一个参数可以改变函数体内this对象的指向。

  如果我们传入的第一个参数是null或undefined,函数体内的this会指向全局对象,在浏览器中则是window。

1
2
3
4
5
var func=function(a,b,c){
alert(this===window);//true
};
func.apply(null,[1,2,3]);
func.apply(undefined,[1,2,3]);

  但如果是在严格模式下,则会严格执行第一个参数指定了函数体内this对象的指向,即传进来null,this就指向null;传进来undefined,this就指向undefined

1
2
3
4
5
var func=function(a,b,c){
"use strict";
alert(this===null);//true
};
func.apply(null,[1,2,3]);

闭包中的this指向

  每个函数在被调用时都会自动取得两个特殊变量:this和arguments。内部函数在搜索这两个变量时,只会搜索到其活动对象为止,因此闭包中永远不可能直接访问外部函数中的这两个变量,this一定是在这个闭包被调用时动态绑定的。如果想使用外层函数的this,最常用的方法就是找一个变量把它保存下来(eg:that)。

利用apply实现的bind函数

  Function.prototype.bind,用来指定函数内部的this指向,其简化版实现可以这样模拟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Function.prototype.bind=function(context){
var that=this;//保存方法调用模式下被调用的原函数
return function(){//闭包返回一个新函数(可访问外层函数的变量和参数)
return that.apply(context,arguments);//改变that原函数的内部this指向为我们指定的context
}
};

var obj1={
name: "Daoma",
};

var func=function(){
alert(this.name);
};

func();//未定义

var func=function(){
alert(this.name);
}.bind(obj1);

func();//Daoma

  通常我们还会实现的稍微复杂一点,可以往func函数中预先填入一些参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Function.prototype.bind=function(){
var that=this,
context=[].shift.call(arguments),//将第一个元素删掉并返回
args=[].slice.call(arguments);//用slice的另一个功能将剩余的参数类数组转换成数组
return function(){
//这里运用了函数柯里化的思想,组合两次分别传入的参数,作为新函数的参数
return that.apply(context,[].concat.call(args,[].slice.call(arguments)));
}
};

var obj1={
name: "daoma",
};

var func=function(a,b,c,d){
alert(this.name);
alert([a,b,c,d]);
}.bind(obj1,1,2);

func(3,4);//即可以传参两次拼到一起

补充bind和call的返回值

  1. fun.bind(thisArg[, arg1[, arg2[, …]]])

  返回由指定的this值和初始化参数改造的原函数拷贝

  1. fun.call(thisArg, arg1, arg2, …)

  返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined。

最后我还有一个需要填的问题坑,留待以后实力提高后解决:JavaScript中Apply调用模式的this指向问题

arguments类数组对象详解

arguments类数组对象详解

arguments对象保存着函数被调用时的参数列表,它是一个类数组对象,有一个length属性但没有任何数组的方法。

  它不能像数组一样,进行排序操作或者往集合里添加一个新的元素。

所以我们经常非常频繁的找Array.prototype对象借用方法。

  像Array.prototype.slice可以把arguments转成真正的数组;想截去arguments列表中头一个元素时,又可以借用Array.prototype.shift方法。

  要了解这种机制的实现原理,需要查看一下V8引擎的源码,比如Array.prototype.push:

1
2
3
4
5
6
7
8
9
function ArrayPush(){
var n=TO_UINT32(this.length); //被push的对象的length
var m=%_ArgumentsLength(); //push的参数个数
for(var i=0;i<m;i++){
this[i+n]=%_Arguments(i); //复制元素
}
this.length=n+m; //修正length属性的值
return this.length;
}

 可见Array.prototype.push实际上是一个属性复制的过程,把参数按照下标依次添加到被push的对象上面,顺便修改了这个对象的属性。所以只要是个对象,不管是数组对象还是类数组对象,都可以操作。

CSS使用border构建图标

CSS使用border构建图标

使用border画三角形

  盒模型中上下左右边框交界处呈现平滑的斜线. 利用这个特点, 通过设置不同的上下左右边框宽度或者颜色可以得到小三角, 小梯形等。具体链接

画朝上朝下的√小图标

  使用border可以画很多与三角形有关的东东,比如我们经常见到的向上向下的“对号图标”就是使用两层border叠加生成的。
border对号小图标

  首先我们需要一个div和两个i,两个i就是用来叠加生成对号图标用的。

HTML:

1
2
3
4
<div class="bottom"><!-- 这里换成top就是向上的对号-->
<i class="arrow1"></i>
<i class="arrow2"></i>
</div>

CSS:

1
2
3
4
5
6
7
8
9
10
11
12
13
.bottom{
width: 20px;
height: 20px;
position: relative;
}
.bottom .arrow1,.bottom .arrow2{
display: block;
position: absolute;
width: 0px;/*宽高设为0才能上下左右的border显示为三角形*/
height: 0px;
border-left: 8px dashed transparent;/*transparent主要为了应用在网页中不盖住底色*/
border-right: 8px dashed transparent;
}

  然后下面关键的就来了,我们给第一个只设置上边框的话在这种情况下是个三角形:border1.png

  然后我们可以给第二个再设置一个白色的三角形然后用绝对定位的偏移使它下移盖住灰色三角形的一部分:border2.png

1
2
3
4
5
6
7
8
9
.bottom .arrow1{
border-top: 8px solid #999;
border-bottom: 8px dashed transparent;
}
.bottom .arrow2{
top: -2px;
border-top: 8px solid #fff;
border-bottom: 8px dashed transparent;
}

  然后我们一开始的那个向下的对号图标就出现啦,向上同理啦,下面就是向上向下放在一起的完整版CSS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
.bottom,.top{
width: 20px;
height: 20px;
position: relative;
}
.bottom .arrow1,.bottom .arrow2,.top .arrow1,.top .arrow2{
display: block;
position: absolute;
width: 0px;
height: 0px;
border-left: 8px dashed transparent;
border-right: 8px dashed transparent;
}
.bottom .arrow1{
border-top: 8px solid #999;
border-bottom: 8px dashed transparent;
}
.bottom .arrow2{
top: -2px;
border-top: 8px solid #fff;
border-bottom: 8px dashed transparent;
}
.top .arrow1{
border-top: 8px dashed transparent;
border-bottom: 8px solid #999;
}
.top .arrow2{
top: 2px;
border-top: 8px dashed transparent;
border-bottom: 8px solid #fff;
}
,