1. margin

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

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

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

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

2. 什么是HTML语义化

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

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

3. 块级元素和内联元素

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

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

4.BFC

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

其实BFC理解起来特别简单,对于html的布局来说有Blockinlineinline-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,因为固定了大小,内部的变化都控制在了内部。元素设置了dispalyflex或者inline-block也会触发BFC

5. flex常用布局

1. flex-direction布局方向,横向还是纵向

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

row水平显示,flex-direction的默认值

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

column垂直显示

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

2. justify-content对齐方向

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

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

flex-end位于容器的结尾

center位于容器的中心

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

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

3. align-items交叉对齐方式

交叉对齐也就是如果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等效。其它情况下,该值将参与基线对齐。

4. flex-wrap换行设置

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

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

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

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

5. align-self子元素交叉对齐方式

align-selfalign-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-size10px,继承的line-height就是2*1020px

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

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

7. rem

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

em是当前标签font-size的倍数。例如如果高度设置2emfont-size设置为10px。那么实际高度就是2*1020px

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

remem类似,不同的是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份。所以vhvw的最大值都是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可以判断基本数据类型,函数类型,引用类型,null类型,symbol类型以及bigint类型

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

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的原型。可以被实例化的对象基本都有原型,数字1Number的实例,就可以通过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, bbB的实例,虽然知道bb是通过new B得到的,但是为什么B.prototype上存在一个say方法,bb就可以直接调用呢,这是因为bb上存在一个__proto__属性指向了Bprototype

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__属性指向什么呢?说了Objectjs的基本对象,也就是最底层对象,所以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,如果donefalse,就把他标记为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函数的.前面是btnDOM对象,那么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变化。常见的坑就是setTimeoutsetTimeout中包裹了一个函数,这个函数谁调用的呢?不是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 inforEachfor...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中的函数是同步代码立即会被执行,打印23,同时修改了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渲染前触发,比如Promisethen。所以说微任务在宏任务之前触发。

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

18. DOM性能

DOM操作是非常昂贵的,所谓昂贵就是指DOM操作会耗费CPU,重复的重排和重绘也就是重复渲染,比较耗时耗费CPU的计算比较多。如果频繁操作可能会造成卡顿的问题,所以需要注意这些地方。

1. 对DOM查询做缓存

用变量存储获取到的页面元素,避免频繁查询页面dom。

2. 将频繁操作改为一次性操作。

多个dom操作建议合并统一操作。

for (var i = 0; i < 10; i++) {
    var div = document.createElement('div');
    document.body.appendChild(div);
}

// 使用文档片段保存操作,一次性插入页面
var frag = document.createDocumentFragment();

for (var i = 0; i < 10; i++) {
    var div = document.createElement('div');
    frag.appendChild(div);
}

document.body.appendChild(frag);

19. BOM

BOM就是浏览器提供的一些api

1. navigator

浏览器信息

// 获取浏览器信息
var ua = navigator.userAgent;

2. screen

屏幕信息

screen.width
screen.height

3. location

地址信息

location.href
location.protocol
location.pathname
location.host
location.origin
location.search
location.hash

4. history

历史记录信息

// 后退
history.back();
// 前进
history.forward();

20. 事件冒泡

事件冒泡就是我在一个标签上绑定了一个事件,当事件被触发的时候会沿着他的祖先标签一级一级的向上传递,祖先中如果有绑定和他相同类型的事件也会触发。

<body onclick>
    <div onclick>
        <p>
            <span onclick>测试</span>
        </p>
    </div>
</body>

比如上面的代码中在span标签中绑定了onclick事件,当点击span标签的时候,会触发自身的onclick事件,然后会向父级找到p标签,p标签并没有绑定事件所以继续向上,找到div标签,div绑定了click事件所以会触发,然后继续向上找到body

事件冒泡的存在是绝对合理的。举个简单的例子,A住在某小区的1号楼1单元1楼,B进入到A的家中肯定也就进入了1楼,1单元,1号楼,这个小区。

span标签在p标签内,p标签又在div标签内,点击了span肯定也就点击了p标签,div标签。

可以通过阻止事件冒泡的方式让事件停止继续向上传递,如果在spanonclick事件中阻止了冒泡那么事件就不会执行到祖先标签中。如果在祖先的任意一级阻止了冒泡,事件就会终止在阻止的这一级不会继续向上传递。

span.onclick = function(event) {
    event.stopPropagation();
}

21. 事件委托

事件委托又叫事件代理,一般是利用事件冒泡的功能对事件进行批量处理。

比如页面中有10li,每点击一个li都要触发一次点击事件,可以给每个li绑定一个点击事件,这样就会绑定10次,也可以给li的父节点绑定事件,只需要绑定一次,根据事件冒泡的原理,点击li的时候绑定在li父级的事件也会触发。

ul.onclick = function() {

}

事件委托一个典型的应用场景是当子元素个数不确定的时候用来解决事件问题。当页面有3li的时候要给每个li绑定一个点击事件。如果页面需要动态增加1li的时候,除了给页面添加这个li之外还要给新增的li绑定事件。

var li = document.createElement('li');

li.onclick = function() {}

ul.appendChild(li);

这就比较麻烦,如果的事件是绑定在li的父节点上时,就可以自由的增删li节点,并且新增的节点也会触发父节点上的事件。因为前面说了,点击了li也相当于点击了父节点。

这种将事件绑定在需要监控的元素祖先节点上的方式一般称为事件委托。

22. JSONP

很多人对jsonp的理解都停留在概念上,没有真正理解过他的原理,他为什么可以跨域,当然不仅仅是script标签不受同源策略影响,实际上jsonp是一种前后端约定的解决方案。

不过现在基本已经很少用到了。因为现在已经有了更流行的CORS方案,相对来说也会更安全,不过jsonp还是有其自身的优势的。

很多人都知道浏览器的同源策略,就是发送请求的页面地址和被请求的接口地址的域名,协议,端口三者必须一致,否则浏览器就会拦截这种请求。浏览器拦截的意思不是说请求发布出去,请求还是可以正常触达服务器的,如果服务器正常返回了浏览器也会接收的到,只是不会交给所在的页面。这一点查看network是可以看到的。

jsonp一般是利用script标签的src属性,对于服务器来说只有请求和响应两种操作,请求来了就会响应,无论响应的是什么。请求的类型实在太多了。

浏览器输入一个url是一个请求,ajax调用一个接口也是一个请求,imgscriptsrc也是请求。这些地址都会触达服务器。那为什么jsonp一般会选用script标签呢,首先大家都知道script加载的js是没有跨域限制的,因为加载的是一个脚本,不是一个ajax请求。

你可以理解为浏览器限制的是XMLHttpRequest这个对象,而script是不使用这个对象的。

仅仅没有限制还不够,还有一个更重要的点因为script是执行js脚本的标签,他所请求到的内容会直接当做js来执行。

这也可以看出,jsonpajax对返回参数的要求是不同的,jsonp需要服务返回一段js脚本,ajax需要返回的是数据。

因此这就要求服务器单独来处理jsonp这中请求,一般服务器接口会把响应的数据通过函数调用的方式返回,例如返回的内容是yd,那就要返回成cb('yd')

cb('yd')

这是一段函数调用的脚本,通过script标签加载之后会立即执行的,如果在全局定义一个cb函数。那么这段脚本执行的时候就会调用到定义的那个函数,函数中的参数就是服务返回的值了。前端也就可以在这个函数中获取到了。

function cb (data) {
    console.log(data);
}

所以说jsonp是前后端共同约定的一种结果。

为什么jsonp需要使用script标签而不用link或者img标签呢?linkimg也不受同源策略的影响,但是如果使用img去做请求不会实现想要的效果,因为img加载的是图片资源,是用来展示的。link加载的是css资源,是以css tree解析的。想要的数据应该用在js中。所以必须使用可以执行js的脚本去请求。

23. 描述cookie, localStorage, sessionStorage区别

1. cookie

本身是用于浏览器服务器通讯的,最早是被借用到本地存储中来,因为在H5之前前端基本没有存储数据的能力,所以只能选择将数据存储在cookie中,但是cookie的真正用处并不是存储数据,他是浏览器和服务器通讯,所以cookie中存储的数据会随请求发送给服务器。

服务器可以获取到浏览器发送过来的cookie,也可以去修改cookie,所以cookie最好的用处是记录用户信息。

当用户登录成功之后,服务器将用户登录标识存储在cookie中,浏览器下次请求服务器的时候,cookie会随请求一并发送给服务器,服务器可以通过这个标识判断用户是否登录,以及用户的权限。请求中携带cookie是一种主动能力,开发者不需要额外设置。

所以cookie的价值在于此而不在于存储前端数据,否则H5的普及会导致cookie的衰败,显然并没有。cookie存储大小一般为4kb。用于前后端通信4kb基本足够了。太多的cookie会导致请求过重。

cookie设置的时候可以设置有效期,存储的path路径,存储的域名,被存储数据是否转译,如果是服务端存储的cookie也可以设置是否只有服务器可操作,前端不得操作。

cookie设置的有效期如果不设置则为临时cookie,当会话结束后会消失,windows系统基本关闭浏览器会话就结束了,mac系统需要退出浏览器才会结束会话。

2. localStorage

H5提供的前端永久存储能力,最大可以存储5M,每个域名都可以存储5M,当超过5M时会抛错,可通过try...catch捕获。

localStorage是前端存储能力,所以他只用于存储,相比较cookie不会发送给服务器,我不知道为啥有些面试官喜欢问localStorage会不会发送给服务端这种问题,localStoragecookie根本就没有可比性,就不是一个东西, 设计初衷都不一样。

localStorage是永久存储,除非手动删除,不然会一直存在浏览器当中。

3. sessionStorage

localStorage类似,sessionStorage也是H5提供的一种存储能力,同样最大可以存储5M,每个域名都可以存储5M,当超过5M时会抛错,可通过try...catch捕获。

sessionStorage是会话存储,当会话结束sessionStorage就消失了,很多人说关闭浏览器sessionStorage才会消失这是不正确的。

sessionStorage是会话结束就会消失,关闭浏览器会话肯定结束了,但是不关闭浏览器会话结束sessionStorage也会消失。

比如在A页面设置了sessionStorageA页面跳转到了B页面,B页面是可以访问到A设置的sessionStorage的,但如果复制B页面的url地址,新开选项卡粘贴Burl回车访问,这个时候就访问不到A设置的sessionStorage。这是因为这样打开的BA不是同一个会话。

24. http状态码

1. 1xx

服务器收到了请求

2. 2xx

请求成功,比如常见的200

3. 3xx

重定向,比如服务器发现浏览器访问的地址不正确就会返回这个状态码告诉前端访问另一个地址。

比如用户访问个人中心的时候,服务器发现用户没有登录或者登录状态失效了,就要通知用户跳转到登录页面重新登录,此时就可以使用302

301是永久重定向,302是临时重定向。如果一个页面的地址迁移了,但之前放出去的地址基本修改不了,可以用301永久转发到新页面,如果只是临时转发到页面使用302

304表示资源未被修改,一般配合协商缓存使用。用户访问浏览器的时候浏览器会向服务器发送请求获取网站的数据,比如浏览器向服务器请求一段js代码,服务器返回给浏览器这个过程是需要网络传输的,网络的快慢决定浏览器拿到这段js的时间也就决定了页面访问的时间。

为了提高效率,浏览器请求服务器获得js之后会在浏览器中备份,当再次请求这个js文件时浏览器会先询问服务器,这个js是否有变化,如果文件变化了,服务器会返回新的js给浏览器状态码为200,浏览器使用后备份,如果js文件没有变化,服务器会告诉浏览器文件未变化,状态码为304,浏览器就时候自己备份的js文件,节省了文件的传输时间。

4. 4xx

基本都是浏览器端的错误,比如常见的404,就是请求的地址不存在,浏览器输入错地址了。403是用户访问的地址没有权限。这个用户不可以访问这个地址。

5. 5xx

基本都是服务器的错误,例如服务器长时间不返回数据就会超时。

其实http的协议和规范就是一个约定,在使用它的时候按照这个约定执行就可以了。

25. Restful API

是一种新的API设计方法,其实也不新了,好多年了,很多公司已经在用了,甚至很多人都觉得他不好用淘汰了。

Restful不是规范,是一种风格,所以可以用也可以不用,看你自己团队的要求。

相比传统API将每个url当做一种功能的设计来说,Restful是把每个url当做一个唯一的资源。

传统的API是将每个url当做一种功能来设计,每一种功能就需要一个api地址,例如新增,修改,删除,这就会出现三个url地址。一个地址对应一个功能。

Restful是把url当做资源来设计,比如想找到电脑里的某一个文件,那么这个文件是有访问路径的。这个路径就是url,访问到的文件就是资源。访问资源的时候是无法传递参数的。

所以Restful的设计方式就是,尽量不用url参数,用method表示操作类型,一个资源地址通过post请求方式表示更新,delete请求方式表示删除。

# 传统的API
/api/list?pageNum=2
# Restful
/api/list/2

26. 刷新操作对缓存的影响

1. 地址栏输入url,前进后退

强制缓存有效,协商缓存有效

2. F5刷新,点击刷新按钮,右键菜单刷新

强制缓存失效,协商缓存有效

3. 强制刷新 ctrl+F5

强制缓存失效,协商缓存失效

27. onload和DOMContentLoaded区别

window.onload是资源全部加载完毕才执行,包括图片等资源。

DOMContentLoadedDOM渲染完成之后才执行,图片可能尚未下载。

28. 节流和防抖

防抖debounce, 比较监听输入框,当用户频繁输入的时候会频繁调用事件,可以防抖阻止重复请求,只在最后一次才请求。

节流throttle和防抖的区别是防抖是最后一次触发,节流是保持一个频率连续触发,比如拖拽的时候要随时拿到该元素拖拽的位置,如果直接用drag事件,则会频繁触发,很容易导致卡顿,一般可以限制无论拖拽速度多快,都会每隔100ms触发一次。

29. 函数声明和函数表达式区别

函数声明的函数可以在声明前调用,表达式创建的函数只能创建后调用。

aaa();
function aaa() {

}

var bbb = function() {

}
bbb();

30. new Object和Object.create区别

{}等同于new Object, 存在原型Object.prototype, {}new Object都是Object的实例。所以{}new Object__proto__等于Object.prototype

Object.create可以自定义原型prototype,如果传入null就是没有原型。可以创建一个干净的对象。

Object.create(null); // 原型为null

Object.create({a: 1}); // 原型为{a: 1}

31. 什么是JSON

json是一种数据格式,本质是一段字符串。json格式与js对象结构一致,对js语言更友好。

json是数据格式,不是js特有的对象,在javaphp中都可以使用json

window.JSON是一个全局对象,是js中提供的用于操作json的方法。JSON.stringifyJSON.parse

32. 获取url中的参数

const query = new URLSearchParams(location.search);
const value = query.get('name');
query.forEach(function(value, key) {
    console.log(value, key);
})
console.log(value);

33. 数组拍平

数组的concat方法可以将数组解开,比如

[1,2].concat([1,2,3]) // [1, 2, 1, 2, 3];

可以利用concat配合递归将多级数组拍平。

function flat(arr) {
    const isDeep = arr.some(item => item instanceof Array);
    if (!isDeep) { // 数组里面没有数组了就返回
        return arr;
    }
    const res = Array.prototype.concat.apply([], arr);
    return flat(res);
}

34. requestAinmateFrame

这是一个动画的API,一般对于动画来说,想要动画流畅每秒动画要更新60帧,这也是人眼适应的频率。也就是16.67ms更新一次。相对setTimeout来说requestAinmateFrame性能更加优秀,对于一些后台标签或隐藏iframe中的内容,requestAinmateFrame会暂停,而setTimeout依然执行。

requestAinmateFrame会在一个恰当的时机执行,可以理解为requestAinmateFrame会检测页面的渲染,当渲染空闲的时候才会执行,而setTimeout是到时间就执行,这就会造成卡UI的情况,UI还没渲染完成setTimeout执行了如果setTimeout中存在渲染UI, 就会卡住之前渲染,造成卡顿。

35. 面试技巧

简历要简洁明了,突出个人技能和项目经验,一般简历写个两页纸就可以了,太多也没人看,太少单薄,我一般面试的时候就只看前两页。

简历中个人技能要写在前面,项目经验写在后面,不要弄反了,项目经验中要描述清楚技术栈。个人介绍,工作经历,教育经历简单就好。

如果个人博客和开源作品质量都不错,可以写在简历中,如果没有或者质量一般最后不要放。

简历要保证能力的真实性,不要造价,水份不要过大,切忌不要写精通,也没几个人能真的做到精通。

如何看待加班?这个还是要看个人,我一般是不推荐加班的,就像借钱,救急不救穷,要说明白怎么看待加班,什么样的加班接受什么样的加班不接受。项目特别急并且价值特别大加加班也是没什么的,常态的加班肯定是不可取的。长时间的加班带来的结果必定的虚假繁荣,生产力的低下。

面试中最好不要挑战面试官,如果面试官说的不对可以低调的或者委婉的转变一下说辞,离开也可以不要和面试官争辩,即使你不想要这份工作。

要给面试官惊喜,证明你可以做的更好知道的更多,一个问题如果你能回答及格就要考虑怎么回答到优秀,但是要做到更好不是更多,别说太多废话,也不要说起没完。例如这个问题还有一个更好的解决办法等等。

遇到不会的问题说出你知道的部分即可,但别岔开话题,给人心虚的感觉,而且面试的过程中你也基本很难岔开话题,这会让面试官很反感。

对于自己的缺点可以换一种话术,委婉一些的表述,人都有缺点这没什么,你可以说你知道自己哪部分知识薄弱最近也正在学习,这样就化缺点为优点了。

转载须知

如转载必须标明文章出处文章名称文章作者,格式如下:

转自:【致前端 - zhiqianduan.com】 前端基础知识  "隐冬"
请输入评论...