1. margin

margin-top为负,元素向上移动

margin-left为负,元素向左移动

margin-right为负,布局单元向左,元素自身不动,后面元素向左。

margin-bottom为负,布局单元上移,自身元素展示不变。下面元素向上。

2. 什么是HTML语义化

html众多的标签中每一种标签都会代表一种单独的意思,比如h1-h6表示标题,ul和ol表示列表,p标签表示段落,strong标签表示强调。a标签表示超链接,img标签表示媒体图片。

每一种标签都有自己的一种意思,html是给计算机浏览器看得,在一个页面中每个部分要表示的意思我们需要通过对应的标签来告诉浏览器。这样更加方便浏览器理解我们网站的构造方便SEO(搜索引擎抓取网站的主要内容)。同时语义化标签也可以让其他人看懂你的布局结构。

3. 块级元素和内联元素

块级元素会独占一行,常见的有display属性为block和table的元素,比如div,p,table,ul,ol。

内联元素一般不会独占一行,如果空间足够会一直向后追加直到外层的宽度包容不下的时候才会换行。常见的有display属性为inline和inline-block的元素,比如span,img, input, button,a等。

4.BFC

BFC全称是块级格式化上下文,这东西面试的时候很多人也喜欢问,网上解释他的文章一搜一大堆,但就是说不到点子上饶了很大一圈反而更加让人云里雾里的。

其实BFC理解起来特别简单,我们都知道对于html的布局来说有Block,inline,inline-block等。BFC的全称就是Block format context,与之对应的还有Inline format context简称IFC,Inline-Block format context简称IBFC。

BFC要说明的就是在一个独立的渲染区域内,这块区域内部的元素的渲染不会影响外部的元素。举个例子来说,比如一个div元素,假如他触发了BFC布局,那么它里面的元素如何布局都不会影响到这个div以外的元素布局。如果没有触发BFC如果div里面的内容太多会将div外侧的其他元素推开,如果触发了BFC那么div里面的元素即使多也不会改变div外部元素的布局。就是这个意思。

一般形成BFC的条件也很简单,比如说脱离了文档流或者限制了布局大小。比如float不为none,也就是元素设置了浮动,我们都知道浮动基本会离开原本的文档流,那么内部的元素无论如何变化都不在这个文档流中,也就影响不到文档中的其他元素。

元素设置了绝对定位和固定定位也会触发BFC,原因也是脱离了文档流。

元素设置了overflow不为visible会触发BFC,因为固定了大小,内部的变化都控制在了内部。元素设置了dispaly为flex或者inline-block也会触发BFC。

5. flex常用布局

flex-direction: row|row-reverse|column|column-reverse|initial|inherit;

row水平现实,flex-direction的默认值

row-reverse与row相同,但是以相反的顺序

column垂直显示

column-reverse与column相同,但是以相反的顺序

justify-content: flex-start|flex-end|center|space-between|space-around|initial|inherit;

flex-start默认值。位于容器的开头

flex-end位于容器的结尾

center位于容器的中心

space-between位于各行之间留有空白的容器内

space-around位于各行之前、之间、之后都留有空白

交叉对齐也就是如果flex-direction是横向align-items就是纵向,如果flex-direction是纵向align-items就是横向。

align-items: stretch|center|flex-start|flex-end|baseline|initial|inherit;

stretch默认值。元素被拉伸以适应容器。如果指定侧轴大小的属性值为'auto',则其值会使项目的边距盒的尺寸尽可能接近所在行的尺寸,但同时会遵照'min/max-width/height'属性的限制

center元素位于容器的中心。弹性盒子元素在该行的侧轴(纵轴)上居中放置。(如果该行的尺寸小于弹性盒子元素的尺寸,则会向两个方向溢出相同的长度)。

flex-start元素位于容器的开头。弹性盒子元素的侧轴(纵轴)起始位置的边界紧靠住该行的侧轴起始边界。

flex-end元素位于容器的结尾。弹性盒子元素的侧轴(纵轴)起始位置的边界紧靠住该行的侧轴结束边界。

baseline元素位于容器的基线上。如弹性盒子元素的行内轴与侧轴为同一条,则该值与'flex-start'等效。其它情况下,该值将参与基线对齐。

flex-wrap: nowrap|wrap|wrap-reverse|initial|inherit;

nowrap默认值。不拆行或不拆列。

wrap在必要的时候拆行或拆列。

wrap-reverse在必要的时候拆行或拆列,但是以相反的顺序。

align-self与align-items的区别是,align-self是对子元素进行设置,align-items是对父元素进行设置。虽然他俩的效果都是针对子元素,但是align-self更加灵活,可以针对每个子元素做不同的设置,align-items只能设置所有子元素。

align-self: auto|stretch|center|flex-start|flex-end|baseline|initial|inherit;

auto默认值。元素继承了它的父容器的 align-items 属性。如果没有父容器则为 "stretch"。

stretch元素被拉伸以适应容器。如果指定侧轴大小的属性值为'auto',则其值会使项目的边距盒的尺寸尽可能接近所在行的尺寸,但同时会遵照'min/max-width/height'属性的限制。

center元素位于容器的中心。弹性盒子元素在该行的侧轴(纵轴)上居中放置。(如果该行的尺寸小于弹性盒子元素的尺寸,则会向两个方向溢出相同的长度)。

flex-start元素位于容器的开头。弹性盒子元素的侧轴(纵轴)起始位置的边界紧靠住该行的侧轴起始边界。

flex-end元素位于容器的结尾。弹性盒子元素的侧轴(纵轴)起始位置的边界紧靠住该行的侧轴结束边界。

baseline元素位于容器的基线上。如弹性盒子元素的行内轴与侧轴为同一条,则该值与'flex-start'等效。其它情况下,该值将参与基线对齐。

6. line-height继承问题

当父级的line-height设置为具体数值的时候,则子元素集成的就是该数值。

body {
    line-height: 24px;
    font-size: 15px;
}
p {
    font-size: 10px;
}

当父级的line-height设置的是比例的时候,子元素集成的就是比例。真实的值是比例乘以自己的font-size大小。假如自己的font-size为10px,继承的line-height就是2*10,20px。

body {
    line-height: 2;
    font-size: 15px;
}
p {
    font-size: 10px;
}

当父级的line-height设置的是百分比的时候,子元素集成的是百分比乘以父级font-size的值。假如父级的font-size为15px,继承的line-height就是200%*15px,30px。

7. rem

对于移动端高速发展的今天,rem早已经不是一个陌生的概念。和px,em相同他是我们css布局的一个数值单位。

em是当前标签font-size的倍数。比如如果我们高度设置2em,font-size设置为10px。那么实际高度就是2*10,20px。

p {
    height: 2em;
    font-size: 10px;
}

rem和em类似,不同的是rem的值取决于根元素,也就是html元素的font-size。

html {
    font-size: 10px;
}
p {
    height: 2rem;
    font-size: 1.5rem;
}

所以我们可以通过rem布局来实现页面的响应式布局,做法也很简单,不同的手机根据屏幕大小来设置对应的html元素的font-size, 页面其他元素的布局都采用rem。这样针对于不同的手机屏幕页面内容展示也会进行相应的缩放。以适应整个屏幕。

8. vw/vh

rem并不是响应式布局的最终解决方案,它本身存在一些问题,上面我们说了rem的值是根据html根元素的font-size值来决定的,根元素的font-size值到底应该设置多少呢?这里我们并不知道。一般的实现方式有两种,一种是通过js获取到页面的宽度进行动态设置,但是我们知道,js的加载一般都是在css之后的,这就会导致页面发生重新渲染。另一种方式是借助media媒体查询,针对不同的宽度设置不同的font-size。但是media是需要列举出来的,也就是所有的情况都要列出来,很不方便。

方式1根据屏幕宽度计算。

var windowWidth = document.documentElement.clientWidth;
document.documentElement.style.fontSize = windowWidth / 7.5 + 'px';

方式2,每种方式都需要列出来。

@media screen and (min-width: 320px) {
    html{
        font-size:50px;
    }
}
@media screen and (min-width: 360px) {
    html{
        font-size:56.25px;
    }
}
@media screen and (min-width: 375px) {
    html{
        font-size:58.59375px;
    }
}
@media screen and (min-width: 400px) {
    html{
        font-size:62.5px;
    }
}
@media screen and (min-width: 414px) {
    html{
        font-size:64.6875px;
    }
}

可以看出来rem实现的响应式布局存在一些问题,但不可否认他是一种很好的响应式方案。

wh是将网页视口高度平分100份,vw是将网页视口宽度平分100份。所以vh和vw的最大高度都是100。

p {
    height: 20vh;
    width: 40vw;
}

div {
    height: 20vmin;
    width: 40vmax;
}

视口高度就是我们常说的innerHeight,也就是抛开屏幕顶部和底部,显示网页内容的那部分高度。

window.innerHeight

屏幕高度是整个屏幕的高度

window.screen.height

client就是body的高度

document.body.clientHeight

9. typeof

typeof可以判断基本数据类型,函数类型,引用类型,nulll类型,symbol类型。

typeof 123; // number
typeof undefined; // undefined;
typeof true; // boolean;
typeof Symbol(); // symbol;
typeof null; // object;
typeof {}; // object;
typeof function() {}; // function;

10. 深拷贝代码实现

function deepClone(obj) {
    obj = obj || {};
    // 不是对象也不是数组直接返回
    if (typeof obj !== 'object' || obj === null) {
        return obj;
    }
    var result = {}; // 默认设置为对象
    if (obj instanceof Array) { // 如果是数组
        result = [];
    }
    for (var key in obj) {
        if (obj.hasOwnProperty(key)) {
            // 递归获取值
            result[key] = deepClone(obj[key]);
        }
    }
    return result;
}

11. 原型和原型链

关于原型和原型链的表述都是我个人的理解,我自己也不知道对不对,但我确实是这么理解的。

很多人对原型和原型链的理解都是起源于OOP(面向对象),出入门的开发者习惯用面向过程的方式编写js,当学习到一定程度的时候就会接触面向对象,比如面向对象中比较复杂的,封装,抽象,接口,继承,多态等概念。

其实面向对象也很好理解,它存在一个类的概念,我一般将类理解为一个模具,比如我们去超市买的一个蒸蛋器,有小熊的,小兔子的,这个模具刻画成什么样,我们用它蒸出来的鸡蛋饼就什么样。这里蒸蛋器就是类,蒸出来的鸡蛋饼就是类创建出来的对象。

我们可以用刀子叉子给这个蒸出来的鸡蛋改变样式,就像我们可以给类创建出来的对象增减方法属性,我们可以给这个蒸蛋器改变结构,那么重新蒸出来的鸡蛋饼就会使用新的样子,这种鸡蛋饼使用蒸蛋器样子的功能我们一般称为继承。就是类创建的对象具备类中的方法,并且每个创建的对象都是独立的,就像我们蒸了10个鸡蛋饼,每个鸡蛋饼都是独立的一样。

面向对象可以提高效率这是毋庸置疑的,因为他可以把一些公共的能力进行封装,使用的时候直接创建实例就可以了,比如这里的蒸蛋器,如果没有蒸蛋器我们想要做10个相同的鸡蛋饼,那就复杂了,先做鸡蛋饼再使用刀子叉子整理成想要的样子,这种工作要重复10次而且还不一定保证做出来的鸡蛋饼相同。

有了蒸蛋器就不一样了,我们只需要花费蒸鸡蛋的时间而不需要关心蒸蛋器如何将鸡蛋饼做成我们想要的样子。

传统的面向对象语言比如说java是自带类系统的,想要创建模具直接使用class关键字就可以了,但是对于ECMAScript来说他是一种基于原型对象的设计。习惯了使用面向对象开发的开发者们喜欢用原型对象来模拟面向对象。

面向对象首先会有一个构造函数,也就是使用new关键字实例化的时候执行的函数。js中通过function关键字来创建类对象,比如下面的A,由于function创建的是一个函数,所以这个函数也就作为了构造函数。在A这个类被new的时候会执行。

function A () {
    console.log('创建A的实例')
}

var aaa = new A();

如果类中存在方法,那么被创建的实例中也会存在这个方法,前面说过js是基于原型对象设计的,所以会将类中的方法挂载原型对象上,prototype。这样创建出来的实例aaa也就具备了say这个方法。面向对象中函数我们称为方法,变量称之为属性。

function A () {
    console.log('创建A的实例')
}

A.prototype.say = function() {
    console.log('say');
}

var aaa = new A();

aaa.say(); // say

这里的prototype我们称之为A的原型。可以被实例化的对象基本都有原型,比如说数字1是Numer的实例,就可以通过Numer.prototype放文档Number的原型,String.prototype访问到String对象的原型,Date.prototype访问到Date对象的原型。不能实例化的对象比如说Math就不存在原型。Math.prototype是个undefined。

我们可以通过在原型prototype的方式追加方法。比如说字符串不具备say的方法。我们可以给String追加say方法。

String.prototype.say = function() {
    console.log('调用了say方法');
}
'abc'.say(); // 调用了say方法

我们知道对象中的属性和方法可以通过继承和添加或者修改得到,假设A继承B,B继承了C,当我们访问A中的属性的时候首先会判断A自身是否有这个属性,如果没有就会去看被他继承的B是否存在这个属性,如果B也没有就回去看B继承的C有没有这个属性,从而一级一级的找下去。

这种一级一级的查找属性看起来就是一个链式结构,我们前文说过js是基于原型实现的对象所以也就有了原型链。原型链就是为对象提供一条寻找属性的通道。

原型链的工作原理也不复杂,如下,我们创建一个类型B, bb是B的实例,我们虽然知道bb是通过new B得到的,但是为什么B.prototype上存在一个say方法,bb就可以直接调用呢,这是因为bb上存在一个proto属性指向了B的prototype。

function B () {
    console.log('创建A的实例')
}

B.prototype.say = function() {
    console.log('调用了say方法');
}

var bb = new B();

bb.__proto__ === B.prototype; // true

所以我们这里得到结论实例bb通过proto与父对象B的原型prototype链接。好奇的你可能会问我们是否可以将B的原型看作是一个实例,那既然他是一个实例,他的proto属性指向的是谁呢?

B.prototype.__proto__

js中有一句话叫万物皆对象,这句话虽然不准确但是很实用,js存在一个最基本的对象Object, 我们可以理解为除了null以外所有对象都是他的子对象。那么这里B的原型的proto就是Object的实例。所里也就指向Object的原型。

B.prototype.__proto__ === Object.prototype; // true

那么Object.prototype的proto属性指向什么呢?我们说了Object是js的基本对象,也就是最底层对象,所以Object.prototype的proto是一个null。可以同构这张图来加深理解。

image.png

12. 闭包

闭包的概念并不复杂,但是他的定义比较绕,我们通过一段代码来体会闭包的概念。

首先我们定义一个makeFn的函数,在这个函数中定义一个变量msg,当这个函数调用之后,msg就会被释放掉。

function makeFn () {
    let msg = 'Hello';
}

maknFn();

如果我们在makeFn中返回一个函数,在这个函数中又访问了msg,那这就是闭包。

和刚刚不一样的是,当我们调用完makeFn之后他会返回一个函数,接收的fn其实就是接收makeFn返回的函数,也就意味着外部的fn对函数内部的msg存在引用。

所以当我们调用fn的时候,也就是调用了内部函数,会访问到msg,也就是makeFn中的变量。

function makeFn () {
    let msg = 'Hello';
    return function() {
        console.log(msg);
    }
}

const fn = maknFn();

fn();

所以闭包就是在另一个作用域(这里是全局),可以调用到一个函数内部的函数(makeFn内部返回的函数),在这个函数中可以访问到这个函数(makeFn)作用域中的成员。

根据上面的描述,闭包的核心作用就是把我们makeFn中内部成员的作用范围延长了,正常情况下makeFn执行完毕之后msg会被释放掉,但是这里因为外部还在继续引用msg,所以并没有被释放。

我们接下来看下下面这个例子, 介绍一下闭包的作用。

这里有一个once函数,他的作用就是控制fn函数只会执行一次,那如何控制fn只能执行一次呢?这里就需要有一个标记来记录,这个函数是否被执行了,我们这里定义一个局部变量done,默认情况下是false,也就是fn并没有被执行。

在once函数内部返回了一个函数,在这个新返回的函数内部先去判断done,如果done为false,就把他标记为true,并且返回fn的调用。

当once被执行的时候,我们创建一个done,并且返回一个函数。这个函数我们赋值给pay。

当我们调用pay的时候,会访问到外部的done,判断done是否为false,如果是将done修改为true,并且执行fn。这样在下一次次调用pay的时候,由于done已经为true了,所以就不会再次执行了。

function once(fn) {
    let done = false;
    return function() {
        if (!done) {
            done = true;
            return fn.apply(this, arguments);
        }
    }
}

let pay = once(function(money) {
    console.log(`${money}`);
});

// 只会执行一次。
pay(1);
pay(2);

闭包的本质就是,函数在执行的时候会放到一个执行栈上执行,当函数执行完毕之后会从执行栈上移除,但是堆上的作用域成员因为被外部引用不能释放,因此内部函数依然可以访问外部函数的成员。

13. this

this应该是很多前端初学者遇到的第一个比较抽象的概念。网上对于this介绍的文档也特别多。可能我对this理解不深,我始终觉得this没有网上那些文章写得那么复杂。一般我把this分成面向对象中的this和其他情况下的this。

先说其他情况,this的值取决于调用他的对象的的值, 也就是调用这个函数的对象,说白了就是"."前面的对象,比如下面的代码onclick函数的"."前面是btn的DOM对象,那么this就是这个DOM对象。

document.getElementById('btn').onclick = function() {
    console.log(this)l
}

函数前面的"."是谁那么this就是谁,比如。

window.a = function() {
    console.log(this);
}
window.a(); // window

上面的代码我们常简写成, 省略了window。

function a () {
    console.log(this)
}
a(); // window

下面的代码也很典型, b函数的前面是a, 所以打印的this就是a对象,如果我们把a.b赋值给c,调用c就相当于window.c(), 所以打印出来的this就是window。

var a = {
    b: function(){
        console.log(this)
    }
}
a.b(); // {a:...}

var c = a.b;

c(); // window

如果找不到调用对象的时候,this也指向window,比如下面代码,显然b的调用是没有对象的,因为b并不在window上,所以没办法使用window.b调用,这里面this也是window。

function a () {
    function b() {
        console.log(this)
    }
    b();
}
a(); // window

所以一般我判断this的指向就是通过函数是谁来调用的,this就指向谁。强调一下函数!!!,有函数就可能发生this变化。常见的坑就是setTimeout,setTimeout中包裹了一个函数,这个函数谁调用的呢?不是window就是找不到,那么无论是window还是找不到对象this都指向window。这里不要弄混。

当然前面我也说了还有一种是面向对象中的this。这里的this一般指向的是实例化后的对象。比如下面的this指向的就是实例化后aa这个对象。这也十分合理,因为我们都知道对象的实例化需要保证每个实例化后的对象各自独立,也只有指向他们各自本身才能实现独立。

function A () {
    console.log(this);
}

A.prototype.say = function() {
    console.log(this);
}

const aa = new A(); // aa

aa.say(); // aa

至于改变this的方法,比如call, apply, bind, 箭头函数,这里就不介绍了,大家应该都会用。

14. for…of

相比于for in和forEach,for…of可以进行异步循环。这是一个非常实用的功能。

(async function() {
   for await (num of asyncIterable) {
     console.log(num);
   }
})();

(async function() {
   for (num of asyncIterable) {
     await num()
   }
})();

15. EventLoop

EventLoop(事件循环)是面试中常问到的一个问题,当然如果你只是编写业务,可能你永远也不需要理解事件循环。不过当你开始阅读一些框架源码的时候就会逐渐的从框架设计中发现事件循环的影子。

事件循环可以帮你更加合理的设计你的框架类库。用白话来说事件循环就是js执行的顺序。这里的js指的是js中异步任务,同步任务,定时器,Promise等内容的执行顺序。

一般我们将定时器,ajax称为宏任务,Promise.then注册的函数称为微任务。

事件循环的执行顺序说起来也比较简单。首先JavaScript代码从上到下执行当遇到定时器等宏任务会将任务放在宏任务队列中等待,剩余的代码继续执行,遇到Promise.then等微任务会将任务放入到微任务队列中等待,继续向下执行。

等到主执行栈中的代码执行完毕,会清空微任务队列,先加入的先执行后加入的后执行,然后再去检查宏任务队列,将可执行的宏任务拿到执行栈中执行,每次只取出一个宏任务,执行完毕再次清空微任务队列,清空完毕再去检查宏任务队列,以此类推。

setTimeout(function() {
    console.log(1);
}, 0);

new Promise(function(resolve) {
    console.log(2);
    for (var i = 0; i < 10; i++) {
        i == 9 && resolve();
    }
    console.log(3);
}).then(function() {
    console.log(4);
});

console.log(5); 

// 2, 3, 5, 4, 1

第一行的setTimeout会创建一个宏任务,放入宏任务队列中;new Promise中的函数是同步代码立即会被执行,打印2和3,同时修改了Promise的状态(意味着执行栈结束后对应的微任务就可以立即执行了)。

Promise.then创建了微任务,放入到微任务队列中。

代码执行到到最后一行打印了数字5,执行栈执行完毕。接着就要清空微任务队列,微任务队列中会打印数字4,微任务执行结束后,宏任务开始执行,打印数字1,所以打印结果是2, 3, 5, 4, 1。

16. EventLoop和DOM渲染

js是单线程的,而且js执行和DOM渲染共用一个线程,js执行的时候需要保留一些时机供DOM渲染。

之前我们介绍过EventLoop, 但我们还少了一些东西,就是DOM渲染,这里我们追加上。我们前面说过js从上到下执行,当主栈的js执行完毕就会执行微任务,微任务完毕执行宏任务。其实并不完全是。正确的应该是微任务之后宏任务之前会先尝试一次DOM渲染,然后再继续事件循环。

所以如果有DOM变更的情况下,微任务执行完毕会先进行DOM渲染,再去执行宏任务,事件循环一次完毕,如果有DOM变更,再次尝试DOM渲染,再执行下一次事件循环。

var span1 = $('<span>第一个</span>');
var span2 = $('<span>第二个</span>');

$('body').append(span1).append(span2);

console.log($('#body').children().length);
alert('弹出');

上面的代码如果在浏览器中运行可以发现,控制台会输出2,表示body中存在两个子元素,这说明我们的代码已经执行了,DOM中追加的两个span元素也已经生效了。但是页面中并不会显示出这两个元素,因为alert会阻断js代码的执行,这里阻止的正是DOM的渲染过程。这也表示js执行之后DOM才会渲染。

17. 为什么微任务比宏任务执行时机早

var span1 = $('<span>第一个</span>');
var span2 = $('<span>第二个</span>');

$('body').append(span1).append(span2);

Promise.resolve().then(function() {
    console.log($('#body').children().length);
    alert('微任务弹出');
})

setTimeout(function() {
    alert('宏任务弹出')
})

上面的代码我们可以看到,弹出微任务弹框的时候,控制台输出了2,但是DOM并没有渲染,这表示微任务是在DOM渲染前执行的。弹出宏任务弹窗的时候页面出现了两个span标签,这也就表示宏任务是在DOM渲染后执行的。

宏任务会在DOM渲染后触发,如setTimeout中的回调。

微任务是在DOM渲染前触发,比如Promise的then。所以我们说微任务在宏任务之前触发。

可以换个思路,Promise微任务是ES6规定的,宏任务是浏览器规定的,所以他们存放的位置是不同的。Promise微任务并不是w3c的标椎,不遵循web apis。