阅读 248

记一次无聊的面试

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.im/post/684490… 来源:掘金

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

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