让前端飞Web前端之路

中高级前端面试知识点汇总

2019-08-29  本文已影响83人  倾城一梦1123

1.1、什么是HTML语义化?有什么好处?

根据内容的结构化(内容语义化),选择合适的标签(代码语义化)便于开发者阅读和写出更优雅的代码的同时让浏览器的爬虫和机器很好地解析。

为了在没有 CSS 的情况下,页面也能呈现出很好地内容结构、代码结构:为了裸奔时好看;

用户体验:例如 title、alt 用于解释名词或解释图片信息、label标签的活用;

有利于 SEO:和搜索引擎建立良好沟通,有助于爬虫抓取更多的有效信息:爬虫依赖于标签来确定上下文和各个关键字的权重;

方便其他设备解析(如屏幕阅读器、盲人阅读器、移动设备)以语义的方式来渲染网页;

便于团队开发和维护,语义化更具可读性,是下一步网页的重要动向,遵循这个标准,可以减少差异化。

1.2、html5有哪些新特性?

HTML5 已经不是 SGML 的子集

新增关于图像,地理位置,缓存,多任务等多个功能

用于媒体回放的 Video 和 Audio

语义化更好的标签如 header、footer、section、aside、nav、canvas、time等等,利于SEO优化

新增的表单元素如 <datalist>、<keygen>、<output>

新增的表单属性如 autocomplete、autofocus、placeholder、required、step等等。

HTML5 Web 存储 sessionStorage 和 localStorage

HTML5 WebSocket

HTML5 离线Web应用(应用程序缓存)

1.3、你能解释一下CSS的盒子模型么?

CSS盒模型是围绕在HTML元素周围的定义 Border(边界),padding(内边距)和

margin(外边距)的矩形空间

Border(边界):定义了元素包含的最大区域

Padding(内边距):定义了边界和内部元素的间距

Margin:定义了边界和任何相邻元素的间距

1.4、CSS选择器有哪些?哪些属性可以继承?

CSS选择符:id选择器(#myid)、类选择器(.myclassname)、标签选择器(div, h1, p)、相邻选择器(h1 + p)、子选择器(ul > li)、后代选择器(li a)、通配符选择器(*)、属性选择器(a[rel=”external”])、伪类选择器(a:hover, li:nth-child)

可继承的属性:font-size, font-family, color

不可继承的样式:border, padding, margin, width, height

优先级(就近原则):!important > [ id > class > tag ]

!important 比内联优先级高

1.5、CSS3新增伪类有那些?

E:nth-last-child(n)

E:nth-of-type(n)

E:nth-last-of-type(n)

E:last-child

E:first-of-type

E:only-child

E:only-of-type

E:empty

E:checked

E:enabled

E:disabled

E::selection

E:not(s)

E::not(.s)

tbody: nth-child(even), nth-child(odd)/*:此处他们分别代表了表格(tbody)下面的偶数行和奇数行(tr)*/等等......

1.6、css3有那些新特性?

RGBA 和透明度

background-image background-origin(content-box/padding-box/border-box)

background-size background-repeat

word-wrap(对长的不可分割单词换行)word-wrap:break-word

文字阴影:text-shadow: 5px 5px 5px #FF0000;(水平阴影,垂直阴影,模糊距离,阴影颜色)

font-face属性:定义自己的字体

圆角(边框半径):border-radius 属性用于创建圆角

边框图片:border-image: url(border.png) 30 30 round

盒阴影:box-shadow: 10px 10px 5px #888888

媒体查询:定义两套css,当浏览器的尺寸变化时会采用不同的属性

1.7、对BFC规范(块级格式化上下文:block formatting context)的理解?

BFC规定了内部的 Block Box 如何布局。

定位方案:

内部的 Box 会在垂直方向上一个接一个放置。

Box 垂直方向的距离由 margin 决定,属于同一个BFC的两个相邻Box的margin会发生重叠。

每个元素的margin box 的左边,与包含块border box的左边相接触。

BFC的区域不会与float box重叠。

BFC是页面上的一个隔离的独立容器,容器里面的子元素不会影响到外面的元素。

计算BFC的高度时,浮动元素也会参与计算。

满足下列条件之一就可触发BFC:

根元素,即html

float 的值不为 none(默认)

overflow 的值不为 visible(默认)

display 的值为 inline-block、table-cell、table-caption

position 的值为 absolute 或 fixed

1.8、 CSS优化、提高性能的方法有哪些?

避免过度约束

避免后代选择符

避免链式选择符

使用紧凑的语法

避免不必要的命名空间

避免不必要的重复

最好使用表示语义的名字。一个好的类名应该是描述他是什么而不是像什么

避免!important,可以选择其他选择器

尽可能的精简规则,你可以合并不同类里的重复规则

1.9、两个div并排,左边div固定宽度,右边宽度自适应”,至少列出4种。

方式一:BFC(块级格式化上下文)

思路:左边定宽 float:left,右边采用 overflow: hidden; /* 触发bfc */

方式二:采用flex布局

这种应该是最简单的方式,右边 flex:1

方式三:采用 display:table的方式来实现布局

父元素 display:table,两个子元素采用 display:table-cell;

方式四:采用calc计算宽度的方式来实现

方式五:采用absolute+margin-left来实现

1.10、Sass和Less的异同之处。

1、Less 环境较 Sass简单

Cass的安装需要安装Ruby环境,Less基于JavaScript,是需要引入Less.js来处理代码输出css到浏览器,也可以在开发环节使用Less,然后编译成css文件,直接放在项目中,有less.app、SimpleLess、CodeKit.app这样的工具,也有在线编辑地址。

2、Less 使用较 Sass简单

LESS 并没有裁剪 CSS 原有的特性,而是在现有 CSS 语法的基础上,为 CSS 加入程序式语言的特性。只要你了解 CSS 基础就可以很容易上手。

3、从功能出发,Sass 较 Less 略强大一些

①sass有变量和作用域。

- $variable,like php;

- #{$variable}like ruby;

- 变量有全局和局部之分,并且有优先级。

② sass 有函数的概念;

- @function和@return以及函数参数(还有不定参)可以让你像js开发那样封装你想要的逻辑。

-@mixin类似function但缺少像function的编程逻辑,更多的是提高css代码段的复用性和模块化,这个用的人也是最多的。

-ruby提供了非常丰富的内置原生api。

③进程控制:

-条件:@if @else;

-循环遍历:@for @each @while

-继承:@extend

-引用:@import

④数据结构:

-$list类型=数组;

-$map类型=object;

其余的也有string、number、function等类型

4、Less 与Sass 处理机制不一样

前者是通过客户端处理的,后者是通过服务端处理,相比较之下前者解析会比后者慢一点

5、关于变量在 Less 和 Sass 中的唯一区别就是 Less 用@,Sass 用$。

###二、Javascript 部分

2.1、介绍JavaScript的基本数据类型

String、Number、Boolean、Null、Undefined、Symbol、BigInt

2.2谈谈this的理解

this 总是指向函数的直接调用者(而非间接调用者)

如果有new关键字,this 指向new 出来的那个对象

在事件中,this 指向目标元素,特殊的是IE的attachEvent中的this总是指向全局对象window。

2.3、null,undefined的区别?

null表示一个对象被定义了,但存放了空指针,转换为数值时为0。

undefined表示声明的变量未初始化,转换为数值时为NAN。

typeof(null) -- object;

typeof(undefined) -- undefined

2.4、 什么是闭包(closure),为什么要用它?

闭包指的是一个函数可以访问另一个函数作用域中变量。常见的构造方法,是在一个函数内部定义另外一个函数。内部函数可以引用外层的变量;外层变量不会被垃圾回收机制回收。

注意,闭包的原理是作用域链,所以闭包访问的上级作用域中的变量是个对象,其值为其运算结束后的最后一个值。

优点:避免全局变量污染。缺点:容易造成内存泄漏。

特性:

a. JavaScript允许你使用在当前函数以外定义的变量

b. 即使外部函数已经返回,当前函数仍然可以引用在外部函数所定义的变量

c. 闭包可以更新外部变量的值

d. 用闭包模拟私有方法

由于闭包会使得函数中的变量都被保存在内存中,内存消耗很大,所以不能滥用闭包,否则会造成网页的性能问题

例子:

function makeFunc() {

    var name = "Mozilla";

    function displayName() {

        console.log(name);

    }

    return displayName;

}

var myFunc = makeFunc();

myFunc();  //输出Mozilla

2.5、如何判断一个对象是否属于某个类?

使用instanceof 即if(a instanceof Person){alert('yes');}

2.6、new操作符具体干了什么呢?

创建一个空对象,并且 this 变量引用该对象,同时还继承了该函数的原型。

属性和方法被加入到 this 引用的对象中。

新创建的对象由 this 所引用,并且最后隐式的返回 this 。

2.6、JS延迟加载的方式有哪些?

JS的延迟加载有助与提高页面的加载速度。

defer和async、动态创建DOM方式(用得最多)、按需异步载入JS

defer:延迟脚本。立即下载,但延迟执行(延迟到整个页面都解析完毕后再运行),按照脚本出现的先后顺序执行。

async:异步脚本。下载完立即执行,但不保证按照脚本出现的先后顺序执行。

2.7、call和apply的区别

call()方法和apply()方法的作用相同,动态改变某个类的某个方法的运行环境。他们的区别在于接收参数的方式不同。在使用call(obj,a1,a2...)方法时,传递给函数的参数必须逐个列举出来。使用apply(obj,[a1,a2...])时,传递给函数的是参数数组。

2.8、数组对象有哪些原生方法,列举一下

pop、push、shift、unshift、splice、reverse、sort、concat、join、slice、toString、indexOf、lastIndexOf、reduce、reduceRight、forEach、map、filter、every、some

2.9、什么是跨域?如何解决?

要明白什么是跨域之前,首先要明白什么是同源策略?

同源策略就是用来限制从一个源加载的文档或脚本与来自另一个源的资源进行交互。那怎样判断是否是同源呢?

如果协议,端口(如果指定了)和主机对于两个页面是相同的,则两个页面具有相同的源,也就是同源。也就是说,要同时满足以下3个条件,才能叫同源:

协议相同

端口相同

主机相同

解决方案:

1.iframe

随着近年来前端技术的飞跃发展以及移动互联网时代的洗礼,iframe的使用渐渐的不被建议,虽然也是一种跨域请求的解决方案,但这里就不再讲述,请读者自行查阅网上资料。

2.jsonp

jsonp是比较常用的方法,我们假设a.com域名需要向b.com发起一个api请求(jsonp的一个缺点是,仅能接受GET方式),则使用JSONP完成该过程的。

3. 通过请求同域下的api,间接获取它域的数据

我们仍以域名a.com/demo.html需获取b.com下的数据为例,这时候只要在a.com下创建一个demo.php,由demo.php通过curl的方式向b.com发起数据请求,并包装请求结果返回给a.com/demo.html页面。这里主要是通过与a.com/demo.html同域下的a.com/demo.php做了一层数据请求代理,避免了前端跨域请求。

4.使用web服务器的反向代理设置

同样是使用代理的思维,但与2不同的是,我们这里使用web服务器的反向代理配置:

Nginx反向代理可以使用 proxy_pass

Apache2的反向代理的配置可以使用ProxyPass

5.设置header头(CORS)

在你要跨域请求的api里,设置header头Access-Control-Allow-Origin: "*";

2.10、如何实现js中的继承

1、原型继承的第一种方式:

function Cat(name){

    this.name=name;

}

//原型继承

Cat.prototype.say=function(){

    alert("你好,我是一只猫,我叫:"+this.name);

}

2、原型继承第二种方式:

function Cat(name) {

    this.name = name;

}

function Animal() {}

Animal.prototype.run = function () {

    alert("动物跑");

};

Cat.prototype = new Animal();

Cat.prototype.constructor=Cat;

3、借用构造函数

function Cat(name,age) {

    Animal.call(this,name,age);

}

function Animal(name,age) {

    this.name = name;

    this.age=age;

}

4、经典继承

function create(obj) {

    if(Object.create) {

    return Object.create(obj); 

    } else {

    function F(){};

    F.prototype = obj;

    return new F();

    }

}

2.11、看下列代码,输出什么?解释原因。

var undefined;//此时undefined这个变量的值是undefined

undefined == null; // true

1 == true;  // true

此时会把布尔类型的值转换为数字类型 true=1 false=0

2 == true;  // false

0 == false;  // true

0 == '';    // true

NaN == NaN;  // false isNaN

[] == false; // true  解释:会把[]和false都通过Number()转换为数字类型

[] == ![];  // true  解释:![]:false

[]==[];//false

一个是number一个是string时,会尝试将string转换为number

一个是number一个是boolean,将boolean转换为number,结果:true:1 false:0

一个是object 另一个是string或number,将Object转换成number或string

所以,对于0、空字符串的判断,建议使用 “===” 。“===”会先判断两边的值类型,类型不匹配时为false。

2.12、已知数组numberArray = [3,6,2,4,1,5];

1) 实现对该数组的倒排,输出[5,1,4,2,6,3]

function reverseArray(arr){

    var result=[];

    //方法1:

    /*for (var i = arr.length - 1; i >= 0; i--) {

        result.push(arr[i]);

    }*/

    //方法2:

    for (var i = 0, len = arr.length; i < len; i++) {

        result.unshift(arr[i]);

    }

    return result;

}

2) 实现对该数组的降序排列,输出[6,5,4,3,2,1]

冒泡排序过程演示

function sortDesc(arr) {

    for (var i = 0, len = arr.length; i < len; i++) {

        for (var j = i + 1, len2 = arr.length; j < len2; j++) {

            //>就是降序 <就是升序

            if (arr[j] > arr[i]) {

                var temp = arr[j];

                arr[j] = arr[i];

                arr[i] = temp;

            }

        }

    }

    return arr;

}

2.13、有这样一个URL:http://item.taobao.com/item.htm?a=1&b=2&c=&d=xxx&e,请写一段JS程序提取URL中的各个GET参数(参数名和参数个数不确定),将其按key-value形式返回到一个json结构中,如{a:’1′, b:’2′, c:”, d:’xxx’, e:undefined}。

答案:

function serlize(url){

    var result={};

    //1、寻找?后面的字符串

    url=url.substr(url.indexOf("?")+1);

    //2、将字符串用&分隔

    var args=url.split("&");//[“a=1”,”b=2”]

    for (var i = 0, len = args.length; i < len; i++) {

        var arg = args[i];

    var item = arg.split('=');

        //3、对象的键=值

        result[item[0]]= item[1];

    }

    return result;

}

serlize(‘http://item.taobao.com/item.htm?a=1&b=2&c=&d=xxx&e‘);

2.14、什么是 Javascript 高阶函数?并写出例子

如果一个函数操作其他函数,即将其他函数作为参数或将函数作为返回值,则称这个函数是一个高阶函数。

我们来看看下面的例子:

function abc(temp){

console.log(temp);

}

function def(temp1,temp2){

temp1(temp2);

}

def(abc,"sos");

执行之后,输出:sos

首先我们定义了两个函数 abc() 和 def() ,

然后执行 def(abc,"sos"),我们把abc 这个函数作为了函数def() 的一个参数,

最后在函数def 中执行了 abc() 这个函数;

2.15、JavaScript 什么是原型链?

原型链 : 实例对象与原型之间的链接,叫做原型链

Function.prototype.a = "a";

Object.prototype.b = "b";

function Person(){}

console.log(Person);    //function Person()

let p = new Person();

console.log(p);        //Person {} 对象

console.log(p.a);      //undefined

console.log(p.b);      //b

想一想p.a打印结果为undefined,p.b结果为b

  解析:

  p是Person()的实例,是一个Person对象,它拥有一个属性值__proto__,并且__proto__是一个对象,包含两个属性值constructor和__proto__。

console.log(p.__proto__.constructor);  //function Person(){}

console.log(p.__proto__.__proto__);    //对象{},拥有很多属性值

我们会发现p.__proto__.constructor返回的结果为构造函数本身,

p.__proto__.__proto__有很多参数

我们调用constructor属性,p.___proto__.__proto__.constructor得到拥有多个参数的Object()函数,Person.prototype的隐式原型的constructor指向Object(),即Person.prototype.__proto__.constructor == Object()

从p.__proto__.constructor返回的结果为构造函数本身得到Person.prototype.constructor == Person()所以p.___proto__.__proto__== Object.prototype

所以p.b打印结果为b,p没有b属性,会一直通过__proto__向上查找,最后当查找到Object.prototype时找到,最后打印出b,向上查找过程中,得到的是Object.prototype,而不是Function.prototype,找不到a属性,所以结果为undefined,这就是原型链,通过__proto__向上进行查找,最终到null结束

console.log(p.__proto__.__proto__.__proto__);  //null

console.log(Object.prototype.__proto__);        //null

大家理解刚才的过程,相信下面这些应该也都明白

//Function

function Function(){}

console.log(Function);  //Function()

console.log(Function.prototype.constructor);    //Function()

console.log(Function.prototype.__proto__);      //Object.prototype

console.log(Function.prototype.__proto__.__proto__);    //NULL

console.log(Function.prototype.__proto__.constructor);  //Object()

console.log(Function.prototype.__proto__ === Object.prototype); //true

总结:

1.查找属性,如果本身没有,则会去__proto__中查找,也就是构造函数的显式原型中查找,如果构造函数中也没有该属性,因为构造函数也是对象,也有__proto__,那么会去它的显式原型中查找,一直到null,如果没有则返回undefined

2.p.__proto__.constructor  == function Person(){}

3.p.___proto__.__proto__== Object.prototype

4.p.___proto__.__proto__.__proto__== Object.prototype.__proto__ == null       

5.通过__proto__形成原型链而非protrotype

最后附上一张图,大家阅读完之后,看图应该可以很容易理解

###三、ES6、ES7、ES8 的相关知识点

ES6

块级作用域 关键字let, 常量const

对象属性赋值简写(property value shorthand)

解构赋值

函数参数 - 默认值、参数打包、 数组展开(Default 、Rest 、Spread)

箭头函数 (Arrow functions)

字符串模板 Template strings ${}

迭代器(Iterators)for (var n of ['a','b','c'])

生成器 (Generators)

class、constructor、extends、super

Modules(模块)

具有CommonJS的精简语法、唯一导出出口(single exports)和循环依赖(cyclic dependencies)的特点。

类似AMD,支持异步加载和可配置的模块加载。

Map + Set + WeakMap + WeakSet

WeakMap、WeakSet作为属性键的对象如果没有别的变量在引用它们,则会被回收释放掉。

Math + Number + String + Array + Object APIs

Proxies:使用代理(Proxy)监听对象的操作

.Symbols:调用symbol函数产生,它接收一个可选的名字参数,该函数返回的symbol是唯一的。

!!!Promises:处理异步操作的对象,用链式调用组织代码

var promise = new Promise((resolve, reject) => {

  this.login(resolve)

}) //链式调用

.then(() => this.getInfo())

.catch(() => { console.log("Error") })

4.ES7

求幂运算符(**)

3 ** 2          // 9

等价于:

Math.pow(3, 2)  // 9

Array.prototype.includes()方法:不能比较复杂类型数据,查找一个值在不在数组里,若在,则返回true,反之返回false。

['a', 'b', 'c'].includes('a')    // true

等价于

['a', 'b', 'c'].indexOf('a') > -1      //true

函数作用域中严格(strict)模式的变更。

装饰器(Decorator):修改类的行为

参数:target(所要修饰的目标类), name(所要修饰的属性名), descriptor(该属性的描述对象)

使用:npm install core-decorators –save

// 将某个属性或方法标记为不可写。

@readonly 

// 标记一个属性或方法,以便它不能被删除; 也阻止了它通过Object.defineProperty被重新配置

@nonconfigurable 

// 立即将提供的函数和参数应用于该方法,允许您使用lodash提供的任意助手来包装方法。 第一个参数是要应用的函数,所有其他参数将传递给该装饰函数。

@decorate 

// 如果你没有像Babel 6那样的装饰器语言支持,或者甚至没有编译器的vanilla ES5代码,那么可以使用applyDecorators()助手。

@extendDescriptor

// 将属性标记为不可枚举。

@nonenumerable

// 防止属性初始值设定项运行,直到实际查找修饰的属性。

@lazyInitialize

// 强制调用此函数始终将此引用到类实例,即使该函数被传递或将失去其上下文。

@autobind

// 使用弃用消息调用console.warn()。 提供自定义消息以覆盖默认消息。

@deprecate

// 在调用装饰函数时禁止任何JavaScript console.warn()调用。

@suppressWarnings

// 将属性标记为可枚举。

@enumerable

// 检查标记的方法是否确实覆盖了原型链上相同签名的函数。

@override 

// 使用console.time和console.timeEnd为函数计时提供唯一标签,其默认前缀为ClassName.method。

@time

// 使用console.profile和console.profileEnd提供函数分析,并使用默认前缀为ClassName.method的唯一标签。

@profile

@noConcurrent 避免并发调用,在上一次操作结果返回之前,不响应重复操作

@makeMutex 多函数互斥,具有相同互斥标识的函数不会并发执行

@withErrToast 捕获async函数中的异常,并进行错误提示

@mixinList 用于分页加载,上拉加载时返回拼接数据及是否还有数据提示

@typeCheck 检测函数参数类型

import {noConcurrent} from './decorators';

methods: {

  @noConcurrent    //避免并发,点击提交后,在接口返回之前无视后续点击

  async onSubmit(){

    let submitRes = await this.$http({...});

    //...

    return;

  }

}

methods: {

  @mixinList({needToast: false})

  async loadGoods(params = {}){

    let goodsRes = await this.$http(params);

    return goodsRes.respData.infos;

  },

  async hasMore() {

    let result = await this.loadgoods(params);

    if(result.state === 'nomore') this.tipText = '没有更多了';

    this.goods = result.list;

  }

}

// 上拉加载调用hasMore函数,goods数组就会得到所有拼接数据

// loadGoods可传三个参数 params函数需要参数 ,startNum开始的页码,clearlist清空数组

// mixinList可传一个参数 needToast 没有数据是否需要toast提示

5.ES8

异步函数(Async function)使用形式:

函数声明: async function foo() {}

函数表达式: const foo = async function() {}

对象的方式: let obj = { async foo() {} }

箭头函数: const foo = async () => {}

this.$http.jsonp('/login', (res) => {

  this.$http.jsonp('/getInfo', (info) => {

    // do something

  })

})

异步编程机制:Generator    async/await

function * 函数名(){

    yieId 'hello';

    yieId 'world';

    return 'ending';

}

var hs = 函数名();

hs.next();

// { value: 'hello', done: false }

hs.next();

// { value: 'world', done: false }

hs.next();

// { value: 'ending', done: true }

hs.next();

// { value: undefined, done: true }

//自动执行Generator函数

async function asyncFunc(params) {

  const result1 = await this.login()

  const result2 = await this.getInfo()

}

调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,必须调用遍历器对象的next方法,使得指针移向下一个状态。每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

yieId:(产出),分段执行,yield表达式是暂停执行的标记,而next方法可以恢复执行

Object.entries()和Object.values()

(1)Object.entries():具有键值对的数据结构,则每一个键值对都将会编译成一个具有两个元素的数组,这些数组最终会放到一个数组中,返回一个二维数组,若目标对象是数组时,则会将数组的下标作为键值返回。键值对中,如果键的值是Symbol,编译时将会被忽略

Object.entries({ one: 1, two: 2 })    //[['one', 1], ['two', 2]]

Object.entries([1, 2])                //[['0', 1], ['1', 2]]

如果对象的key值是数字,则返回值会对key值进行排序,返回的是排序后的结果。

Object.entries({ 3: 'a', 4: 'b', 1: 'c' })    //[['1', 'c'], ['3', 'a'], ['4', 'b']]

//对象属性的遍历

let obj = { one: 1, two: 2 };

for (let [k,v] of Object.entries(obj)) {

  console.log(`${JSON.stringify(k)}: ${JSON.stringify(v)}`);

}

//输出结果如下:

'one': 1

'two': 2

(2)Object.values():只返回自己的键值对中属性的值

Object.values({ one: 1, two: 2 })            //[1, 2]

Object.values({ 3: 'a', 4: 'b', 1: 'c' })    //['c', 'a', 'b']

字符串填充:padStart和padEnd

padStart函数:通过填充字符串的首部来保证字符串达到固定的长度,默认情况下使用空格填充

padEnd:填充字符串的尾部来保证字符串的长度的。

'Vue'.padStart(10)          //'      Vue'

'Vue'.padStart(10, '_*')          //'_*_*_*_Vue'

'Vue'.padEnd(10, '_*')          //'Vue_*_*_*_'

Object.getOwnPropertyDescriptors():返回目标对象中所有属性的属性描述符,该属性必须是对象自己定义的,不能是从原型链继承来的。

该方法返回的描述符,会有两种类型:数据描述符、存取器描述符

返回结果中包含的键可能的值有:configurable、enumerable、value、writable、get、set。

let obj = {

  id: 1,

  name: 'test',

  get gender() {

    console.log('gender')

  },

  set grade(g) {

    console.log(g)

  }

}

Object.getOwnPropertyDescriptors(obj, 'id')

//输出结果为:

{

  id: {

    configurable: true,

    enumerable: true,

    value: 1,

    writable: true

  }

}

和assign区别

Object.assign(obj)

//输出结果为:

{

  gender: undefined

  id: 1,

  name: 'test'

}

共享内存和原子(共享阵列缓冲区,Shared memory and atomics)笔记待完善

新的构造函数SharedArrayBuffer、具有辅助函数的命名空间对象Atomics

多线程并发读写数据

添加尾部逗号而不报错

###四、前端性能优化

1、雪碧图技术

这个很简单,把每个小图标都整合到一张大图上面,极大的减轻http请求数,同时能够让图片快速加载进来。

考虑到当前的5g的发展前景,以后图片不会造成加载延迟的现象。

2、浏览器渲染机制

输入一个网址:我们得到服务端html文件。

根据html文件,从头到尾的一个个的依次渲染页面渲染页面。

但是遇到图片——不会等待图片的加载完毕,会直接渲染下面的标签。

如果图片加载出来——根据图片选择,由于图片要占用空间,决定是否重新加载页面,这个概念叫reflow。(优化的方式——给图片宽高)。

reflow和什么相关:占位面积、定位方式、边距。

对于样式中的颜色变化,叫做repaint、这个就只需要把颜色改变。所以性能上来说,repaint稍微比reflow高点。

repaint和什么相关:和颜色变化相关

3、webpack、gulp等打包工具的使用

压缩代码,减少了代码体积。

可以把多个css文件,多个js文件,合并为一个css文件/js文件。

合并文件,让我们减少了http请求数。

4、避免页面跳转,也就是使用单页面应用的开发。

每次页面跳转,就是一次html文件的下载过程。而这个过程,我们首先从服务端下载网页,再进行渲染,网页性能体验会很差。而单页面应用,它从一开始,就把完整的网页给加载到本地。

5、延迟加载、懒加载技术

什么是懒加载技术:

原理:先将img标签中的src链接设为同一张图片(空白图片),将其真正的图片地址存储再img标签的自定义属性中(比如data-src)。当js监听到该图片元素进入可视窗口时,即将自定义属性中的地址存储到src属性中,达到懒加载的效果。

这样做能防止页面一次性向服务器响应大量请求导致服务器响应慢,页面卡顿或崩溃等问题。

6、将css放在HEAD中

如果将 CSS放在其他地方比如 BODY中,则浏览器有可能还未下载和解析到 CSS就已经开始渲染页面了,这就导致页面由无 CSS状态跳转到 CSS状态,用户体验比较糟糕。除此之外,有些浏览器会在 CSS下载完成后才开始渲染页面,如果 CSS放在靠下的位置则会导致浏览器将渲染时间推迟。

7、Vue项目的按需加载

vue中的懒加载是通过webpack的代码分割来实现的,下面是官网文档:https://router.vuejs.org/zh-cn/advanced/lazy-loading.html

主要是在加载路由的时候,使用:

const Main = r => require.ensure([], () => r(require(‘../views/main/Main’)))

require.ensure就是webpack提供的异步加载的方式。

8、关于React的性能优化

react中,进行性能优化的核心就是shouldComponentDidMount周期函数。

9、设置合理的http缓存

http请求中,我们可以合理的设置headers,就能达到缓存的目的。

有两种常见的缓存,强制缓存和对比缓存:

第一种:强制缓存。 

直接访问浏览器中的缓存数据,如果存在,则直接使用浏览器中的数据。如果不存在,则再向服务器发送请求,然后得到服务器的数据,再把这些数据存在浏览器中。

第二种:对比缓存。 

首先,获取浏览器中的数据缓存标识,再获取服务器上面的数据缓存标识,如果相匹配,则直接从浏览器中获取数据,如果不匹配,则从服务器上获取数据。

关于缓存标识,有两类标识:

第一类:

第一次请求,服务器会返回一个Last-Modified。

下一次请求,浏览器会自动在headers中添加一条If-Modified-Since属性,记录的就是上一次数据发生修改的时间。

第二类:

第一次请求,服务端返回一个Etag资源唯一标识符。

第二次请求,浏览器会自动携带一个If-None_Match标识符。

应用程序缓存

创建cache manifest文件,通过给html文件中的HTML标签添加一个manifest属性来实现的。

<!DOCTYPE HTML>

<html manifest="demo.appcache">

<body>

文档内容......

</body>

</html>

###五、Vue、React、Angular 的异同点

1、Angular特性:

由自己实现一套模板编译规则,数据变化依赖脏检查,

基本属性包括:数据双向绑定、基本模板指令、自定义指令、表单验证、路由操作、依赖注入、过滤器、内置服务、自定义服务、组件、模块。

运行效率较低,数据变更检测方式。

学习angular会迫使你学习特有的预发,上手成本很大,代码看起来很干净

依赖注入,即一个对象将依赖项提供给另一个对象(客户端)的模式。导致更多的灵活性和更干净的代码。

Angular 最适合单页应用(SPA),因为它可能太臃肿而不能用于微服务。

框架比较臃肿,每次用啥功能要引入一大堆东西

Angular错误提示不够清晰明显,对于初级开发者,很难看懂Angular的错误提示。(个人认为这是最大的不好之处,当初学习这个遇到很多坑啊),而且定位bug很难。

面向对象编程的思想,Angular由后端开发人员设计的前端框架。

详细比较:React和Vue的区别

2、React特性:

单向绑定,先更新model,然后渲染UI元素,数据在一个方向流动,使得调试更加容易。代码冗余,各种生命周期太麻烦,刚开始接触好难记。

用了虚拟DOM。(对虚拟DOM的理解刚开始我不是很理解概念,建议大家去看【深入REACT技术栈】这本书有很好的讲解)

更适合大型应用和更好的可测试性

Web端和移动端原生APP通吃

更大的生态系统,更多的支持和好用的工具

3、Vue特性

模板和渲染函数的弹性选择

简单的语法和项目配置

更快的渲染速度和更小的体积四

4、Vue和React共同点

用虚拟DOM实现快速渲染

轻量级

响应式组件

服务端渲染

集成路由工具,打包工具,状态管理工具的难度低

5:不同点

vue  控制器:无;过滤器 :无 ;指令:有;渲染指令: 有 ;数据绑定:双向;

React  控制器:无;过滤器 :无 ;指令:无;渲染指令 : 无 ;数据绑定:单向;

angular 控制器:有;过滤器 :有 ;指令:有;渲染指令 : 有 ;数据绑定:双向;

###六、Webpack 如何对项目构建优化

请移驾我的另一篇 Chat 文章https://gitbook.cn/gitchat/activity/5bed34555748cb6bd2780d4b

###七、首屏优化

1. DNS预解析

2.域名收敛

既然DNS解析比较耗时,每个连接都要建立链路,那么我们就可以通过减少ajax到后台的域名地址,通过反向代理去做。

3. 链路复用

因为每一次链接数都要建立3次TCP握手,通过keep-alive,可以保证建立的连接不会被关闭, 下次请求可以直接发送数据,这样可以减少将近200ms的时间,当然会增加一部分Server的内存消耗,要预先扩容;http2.0已经支持了这些特性;

4. 资源内联

在首屏里,一些资源都是分散开发的,在一些简单的页面中,有些内容可以内联进去,不然像一些css页面要等到html页面解析完成后,再去解析外联的css; 可以通过打包工具在发布的时候就能完成;

5.组件化开发

首先一点是按需加载,就是首屏显示的东西,哪些要加载,哪些无需加载,可以区分开来,而且可以异步方式来加载。

我们通常是这么做的,我们可以先加载一些公用的东西,然后通过路由,去加载首屏上可以看到选项卡的资源,哪些有用户交互之后的东西,可以稍后在加载;

但是这样会有一个问题,有时候我们的js处理的比较快,但是css文件处理的比较快,这样就会使的页面比较混乱,所以可以通过异步打包工具,将css和js打包在一起

6. 服务端渲染

一个前后端分离的项目,通过ajax请求数据,这样一个页面的渲染路径会拉的很长,页面先渲染html,然后在渲染js,js才发送ajax请求,等到数据回来再进行渲染,所以不管怎么样都至少要3个RTT请求时间;这种模式,在网络环境比较差的情况下,是很难接受的,

所以我们又回归到PHP时代;通过服务器端渲染,就可以控制用户,哪些东西是要打包的。可以一次性知道用户需要哪些数据;加上现在的NodeJs环境以及React,这就使得一些东西既可以在前端的浏览器进行渲染,也可以在服务器中进行字符串拼接,一起吐出来;

比如在无限长的滚动列表中,对图片进行懒加载,首先不给图片赋值src属性,通过插入dom计算可视区,在可视区内则赋值src,在可视区之外,交给scroll去处理,将要进入可视区时才赋值;

7.利用缓存

资源长缓存:不是通过304去判断缓存,而是通过max-age来做的,通过md5命名文件,然后通过自动化构建工具去实现;主要聊一下数据层缓存与数据复用;

从客户端的启发;客户端会将上次缓存的数据重新渲染一遍;所以数据缓存的设计可以设计为

可以在ajax请求的数据没有回来之前,用一些配置的假数据,先把坑站好,然后等待线上的数据回来后,在把坑填好,这样减少了白屏的时间。这样给用户感觉比较快,但不是真的快,交互性能确实变好了;

在有些时候,并不是所有的数据都是加载进来的,像贴吧和知乎之类的,展示的一些storage里的数据,都是带有省略号的缓存数据,等用户真的点开之后才去加载该帖子的全部数据。每次请求的数据又重新将原来的缓存覆盖;

8.离线包

其实对于一些并发量比较大,向过年时候的抢红包业务,并发量是几个亿。这样,对于一些经常使用的用户,可以几天前就将离线包推送过去,进行下载,通过二进制比较,进行动态增量更新,而不用每次都下载,这么大的并发量,同时下载,带宽肯定爆,服务器肯定爆。

9.减少请求次数

10.减少对dom的操作

###七、移动端如何做适配

现在最常用的就是一稿设计多端适配方案—— rem + flexible

rem是什么?

rem(font size of the root element)是指相对于根元素的字体大小的单位。简单的说它就是一个相对单位。看到rem大家一定会想起em单位,em(font size of the element)是指相对于父元素的字体大小的单位。它们之间其实很相似,只不过一个计算的规则是依赖根元素一个是依赖父元素计算。

REM自适应JS

//designWidth:设计稿的实际宽度值,需要根据实际设置

//maxWidth:制作稿的最大宽度值,需要根据实际设置

//这段js的最后面有两个参数记得要设置,一个为设计稿实际宽度,一个为制作稿最大宽度,例如设计稿为750,最大宽度为750,则为(750,750)

;(function(designWidth, maxWidth) {

var doc = document,

win = window,

docEl = doc.documentElement,

remStyle = document.createElement("style"),

tid;

function refreshRem() {

var width = docEl.getBoundingClientRect().width;

maxWidth = maxWidth || 540;

width>maxWidth && (width=maxWidth);

var rem = width * 100 / designWidth;

remStyle.innerHTML = 'html{font-size:' + rem + 'px;}';

}

if (docEl.firstElementChild) {

docEl.firstElementChild.appendChild(remStyle);

} else {

var wrap = doc.createElement("div");

wrap.appendChild(remStyle);

doc.write(wrap.innerHTML);

wrap = null;

}

//要等 wiewport 设置好后才能执行 refreshRem,不然 refreshRem 会执行2次;

refreshRem();

win.addEventListener("resize", function() {

clearTimeout(tid); //防止执行两次

tid = setTimeout(refreshRem, 300);

}, false);

win.addEventListener("pageshow", function(e) {

if (e.persisted) { // 浏览器后退的时候重新计算

clearTimeout(tid);

tid = setTimeout(refreshRem, 300);

}

}, false);

if (doc.readyState === "complete") {

doc.body.style.fontSize = "16px";

} else {

doc.addEventListener("DOMContentLoaded", function(e) {

doc.body.style.fontSize = "16px";

}, false);

}

})(750, 750);

第一个参数是设计稿的宽度,一般设计稿有640,或者是750,你可以根据实际调整

第二个参数则是设置制作稿的最大宽度,超过750,则以750为最大限制。

在小王待过的公司里面,设计稿都是按750来设计的。不过管它用什么尺寸来设计,适配理念都一样,以不变应万变。

使用1rem=100px转换你的设计稿的像素,例如设计稿上某个块是100px*300px,换算成rem则为1rem*3rem。

这样子不管什么类型的手机,就都可以是适配了。像那种用多媒体查询针对不同设备尺寸写样式的方式,就让见鬼去吧。

###八、跨终端如何做适配

先来个图压压惊,图有点老,现在都iPhonx了。

如果要适配这么多设备是不是很头疼!!!

###九、MVVM 框架的知识点。

####Vue 部分

Vue组件之间如何通信

一. 父子之间的通信

1. 父组件-》子组件(props down)

①通过属性

步骤1:父组件在调用子组件时传值

<son myName="michael" myPhone='123'></son> <son :myName="userList[0]"></son>

步骤2:子组件通过props得到父组件的传过来的数据

Vue.component('son',{ props:['myName','myPhone'] })

②通过$parent

直接在子组件中通过this.$parent得到调用子组件的父组件

2、子组件-》父组件(events up)

①events up

步骤1:在父组件中 调用子组件的时候 绑定一个自定义事件 和 对应的处理函数

methods:{ recvMsg:function(msg){ //msg就是传递来的数据 } }, template:' <son @customEvent="recvMsg"></son> '

步骤2:在子组件中 把要发送的数据通过触发自定义事件传递给父组件

this.$emit('customEvent',123)

② $refs

步骤1:在调用子组件的时候 可以指定ref属性

`<son ref='zhangsan'></son>`

步骤2:通过$refs

得到指定引用名称对应的组件实例

this.$refs.zhangsan

二、兄弟组件间的通信

步骤1:创建一个Vue的实例 作为事件绑定触发的公共的对象

var bus = new Vue();

步骤2:在接收方的组件 绑定 自定义的事件

bus.$on('customEvent',function(msg){ console.log(msg); //msg是通过事件传递来的数据 (传递来的123) });

步骤3:在发送方的组件 触发 自定义的事件

bus.$emit('customEvent',123);

除了以上几种方式外,还有 Vuex、路由传参和缓存。

二、Vue的双向数据绑定原理是什么?

vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter和getter

这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化

第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:

1、在自身实例化时往属性订阅器(dep)里面添加自己

2、自身必须有一个update()方法

3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

三、详细说下你对vue生命周期的理解?

总共分为8个阶段创建前/后,载入前/后,更新前/后,销毁前/后

创建前/后: 在beforeCreated阶段,vue实例的挂载元素$el和数据对象data都为undefined,还未初始化。在created阶段,vue实例的数据对象data有了,$el还没有。

载入前/后:在beforeMount阶段,vue实例的$el和data都初始化了,但还是挂载之前为虚拟的dom节点,data.message还未替换。在mounted阶段,vue实例挂载完成,data.message成功渲染。

更新前/后:当data变化时,会触发beforeUpdate和updated方法。

销毁前/后:在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在

四、你是怎么理解vuex的?

vuex可以理解为一种开发模式或框架。

通过状态(数据源)集中管理驱动组件的变化(好比spring的IOC容器对bean进行集中管理)。

应用级的状态集中放在store中; 改变状态的方式是提交mutations,这是个同步的事物; 异步逻辑应该封装在action中。

五、聊聊你对Vue.js的template编译的理解?

简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点)

首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。

然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)

####React 部分

一、react生命周期及相关用法

react生命周期分为初始化阶段、运行阶段、销毁阶段。

(1) 初始化阶段:

getDefaultProps:获取实例的默认属性

getInitialState:获取每个实例的初始化状态

componentWillMount:实例挂载之前

Render:渲染组件

componentDidMount:实例挂载完成。一般在这个函数中与后台进行初始化数据交互。

(2)运行阶段:

componentWillReceiveProps:父组件改变时调用。

sholudComponentUpdate:主要是用来手动阻止组件渲染,一般在这个函数中做组件的性能优化。

componentWillUpdate:组件数据更新前调用

componentDidUpdate:组件数据更新完成时调用

(3)销毁阶段:

componentUnmount:销毁阶段。一般用来销毁不用的变量或者是解除无用定时器以及解绑无用事件。防止内存泄漏问题。

二、你了解 Virtual DOM 吗?解释一下它的工作原理。

Virtual DOM 是一个轻量级的 JavaScript 对象,它最初只是 real DOM 的副本。它是一个节点树,它将元素、它们的属性和内容作为对象及其属性。 React 的渲染函数从 React 组件中创建一个节点树。然后它响应数据模型中的变化来更新该树,该变化是由用户或系统完成的各种动作引起的。

Virtual DOM 工作过程有三个简单的步骤。

1.每当底层数据发生改变时,整个 UI 都将在 Virtual DOM 描述中重新渲染。

2.然后计算之前 DOM 表示与新表示的之间的差异。

3.完成计算后,将只用实际更改的内容更新 real DOM。

三、总结一下Redux的三大原则和数据流的管理

Redux三大原则:

1、单一数据源,这个应用的state被存储在一棵object tree中,并且这个object tree只存在于唯一的Store中。

2、state是只读的,唯一改变state的方法就是触发action,action是一个用于描述已发生事件的普通对象。

3、使用纯函数来执行修改,为了描述action如何改变state tree,需要编写reducer。

4、具体工作步骤如下:

Redux数据流的管理:

1、action:把数据传递到Store,唯一数据来源。

2、reducer:action只描述有事情发生,reducer指明如何更新state,即设计state结构和action处理。

3、Store:把action和reducer联系到一起,负责维持、获取和更新state。

4、生命周期:数据流严格且单向

调用Store.dispatch(action)->Store调用传入的reducer函数,Store会把两个参数传入reducer:当前的state树和action->根reducer将多个子reducer输出合并成一个单一的state树->Store保存了根reducer,并返回完整的state树。

四、Redux与它的中间件

redux是一个可预测的状态容器,

react-redux是将store和react结合起来,使得数据展示和修改对于react项目而言更简单

redux中间件就是在dispatch action前对action做一些处理

redux-thunk用于对异步做操作

redux-actions用于简化redux操作

redux-promise可以配合redux-actions用来处理Promise对象,使得异步操作更简单

redux-sage可以起到一个控制器的作用,集中处理边际效用,并使得异步操作的写法更优雅。

//这里还需要大家自己再去多了解一下redux中间件的知识。

###十、实现一个观察者模式

举个生活比较常见常见的例子,比如你去面试之后,面试官看你表现不错,最后会跟你要联系方式,以便之后可以联系你。在这角色扮演当中,你就是“订阅者”,面试官就是“发布者”。

那么发布订阅模式是咋实现的呢?

思路:

给定一个发布者

面试者将联系方式给发布者

发布者的一个列表有各种职位(web端的,java 的),里面记载回调函数以便通知这些面试者

最后发布消息的时候,会遍历这个列表的职位的回调函数,告诉面试者面试这个职位是通过还是不通过

如果面试者取消了订阅,那么将回调函数和之前的回调函数作对比,如果相等,就将这个面试者的上班通知去掉

var Event = (function() {

  var events = {}; //发布者

  //subscribe也就是订阅,post 代表面试者要面的职位,callback表示为回调函数

  function subscribe(post, callback) {

    events[post] = events[post] || []; //发布者的列表里有没有这个面试职位,如果没有就创建一个空数组

    events[post].push(callback);

  }

  //publish 表示发布

  function publish() {

    var post = Array.prototype.shift.call(arguments); //第一个参数指定“键”

    var fns = events[post]; //设置缓存,提高性能

    if (!fns) { //如果发布者的列表里没有这个职位,那肯定是不能发布

      return;

    }

    for (var i = 0; i < fns.length; i++) { //遍历当前的职位的数组里有几个面试者

      fns[i].apply(this, arguments);

    }

  }

  //unsubscribe 表示取消订阅

  function unsubscribe(post, fn) {

    var fns = events[post];

    if (fns) {

      if (fn) {

        for (var i = fns.length; i >= 0; i--) {

          if (fns[i] === fn) fns.splice(i, 1);

        }

      } else {//如果没有传入fn回调函数,直接取消post对应消息的所有订阅

        fns = [];

      }

    } else {//如果发布者的列表没有这个职位,直接 return

      return;

    }

  }

  return {

    subscribe: subscribe,

    publish: publish,

    unsubscribe: unsubscribe

  };

})();

测试:

var fn1 = function(time) {

  console.log("小明你通过了面试,上班时间:" + time);

};

var fn2 = function(time) {

  console.log("小强你通过了面试,上班时间:" + time);

};

//小明将联系方式给了发布者,发布者(hr)觉得小明不错,可以通过,于是在列表(java)里写下了一些回调函数,到时候发布的时候将上班时间告诉小明

Event.subscribe("java", fn1);

//小强也订阅了

Event.subscribe("java", fn2);

Event.publish("java", "2017-10-01");

/*输出:

小明你通过了面试,上班时间:2017-10-01

小强你通过了面试,上班时间:2017-10-01

*/

Event.unsubscribe("java", fn1);//删除小明的上班通知

Event.publish("java", "2017-10-01");

/*输出:

小强你通过了面试,上班时间:2017-10-01

*/

###十一、实现一个数组去重的函数

//其实数组去重的方法有好多种,这就介绍常用的几种就够了

第一种:

let arr = [1, 'a', 'a', 'b', 'd', 'e', 'e', 1, 0, 2, 2, 3];

function unique(arr){

    return [...(new Set(arr))];

}

console.log(unique(arr)); // [1, "a", "b", "d", "e", 0, 2, 3]

第二种:

let arr = [1, 'a', 'a', 'b', 'd', 'e', 'e', 1, 0, 2, 2, 3];

function unique(arr){

    return Array.from(new Set(arr));

}

console.log(unique(arr)); // [1, "a", "b", "d", "e", 0, 2, 3]

第三种:

let arr = [1, 'a', 'a', 'b', 'd', 'e', 'e', 1, 0, 2, 2, 3];

function unique(arr){

    return arr.reduce((prev,cur) => prev.includes(cur) ? prev : [...prev,cur],[]);

}

console.log(unique(arr)); // [1, "a", "b", "d", "e", 0, 2, 3]

第四种:最普通的一种,想了解更多,自己多多研究吧。

let arr = [1, 'a', 'a', 'b', 'd', 'e', 'e', '1', 0, 2, 2, 3];

function unique(arr){

    let newArr = [];

    let obj = {};

    for (let i = 0; i < arr.length; i++) {

        if (!obj[typeof arr[i] + arr[i]]) {

            obj[typeof arr[i] + arr[i]] = 1;

            newArr.push(arr[i]);

        }

    }

    return newArr;

}

console.log(unique(arr)); // [1, "a", "b", "d", "e", "1", 0, 2, 3]

###十二、介绍Promise的原理

在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。

  (1) promise 对象初始化状态为 pending。

  (2) 当调用resolve(成功),会由pending => fulfilled。

  (3) 当调用reject(失败),会由pending => rejected。

  因此,看上面的的代码中的resolve(num)其实是将promise的状态由pending改为fulfilled,然后向then的成功回掉函数传值,reject反之。但是需要记住的是注意promsie状态 只能由 pending => fulfilled/rejected, 一旦修改就不能再变(记住,一定要记住,下面会考到)。

  当状态为fulfilled(rejected反之)时,then的成功回调函数会被调用,并接受上面传来的num,进而进行操作。promise.then方法每次调用,都返回一个新的promise对象 所以可以链式写法(无论resolve还是reject都是这样)。

Promise也是面试必问的一个知识点,多多学习。

###十三、JavaScript对象的浅拷贝与深拷贝实例分析

1、浅拷贝

仅仅复制对象的引用,而不是对象本身

var person = {

  name: 'Alice',

  friends: ['Bruce', 'Cindy']

}

var student = {

  id: 30

}

student = simpleClone(person, student);

student.friends.push('David');

function simpleClone(oldObj, newObj) {

  var newObj = newObj || {};

  for (var i in oldObj)

    newObj[i] = oldObj[i];

  return newObj;

}

console.log(person.friends);//["Bruce", "Cindy", "David"]

2、深拷贝

把复制的对象所引用的全部对象都复制一遍,能够实现真正意义上的数组和对象的拷贝。

浅拷贝的问题:如果父对象的属性值为一个数组或另一个对象,那么实际上子对象获得的只是一个内存地址,而不是对父对象的真正拷贝,因此存在父对象被篡改的可能。

方法1:

var person = {

  name: 'Alice',

  friends: ['Bruce', 'Cindy']

}

var student = {

  id: 30

}

student = deepClone(person, student);

student.friends.push('David');

function deepClone(oldObj, newObj) {

  var newObj = newObj || {};

  newObj = JSON.parse(JSON.stringify(oldObj));

  return newObj;

}

console.log(person.friends); // 'Bruce', 'Cindy'

方法2:

function deepClone(oldObj, newObj) {

  var newObj = newObj || {};

  for (var i in oldObj) {

    var prop = oldObj[i];

    if (prop === newObj)

          continue;

    if (typeof prop === 'object')

      newObj[i] = (prop.constructor === Array) ? [] : Object.create(prop);

    else

      newObj[i] = prop;

  }

  return newObj;

}

###十四、彻底弄清 Callback、Promise、Generator以及Async/await

一、回调函数

所谓回调函数(callback),就是把任务分成两步完成,第二步单独写在一个函数里面,等到重新执行这个任务时,就直接调用这个函数。

例如Node.js中读取文件

fs.readFile('a,txt', (err,data) = >{

  if(err) throw err;

  console.log(data);

})

上面代码中readFile的第二个参数就是回调函数,等到读取完a.txt文件时,这个函数才会执行。

二、Promise

使用回调函数本身没有问题,但有“回调地狱”的问题。

假定我们有一个需求,读取完A文件之后读取B文件,再读取C文件,代码如下

fs.readFile(fileA,  (err, data) => {

  fs.readFile(fileB,  (err, data) => {

      fs.readFile(fileC, (err,data)=>{

        //do something

    })

  });

});

可见,三个回调函数代码看来就够呛了,有时在实际业务中还不止嵌套这几个,难以管理。

这时候Promise出现了!它不是新的功能,而是一种新的写法,用来解决“回调地狱”的问题。

我们再假定一个业务,分多个步骤完成,每个步骤都是异步的,而且依赖于上一个步骤的结果,用setTimeout()来模拟异步操作

/**

* 传入参数 n,表示这个函数执行的时间(毫秒)

* 执行的结果是 n + 200,这个值将用于下一步骤

*/

function A(n) {

    return new Promise(resolve => {

        setTimeout(() => resolve(n + 200), n);

    });

}

function step1(n) {

    console.log(`step1 with ${n}`);

    return A(n);

}

function step2(n) {

    console.log(`step2 with ${n}`);

      return A(n);

}

function step3(n) {

    console.log(`step3 with ${n}`);

    return A(n);

}

上面代码中有4个函数,A()返回一个Promise对象,接收参数n,n秒后执行resolve(n+200)。step1、 step2、step3对应三个步骤

现在用Promise实现这三个步骤:

function doIt() {

    console.time('do it now')

    const time1 = 300;

    step1(time1)

          .then( time2 =>step2(time2))

          .then( time3 => step3(time3))

          .then( result => {

              console.log(`result is ${result}`)

          });

}

doIt();

输出结果如下

step1 with 300

step2 with 500

step3 with 700

result is 900

result是step3()的参数700+200 = 900。

可见,Promise的写法只是回调函数的改进,用then()方法免去了嵌套,更为直观。

但这样写绝不是最好的,代码变得十分冗余,一堆的then。

所以,最优秀的解决方案是什么呢?

开头暴露了,就是async/await

讲async前我们先讲讲协程与Generator

三、协程

协程(coroutine),意思是多个线程相互协作,完成异步任务。

它的运行流程如下

协程A开始执行

协程A执行到一半,暂停执行,执行的权利转交给协程B。

一段时间后B交还执行权

协程A重得执行权,继续执行

上面的协程A就是一个异步任务,因为在执行过程中执行权被B抢了,被迫分成两步完成。

读取文件的协程代码如下:

function task() {

  // 其他代码

  var f = yield readFile('a.txt')

  // 其他代码

}

task()函数就是一个协程,函数内部有个新单词yield,yield中文意思为退让,

顾名思义,它表示执行到此处,task协程该交出它的执行权了。也可以把yield命令理解为异步两个阶段的分界线。

协程遇到yield命令就会暂停,把执行权交给其他协程,等到执行权返回继续往后执行。最大的优点就是代码写法和同步操作几乎没有差别,只是多了yield命令。

这也是异步编程追求的,让它更像同步编程

四、Generator函数

Generator是协程在ES6的实现,最大的特点就是可以交出函数的执行权,懂得退让。

function* gen(x) {

    var y = yield x +2;

    return y;

  }

  var g = gen(1);

  console.log( g.next()) // { value: 3, done: false }

  console.log( g.next()) // { value: undefined, done: true }

上面代码中,函数多了*号,用来表示这是一个Generator函数,和普通函数不一样,不同之处在于执行它不会返回结果,

返回的是指针对象g,这个指针g有个next方法,调用它会执行异步任务的第一步。

对象中有两个值,value和done,value 属性是 yield 语句后面表达式的值,表示当前阶段的值,done表示是否Generator函数是否执行完毕。

下面看看Generator函数如何执行一个真实的异步任务

var fetch = require('node-fetch');

function* gen(){

  var url = 'https://api.github.com/users/github';

  var result = yield fetch(url);

  console.log(result.bio);

}

var g = gen();

var result = g.next();

result.value.then( data => return data.json)

                  .then (data => g.next(data))

上面代码中,首先执行Generator函数,得到对象g,调用next方法,此时

result ={ value: Promise { <pending> }, done: false }

因为fetch返回的是一个Promise对象,(即value是一个Promise对象)所以要用then才能调用下一个next方法。

虽然Generator将异步操作表示得很简洁,但是管理麻烦,何时执行第一阶段,又何时执行第二阶段?

是的,这时候到Async/await出现了!

五、Async/await

从回调函数,到Promise对象,再到Generator函数,JavaScript异步编程解决方案历程可谓辛酸,终于到了Async/await。很多人认为它是异步操作的最终解决方案(谢天谢地,这下不用再学新的解决方案了吧)

其实async函数就是Generator函数的语法糖,例如下面两个代码:

var gen = function* (){

  var f1 = yield readFile('./a.txt');

  var f2 = yield readFile('./b.txt');

  console.log(f1.toString());

  console.log(f2.toString());

};

var asyncReadFile = async function (){

  var f1 = await  readFile('./a.txt');

  var f2 = await  readFile('./b.txt');

  console.log(f1.toString());

  console.log(f2.toString());

};

上面的为Generator函数读取两个文件,下面为async/await读取,比较可发现,两个函数其实是一样的,async不过是把Generator函数的*号换成async,yield换成await。

1.async函数用法

上面说了async不过是Generator函数的语法糖,那为什么要取这个名字呢?自然是有理由的。

async是“异步”,而await是async wait的简写,即异步等待。所以应该很好理解async用于声明一个function是异步的,await用于等待一个异步方法执行完成

下面来看一个例子理解async命令的作用

async function test() {

  return "async 有什么用?";

}

const result = test();

console.log(result)

输出:

Promise { 'async 有什么用?' }

可以看到,输出的是一个Promise对象!

所以,async函数返回的是一个Promise对象,如果直接return 一个直接量,async会把这个直接量通过PromIse.resolve()封装成Promise对象

注意点

一般来说,都认为await是在等待一个async函数完成,确切的说等待的是一个表示式,这个表达式的计算结果是Promise对象或者是其他值(没有限定是什么)

即await后面不仅可以接Promise,还可以接普通函数或者直接量。

同时,我们可以把async理解为一个运算符,用于组成表达式,表达式的结果取决于它等到的东西

等到非Promise对象 表达式结果为它等到的东西

等到Promise对象  await就会阻塞后面的代码,等待Promise对象resolve,取得resolve的值,作为表达式的结果

还是那个业务,分多个步骤完成,每个步骤依赖于上一个步骤的结果,用setTimeout模拟异步操作。

/**

* 传入参数 n,表示这个函数执行的时间(毫秒)

* 执行的结果是 n + 200,这个值将用于下一步骤

*/

function takeLongTime(n) {

    return new Promise(resolve => {

        setTimeout(() => resolve(n + 200), n);

    });

}

function step1(n) {

    console.log(`step1 with ${n}`);

    return takeLongTime(n);

}

function step2(n) {

    console.log(`step2 with ${n}`);

    return takeLongTime(n);

}

function step3(n) {

    console.log(`step3 with ${n}`);

    return takeLongTime(n);

}

async实现方法

async function doIt() {

    console.time("doIt");

    const time1 = 300;

    const time2 = await step1(time1);

    const time3 = await step2(time2);

    const result = await step3(time3);

    console.log(`result is ${result}`);

    console.timeEnd("doIt");

}

doIt();

输出结果和上面用Promise实现是一样的,但这个代码结构看起来清晰得多,几乎跟同步写法一样。

2. async函数的优点

(1)内置执行器

Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。

(2) 语义化更好

async 和 await,比起星号和 yield,语义更清楚了。async 是“异步”的简写,而 await 可以认为是 async wait 的简写。所以应该很好理解 async 用于申明一个 function 是异步的,而 await 用于等待一个异步方法执行完成。

(3)更广的适用性

yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。

以上就是中大厂的面试知识点汇总,算不上包罗万象,但是涵盖的知识点还是比较齐全的,只要你能认真看完本文并理解了,那面试应该是没啥大问题的。当然,其中也少不了你自己的努力,还有更多的知识点需要你去学习。前端就是这样,招聘的是工程师,干活却是螺丝钉。这就要求各位前端coder们,不能只注重业务,平时也要给自己充电,知其然知其所以然,扩展自己的知识范围,懂得越多,在前端这条路上也才能走的更稳、更远。

最后,希望看过我文章的都找到好工作,也不枉我辛苦一场!!!

上一篇下一篇

猜你喜欢

热点阅读