记一次无聊的面试

467 阅读16分钟

1 盒模型

1.1 概念

当你对一个文档进行布局时,浏览器引擎会根据css-box模型将所有元素描述为一个盒子,css会决定这些盒子的大小,位置,属性(颜色边框等)

1.2 分类

盒模型分为两类:IE盒模型和标准盒模型,两者区别在于

IE盒模型的width/height = content +border + padding
标准盒模型的width/height = content

IE盒模型

标准盒模型

1.3 IE盒模型的引用场景

当你想让两个子容器float:left,宽度各50%,然后给一点padding,最后让子容器并排充满父容器,一切想的挺美好,然而你发现结果并不是这么美好,因为子容器的盒子宽度已经超出了父容器的一半,导致了折行,于是,width就不能50%了,只能是50%再减去padding的像素值

1.4 改变盒模型

// W3C 标准盒模型(浏览器默认)
box-sizing: content-box;
// IE 怪异盒模型
box-sizing: border-box;
//避免css在不同浏览器下的表现不同,最好加上
*, *:before, *:after {
  -moz-box-sizing: border-box;
  -webkit-box-sizing: border-box;
  box-sizing: border-box;
}

2. BFC(块级元素格式化上下文)

2.1 BFC原理

  1. 在BFC的垂直方向上,边距会发生重叠
  2. BFC区域不会与浮动区域重叠
  3. BFC在页面上是一个独立的容器,与其他元素互不影响
  4. 计算BFC高度时,浮动元素也会参与计算

2.2创建BFC

  1. float 值不为 none,只要设置了浮动,当前元素就创建了一个 BFC
  2. position值不为static,只要设置了定位,当前元素就创建了一个 BFC
  3. display 值不为默认,只要设置了display,当前元素就创建了一个 BFC
  4. overflow 值不为 visible,只要设置了overflow,当前元素就创建了一个 BFC

2.3BFC特性

  1. 使 BFC 内部浮动元素不会到处乱跑;

当子元素设置position或是float时,子元素会乱跑,给父元素设置BFC,子元素即可以依旧被父元素包裹 2. 和浮动元素产生边界。

一般情况下如果没有 BFC的话,我们想要让普通元素与浮动元素产生左右边距,需要将 maring 设置为浮动元素的宽度加上你想要产生边距的宽度。

这里的浮动元素的宽度为 200px ,如果想和它产生 20px 的右边距,需要将非浮动元素的 margin-left 设置为 200px+20px 。

2.4 BFC使用场景

  1. 解决边距重叠问题 当元素都设置了 margin 边距时,margin将取最大值。为了不让边距重叠,可以给子元素加一个父元素,并设置该父元素为 BFC
<div class="box" id="box">
  <p>Lorem ipsum dolor sit.</p>
  <div style="overflow: hidden;">
    <p>Lorem ipsum dolor sit.</p>
  </div>
  <p>Lorem ipsum dolor sit.</p>
</div>
  1. 侵占浮动元素的位置
.one {
    float: left;
    width: 100px;
    height: 100px;
    background-color: pink;
  }
  .two {
    height: 200px;
    background-color: red;
    /* 设置 BFC */
    overflow: hidden;
  }
  
  
<div class="one"></div>
<div class="two"></div>

侵占

设置BFC

3 圣杯布局和双飞翼布局

圣杯布局和双飞翼布局解决的问题是一样的,就是两边定宽,中间自适应的三栏布局(中间栏放在文档流前面优先渲染!)

圣杯布局和双飞翼布局解决问题的方案在前一半是相同的: 也就是三栏全部浮动,但左右两栏加上负margin让其跟中间栏div并排。

不同在于解决 “中间栏div内容不被遮挡” 问题的思路不一样。

3.1 圣杯布局

3.1.1 浮动

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <script src="http://lib.sinaapp.com/js/jquery/2.0.2/jquery-2.0.2.min.js"></script>
</head>
<style>
  body {
    min-width: 550px;  /* 2x leftContent width + rightContent width */
    font-weight: bold;
    font-size: 20px;
  }

  #header, #footer {
    background: rgba(29, 27, 27, 0.726);
    text-align: center;
    height: 60px;
    line-height: 60px;
  }
  #footer {
    clear: both;
  }

  #container {
    padding-left: 200px;   /* leftContent width */
    padding-right: 150px;  /* rightContent width */
    overflow: hidden;
  }

  #container .column {
    position: relative;
    float: left;
    text-align: center;
    height: 300px;
    line-height: 300px;
  }

  #center {
    width: 100%;
    background: rgb(206, 201, 201);
  }

  #left {
    width: 200px;           /* leftContent width */
    right: 200px;           /* leftContent width */
    margin-left: -100%;
    background: rgba(95, 179, 235, 0.972);
  }

  #right {
    width: 150px;           /* rightContent width */
    margin-right: -150px;   /* rightContent width */
    background: rgb(231, 105, 2);
  }

</style>

<body>
  <div id="header">#header</div>
  <div id="container">
    <div id="center" class="column">#center</div>
    <div id="left" class="column">#left</div>
    <div id="right" class="column">#right</div>
  </div>
  <div id="footer">#footer</div>


</body>

</html>

3.1.2 弹性布局

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <script src="http://lib.sinaapp.com/js/jquery/2.0.2/jquery-2.0.2.min.js"></script>
</head>
<style>
  body {
    min-width: 550px;  
    font-weight: bold;
    font-size: 20px;
  }
  #header, #footer {
    background: rgba(29, 27, 27, 0.726);
    text-align: center;
    height: 60px;
    line-height: 60px;
  }
  #container {
   display: flex;
  }
  #container .column {
    text-align: center;
    height: 300px;
    line-height: 300px;
  }
  #center {
    flex: 1;
    background: rgb(206, 201, 201);
  }
  #left {
    width: 200px;        
    background: rgba(95, 179, 235, 0.972);
  }
  #right {
    width: 150px;           
    background: rgb(231, 105, 2);
  }
</style>

<body>
  <div id="header">#header</div>
  <div id="container">
    <div id="left" class="column">#left</div>
    <div id="center" class="column">#center</div>
    <div id="right" class="column">#right</div>
  </div>
  <div id="footer">#footer</div>
</body>

</html>

3.2 双飞翼布局

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>实现三栏水平布局之双飞翼布局</title>
    <style type="text/css">
    .left, .main, .right {
        float: left;
        min-height: 130px;
        text-align: center;
    }
    .left {
        margin-left: -100%;
        background: green;
        width: 200px;
    }
    .right {
        margin-left: -300px;
        background-color: red;
        width: 300px;
    }
    .main {
        background-color: blue;
        width: 100%;
    }
    .content{
      /* 关键点:用margin把div挤到中间正常展示*/
        margin: 0 300px 0 200px;
    }
    </style>
</head>
<body>
<div class="container"> 
&emsp;   <div class="main">
    &emsp;&emsp; <div class="content">main</div> 
       </div>
&emsp;&emsp;<div class="left">left</div> 
&emsp;&emsp;<div class="right">right</div> 
</div>
</body>
</html>

4 原型与原型链

4.1原型概念

JS中一切皆对象,而每个对象都有一个原型(Object除外),这个原型,大概就像Java中的父类,所以,基本上你可以认为原型就是这个对象的父对象,即每一个对象(Object除外)内部都保存了它自己的父对象,这个父对象就是原型。一般创建的对象如果没有特别指定原型,那么它的原型就是Object(这就很类似Java中所有的类默认继承自Object类)。

4.2 对象创建

在js中,对象创建方法常见的如下

//第一种,手动创建
var a={'name':'lala'};   

//第二种,构造函数
function A(){
    this.name='lala';
}
var a=new A();

//第三种,class (ES6标准写法)

class A{
    constructor(){
        //super();此处没有使用extends显式继承,不可使用super()
        this.name='lala';
    }
}
var a=new A()
//其实后面两种方法本质上是一种写法

这三种写法创建的对象的原型(父对象)都是Object,需要提到的是,ES6通过引入class ,extends等关键字,以一种语法糖的形式把构造函数包装成类的概念,更便于大家理解。是希望开发者不再花精力去关注原型以及原型链,也充分说明原型的设计意图和类是一样的。

4.3 查看对象原型

当对象被创建之后,查看它们的原型的方法不止一种,以前一般使用对象的proto属性,ES6推出后,推荐用Object.getPrototypeOf()方法来获取对象的原型

function A(){
    this.name='lala';
}
var a=new A();
console.log(a.__proto__)  
//输出:Object {}

//推荐使用这种方式获取对象的原型
console.log(Object.getPrototypeOf(a))  
//输出:Object {}

无论对象是如何创建的,默认原型都是Object,在这里需要提及的比较特殊的一点就是,通过构造函数来创建对象,函数A本身也是一个对象,而A有两个指向表示原型的属性,分别是proto和prototype,而且两个属性并不相同

function A(){
    this.name='lala';
}
var a=new A();
console.log(A.prototype)  
//输出:Object {}

console.log(A.__proto__)  
//输出:function () {}
console.log(Object.getPrototypeOf(A))
//输出:function () {}

函数的的prototype属性只有在当作构造函数创建的时候,把自身的prototype属性值赋给对象的原型。而实际上,作为函数本身,它的原型应该是function对象,然后function对象的原型才是Object。

4.4原型的用法

其实原型和类的继承的用法是一致的:当你想用某个对象的属性时,将当前对象的原型指向该对象,你就拥有了该对象的使用权了。

function A(){
    this.name='world ';
}
function B(){
    this.bb="hello"
    }
var a=new A();
var b=new B();

Object.setPrototypeOf(a,b);
//将b设置为a的原型,此处有一个问题,即a的constructor也指向了B构造函数,可能需要纠正
a.constructor=A;
console.log(a.bb)
//输出 hello

如果使用ES6来做的话则简单许多,甚至不涉及到prototype这个属性

class B{
     constructor(){

        this.bb='hello'
     }
}
class A  extends B{
     constructor(){
        super()
        this.name='world'
     }
}

var a=new A();
console.log(a.bb+" "+a.name);
//输出hello world


console.log(typeof(A))
//输出  "function"

怎么样?是不是已经完全看不到原型的影子了?活脱脱就是类继承,但是你也看得到实际上类A 的类型是function,所以说,本质上class在JS中是一种语法糖,JS继承的本质依然是原型,不过,ES6引入class,extends 来掩盖原型的概念也是一个很友好的举动,对于长期学习那些类继承为基础的面对对象编程语言的程序员而言。

4.5 原型链

这个概念其实也变得比较简单,可以类比类的继承链条,即每个对象的原型往上追溯,一直到Object为止,这组成了一个链条,将其中的对象串联起来,当查找当前对象的属性时,如果没找到,就会沿着这个链条去查找,一直到Object,如果还没发现,就会报undefined。那么也就意味着你的原型链不能太长,否则会出现效率问题

5 作用域与作用域链

5.1 作用域

作用域是在运行时代码中的某些特定部分中变量,函数和对象的可访问性。换句话说,作用域决定了代码区块中变量和其他资源的可见性。简单来说作用域就是用来隔离变量,不同作用域下的同名变量不会有冲突。

5.1.1 全局作用域

任何地方都能访问到的对象拥有全局作用域

  1. 函数外面定义的变量拥有全局作用域
var a = 2;

function fn() {
	var b =1;
	return b;
}

console.log(fn())//1
console.log(a);//2
console.log(b);//b is not defined
  1. 未定义直接赋值的变量自动声明为拥有全局作用域
var a = 2;

function fn() {
	b =1;
	return b;
}


console.log(fn())//1
console.log(a);//2
console.log(b);//1
  1. window对象的属性拥有全局作用域

5.1.2 局部作用域

局部作用域一般只在固定的代码片段内可访问到,最常见的例如函数内部,所以在一些地方会把这种作用域成为函数作用域。

上文代码一中,b是函数内部声明并赋值,拥有局部作用域,只能带函数fn内部使用,在fn外部使用就会报错,这就是局部作用域的特性,外部无法访问。

5.1.3 ES6的块级作用域

ES5只有全局作用域和函数作用域,没有块级作用域,会带来下面问题:

  1. 变量提升
  2. 用来计数的循环变量泄露为全局变量

ES6引入了块级作用域,明确允许在块级作用域中声明函数,let和const命令都涉及块级作用域。

块级作用域允许声明函数只在使用大括号的情况下成立,如果未使用大括号,会报错。

if (true) {
function func1() {} // 不报错
}

if (true)
function func2() {} // 报错

5.2作用域链

通俗地讲,当声明一个函数时,局部作用域一级一级向上包起来,就是作用域链。

1.当执行函数时,总是先从函数内部找寻局部变量

2.如果内部找不到(函数的局部作用域没有),则会向创建函数的作用域(声明函数的作用域)寻找,依次向上

var a =1;

function fn() {
	var a = 10;
	function fn1() {
		var a =20;
		console.log(a);//20
	}

	function fn2 () {
		console.log(a);//10
	}
	fn1();
	fn2();
}

fn();
console.log(a)//1

当执行fn1时,创建函数fn1的执行环境,并将该对象置于链表开头,然后将函数fn的调用对象放在第二位,最后是全局对象,作用域链的链表的结构是fn1->fn->window。从链表的开头寻找变量a,即fn1函数内部找变量a,找到了,结果是20。

同样,执行fn2时,作用域链的链表的结构是fn2->fn->window。从链表的开头寻找变量a,即fn2函数内部找变量a,找不到,于是从fn内部找变量a,找到了,结果是10。

最后在最外层打印出变量a,直接从变量a的作用域即全局作用域内寻找,结果为1。

5.3 闭包

提到作用域就不得不提到闭包,简单来讲,闭包外部函数能够读取内部函数的变量。

优点:闭包可以形成独立的空间,永久的保存局部变量。

缺点:保存中间值的状态缺点是容易造成内存泄漏,因为闭包中的局部变量永远不会被回收

function fn1() {

	var n = 999;
	nAdd = function () {
		n += 1
	}

	function fn2() {
		console.log(n)
	}

	return fn2

}

var result = fn1();
result();//999
nAdd();//执行n +=1;
result();//1000

6.this、 call、apply、bind

6.1 this指向

在 ES5 中,其实 this 的指向,始终坚持一个原理:this 永远指向最后调用它的那个对象

    var name = "windowsName";
    function a() {
        var name = "Cherry";

        console.log(this.name);          // windowsName

        console.log("inner:" + this);    // inner: Window
    }
    a();
    console.log("outer:" + this)         // outer: Window

调用a的时候,前面没有调用对象那么即为全局对象window。这里我们没有使用严格模式,如果使用严格模式的话,全局对象就是 undefined,那么就会报错 Uncaught TypeError: Cannot read property 'name' of undefined。

    var name = "windowsName";
    var a = {
        name: "Cherry",
        fn : function () {
            console.log(this.name);      // windowsName
        }
    }

    var f = a.fn;
    f();

这里你可能会有疑问,为什么不是 Cherry,这是因为虽然将 a 对象的 fn 方法赋值给变量 f 了,但是没有调用,再接着跟我念这一句话:“this 永远指向最后调用它的那个对象”,由于刚刚的 f 并没有调用,所以 fn() 最后仍然是被 window 调用的。所以 this 指向的也就是 window。 出处。

6.2 改变this指向

目前主要有以下几种方法

  • 使用ES6箭头函数
  • 在函数内部使用this_= this
  • 使用apply、call、bind
  • new实例化一个对象

6.2.1 箭头函数

箭头函数的 this 始终指向函数定义时的 this,而非执行时。箭头函数中没有 this 绑定,必须通过查找作用域链来决定其值,如果箭头函数被非箭头函数包含,则 this 绑定的是最近一层非箭头函数的 this,否则,this 为 undefined”。

    var name = "windowsName";

    var a = {
        name : "Cherry",

        func1: function () {
            console.log(this.name)     
        },

        func2: function () {
            setTimeout( () => {
                this.func1()
            },100);
        }

    };

    a.func2()     // Cherry

6.2.2 函数内部使用 this_ = this

如果不使用 ES6,那么这种方式应该是最简单的不会出错的方式了,我们是先将调用这个函数的对象保存在变量 this_ 中,然后在函数中都使用这个 this_,这样 this_ 就不会改变了

    var name = "windowsName";

    var a = {

        name : "Cherry",

        func1: function () {
            console.log(this.name)     
        },

        func2: function () {
            var this_ = this;
            setTimeout( function() {
                this_.func1()
            },100);
        }

    };

    a.func2()       // Cherry

6.2.3 使用apply、call、bind

//apply
    var a = {
        name : "Cherry",

        func1: function () {
            console.log(this.name)
        },

        func2: function () {
            setTimeout(  function () {
                this.func1()
            }.apply(a),100);
        }

    };

    a.func2()            // Cherry
    
//call
    var a = {
        name : "Cherry",

        func1: function () {
            console.log(this.name)
        },

        func2: function () {
            setTimeout(  function () {
                this.func1()
            }.call(a),100);
        }

    };

    a.func2()            // Cherry
//bind
    var a = {
        name : "Cherry",

        func1: function () {
            console.log(this.name)
        },

        func2: function () {
            setTimeout(  function () {
                this.func1()
            }.bind(a)(),100);
        }

    };

    a.func2()            // Cherry

6.3 apply、call、bind 区别

apply定义

apply() 方法调用一个函数, 其具有一个指定的this值,以及作为一个数组(或类似数组的对象)提供的参数

语法

fun.apply(thisArg, [argsArray])
  • thisArg:在 fun 函数运行时指定的 this 值。需要注意的是,指定的 this 值并不一定是该函数执行时真正的 this 值,如果这个函数处于非严格模式下,则指定为 null 或 undefined 时会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的 this 会指向该原始值的自动包装对象。
  • argsArray:一个数组或者类数组对象,其中的数组元素将作为单独的参数传给 fun 函数。如果该参数的值为null 或 undefined,则表示不需要传入任何参数。从ECMAScript 5 开始可以使用类数组对象。浏览器兼容性请参阅本文底部内容

6.3.1 apply与call的区别

两者基本类似,区别在于传入的参数不同 call语法

fun.call(thisArg[, arg1[, arg2[, ...]]])

apply 和 call 的区别是 call 方法接受的是若干个参数列表,而 apply 接收的是一个包含多个参数的数组。

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.apply(a,[1,2])     // 3

    var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.call(a,1,2)       // 3

6.3.2 bind与apply、call的区别

bind()方法创建一个新的函数,当被调用时,将其this关键字设置为提供的值,在调用新函数时,在任何提供之前提供一个给定的参数序列。

所以bind是创建一个新的函数,必须手动调用

 var a ={
        name : "Cherry",
        fn : function (a,b) {
            console.log( a + b)
        }
    }

    var b = a.fn;
    b.bind(a,1,2)()           // 3

7 手动实现apply、 call、 bind

7.1 实现call

Function.prototype._call = function (context = window) {
    var context = context;
    context.fn = this;

    var args = [];
    for(var i = 1, len = arguments.length; i < len; i++) {
        args.push('arguments[' + i + ']');
    }

    var result = eval('context.fn(' + args +')');

    delete context.fn
    return result;
}

// 测试一下
var value = 2;

var obj = {
    value: 1
}

function bar(name, age) {
    console.log(this.value);
    return {
        value: this.value,
        name: name,
        age: age
    }
}

bar._call(null); // 2

console.log(bar._call(obj, 'kevin', 18));
// 1
// Object {
//    value: 1,
//    name: 'kevin',
//    age: 18
// }

7.2 实现apply

Function.prototype.apply = function (context = window, arr) {
    var context = context;
    context.fn = this;

    var result;
    if (!arr) {
        result = context.fn();
    }
    else {
        var args = [];
        for (var i = 0, len = arr.length; i < len; i++) {
            args.push('arr[' + i + ']');
        }
        result = eval('context.fn(' + args + ')')
    }

    delete context.fn
    return result;
}

7.3 实现bind

Function.prototype._bind = 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 () {
        var bindArgs = Array.prototype.slice.call(arguments);
        return self.apply(this instanceof fNOP ? this : context, args.concat(bindArgs));
    }

    fNOP.prototype = this.prototype;
    fBound.prototype = new fNOP();
    return fBound;
}

8 手动实现jq的$each

// 通过字面量方式实现的函数each
var each =  function(object, callback){
  var type = (function(){
          switch (object.constructor){
            case Object:
                return 'Object';
                break;
            case Array:
                return 'Array';
                break;
            case NodeList:
                return 'NodeList';
                break;
            default:
                return 'null';
                break;
        }
    })();
    // 为数组或类数组时, 返回: index, value
    if(type === 'Array' || type === 'NodeList'){
        // 由于存在类数组NodeList, 所以不能直接调用every方法
        [].every.call(object, function(v, i){
            return callback.call(v, i, v) === false ? false : true;
        });
    }
    // 为对象格式时,返回:key, value
    else if(type === 'Object'){
        for(var i in object){
            if(callback.call(object[i], i, object[i]) === false){
                break;
            }
        }
    }
}

参考文献

作者:拉丁吴 链接:juejin.cn/post/684490… 来源:掘金

作者:sunshine小小倩 链接:juejin.cn/post/684490… 来源:掘金

作者:Chris_Ping 链接:juejin.cn/post/684490… 来源:掘金