ES6常用方法总结—Promise

2019
20/05

1.Promise用法

Promisethen方法会返回一个新的Promise

let p = function(){
    return new Promise((resolve, reject) => {
        try {
            setTimeout(()=>{
               return resolve()
            },500)
        } catch (error) {
            return reject(error)
        }
    })
}

async function do(){
    let res = await p()
    console.log(res)
}

p().then((res)=>{
    console.log(res)
},(err)=>{
    console.log(err)
})

p().then((res)=>{
    console.log(res)
}).catch((err)=>{
    console.log(err)
})

p().then((res)=>{
    console.log(res)
    return p()
}).then((res)=>{
    console.log(res)
}).catch((err)=>{
    console.log(err)
})

2.Promise.all用法(ES11)

Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

另外Promise.allSettled看起来像是对Promise.all的一种补充,缓解了使用Promise.all碰到reject的痛点问题。

一句话概括Promise.allSettledPromise.all的最大不同:Promise.allSettled会执行所有的Promise。而Promise.all只要遇到第一个reject的结果,整体就会reject

Promise.allSettled执行后返回的是对象数组,每个对象中包含了各Promise的执行状态(fulfilledrejected)和结果值。

Promise.allSettled()Promise.all()用来做批量异步处理。如果说每一个异步都需要得到结果,就用allSettled()。如果说每一个异步都需要成功,才能往下进行,就用all ()。

let p1 = new Promise((resolve, reject) => {
  resolve('成功了')
})

let p2 = new Promise((resolve, reject) => {
  resolve('success')
})

let p3 = Promse.reject('失败')

Promise.all([p1, p2]).then((result) => {
  console.log(result)               //['成功了', 'success']
}).catch((error) => {
  console.log(error)
})

Promise.all([p1,p3,p2]).then((result) => {
  console.log(result)
}).catch((error) => {
  console.log(error)      // 失败了,打出 '失败'
})

//---------------------

async function fn6(){
	let [a,b,c] = await Promise.all([
		await '成功了a',
		new Error('失败了b'), 
		await '成功了c',
	])
	return [a,b,c]
}
fn6().then(res=>{
	console.log('fn6-res',res)   //fn6-res [ '成功了a', Error: 失败了b, '成功了c' ]
})

//------------------------------

async function foo(){
  return Promise.allSettled([
    await new Promise((resolve) => setTimeout(() => resolve("1"), 2000)),
    await new Promise((resolve, reject) => setTimeout(() => resolve("2")))
  ]);
}

foo().then((data) => {
    console.log(data);  //[{status: 'fulfilled', value: '1'},{status: 'fulfilled', value: '2'}]
})

3.promise原理

promise的核心原理其实就是发布订阅模式,通过两个队列来缓存成功的回调(onResolve)和失败的回调(onReject)。实则也是通过回调函数方式实现异步。

4.promise特点

  1. Promise状态不受外部影响。Promise有三个状态:pending进行中、fulfilled已成功、rejected已失败。只有异步操作的结果才可以决定当前是哪个状态,其他任何操作都无法改变状态。(这也就是promise许诺的由来)
  2. 一旦状态改变,就不会再变。Promise对象状态改变只有两种可能:从pending改到fulfilled或者从pending改到rejected,只要这两种情况发生,状态就凝固了不会再变。这时候就称为定型resolved

5.原生实现Promise(手写Promise)

使用发布订阅模式+状态切换原理实现。由于原型方法诸如then、all

function MyPromise(execution) {
	// 静态属性(常量)
	MyPromise.PENDING = 'pending'
	MyPromise.FULFILLED = 'fulfilled'
	MyPromise.REJECTED = 'rejected'

	this.status = MyPromise.PENDING // 状态
	this.reason = null // 失败原因
	this.value = null // 成功返回值
	// 收集依赖缓存(函数)
	this.resolveDeps = []
	this.rejectDeps = []
	// 以上的遍历也可设为私有变量,再添加get/set方法,这里为简化没做

	// this缓存
	var self = this

	// 成功回调
	function resolve(res) {
		// 判断状态
		if (self.status === MyPromise.PENDING) {
			self.value = res
			self.status = MyPromise.FULFILLED

			// 这种方式可以遍历数组且执行后可以清空数组,简化清空数组的步骤
			while (self.resolveDeps.length) {
				// 发布给所有订阅
				self.resolveDeps.shift()(res)
			}
		}
	}

	// 失败回调
	function reject(err) {
		// 判断状态
		if (self.status === MyPromise.PENDING) {
			self.reason = err
			self.status = MyPromise.REJECTED
			// 发布给订阅
			while (self.rejectDeps.length) {
				self.rejectDeps.shift()(err)
			}
		}
	}


	// 判断参数传入是否是函数
	if (typeof execution === 'function') {

		// 捕获错误
		try {
			// 执行传入的执行器
			execution(resolve, reject)

		} catch (err) {
			reject(err)
		}

	} else {
		throw 'MyPromise 的参数必须为函数。'
	}
}


MyPromise.prototype.then = function(onResolved, onRejected) {
	if ((onRejected && typeof onRejected !== 'function') || typeof onResolved !== 'function') {
		throw 'then 方法的参数必须为函数。'
	}

	// then方法链式 返回新promise
	return new MyPromise((resolve, reject) => {

		const resolvedCb = (val) => {
			let nextVal = onResolved(val);

			if (nextVal instanceof MyPromise) { // promise对象

				nextVal.then(resolve, reject)

			} else { // 普通值

				resolve(nextVal);

			}
		}

		const rejectedCb = (reason) => {
			let nextVal = onRejected(reason);

			if (nextVal instanceof MyPromise) { // promise对象

				nextVal.then(resolve, reject)

			} else { // 普通值

				resolve(nextVal)

			}
		}

		// 执行器是异步操作时,status还是pending,
		// 所以此时并不知道是成功还是失败回调,那么这里就需要把这两个回调
		// 存储起来
		this.resolveDeps.push(resolvedCb)
		this.rejectDeps.push(rejectedCb)
	})
}

MyPromise.prototype.catch = function(onRejected) {
	if (typeof onRejected === 'function') {
		// 收集依赖
		this.addRejectDeps(onRejected)
	} else {
		throw 'catch 方法的参数必须为函数。'
	}
}

MyPromise.prototype.finally = function() {}

MyPromise.prototype.all = function() {}

MyPromise.prototype.allSettled = function() {}

// 测试
var demo = function() {
	return new MyPromise((resolve, reject) => {
		setTimeout(() => {
			resolve(123435)
		}, 1000)
	})
}
demo().then((res) => {
	console.log('=========res', res)
	return new MyPromise((resolve, reject) => {
		setTimeout(() => {
			resolve('erersadad')
		}, 1000)
	})
}).then((res) => {
	console.log('=========res2', res)
})

6.promise和async/await、generator区别

Async/await 是Javascript编写异步程序的新方法。以往的异步方法无外乎回调函数和Promise。但是Async/await建立于Promise之上。

async/await:

  1. async/await 是写异步代码的新方式,以前的方法有回调函数和Promise。
  2. async/await 是基于Promise实现的,它不能用于普通的回调函数。
  3. 在主体函数之前使用了async关键字,在函数体内,使用了await关键字。
  4. wait关键字只能出现在用async声明的函数体内。
  5. 当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。
  6. async/await 与Promise一样,是非阻塞的。
  7. async/await 使得异步代码看起来像同步代码。

promise和async的区别:

  1. async/await 更加语义化,更简洁
  2. async/await可以使用try{}catch(){}捕获错误
  3. 深层嵌套可以使用promise.then方法进行链式请求,也可以使用promise.all方法。但最简洁的方式是使用async
  4. async 的优越性就是把每次异步返回的结果从 then 中拿到最外层的方法中,不需要链式调用

async 和 Generator区别:

  1. async是generator函数的语法糖。
  2. generator 函数是将函数分步骤阻塞 ,只有主动调用 next() 才能进行下一步
var gen1 = function* () {
    var f1 = yield 1;
    var f2 = yield 2;
    console.log(f1);
    console.log(f2);
};

var aaa = gen1() //undefined

aaa.next() //{value: 1, done: false} //执行f1

aaa.next() //{value: 2, done: false} //执行f2

aaa.next() 
//undefined
//undefined
//{value: undefined, done: true} 最终执行完毕

async对Generator函数做了以下4点改变:

  1. 内置执行器: async函数自带执行器,简单的说async函数就相当于自执行的Generator函数
  2. 更好的语义: async表示函数里有异步操作,await表示紧跟再后面的表达式需要等待结果
  3. 更广的适用性: yield命令后,只能是Thunk函数或Promise对象,而async函数的await命令后面,可以是Promise对象和原始类型的值
  4. 返回值是Promise对象,可以使用then方法指定下一步操作

7.Promise如何转async/await(异步转同步)

new Promiose()async

async function getdata (params) {
  return 'hllo Nodejs'
}
//等价于
function getdata () {
   return Promise.resolve('hllo Nodejs')
}

// async 返回 reject
async function getdata2 (params) {
  return Promise.reject(1234)
}
// 或者
async function aaa (){
  throw 1234
}
// 使用then执行async
getdata2().catch(a=>console.log(a))  //1234

Promiose.then()await

async function fooAsync () {
  const data = await 1
  console.log(data)
}
//等价于
function fooAsync () {
  return Promise.resolve(1).then((data) => console.log(data))
}

await 后面可以是Promise或任意表达式,async函数返回的也是Promise

async function foo1() {
    const result1 = await new Promise((resolve) =>  resolve('1'))
    const result2 = await new Promise((resolve) => resolve('2'))
    return result1 + result2
}

async function foo2() {
    const result = await foo1()
}

async返回一个数组

内部是使用Promise.all()方法来执行,返回可迭代的数组,全部成功才返回成功,一个失败所有的都失败。

async function foo() {
    const result1 = await new Promise((resolve) => {
        setTimeout(() => resolve({ name: "selfsummer" }), 2000);
    });
    const result2 = await new Promise((resolve, reject) => setTimeout(() => resolve({ name: "自夏" })));
    return [result1, result2];
}

foo().then((data) => {
    Array.isArray(data)    // true
    console.log(data)     //[ { name: 'selfsummer' }, { name: '自夏' } ]
}).catch((err) => {
    console.log(err);
});

8.promise和setTimeout区别

Promise本身是同步的立即执行函数, 当在executor中执行resolve或者reject的时候, 此时是异步操作, 会先执行then/catch等,当主栈完成后,才会去调用resolve/reject中存放的方法执行

setTimeout不会阻碍后续代码执行,会放到任务队列中。可以理解为一个异步函数。但论其原理实则不是严格意义上的异步,而只是推迟处理。

Promise中的异步操作(promise.then)是微任务。而setTimeout属于宏任务。微任务要优先于宏任务执行。

js事件循环机制总结

9.promise是异步还是同步

promise本身是同步的,其then方法是异步的(promise.then属于异步微任务,then中的方法,必须等到所有的同步任务执行完才执行)。promise是无法取消的,一旦新建就会立即执行。如果不设置回调函数,promise内部的错误是无法反应到外部的。当处于“pending”状态时,无法得知目前进展到哪个阶段(刚刚开始还是即将完成)。

异步的三种实现方式

  1. 回调函数(回调函数不一定是异步 , 但异步一定有回调函数)
  2. 事件
  3. promise 对象

10.Promise在resolve和reject执行之后,后续代码还会执行吗

会的。因为resolve和reject本身是回调函数,无法中断后续代码。需要使用return去中断。所以一般使用默认加return较妥。

function p() {
      return new Promise((resolve, reject) => {
        console.log(222)
        setTimeout(() => {
          console.log('async in p')
          reject('err in p')
          console.log('content after p reject')
        }, 200)
      })
}
function p1() {
      return new Promise((resolve, reject) => {
        console.log(333)
        setTimeout(() => {
          console.log('async in p1')
          resolve('resolev in p1')
          console.log('content after p1 resolve')
        }, 250)
      })
}
function p2() {
      return new Promise((resoleve, reject) => {
        console.log(444)
        setTimeout(() => {
          console.log('async in p2')
          return reject(new Error('p2error'))
          console.log('content after p2 return')
        }, 300)
      })
}

10.Promise的resolve和reject能否用return和throw代替

return无法取代resolve,因为return无法改变 <pending> 状态为<fulfilled>,在底层,需要执行resolve回调函数,才能改变状态。

throw可以取代reject,因为在底层,会有try/catch的错误捕获,如若出错,会将状态置为<rejected>

当然,在async函数中可以使用return和throw实现。

细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)

2019
18/05

前端与后端渲染方式的发展大致经历了这样几个阶段:后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)。

1. 后端模板渲染

前端与后端最初的渲染方式是后端模板渲染,就是由后端使用模板引擎渲染好 html 后,返回给前端,前端再用 js 去操作 dom 或者渲染其他动态的部分。

这个过程大致分成以下几个步骤:

说明:

  1. 前端请求一个地址 url
  2. 后端接收到这个请求,然后根据请求信息,从数据库或者其他地方获取相应的数据
  3. 使用模板引擎(如 java>jspphp>smarty)将这些数据渲染成 html
  4. 将 html 文本返回给前端

在这个过程中,前端的 html 代码需要嵌入到后端代码中(如 javaphp),并且在很多情况下,前端源代码和后端源代码是在一个工程里的。

所以,不难看出,这种方式的有这样的几个不足:

  1. 前后端杂揉在一起,不方便本地开发、本地模拟调试,也不方便自动化测试
  2. 前端被约束在后端开发的模式中,不能充分使用前端的构建生态,开发效率低下
  3. 项目难以管理和维护,也可能会有前后端职责不清的问题

尽管如此,但因为这种方式是最早出现的方式,并且这种渲染方式有一个好处,就是前端能够快速呈现服务器端渲染好的页面,而不用等客户端渲染,这能够提供很好的用户体验与 SEO 友好,所以当下很多比较早的网站或者需要快速响应的展示性网站仍然是使用这种方式。

2. 客户端渲染

随着前端工程化与前后端分离的发展,以及前端组件化技术的出现,如 react、vue 等,客户端渲染已经慢慢变成了主要的开发方式了。

与后端模板渲染刚好相反,客户端渲染的页面渲染都是在客户端进行,后端不负责任何的渲染,只管数据交互。

这个过程大致分成以下几个步骤:

CSR

说明:

  1. 前端请求一个地址 url
  2. 后端接收到这个请求,然后把相应的 html 文件直接返回给前端
  3. 前端解析 js 后,然后通过 ajax 向后台获取相应的数据
  4. 然后由 js 将这些数据渲染成页面

这样一来,前端与后端将完全解耦,数据使用全 ajax 的方式进行交互,如此便可前后端分离了。

其实,不难看出,客户端渲染与前后端分离有很大的好处:

  1. 前端独立出来,可以充分使用前端生态的强大功能
  2. 更好的管理代码,更有效率的开发、调试、测试
  3. 前后端代码解耦之后,能更好的扩展、重构

所以,客户端渲染与前后端分离现在已经是主流的开发方式了。

但这种方式也有一些不足:

  1. 首屏加载缓慢,因为要等 js 加载完毕后,才能进行渲染
  2. SEO 不友好,因为 html 中几乎没有可用的信息

3. node 中间层

为了解决客户端渲染的不足,便出现了 node 中间层的理念。

传统的 B/S 架构中,是 浏览器->后端服务器->浏览器,上文所讲的都是这种架构。

而加入了 node 中间层之后,就变成 浏览器->node->后端服务器->node->浏览器。

这个过程大致分成以下几个步骤:

SSR

说明:

  1. 前端请求一个地址 url
  2. node 层接收到这个请求,然后根据请求信息,向后端服务器发起请求,获取数据
  3. 后端服务器接收到请求,然后根据请求信息,从数据库或者其他地方获取相应的数据,返回给 node 层
  4. node 层根据这些数据渲染好首屏 html
  5. node 层将 html 文本返回给前端

一个典型的 node 中间层应用就是后端提供数据、node 层渲染模板、前端动态渲染。

这个过程中,node 层由前端开发人员掌控,页面中哪些页面在服务器上就渲染好,哪些页面在客户端渲染,由前端开发人员决定。

这样做,达到了以下的目的:

  1. 保留后端模板渲染、首屏快速响应、SEO 友好
  2. 保留前端后分离、客户端渲染的功能(首屏服务器端渲染、其他客户端渲染)

但这种方式也有一些不足:

  1. 增加了一个中间层,应用性能有所降低
  2. 增加了架构的复杂度、不稳定性,降低应用的安全性
  3. 对开发人员要求高了很多

4. 服务器端渲染(ssr)

大部分情况下,服务器端渲染(ssr)与 node 中间层是同一个概念。

服务器端渲染(ssr)一般特指,在上文讲到的 node 中间层基础上,加上前端组件化技术在服务器上的渲染,特别是 react 和 vue。

react、vue、angular 等框架的出现,让前端组件化技术深入人心,但在一些需要首屏快速加载与 SEO 友好的页面就陷入了两难的境地了。

因为前端组件化技术天生就是给客户端渲染用的,而在服务器端需要被渲染成 html 文本,这确实不是一件很容易的事,所以服务器端渲染(ssr)就是为了解决这个问题。

好在社区一直在不断的探索中,让前端组件化能够在服务器端渲染,比如 next.js、nuxt.js、razzle、react-server、beidou 等。

一般这些框架都会有一些目录结构、书写方式、组件集成、项目构建的要求,自定义属性可能不是很强。

以 next.js 为例,整个应用中是没有 html 文件的,所有的响应 html 都是 node 动态渲染的,包括里面的元信息、 css、js 路径等。渲染过程中, next.js 会根据路由,将首页所有的组件渲染成 html,余下的页面保留原生组件的格式,在客户端渲染。

5. SSR优缺点

1)优点:

  • 更好的 SEO:因为 SPA 页面的内容是通过 Ajax 获取,而搜索引擎爬取工具并不会等待 Ajax 异步完成后再抓取页面内容,所以在 SPA 中是抓取不到页面通过 Ajax 获取到的内容;而 SSR 是直接由服务端返回已经渲染好的页面(数据已经包含在页面中),所以搜索引擎爬取工具可以抓取渲染好的页面;
  • 首屏加载更快:SPA 会等待所有 Vue 编译后的 js 文件都下载完成后,才开始进行页面的渲染,文件下载等需要一定的时间等,所以首屏渲染需要一定的时间;SSR 直接由服务端渲染好页面直接返回显示,无需等待下载 js 文件及再去渲染等,所以 SSR 有更快的内容到达时间;

2)缺点:

  • 更多的开发条件限制:例如服务端渲染只支持 beforCreate 和 created 两个钩子函数,这会导致一些外部扩展库需要特殊处理,才能在服务端渲染应用程序中运行;并且与可以部署在任何静态文件服务器上的完全静态单页面应用程序 SPA 不同,服务端渲染应用程序,需要处于 Node.js server 运行环境;
  • 更多的服务器负载:在 Node.js 中渲染完整的应用程序,显然会比仅仅提供静态文件的 server 更加大量占用CPU 资源,因此如果你预料在高流量环境下使用,请准备相应的服务器负载,并明智地采用缓存策略。

6. spa & ssr选择

  1. 不需要首屏快速加载、SEO 友好的,用全客户端渲染。
  2. 需要首屏快速加载、SEO 友好的,如果用了如 react、 vue 等组件化技术,将不得不用 node 中间层与服务器端渲染。
  3. 如果技术团队不支持,不建议在需要首屏快速加载、SEO 友好的地方使用如 react、 vue 等组件化技术。
  4. 前后端分离之后也可以做后端模板渲染,这样前端的调试可以搭配 handlebars、ejs 等模板引擎进行本地调试,而后端的调试则需要到测试机了。

Vue 服务端渲染简介和实践

2019
18/05

SSR , Server Side Render的简称, 服务端渲染. 首先服务端渲染并不神秘, 在 ajax 兴起之前, 所有 web 应用都是服务端渲染, 服务器直接返回 html 文本给浏览器, 用户操作比如在 A 页面注册提交表单, 跳转到B 页面, 服务器需要返回两个页面. 这样的弊端显而易见, 加大了服务器的消耗, 随着 JavaScript 的发展, ajax 技术的出现, 客户端的操作通过请求接口的形式与服务器交互, 服务器不用返回整个页面, 而只是数据. 后来出现了后端模版, 比如 jsp, cshtml

<table>
    <c:forEach var="data" items="${datas}" varStatus="loop">
    <tr>
        <td>${loop.index + 1}</td>
        <td>${data.time}</td>
        <td>${data.msg}</td>
    </tr>
    </c:forEach>
</table>

用户在首次进入页面的时候, 通过服务端渲染给出 html, 用户操作使用 ajax 与服务端交互, 动静混合的形式.

后来随着 JavaScript 的发展, 前端模版和近年SPA 框架的发展, 呈现页面完全静态化, 动态内容交给前端(Javscript), 服务器只提供数据(一般以 json 的形式). 用户看到页面, 大致上需要如下过程(忽略 cdn 等)

1.浏览器加载所有静态资源(html,css,js等)–> 2.js 发起请求获取数据 –> 3.渲染页面 –> 呈现用户

好处是前后端完全分离(开发部署), 各司其职, 同时也节约服务器资源(只有数据交互).
此时用户所获取的 html 只是如下的片段:

<!DOCTYPE html><html class=has-full>
<head><meta charset=utf-8>
<title>人事管理系统</title>
<link href=/static/css/app.0c7e1e58d27be30db979adc44f7cd4eb.css rel=stylesheet>
</head>
<body><div id=app></div>
<script type=text/javascript src=/static/js/manifest.ca2797291add890279b8.js></script>
<script type=text/javascript src=/static/js/vendor.ee32e29412ede428634a.js></script>
</body>
</html>

其中2, 3 步骤是最耗费时间的, 因为获取数据受到用户网络, 服务器带宽等条件的显示, 而且可以通过业务数据再次加载一些静态资源. 随着业务的复杂, 打包处理 bundle 逐渐增大, 用户看到页面的时间(首屏), 即内容到达时间(time-to-content)将延长, 降低用户体验, 对电商网站流量转换率影响比较明显.

ssr 所做的事情

借用 react ssr 的两张图说明问题( vue 的 ssr 和 react 同理)

SSR服务端渲染:
SSR
CSR客户端渲染:
CSR
最大的差异是, 服务端直接返回的 渲染完毕html 页面, 获取业务数据, 填充业务组件都在服务端完成, 用户能够更快的看到页面内容, 同时也有利于爬虫抓取(SEO).

但是 ssr 也不是万能的, 需要 node 服务器, 很耗费性能, 需要做好缓存和优化, 相当于空间换时间. 全站 ssr 明显不可取, 现在流行较多的是 首屏 ssr ,甚至 首屏部分 ssr

参考资料
前后端渲染之争
Vue 全站服务器渲染 SSR 实践

Nuxt

有上述可知, ssr 应该有两个代码入口, 服务端和客户端, 通过 webpack 打包之后为别为 server-bundle 和 client-bundle, 页面第一次呈现, 通过 server-bundle , 获取业务数据, 填充数据, 渲染组件, 发送 html 给浏览器, 之后用户操作通过 client-bundle, 依旧是在浏览器范围内.

从零开始配置vue ssr 是比较困难的, 幸好有 nuxt api.

nuxt预设了 vue 服务端渲染的一些配置, 约定大于配置,

pages 路由, vuex 模块划分

Quick start

vue init nuxt-community/starter-template my-project
# 安装依赖
yarn install
# 开发模式运行
yarn run dev
# build 生成环境
yarn run build
# 运行已 build 的代码
yarn run start

目录结构
layouts, middleware, pages, static, store 目录必须存在

配置文件nuxt.config.js,
尽可能罗列了 nuxt.config.js可配置项和默认值

module.exports = {
  cache: {},
  css: [
    // 加载一个 node.js 模块
    //  'hover.css/css/hover-min.css',
    //  // 同样加载一个 node.js 模块,不过我们定义所需的预处理器
    //  { src: 'bulma', lang: 'sass' },
    //  // 项目中的 CSS 文件
    //  '~assets/css/main.css',
    //  // 项目中的 Sass 文件
    //  { src: '~assets/css/main.scss', lang: 'scss' } // 指定 scss 而非 sass
  ],

  // 默认 true
  dev: process.env.NODE_ENV !== 'production',

  // 创建环境变量
  env: {},

  // 配置 Nuxt.js 应用生成静态站点的具体方式。
  genetate: {
    dir: '',
    minify: '',
    routes: [],
  },

  /*
    * vue-meta
    * Headers of the page
    */
  head: {
    title: 'ssr-vue',
    meta: [
      { charset: 'utf-8' },
      { name: 'viewport', content: 'width=device-width, initial-scale=1' },
      { hid: 'description', name: 'description', content: 'Nuxt.js project' }
    ],
    link: [
      { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
    ]
  },
  /*
  ** Customize the progress bar color
  */
  loading: { color: '#3B8070' },
  /*
  ** Build configuration
  */
  build: {
    /*
    ** Run ESLint on save
    */
    extend (config, { isDev, isClient }) {
      if (isDev && isClient) {
        config.module.rules.push({
          enforce: 'pre',
          test: /\.(js|vue)$/,
          loader: 'eslint-loader',
          exclude: /(node_modules)/
        })
      }
    }
  },

  performance: {
    gzip: false,
    prefetch: true
  },

  // 引入 Vue.use 的插件
  plugins: [],

  // 默认当前路径
  rootDir: process.cwd(),

  router: {
    base: '',
    mode: 'history',
    linkActiveClass: 'nuxt-link-active',
    scrollBehavior: (to, from, savedPosition) => {
      // savedPosition 只有在 popstate 导航(如按浏览器的返回按钮)时可以获取。
      if (savedPosition) {
        return savedPosition
      } else {
        let position = {}
        // 目标页面子组件少于两个
        if (to.matched.length < 2) {
          // 滚动至页面顶部
          position = { x: 0, y: 0 }
        }
        else if (to.matched.some((r) => r.components.default.options.scrollToTop)) {
          // 如果目标页面子组件中存在配置了scrollToTop为true
          position = { x: 0, y: 0 }
        }
        // 如果目标页面的url有锚点,  则滚动至锚点所在的位置
        if (to.hash) {
          position = { selector: to.hash }
        }
        return position
      }
    },
    // default
    middleware: 'user-agent',
    // 扩展路由
    extendRoutes: () => {},

    // 默认同 rootDir
    srcDir: this.rootDir,

    transition: {
      name: 'page',
      mode: 'out-in'
    },
    watchers: {
      chokidar: {}, // 文件监控
      webpack: {
        aggregateTimeout: 300,
        poll: 1000
      }
    }
  }
}

pages 路由

路由, 约定大于配置, 支持动态, 嵌套, 动态嵌套路由, 过渡效果和中间件,
通过文件夹目录名称, 组件名称, 生成路由配置,
默认的 transitionName 为 page, 可在 assets 中添加全局的过渡效果

路由中间件:
在匹配页面之前执行;
nuxt.config.js –> 执行middleware –> 匹配布局 –> 匹配页面

路由中间件

视图

模版

默认的 html 模版: 应用根目录下的 app.html 文件, 没有改文件, 则采用默认的模版

页面

页面就是我们最熟悉的.vue文件, 单文件组件, 但是 nuxt 有一些不同的地方, 混入了 asyncData, fetch, head 三个方法, 还有 指定 layout, transition, scrollToTop, validate, middleware配置项

asyncData 和 fetch都是获取数据的方法, 不同的是, asyncData是请求接口的数据, fetch 是用于填充store 数据, 不会设置组件的数据, 两者都在页面加载之前调用

代码build 之后有服务端和客户端两个入口, build 之后对应为 client.js 和 server.js,
asyncData,和 fetch 第一次在服务端执行, 第二次切换页面后在浏览器执行

head 方法相关使用方法, 可参考 vue-meta
nuxt.config.js 默认定义了全局的 mota 标签

页面相关 API

异步数据

通过 asyncData 获取异步数据, 第一个参数为上下文对象 context, 推荐使用 promise 或者 async/await

asyncData
context 对象

资源文件

项目中编写 js 文件和普通项目一样. 通过 webpack 处理, 对于一些不需要 webpack loader 处理的静态资源文件, 必须放在项目根目录下的static文件夹中, 项目中直接使用/引用相关资源,
需要 webpack 处理的 静态文件,

可以覆盖 nuxt.config.js 中build 字段中 loaders 中 url-loader 或者 file-loader的 默认配置, 进行自定义设置

loaders 配置

[
  {
    test: /\.(png|jpe?g|gif|svg)$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1KO
      name: 'img/[name].[hash:7].[ext]'
    }
  },
  {
    test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
    loader: 'url-loader',
    query: {
      limit: 1000, // 1 KO
      name: 'fonts/[name].[hash:7].[ext]'
    }
  }
]

npm 模块 & 插件

服务端渲染, node 直接返回 html 给客户端, 所以 npm模块和插件应在 整个应用 实例化之前运行, 并且必须支持 ssr(服务端获取不到 window 等对象), 组件的生命周期只有beforeCreate 和created在client 和 server 均调用. 其余钩子函数只在 client 调用.

node_modules 中安装的模块在组件中可以直接使用

import someModules from 'some-module'

避免多个组件引用同一个模块重复打包的问题, 需要在 nuxt.config.js 中配置 vendor(路径和 plugin一致), 尽量将第三方模块打包至单独的文件中去

module.exports = {
    build: {
        vendor: ['path/to/your/modules'],
    },
    plugins: ['path/to/your/modules']
}

可以区分server 端插件和 client 端plugin, ssr 为 true 则只在服务端使用, 为 false 则反之

plugins: [{src: 'path/to/your/modules', ssr: false}]

ssr 部署及 pm2 的使用

使用 nuxt 官方模版新建的项目, 可以运行yarn build 命令进行构建, build 模板为项目根路径下的 .nuxt 文件夹, 其中 client.js 为客户端入口, server.js 为服务端入口, 通过命令nuxt start 启动 build 之后的代码.

通过简单命令启动在生成环境并不是好办法, 因此我们需要工具, 推荐pm2;

简单来说, PM2是node编写的, 进程管理工具,可以利用它来简化很多应用管理的繁琐任务,如性能监控、自动重启、负载均衡等。

安装之类的不在赘述, 具体参考文档 pm2

简单示例

在项目根目录新建 start.sh, 内容如下

#! /bin/bash
nuxt build
nuxt start

同样在项目根目录新建pm2.config.js, 内容如下

module.exports = {
  apps: [
    {
      name: 'test',
      script: './start.sh',
      env: {
        NODE_ENV: 'development'
      },
      env_production: {
        NODE_ENV: 'production'
      }
    }
  ]
}

参数设置可参考pm2 文档, 这只是个简单示例, 正式环境需要设置集群模式等
运行

pm2 start /path/to/pm2.config.js

即可开启 ssr 服务, 正式环境需要 nginx 代理到80或者443端口

作者:ethan_you
来源:简书

js知识总结—ES6篇(二)

2019
16/05

1.Fetch替代Ajax

Fetch并不是ES6的语法(应该是ES7),而是未来用于替代XMLHttpRequest的API, 它是W3C的正式标准。

Fetch API提供了一个fetch()方法,它被定义在BOM的window对象(全局方法)中。 该方法返回的是一个Promise对象。

fetch 规范与 jQuery.ajax() 不同:

  1. 当接收到一个代表错误的 HTTP 状态码时,从 fetch() 返回的 Promise 不会被标记为 reject, 即使响应的 HTTP 状态码是 404 或 500。相反,它会将 Promise 状态标记为 resolve (但是会将 resolve 的返回值的 ok 属性设置为 false ),仅当网络故障时或请求被阻止时,才会标记为 reject。
  2. fetch() 可以接受跨域 cookies;你也可以使用 fetch() 建立起跨域会话。跨域网站的Set-Cookie 头部字段将会被无视。
  3. fetch 不会发送 cookies。除非你使用了same-origin 的初始化选项。
  4. fetch发送post请求的时候发送2次,第一次状态码是204,第二次才成功原因,因为你用fetch的post请求的时候,导致fetch 第一次发送了一个Options请求,询问服务器是否支持修改的请求头,如果服务器支持,则在第二次中发送真正的请求。(跨域)

使用方法:

fetch('http://example.com/movies.json')
  .then(function(response) {
    return response.json();
  })
  .then(function(myJson) {
    console.log(myJson);
  });

// 传参
var url = 'https://example.com/profile';
var data = {username: 'example'};

fetch(url, {
  method: 'POST', // or 'PUT'
  body: JSON.stringify(data), // data can be `string` or {object}!
  headers: new Headers({
    'Content-Type': 'application/json'
  })
}).then(res => res.json())
.catch(error => console.error('Error:', error))
.then(response => console.log('Success:', response));

2.介绍一下Symbol

Symbol是ES6新属性(不是构造函数,不能使用new关键字),代表用给定名称作为唯一标识,这种类型的值可以这样创建:let id = Symbol("id")

Symbol可以确保值的唯一,即使采用相同的名称,也会产生不同的值。

  1. Symbol一般用于对象的key。let id = Symbol("id"); let obj = {[id]:1}; 
  2. 获取obj对象的所有Symbol:Object.getOwnPropertySymbols(obj)
  3. 获取含Symbol的对象的所有key不能使用Object.keys(obj),而是Reflect.ownKeys(obj)
  4. Reflect是一个内置的对象,它提供拦截 JS 操作的方法。这些方法与proxy handlers的方法相同

3.Map数据结构

js的对象,本质上是键值对的集合,但是传统上只能用字符串当作键。

ES6提供了Map数据结构,它类似于对象,也是键值对的集合,但是“键”的范围不限于字符串(优势),各种类型的值(包括对象)都可以当作键。

let m=new Map();
let o={p:'key'};
m.set(o,'aaa');
m.get(o);    // 'aaa'

作为构造函数,Map也可以接受一个数组作为参数。该数组的成员是一个个表示键值对的数组。

let map=new Map([
    ['name','张三'],
    ['age',18]
])
map.size    // 2
map.get("name") //"张三"

4.Map与对象比较,其优势

  1. Object的键只能是字符串,Map的键可以是任意类型的值(包括对象)。所以Map是一种更完善的Hash结构实现。
  2. 我们可以轻易得改变或删除Object的属性或方法,安全性较低。但Map能够避免这个问题。Map提供了set和delete方法来重设或删除属性。
  3. Object使用for.in遍历会将其原型链上的属性和方法同时遍历。我们需要通过obj.hasOwnProperty来筛选当前的属性是在自己,还是在原型链上的​。或者使用Object.keys(obj)方法取出键数组再进行遍历。而Map提供了forEach​/keys​/values等方法。

5.Map 与 WeakMap 的区别

Map 和 WeakMap 是两种数据结构,可用于操纵键和值之间的关系。WeakMap(弱引用的Map)也用于生成键值对的集合。

WeakMap可以使用set方法添加成员,也可以用get方法获取值。也可以接收一个数组作为构造函数的参数。

const wm1 = new WeakMap()
const key = {foo:1}
wm1.set(key,2)
wm1.get(key)  //2

// 接受复合数组为参数
const key2 = [1,2,3]
const wm2 = new WeakMap([
    [key,'foo'],
    [key2,'bar']
])
wm2.get(key2)  //bar

区别:

  1. WeakMap只接受对象(null除外、Symbol除外)作为键名,不接受其他类型的值作为键名。而Map键名可以是任意类型。
  2. WeakMap键名所指向的对象不计入垃圾回收机制。(键名所指向的对象被删除,该键值对就会自动清除。该键名指向的对象所占内存会被清除)
  3. WeakMap弱引用只针对键名而不是键值。键值依然正常引用。(键值引用的对象被删除,该键值对依然存在,该值引用的对象所占内存依然不被回收)

作用:

  1. 之所以键只接受对象,就是为了应对键所对应的对象可能会在将来消失的场景
  2. 有助于防止内存泄漏。

WeakMap设计目的在于,有时候我们想在某个对象上面放一些数据,但是会形成对这个对象的引用,一旦不在需要这个对象,我们除删除这个对象之外,也必须删除这个引用,否则垃圾回收机制就不会释放这个对象所占的内存,如果忘了删,就会造成内存泄漏。

var aaa = {a:1}
var bbb = [aaa,'sadad']
// 当不在需要aaa时
aaa = null
// 但bbb对aaa的引用依然存在

WeakMap键名所引用的对象都是弱引用,即垃圾回收机制(CG)不该将该引用考虑在内,因此,只要引用的对象及其他引用都删除,CG就会释放该对象的内存。也就是说,一旦不需要,WeakMap中键名对象和所对应的键值对就会消失(亲测不会立即清除,而是在下次CG回收时清除),不需要手动删除。

6.for…in、for…of、forEach()有什么区别

for…in

  1. 遍历的是数据结构中的key(遍历对象返回的对象key值,遍历数组返回的数组的索引)
  2. 不仅可以遍历对象构造器上的key,还会遍历对象原型上的可枚举key
  3. 也可以用来遍历数组,但是他是为遍历对象属性而构建的,所以不建议遍历数组。(而且他会遍历原型上可枚举的属性或方法,所以在原型上若有自定义的可枚举的方法也会被遍历出来导致出错,比如自定义:Array.prototype.a = xxx
  4. 不支持遍历MapSet
  5. 不支持breakcontinuereturn关键字
  6. 遍历是随机的
  7. 更适合用来遍历对象

将原型上可枚举的key设为不可枚举方式:

function aaa(){
  this.a=1
}
aaa.prototype.b = 2

for(let key in new aaa()){
  console.log(key)
}

// 结果 a b

// 设为不可枚举
Object.defineProperty(aaa.prototype, 'b',
  { enumerable: false }
)

for(let key in new aaa()){
  console.log(key)
}

// 结果 a

for…of

  1. 遍历的是数据结构中的value,且只能遍历可迭代的对象,或者说类数组结构,如Array, Map, Set, String, arguments, NodeList(Dom元素集合)等
  2. ES6新引入的特性
  3. 支持SetMap对象类型
  4. 不能循环普通的对象(会直接报错,因为普通对象不是类数组结构),需要通过和Object.keys()搭配
  5. 它可以与breakcontinuereturn关键字配合使用
  6. 遍历是有序的
// 遍历Map
var bbb = new Map([['a',1],['b',2]])

for(let val of bbb){ console.log(val) }

// ['a', 1]
// ['b', 2]

forEach()

  1. 用来遍历数组,不能遍历普通对象
  2. 不能使用breakcontinue中断循环,也不能使用 return 语句返回到外层函数
  3. 可以遍历value和引索

7.在js中使用ES6的模块化语法ES Module

script标签中使用type="module"属性可以原生实现ES Module

 // 方法 1 : 引入module.js,然后在script标签里面调用
  <script type="module">
    import test from './module.js';
    console.log(test())
  </script>
 
  // 方法 2 : 直接引入index.js,使用src引入
  <script type="module" src="./index.js"></script>

8.includes方法及其与indexOf区别

ES6之includes方法详解及其与indexOf区别

js知识总结—ES6篇(一)

2019
15/05

1.列举ES6的一些新特性

  1. 默认参数
  2. 模板字符串
  3. 解构赋值
  4. 增强的对象字面量
  5.  箭头函数
  6. Promises 异步
  7. generator和async/await
  8. 块作用域 和let和const
  9. Class 类
  10. Modules 模块

2.let ,const,var及其区别

JS代码在执行前会进行预解析。预解析会进行变量提升。

var 声明的变量会发生提升(提升到当前作用域顶部)。虽然变量还没有被声明,但是我们却可以使用这个未被声明的变量,这种情况就叫做提升,并且提升的是声明。

注意:是申明的提升,赋值并不会提升。赋值和申明并不同时发生。只有先声明才能使用这个变量,后申明的值前面无法直接使用。

(这里注意undefined的写法,避免手写代码时写错)

console.log(a) // undefined,如果后面没有申明变量a,这里就不是undefined而是直接报错:is not defined
var a = 1
// 同理如下
var a1 = a2+1
var a2 = 10
console.log(a1) // NaN
//----------------------------------------
var a = 10
var a
console.log(a) // 10
//原因:var会忽略同一个变量声明,在一个作用域中,如果已经有一个变量使用var声明了,那JS会忽略后续的同一个变量声明。
var a = 'hello world';
var a = undefined;
console.log(a) //undefined
// var a = undefined虽然作用等同于var a 但由于赋值的覆盖,所以会打印undefined
//----------------------------------------
console.log(a) // ƒ a() {} 这就是为什么函数可以放到下面,在上面依然能调用的原因
function a() {}
var a = 1 // 函数会被提升,且优先于变量
//-----------------------------------------
let a = 1
let a = 2 //error:Identifier 'a' has already been declared
// let 无法重复声明变量

let和const 声明的变量不会提升。

  1. let 和 const 在全局作用域下声明变量,变量并不会被挂载到 window 上,而 var 会被挂载到window对象中。
  2. let 和 const 在声明 变量 之前如果使用了这个变量,就会出现报错,是因为存在暂时性死区。(let实际也会进行提升,只不过因为暂时性死区导致了并不能在声明前使用。)
  3. let和const无法重复声明,而var可以重复申明,且var后声明的值会覆盖前值(同名函数也会覆盖之前的函数)。如果后申明的重复变量未赋值,会被忽略。
  4. var没有块级作用域,只有函数作用域。而let和const有块级作用域
  5. const申明了变量后必须赋值。不赋值会报错。(const a;是错误的)
  6. const定义的常量不能被修改。
console.log(a) // Uncaught ReferenceError: a is not defined
let a // 这里将提升限制了,所以提示a未申明

函数作用域:

function fn(){
   var aaa = 1
}
fn()
console.log(aaa) // 报错,未定义

经典面试题:

var a = 1;
fn();
function fn(){
    console.log(a); // undefined
    var a = 2
    console.log(a) // 2
}

// 第一个打印 undefined 是由于
// 函数申明也会被提升,且优先于变量
// 函数作用域中的变量的申明也会提升到函数最顶部
// 函数作用域中变量为局部变量,作用域链优先取局部值,局部没有才会找外层
// 上述代码等同于下:

function fn(){
    var a
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}
var a = 1;

面试题2

function fn(){
    console.log(a); // undefind
    a = 2
    console.log(a) // 2
}
fn() // 在这里执行时候,a只是提升申明,还没有赋值
var a = 1;

//----------------------------

function fn(){
    console.log(a); // 1
    a = 2
    console.log(a) // 2
}
var a = 1;
fn() // 在这里执行,a已经被赋值

块级作用域:

js有全局作用域、函数作用域。在ES6之后新增块级作用域。也就是`{}`中的作用域。if和for语句是典型的块级作用域。var在函数作用域中不会垮函数访问,而可以垮块级访问。也就是说var只承认函数作用域。
for(var i=0;i<3;i++){
    console.log("a:",i)
}
console.log("b:",i)
// a: 0
// a: 1
// a: 2
// b: 3

for(let j=0;j<3;j++){
    console.log("a:",j)
}
console.log("b:",j)
// a: 0
// a: 1
// a: 2
// j is not defined

当 JS 解释器在遇到非匿名的立即执行函数时,会创建一个辅助的特定对象,然后将函数名称作为这个对象的属性,因此函数内部才可以访问到当前的函数(只读),而不会指向全局的变量。但在全局中,访问不到该函数。

var foo = 1
(function foo() {
    foo = 10
    console.log(foo)
}()) // -> ƒ foo() { foo = 10 ; console.log(foo) }

console.log(foo) //1

小结:

函数提升优先于变量提升,函数提升会把整个函数挪到作用域顶部,变量提升只会把声明挪到作用域顶部,而申明的值不会提升

var 存在提升,我们能在声明之前使用。let、const 因为暂时性死区的原因,不能在声明前使用

var 在全局作用域下声明变量会导致变量挂载在 window 上,其他两者不会

let 和 const 作用基本一致,但是后者声明的变量不能再次赋值

var只能提升变量或函数的申明,无法提升申明时的赋值。

var会忽略同一个变量声明,在一个作用域中,如果已经有一个变量使用var声明了,那JS会忽略后续的同一个变量声明。但是赋值还是继续有效。

更多容易出错的题目:

关于变量提升和作用域相关的题目

3.为什么要使用模块化?都有哪几种方式,有什么特点?

前端模块化时将复杂的js文件分为多个独立的模块。用于重用和可维护。这样会引来模块之间相互依赖的问题。所以有了commonJS规范,AMD,CMD等规范,以及打包工具webpack,gulp等。

使用模块化可以给我们带来以下好处

  1. 解决命名冲突和作用域污染
  2. 提供可复用性
  3. 提高代码可维护性

在早期,使用立即执行函数实现模块化是常见的手段,通过函数作用域解决了命名冲突、污 染全局作用域的问题。

早期的模块化是AMD 和 CMD技术。但现在基本不用了。像以前的requireJS 是AMD方 法。Sea.js是CMD技术。

AMD,异步模块定义(Asynchronous Module Definition),它是依赖前置 (依赖必须一开始就写好)会先尽早地执行(依赖)模块 。换句话说,所有的require都被提前执行(require 可以是全局或局部 )。

AMD规范只定义了一个函数 define,它是全局变量。用法:

defind(id, dependencies, factory)

CMD(Common Module Definition)更贴近 CommonJS Modules/1.1 和 Node Modules 规范,一个模块就是一个文件;它推崇依赖就近,想什么时候 require就什么时候加载,实现了懒加载(延迟执行 ) ;它也没有全局 require, 每个API都简单纯粹 。

在 CMD 规范中,一个模块就是一个文件。代码的书写格式如下:

define(factory);

define(function(require, exports, module) {
    // 模块代码
});

AMD与CMD的比较

  • AMD:依赖前置,预执行(异步加载:依赖先执行)。CMD:依赖就近,懒(延迟)执行(运行到需加载,根据顺序执行)
// CMD
define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething();
    // 省略1万行
    var b = require('./b') // 依赖可以就近书写
    b.doSomething();
})

// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
    a.doSomething();
    // 省略1万行
    b.doSomething();
})

CommonJS 最早是 Node 在使用,目前也仍然广泛使用。基本语法:

module.exports = {} // 导出全部
exports.a = {} // 部分导出
var a = require(“./a.js”) // 引入

ES Module 是最新的原生实现的模块化方式。与 CommonJS 有以下几个区别:

CommonJS 支持动态路径导入,也就是 require(${path}/xx.js),后者目前不支持,但是已有提案。

CommonJS 是同步导入,因为用于服务端,文件都在本地,同步导入即使卡住主线程影响也不大。而后者是异步导入,因为用于浏览器,需要下载文件,如果也采用同步导入会对渲染有很大影响。

CommonJS 在导出时都是值拷贝,就算导出的值变了,导入的值也不会改变,所以如果想更新值,必须重新导入一次。但是 ES Module 采用实时绑定的方式,导入导出的值都指向同一个内存地址,所以导入值会跟随导出值变化。

ES Module 会编译成 require/exports 来执行的。

// 引入模块 API
import XXX from './a.js'
import { XXX } from './a.js'
// 导出模块 API
export function a() {}
export default function()

4.Proxy 可以实现什么功能

Vue3.0 通过 Proxy 来替换原本的 Object.defineProperty 来实现数据响应式(双向绑定)。

let p = new Proxy(target, handler) //target 代表需要添加代理的对象,handler 用来自定义对象中的操作,比如可以用来自定义 set 或者 get 函数。

与defineProperty 区别:

  1. Proxy 无需递归为每个属性添加代理,一次即可完成所有父子元素的监听,性能更好
  2. 在vue中Object.defineProperty有一些数据更新不能监听到,但是 Proxy 可以完美监听到任何方式的数据改变,缺陷是浏览器兼容性不好。

优势:

  • 可以劫持整个对象
  • 可以监听对象动态属性的添加
  • 可以监听到数组的变化,包括数组的索引和数组length属性
  • 可以监听删除属性
  • 操作时不是对原对象操作,new Proxy 返回⼀个新对象(它不直接操作对象,而是代理模式,通过对象的代理对象进行操作)。

5.map, filter, reduce 各自有什么作用?

map 作用是生成一个新数组,遍历原数组,将每个元素拿出来做一些变换然后放入到新的 数组中。map 的回调函数接受三个参数,分别是当前索引元素,索引,原数组。

filter 的作用也是生成一个新数组,在遍历数组的时候将返回值为 true 的元素放入新数组。 作用:过滤出需要的元素。filter 的回调函数也接受三个参数,用处也相同。

reduce 可以将数组中的元素通过回调函数最终转换为一个值。如计算数组元素的和:

const arr = [1, 2, 3]
const sum = arr.reduce((acc, current) => acc + current, 0)
console.log(sum)

reduce 接受两个参数,分别是回调函数和初始值。

首先初始值为 0,该值会在执行第一次回调函数时作为第一个参数传入

回调函数接受四个参数,分别为累计值、当前元素、当前索引、原数组,后三者想必大家都可以明白作用,这里着重分析第一个参数

在一次执行回调函数时,当前值和初始值相加得出结果 1,该结果会在第二次执行回调函数时当做第一个参数传入

所以在第二次执行回调函数时,相加的值就分别是 1 和 2,以此类推,循环结束后得到结果 6

6.箭头函数和普通函数区别

语法不同:语法简洁,箭头函数省去了function关键字,采用箭头=>来定义函数。函数的参数放在=>前面的括号中,函数体跟在=>后的花括号中。

参数写法不同:

① 如果箭头函数没有参数,直接写一个空括号即可。
② 如果箭头函数的参数只有一个,也可以省去包裹参数的括号。
③ 如果箭头函数有多个参数,将参数依次用逗号(,)分隔,包裹在括号中即可。

函数体不同:

①如果箭头函数的函数体只有一句代码,就是简单返回某个变量或者返回一个简单的JS表达式,可以省去函数体的大括号{ }和return关键字。

let f = val => val;// 等同于let f = function (val) { return val };
let sum = (num1, num2) => num1 + num2;// 等同于let sum = function(num1, num2) {
return num1 + num2;};

②如果箭头函数的函数体只有一句代码,就是返回一个对象,可以像下面这样写:

// 用小括号包裹要返回的对象,不报错
let getTempItem = id => ({ id: id, name: "Temp" });

③如果箭头函数的函数体只有一条语句并且不需要返回值(最常见是调用一个函数),可以给这条语句前面加一个void关键字,代表无返回值。

let fn = () => void doesNotReturn();

箭头函数最常见的用处就是简化回调函数。

[1,2,3].map(x => x * x);// map箭头函数写法
var result = [2, 5, 1, 4, 3].sort((a, b) => a - b);// 排序箭头函数写法

箭头函数不会创建自己的this

没有自己的this,它只会从自己的作用域链的上一层继承this。它会捕获自己在定义时(注 意,是定义时,不是调用时)所处的外层执行环境的this,并继承这个this值。所以,箭头函数 中this的指向在它被定义的时候就已经确定了,之后永远不会改变。

所以在使用时,普通函数需要用到外部的this时要将this赋值给另一个变量传进去,而箭头 函数不需要。直接可以再函数内部使用外部的this。常见使用场景为vue的方法。

普通函数简单调用时this指向函数本身。注意是在调用时才会确定this指向。

普通函数作为对象的方法调用时,this指向它所属的对象。

var id = 'GLOBAL';var obj = {
  id: 'OBJ',
  a: function(){
    console.log(this.id);
  },
  b: () => {
    console.log(this.id);
  }};
obj.a(); // 'OBJ'
obj.b(); // 'GLOBAL'

箭头函数继承而来的this指向永远不变,永远是定义时外层环境this

.call()/.apply()/.bind()无法改变箭头函数中this的指向

.call()/.apply()/.bind()方法可以用来动态修改函数执行时this的指向,但由于箭头函数的this 定义时就已经确定且永远不会改变。所以使用这些方法永远也改变不了箭头函数this的指向,虽 然这么做代码不会报错。

由于this指向不是自身,所以箭头函数不能作为构造函数使用,或者说构造函数不能定义成箭头函数,否则用new调用时会报错。

箭头函数没有自己的arguments,在箭头函数中访问arguments实际上获得的是外层局部(函数)执行环境中的值。可以在箭头函数中使用rest参数(三点运算符)代替arguments对象。

Var a = (...values)=> {
  console.log(values[0]) // 2
  console.log(values[1]) // 5
  console.log(values[2]) // 8
}


箭头函数不会创建this,不能作为构造函数,不能使用new关键字。另外箭头函数没有原型prototype

纯CSS+HTML制作三角形和圆|实心+空心

2019
11/05

我们的网页因为 CSS 而呈现千变万化的风格。这一看似简单的样式语言在使用中非常灵活,只要你发挥创意就能实现很多比人想象不到的效果。特别是随着 CSS3 的广泛使用,更多新奇的 CSS 作品涌现出来。

本文是流行的三角形和圆的绘制方法。

<style type="text/css">
    .clearfix{zoom:1;} /* clearfix for IE */
    .clearfix:after{content:"";display:block;clear:both;visibility:hidden;}
    .triangle-solid>div,.triangle-hollow>div{float: left;margin-right: 50px;}
    .top{
        position: relative;
        width: 0;
        height: 0;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-bottom: 50px solid #000;
    }
    .bottom{
        position: relative;
        width: 0;
        height: 0;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-top: 50px solid #000;
    }
    .left{
        position: relative;
        width: 0;
        height: 0;
        border-top: 25px solid transparent;
        border-right: 25px solid #000;
        border-bottom: 25px solid transparent;
    }
    .right{
        position: relative;
        width: 0;
        height: 0;
        border-top: 25px solid transparent;
        border-bottom: 25px solid transparent;
        border-left: 25px solid #000;
    }
    .triangle-hollow>.top:after{
        content: '';
        position: absolute;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-bottom: 50px solid #fff;
        left: -50px;
        top: 5px;
    }
    .triangle-hollow>.bottom:after{
        content: '';
        position: absolute;
        border-left: 50px solid transparent;
        border-right: 50px solid transparent;
        border-top: 50px solid #fff;
        left: -50px;
        top: -55px;
    }
    .triangle-hollow>.left:after{
        content: '';
        position: absolute;
        border-top: 25px solid transparent;
        border-right: 25px solid #fff;
        border-bottom: 25px solid transparent;
        left: 5px;
        top: -25px;
    }
    .triangle-hollow>.right:after{
        content: '';
        position: absolute;
        border-top: 25px solid transparent;
        border-bottom: 25px solid transparent;
        border-left: 25px solid #fff;
        left: -30px;
        top: -25px;
    }
    .round{
        width: 50px;
        height: 50px;
        background: #000;
        border-radius: 50%;
        -moz-border-radius: 50%;
        -webkit-border-radius: 50%;
    }
    .round-hollow{
        position: relative;
    }
    .round-hollow:after{
        position: absolute;
        content: '';
        width: 46px;
        height: 46px;
        background: #fff;
        border-radius: 50%;
        -moz-border-radius: 50%;
        -webkit-border-radius: 50%;
        top:2px;
        left:2px;
    }
</style>
<body>
    <h2>实心三角</h2>
    <div class="triangle-solid clearfix">
        <div class="top"></div>
        <div class="bottom"></div>
        <div class="left"></div>
        <div class="right"></div>
    </div>
    <h2>空心三角</h2>
    <div class="triangle-hollow clearfix">
        <div class="top"></div>
        <div class="bottom"></div>
        <div class="left"></div>
        <div class="right"></div>
    </div>
    <h2>实心圆</h2>
    <div class="round"></div>
    <h2>空心圆</h2>
    <div class="round round-hollow"></div>
</body>

纯CSS+HTML制作饼图和条形图

2019
11/05

使用html+css制作的图案或图形化内容响应速度最快,如下贴出我在开发过程中使用CSS+HTML制作的饼图和条形图,有需要的可以拿去。

CSS制作饼图

<div class="pie" style="animation-delay: -0.05s;"><span>5%</span></div>
<style type="text/css">
    @keyframes spin{
        to{transform: rotate(.5turn);}
    }
    @keyframes bg{
        50% {background: blue;}
    }
    .pie{
        position: relative;
        width: 200px; 
        height: 200px; 
        border-radius: 50%; 
        background: red;
        background-image:linear-gradient(to right, transparent 50%, blue 0);
    }
    .pie>span{
        position: absolute;
        line-height: 200px;
        width: 100%;
        color:#fff;
        text-align: center;
        font-size: 25px;
    }
    .pie::before{
        position: absolute;
        content: '';
        display: block;
        height: 100%;
        width: 50%;
        left: 50%;
        top: 0;
        border-radius: 0 100% 100% 0/50%;
        background-color: inherit;
        transform-origin: left;
        animation: spin .5s linear infinite,bg 1s step-end infinite;
        animation-play-state: paused;
        animation-delay: inherit;
    }
</style>

CSS制作条形图

<div class="barChart">
    <div class="x">
        <span>张三</span>
        <span>李四</span>
    </div>
    <div class="y">
        <div><span>0</span></div>
        <div><span>10</span></div>
        <div><span>20</span></div>
        <div><span>30</span></div>
        <div><span>40</span></div>
        <div><span>50</span></div>
    </div>
    <div class="bars">
        <div class="bar" style="height:150px;"><span>50</span></div>
        <div class="bar" style="height:90px;"><span>30</span></div>
    </div>
</div>
<style type="text/css">
    .barChart{
        position: relative;
        width: 80%;
        height: 400px;
    }
    .barChart>.x{
        bottom: 0;
        position: absolute;
        width: 100%;
        display: flex;
        flex-direction: row;
        align-items:flex-end;
        justify-content: space-around;
    }
    .barChart>.x>span{
        transform-origin:left top; 
        transform: rotate(45deg);
        width: 0;
        white-space: nowrap;
    }
    .barChart>.y{
        position: absolute;
        bottom: 30px;
        width: 100%;
        display: flex;
        flex-direction: column-reverse;
        align-items: flex-start;
        justify-content: flex-start;
    }
    .barChart>.y>div{
        border-bottom: 1px solid #00af16;
        width: 100%;
        height: 29px;
    }
    .barChart>.y>div>span{
        display: inline-block;
        transform: translateY(20px);
        margin-left: -40px;
        width: 30px;
        background: white;
        text-align: right;
    }
    .bars{
        position: absolute;
        bottom: 31px;
        width: 100%;
        display: flex;
        flex-direction: row;
        align-items: flex-end;
        justify-content: space-around;
    }
    .bars>.bar{
        width: 5%;
        background-image: linear-gradient(to right, #b8f123 0%, #b8f123 47%, #79a602 50%, #79a602 100%);
        display: inline-block;
    }
    .bars>.bar>span{
        display: block;
        text-align: center;
        margin-top: -22px;
    }
</style>

js知识总结—理论知识篇(二)

2019
15/03

1.js事件循环机制

js是一门单线程语言,但却能优雅地处理异步程序,在于js的事件循环机制。

浏览器是多进程的,浏览器每一个 tab 标签都代表一个独立的进程,其中浏览器渲染进程(浏览器内核)属于浏览器多进程中的一种,主要负责页面渲染,脚本执行,事件处理等
其包含的线程有:GUI 渲染线程(负责渲染页面,解析 HTML,CSS 、构造 DOM 树)、JS 引擎线程、事件触发线程、定时器触发线程、http 请求线程等主要线程。

js执行线程:

  1. 主线程:也就是 js 引擎执行的线程,这个线程只有一个,页面渲染、函数处理都在这个主线程上执行。
  2. 工作线程:也称幕后线程,这个线程可能存在于浏览器或js引擎内,与主线程是分开的,处理文件读取、网络请求、定时器等异步事件。

任务队列( Event Queue )

所有的任务可以分为同步任务和异步任务,同步任务一般会直接进入到主线程中立即执行;而异步任务会通过任务队列的机制(先进先出的机制)来进行协调。如图:

事件循环:主线程内的任务先执行完毕,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是我们说的 Event Loop (事件循环)。

宏任务和微任务:任务包含两种,宏任务(Macro Task)和微任务(Micro Task)。微任务要优先于宏任务执行。故主线程任务执行完毕后会先读取任务队列中的微任务,将微任务按照先进先出的原则全部执行且清空后,再去读取任务队列中的宏任务。。。若没有宏任务,则进行下一次事件循环(Next Tick)。在事件循环中,每进行一次循环操作称为tick。(如果有需要优先执行的逻辑,放入microtask 队列会比 task 更早的被执行。)

宏任务主要包含:script( 整体代码)、setTimeoutsetInterval、I/O、UI 交互事件、setImmediate(IE、Node.js )、requestAnimationFrame

宏任务的优先级: 主代码块 > setImmediate > MessageChannel > requestAnimationFrame > setTimeout / setInterval

微任务主要包含PromiseMutaionObserver(IOS)、process.nextTick(Node.js )

微任务的优先级: process.nextTick > Promise > MutationObserver

console.log('script start');

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

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

//输出的顺序是:script start, script end, promise1, promise2, setTimeout

//-------------------------------------------

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

//注意new Promise并不是异步,只是一个实例化对象,期回调先被执行。
//setTimeout遵循先进先出执行
//输出的顺序依次是:script start, promise1, script end, then1, timeout1, timeout2

2.setTimeout(fn,100)中的100毫秒时如何权衡的

当我们执行到setTimeout定时器代码时候,会把代码放到一个定时触发器线程,然后开始计时。计时结束后,会将定时的回调函数插入任务队列。必须等到主线程中的代码及先进入队列的任务执行完,主线程才会去执行这个回调。所以有可能要等很久,所以没办法保障回调函数一定会来100毫秒后执行。

这里注意,并不是等主线程执行完毕才开始计时,而是执行到定时器代码块时候就开始计时了。影响回调函数执行时间的是主线程的执行时间。

定时触发器线程

浏览器定时计数器并不是由JS引擎计数的,,因为JS引擎是单线程的,如果处于阻塞线程状态就会影响记计时的准确。因此通过单独线程来计时并触发定时是更为合理的方案。

3.js基本数据类型和引用数据类型的区别

  1. js基本数据类型有:Number、String 、Boolean、Null和Undefined;引用类型有:Object 、Array 、Function。
  2. 基本数据类型的值是不可变的,任何方法都无法改变一个基本类型的值,当这个变量重新赋值之后看起来变量的值是变了,但这里实际只是变量名指向变量的一个指针变了(改变的是指针的指向)。该变量是不会变的。引用类型时可以被改变的。一旦重新赋值,其值则被改变。
  3. 基本数据类型不可以添加属性和方法,但是引用类型可以。
  4. 基本数据类型的赋值都是简单赋值,如果从一个变量向另一个变量赋值基本类型的值,会在变量对象上创建一个新的值。然后把该值复制到为新变量分配的内存位置上。而引用类型的赋值是对象的引用。
  5. 基本数据类型的比较是值的比较,引用类型的比较是引用的比较。引用数据了类型比较的是内存地址是否相同。
  6. 基本数据类型是存放在栈区的,引用数据类型会同时保存在栈区和堆区。栈区保存的是一个地址。

4.一个单机网页游戏老是卡顿或崩溃,原因可能是什么

  1. 内存溢出问题:及时清理内存,将无用对象回收(垃圾回收机制)
  2. 资源文件过大:选择较小的图片或压缩图片
  3. 资源加载过慢:预加载资源,在游戏初始化之前就加载资源
  4. 动画绘制频率问题:每一帧的绘制间隔时间与显示器刷新频率保持一致。
  5. 单机游戏与网络无关。如果是网络游戏,ping值过高,请确认自己网络是否延迟过高。

5.函数式编程思想

定义:函数式编程(FP)是一种编程范式(其针对与面向对象编程范式OOP,函数式与面向对象冲突)。函数式编程是把运算过程尽量写成一系列嵌套的函数调用。 这里的函数式指的是数学函数,而非编程语言中的函数。

纯函数:函数式编程允许我们编写的函数是纯函数。纯函数的特点:

  1. 如果给定相同的参数,则返回相同的结果(也称为确定性)。可以理解为返回结果不因除参数外的任何变量的改变而改变。
  2. 它不会引起任何副作用。比如修改全局对象或修改通过引用传递的参数。

函数式编程的特点:

  1. 数据和行为分离(OOP是把数据和行为打包)
  2. 不可修改性(immutability。OOP以及面向过程编程很多时候都会对内存状态进行修改,比如给变量或成员重新赋值、往一个集合里添加/删除数据等,FP不允许这么干)
  3. 一等公民是函数(函数可以赋值给变量,可以当参数传递,函数的返回值可以是函数。OOP里的一等公民只有对象)
  4. 闭包

函数式编程的优势:

  1. 并行:纯函数无需任何修改即可并行执行。
  2. 无副作用:不会修改全局变量和引用变量,不会干扰其他程序。
  3. 更容易测试:函数式程序的 bug 不依赖于与任何与其无关的代码,你遇到的问题就总是可以再现。我们可以使用不同的上下文对纯函数进行单元测试。
  4. 引用透明性:如果一个函数对于相同的输入始终产生相同的结果,那么它可以看作透明的。
  5. 不可变性:只要参数不变,返回值永远不变
  6. 高内聚低耦合:函数是一阶公民。函数可以作为参数或返回值。我们可以组合不同的函数来创建具有新行为的新函数。

6.JS函数柯里化

柯里化(Currying)是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术。

// 普通的add函数
function add(x, y) {
    return x + y
}

// Currying后
function curryingAdd(x) {
    return function (y) {
        return x + y
    }
}

add(1, 2)           // 3
curryingAdd(1)(2)   // 3

Currying有哪些好处

① 提高复用率

// 正常正则验证字符串 reg.test(txt)

// 函数封装后
function check(reg, txt) {
    return reg.test(txt)
}

check(/\d+/g, 'test')       //false
check(/[a-z]+/g, 'test')    //true

// Currying后
function curryingCheck(reg) {
    return function(txt) {
        return reg.test(txt)
    }
}

var hasNumber = curryingCheck(/\d+/g)
var hasLetter = curryingCheck(/[a-z]+/g)

hasNumber('test1')      // true
hasNumber('testtest')   // false
hasLetter('21212')      // false

② 提前确认

var on = function(element, event, handler) {
    if (document.addEventListener) {
        if (element && event && handler) {
            element.addEventListener(event, handler, false);
        }
    } else {
        if (element && event && handler) {
            element.attachEvent('on' + event, handler);
        }
    }
}

// 提前确认调用哪个方法,不用每次调用都判断
var on = (function() {
    if (document.addEventListener) {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.addEventListener(event, handler, false);
            }
        };
    } else {
        return function(element, event, handler) {
            if (element && event && handler) {
                element.attachEvent('on' + event, handler);
            }
        };
    }
})();

③ 延迟运行

Function.prototype.bind = function (context) {
    var _this = this,args = Array.from(arguments).slice(1)
    return function() {
        return _this.apply(context, args)
    }
}

7.立即执行函数的作用

作用:

  1. 不必为函数命名,避免了污染全局变量
  2. 立即执行函数内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量
  3. 立即执行函数执行完后就立刻被销毁,避免常驻内存。(所以命名的立即执行函数外部无法访问,实际情况是已被销毁)
(function foo() {
    console.log(123);
})();
//已被销毁,所以访问不到。所以一般用匿名函数
foo();  // ReferenceError: foo is not defined

// 要访问自执行函数的执行结果
var num = (function (a, b) {
    return a + b;
})(1, 2);

//其实这种写法不用括号把函数包起来也是可以的
var num = function (a, b) {
    return a + b;
}(1, 2);
//在执行赋值操作的时候,上面的代码已经变成了一个表达式,而 JavaScript 解释器认为 函数表达式 是可以直接被执行的。

// 如果要再次访问自执行函数,需要将其作为返回值
// 这时就不能用匿名函数了
var num = (function aaa(a, b) {
    console.log(a+b)
    return aaa
})(1, 2);
num(3,4)
num(3,4)(5,6)

// 利用arguments.callee方法返回当前函数用于访问自执行函数
// 这样免得给匿名函数命名
// 但arguments相关的方法在严格模式下会报错
var num = (function(a, b) {
    console.log(a+b)
    return arguments.callee
})(1, 2);

使用场景:

  1. 立即执行函数执行完后就立刻被销毁,所以如果函数只被执行一次的时候就可以将其换成立即执行函数。
  2. 创建独立作用域,避免变量干扰。
  3. 用于初始化方法(数据初始化)。可避免内部变量污染全局。
  4. 可以用来封装一些插件、轮子、方法。

ES6中,块级作用域已经可以取代立即执行函数。可以将需要立即执行的代码放到花括号{}中。

8.定时器的最小间隔(延迟)时间

HTML5标准规定

  • setTimeout的最短时间间隔是4毫秒;
  • setInterval的最短间隔时间是10毫秒,也就是说,小于10毫秒的时间间隔会被调整到10毫秒

js知识总结—常用功能代码实现

2019
05/03

这里是一些常用的功能、工具类代码写法,长期完善。

1.防抖

触发高频事件后n秒内函数只会执行一次(执行最后一次),如果n秒内高频事件再次被触发,则重新计算时间。

将多次操作合并为一次操作进行。原理是维护一个计时器,规定在delay时间后触发函数,但是在delay时间内再次触发的话,就会取消之前的计时器而重新设置。这样一来,只有最后一次操作能被触发。

实现方式:每次触发事件时设置一个延迟调用方法,并且取消之前的延时调用方法

缺点:如果事件在规定的时间间隔内被不断的触发,则调用方法会被不断的延迟

//防抖debounce代码:
function debounce(fn, delay = 500) {
    let timeout = null; // 创建一个标记用来存放定时器的返回值
    // 闭包返回的函数使用普通函数而非箭头函数,是为了其中this指向赋给传入的fn,否则this将指向外层
    return function () {
        // 每当用户输入的时候把前一个 setTimeout clear 掉
        clearTimeout(timeout); 
        // 然后又创建一个新的 setTimeout, 这样就能保证interval 间隔内如果时间持续触发,就不会执行 fn 函数
        // 定时器中钩子使用箭头函数便于里面this指向外层function
        timeout = setTimeout(() => {
            fn.apply(this, arguments);
        }, delay);
    };
}
// 处理函数
function handle() {
    console.log(Math.random());
}
// 滚动事件
window.addEventListener('scroll', debounce(handle));

2.节流

高频事件触发,但在n秒内只会执行一次(执行第一次),所以节流会稀释函数的执行频率。

使得一定时间内只触发一次函数。原理是通过判断是否有延迟调用函数未执行。

实现方式:每次触发事件时,如果当前有等待执行的延时函数,则直接return。

特点:在一定时间内只存在一个定时器,执行完毕才能开启下次计时。

这里的this和arguments可以用es6箭头函数代替。

//节流throttle代码:
function throttle(fn,delay) {
    let timer = null; // 通过闭包保存一个标记
    return function () {
         // 在函数开头判断是否已有定时,若已经开启则直接返回
        if (timer) return;
        // 将外部传入的函数的执行放在setTimeout中
        timer = setTimeout(() => { 
        // 最后在setTimeout执行完毕后再把标记设置null,表示可以执行下一次循环了。
        // 在一定时间内只存在一个定时器,执行完毕才能开启下次计时
            fn.apply(this, arguments);
            timer = null;
        }, delay);
    };
}

//用时间戳实现节流
function throttle(handler, delay) {
    let lastTime = Date.now();
    return function () {
        let nowTime = Date.now();
        if (nowTime - lastTime > delay) {
            handler.apply(this, arguments);
            lastTime = nowTime;
        }
    }
}

function sayHi(e) {
    console.log(e.target.innerWidth, e.target.innerHeight);
}
window.addEventListener('resize', throttle(sayHi));

3.实现一个once函数,传入函数只执行一次。

// 实际也是一个防抖,利用闭包原理
function once(fn){
    var flag = true
    return function(){
        if(flag){
            fn.apply(null,arguments)
            flag = false
        }
    }
}

4.将原生ajax封装为promise

这里考察原生ajax知识点和promise用法。

var myAjax = (url,type="GET",data={})=>{
	let params = (d) => {
		let arr = []
		for(let prop in d){
			arr.push(prop + "=" + d[prop])
		}
		return arr.join("&")
	}
	
	return new Promise((resolve,reject)=>{
		let xhr = new XMLHttpRequest()
		let isGet = type.toLowerCase()=="get"
		let sendData = isGet?null:data
		let _url = isGet?`${url}?${params(data)}`:url
		xhr.open(type,_url,true)	//第三个参数为可选,true为异步,false为同步模式。默认true
		xhr.send(sendData)
		xhr.onreadystatechange = () =>{
			if(xhr.readyState==4){
				if(xhr.status>=200 && xhr.status<300 || xhr.status==304){
					resolve(JSON.parse(xhr.responseText))
				}else{
					reject(xhr.status+"请求失败")
				}
			}
		}
	})
}

5.js监听对象属性改变

① 在ES5中可以使用Object.difineProperty来实现已有属性的监听。

var Book = {name:"钢铁是怎样炼成的"}

// 劫持
objProxy(Book,"name")

// 闭包封装
function objProxy(obj,key){
    var val = obj[key]
    Object.defineProperty(obj, key, {
        set: function (newVal) {
            val = newVal
            console.log('你取了一个书名叫做' + newVal)
        },
        get: function () {
            return '《' + val + '》'
        }
    })
}

// 测试
console.log(Book); // {}
console.log(Book.name); // "《钢铁是怎样炼成的》"
Book.name = "三国演义";  //你取了一个书名叫做三国演义
console.log(Book.name); // "《三国演义》"

② 在ES6中使用Proxy进行

var Book = {name:"钢铁是怎样炼成的"}
var proxyBook = new Proxy(Book, {
	set: function (target, prop, newVal) {
	    target[prop] = newVal
	    console.log('你取了一个书名叫做' + newVal)
	},
	get: function (target, prop) {
	    return '《' + target[prop] + '》'
	}
})

//测试
console.log(proxyBook);	//Proxy {name: "钢铁是怎样炼成的"}
console.log(proxyBook.name); //"《钢铁是怎样炼成的》"
proxyBook.name = "水许传"; //你取了一个书名叫做水许传
proxyBook.price = 68;	//你取了一个书名叫做68
console.log(proxyBook);	//Proxy {name: "水浒传", price: 68}

6.用setTimeout实现setInterval

使用setInterval在处理某些重复事件(如果定时器代码在代码再次添加到任务队列之前还没执行完成,就会导致定时器代码连续运行好几次而之间没有间隔)时会有两个问题:①某些间隔会被跳过;②多个定时器代码执行时间可能会比预期小。

如下使用setTimeout取代setInterval

// 递归思路
function myInterval(){
	//do something
	setTimeout(myInterval,200)
}
setTimeout(myInterval,200)

// 或者
// arguments.callee代表当前函数,多用于递归调用
setTimeout(function(){
	//do something
	setTimeout(arguments.callee,200)
},200)

7.sleep实现

循环阻塞:传统思路实现sleep效果,需要将程序阻塞。而阻塞一般使用while方式。这种方式性能较差,且容易造成死循环。

function sleep(delay){
	var nextTime = Date.now() + delay
	while(Date.now() < nextTime);
        return true
}
// 测试
sleep(5000);
console.log(1)

async/await方法:这种方法需要与promise和定时器搭配。但这种方法必须封装到async函数中,若将async放到上下文中,同样是异步执行。

function sleep(delay){
	return new Promise((resolve)=>{
		setTimeout(()=>{
			resolve(true)
		},delay)
	})
}
(async function(){
	await sleep(5000)
	console.log(2)
})()

8.写一个函数,第一秒打印1,第二秒打印2

使用let块级作用域+延时器

for(let i=1;i<5;i++){
    setTimeout(()=>{
        console.log(i)
    },1000*i)
}

使用闭包+延时器

for(var i=1;i<5;i++){
    (function(i){
        setTimeout(()=>{
            console.log(i)
        },1000*i)
    })(i)
}

使用闭包+setInterval

function fn(){
    var i=1
    var timer = setInterval(()=>{
        console.log(i)
        i++
        i>5 && clearInterval(timer)
    },1000)
}

9.在数组原型链上实现数组去重的方法

Array.prototype.distinct = function(){
	var newArr = []
	for(var i=0;i<this.length;i++){
		if(newArr.indexOf(this[i]) < 0){
			newArr.push(this[i])
		}
	}
	return newArr
}

// 由于for...in遍历数组时,还会遍历原型上的属性和方法
// 所以在自定义数组原型方法时有必要将其设为不可枚举(原生的方法都不可枚举)
Object.defineProperty(aaa.prototype, 'distinct', { enumerable: false } )

10.下划线命名与驼峰命名互转

// 下划线转驼峰
function _toHump(key){
	// 去除首尾的下划线
	// var _key = key.replace(/(^_*)|(_*$)/g,"")
	var splitKey = key.split("_")
	var HumpKey = splitKey.map((item,index)=>{
		if(index>0 && item!=""){
			item = item[0].toUpperCase()+item.slice(1)
		}
		return item
	})
	return HumpKey.join("")
}

// 驼峰转下划线
function hump2_(key){
	var splitKey = key.split("")
	var HumpKey = splitKey.map((item,index)=>{
		// 判断是否为大写字母
		if(index>0 && item>="A" && item <= "Z"){
			item = "_" + item
		}
		return item
	})
	return HumpKey.join("")
}

11.页面加载进度实现

思路:利用document.onreadystatechange事件判断页面加载状态。页面加载会触发两次此事件,分别为document.readyState="interactive"(可交互:文档已被解析但资源文件、样式文件和框架未加载)和document.readyState="complete"(文档和所有子资源已完成加载,onload事件即将触发)。complete状态可以将进度赋值100%。中间可以用定时器动态得改变进度得百分比。为避免进度已经100%但文档还未加载完成的情况,可以设定一个等待的值如80%时清除定时器。

!function(){
	var n = 0
	var timer = setInterval(function(){
		if(n==100 || n==80){
			clearInterval(timer)
		}
		n+=10
		console.log(n)
	},50)
	document.onreadystatechange = function(){
		if(document.readyState=="complete"){
			n = 100
			console.log(n)
		}
	}
}()

12.手写parseInt方法

思路:使用隐式转换。如+"10"===1010+""==="10"

function _parseInt(str) {
	let str_type = typeof str;
	
	// 如果类型不是 string 或 number 类型返回NaN
	if (str_type !== 'string' && str_type !== 'number') {
		return NaN
	}
	
	// 如果是数字转为字符串
	str = str_type == 'number' ? str+"" : str

	// 字符串处理
	str = str.trim().split(".")[0]	//取小数点前面的字符
	var strArr = str.split('')	//转为字符串数组
	
	if (!strArr.length) {
		// 如果为空则返回 NaN
		return NaN
	}
	
	var strArrNum = strArr.map(function(str) {
		return +str	//隐式转换为数字
	})
	
	var num = strArrNum.reduce(function(x, y) {
		return x * 10 + y	//将数字数组转为整个数字
	})
	
	return num
}

_parseInt('123') //123
_parseInt('123.9') //123
_parseInt(123) //123
_parseInt(123.9) //123
_parseInt() //NaN
_parseInt("") //NaN
_parseInt("QWQW") //NaN

13.原生实现bind

Function.prototype.mybind = function (context) {
    // 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
    // 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
    context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

    var args = Array.from(arguments).slice(1)
    context.fn = this

    return function() {
        return context.fn(...args)
    }
}

// 手写call
Function.prototype.myCall = function(context){
	// 先判断是否有参数且第一个参数为对象,将其作为this指向,否则使用window
	// 这里第一个参数直接用形参,其他参数使用arguments,也可以使用扩展运算符获取
	context = context && typeof context==='object'?context:(typeof window === 'undefined'?global:window);

	// 这里的this是需要使用call的函数,将其赋给传入的指定this指向的对象,这样这个函数就指向了那个传入的对象
	// 函数中的this指向调用它的对象
	context.fn = this
	const res = context.fn(...[...arguments].slice(1))

	//删除这个挂载的函数
	delete context.fn

	// 将执行结果返回
	return res
}

14.找出数组重复项

思路1:遍历数组每一项,找出其之后的选项中是否有此项

function findRepeat(arr){
    var tempArr = []
    arr.forEach((item,index)=>{
        if(tempArr.indexOf(item)<0  && arr.slice(index+1).indexOf(item)>=0){
            tempArr.push(item)
        }
    })
    return tempArr
}

思路2:利用sort方法,先使用sort方法将数组排序,再来判断找出重复元素

function res(arr){
    var temp=[];
    arr.sort().sort(function(a,b){
        if(temp.indexOf(a)===-1 && a===b){
            temp.push(a)
        }
    })
    return temp;
}

思路2:利用ES6的数组filter方法,通过判断第一个重复项引索与当前引索不同找出重复项,然后再将重复项去重

Array.from(new Set(arr.filter((item,index)=>arr.indexOf(item)!==index)))

15.递归实现扁平数组->树结构

将arr1转为arr2

var arr1 = [
	{id:1,pid:0},
	{id:2,pid:1},
	{id:3,pid:1},
	{id:4,pid:3}
]

var arr2 = [
	{
		id:1,
		pid:0,
		children:[
			{
				id:2,
				pid:1,
				children:[]
			},
			{
				id:3,
				pid:1,
				children:[
					{
						id:4,
						pid:3,
						children:[]
					}
				]
			},
		]
	}
]

思路:递归将children作为参数进行运算

function fn(arr){
	let res = arr.filter(obj=>obj.pid===0)
	let doChildren = function(childArr){
		childArr.forEach(item=>{
			item.children = arr.filter(obj=>obj.pid===item.id)
			if(item.children.length){
				doChildren(item.children)
			}
		})
	}
	doChildren(res)
	return res
}

// 最简方案
function formatToTree(ary, pid) {
        return ary
          .filter((item) =>
            // 如果没有父id(第一次递归的时候)将所有父级查询出来
            // 这里认为 item.parentId === 1 就是最顶层 需要根据业务调整
            pid === undefined ? item.pid === 0 : item.pid === pid
          )
          .map((item) => {
            // 通过父节点ID查询所有子节点
            item.children = formatToTree(ary, item.id);
            return item;
          });
}

16.数组扁平化

数组扁平化是指将一个多维数组变为一维数组。

思路1递归:reduce遍历数组每一项,若值为数组则递归遍历,否则concat

function flatten(arr) {  
    return arr.reduce((result, item)=> {
        return result.concat(Array.isArray(item) ? flatten(item) : item);
    }, []);
}

思路2:toString 将多维数组变为字符串然后再用split分割还原为数组

arr.toString().split(',')

思路3:join也可以将多维数组变为字符串然后再用split分割还原为数组

arr.join().split(',')

思路3:普通递归法,这里略

思路4:扩展运算符将多维数组拆开为单个元素,然后利用concat传多个值

[].concat(1,2,3) //[1,2,3]
[].concat(...["1","2",["3","4"]]) //[1,2,3,4]

17.两个数组判断相等

两个数组是没办法直接判断是否相等的,arr1==arr2结果必然是false,因为数组是引用类型,内存地址不同。

若要判断数组相等与否,要考虑数组元素的顺序(数组是有序的,如果不需要考虑顺序,可以不用排序),数组元素的类型(如果是嵌套数组还要使用递归)。

这里我们不考虑数组元素为不同引用的对象问题,这里默认如果其中两个对象引用地址不同则直接为不等。

数组的扩展方法最好是放到数组原型上,方便直接继承。

Array.prototype.equals = function (arr) {
    if (!arr) return false;
    if (this.length !== arr.length) return false;
    // 这里使用普通for循环便于配合return
    for (let i = 0; i < this.length; i++){
        let thisItem = this[i], arrItem = arr[i];
        // 同为数组,递归判断
        if (Array.isArray(thisItem) && Array.isArray(arrItem)){
            if (!thisItem.equals(arrItem)) return false;
        }
        // 直接判断是否相等
        else if (thisItem !== arrItem) return false;
    }
    return true
}
// 使用
let aaa = [1,2,[1,2]]
aaa.equals([1,2,[1,2]]) // true

// 由于for...in遍历数组时,还会遍历原型上的属性和方法
// 所以在自定义数组原型方法时有必要将其设为不可枚举(原生的方法都不可枚举)
Object.defineProperty(aaa.prototype, 'equals', { enumerable: false } )

js知识总结—理论知识篇(一)

2019
04/03

1. js 是由哪三部分构成?

  1. ECMAScript(JavaScript语法 )
  2. DOM 文档对象模型
  3. BOM 浏览器对象模型

ECMAScript

ECMAScript 是由ECMA 国际( 原欧洲计算机制造商协会)进行标准化的一门编程语言,这种语言在万维网上应用泛。ECMAScript 规定了JS的编程语法和基础核心知识,是所有浏览器厂商共同遵守的一套JS语法工业标准。

作用:提供JS核心语言功能

DOM —文档对象模型

文档对象模型(Document Object Model,简称DOM),是W3C组织推荐的处理可扩展标记语言的标准编程接口。 通过 DOM 提供的接口可以对页面上的各种元素进行操作(大小、位置、颜色等)。DOM 通过创建树来表示文档,从而使开发者对文档的内容和结构进行操作。

作用:提供访问操作网页内容的方法和接口。如果没有 DOM,就不能渲染页面和对页面经行操作了。

BOM —浏览器对象模型

浏览器对象模型(Browser Object Model),不同浏览器提供了可以对浏览器窗口进行访问和操作的方法。

例如:弹出新的浏览器窗口,移动、改变和关闭浏览器窗口,提供详细的网络浏览器信息(navigator object),详细的页面信息(location object),详细的用户屏幕分辨率的信息(screen object),对浏览器存储的的支持等等。

BOM作为JavaScript的一部分并没有相关标准的支持,每一个浏览器都有自己的实现,虽然有一些非事实的标准,但还是给开发者带来一定的麻烦。

作用:提供与浏览器交互的方法和接口。

2.如何理解 JS 中的this关键字?

“this” 一般是表示当前作用域所在的对象。在JS函数中的this关键字由函数的调用者决定,谁调用就this就指向哪个。如果找不到调用者,this将指向windows对象。

3.什么是闭包

什么是闭包:《js高级程序设计》对闭包的定义为,闭包,其实是一种语言特性,它是指的是程序设计语言中,允许将函数看作对象,然后能像在对象中的操作般在函数中定义实例(局部)变量,而这些变量能在函数中保存到函数的实例对象销毁为止,其它代码块能通过某种方式获取这些实例(局部)变量的值并进行应用扩展。

说白一点,闭包,并不是函数,但离不开函数。闭包是一种语言特性。什么特性呢?可以在函数中封装私有变量,私有变量不会污染全局。且私有变量能够通过其他方式(比如将函数作为返回值或者参数,也就是高级函数)让函数外部能够访问到其私有变量。而且,这个私有变量不会被回收。直到这个函数的实例被销毁或者手动销毁此变量。等于说变相延长了变量的生命周期。

如何形成闭包:当一个函数作用域中的变量被另一个函数引用的时候,就已经形成了闭包。

另一种解释:函数中嵌套函数,且外层函数局部变量被子函数访问。就会形成闭包。一个函数的返回值或参数是函数,也算是嵌套函数。

实际上,如果一个函数访问了它的外部变量,那么它就已经形成了闭包。

function add(n){
  var num = n
  return function addTo(x){
    return x + num
  }
}
addTwo = add(2)
addTwo(5)

内存:一个不能被回收的栈内存就是闭包。

作用:

  1. 封装私有变量或方法。在JS中没有明确的方法来创建私有方法,但是闭包可以。
  2. 外部想要访问函数内部变量,可以使用闭包。可以像访问全局变量一样去使用局部变量,延长了变量的生命周期。
  3. 可以创建不同的实例,且互不干扰。类似于面向对象。比如Vue组件中的deta使用函数和返回值定义数据。实际上就是工厂模式的对象。互不干扰。再比如Vue3和react中的hooks写法。

优点:

  1. 可以读取函数内部变量。
  2. 延长变量生命周期。
  3. 避免全局变量的污染

缺点:

  1. 闭包会常驻内存,增加内存消耗。
  2. 不恰当的闭包有可能会造成内存泄漏。

闭包有三个特性:

  1. 函数嵌套函数
  2. 函数内部引用了外部的参数和变量
  3. 参数和变量不会被垃圾回收机制回收

延伸:

  1. js作用域
  2. js作用域链
  3. js栈内存和堆内存
  4. js垃圾回收机制
  5. js中真的有堆和栈吗

4.js的作用域

作用域就是变量的使用范围,也就是在代码的哪些部分可以访问这个变量,哪些部分无法访问到这个变量,换句话说就是这个变量在程序的哪些区域可见。

作用域就是代码的执行环境,作用域存在于栈内存。

原生js(不含ES6)没有块级作用域只有局部作用域(函数内部)和全局作用域。

一般认为的作用域是词法作用域(也就是静态作用域,如函数的作用域在函数定义的时候就确定了),常见的有函数作用域(如果在函数内部给一个未定义的变量赋值,这个变量会转变为一个全局变量)和块作用域(块作用域把标识符限制在{}中)

作用域链:多个作用域对象连续引用形成的链式结构。一般情况下,变量在当前作用域中取值。但是如果在当前作用域中没有查到值,就会向上级(由下到上,由内而外)作用域去查,直到查到全局作用域,这么一个查找过程形成的链条就叫做作用域链。注意:只有函数才能形成作用域。

ES6在有了let以后,拥有了块级作用域。但ES6的块级作用域并不是真正意义上的块级作用域。而在浏览器底层是用函数模拟的。也就是说,它的块级也是函数作用域(脚本作用域)。

//除函数外,普通的语句{}不会形成作用域,仍然属于全局。故如下代码可以打印if语句内申明的变量
var a = true;
if(a){var b = 222};
console.log(b); //222

//等同于如下,但建议用下面方式写,语义化更好
var a = true,b;
if(a){b = 222};
console.log(b); //222

函数会形成局部作用域。局部作用域内的变量申明不会污染外部作用域。

//函数和全局作用域
var a=100 //定义全局作用域变量
function fn(){
    var a=200  //在函数中定义变量不会污染外面定义的变量,只能函数里面用
    console.log('fn', a) 
}
console.log('1', a)  //1 100
fn() //fn 200

改变函数作用域的方法:

  1. eval()方法接收一个字符串作为参数,这个字符串的作用域与上下文同域。
  2. with关键字,是重复引用同一个对象的多个属性的快捷方式。

5.解释jsonp的原理,以及为什么不是真正的ajax?

原理:Jsonp就是利用浏览器可以动态地插入一段js并执行;

ajax的核心是 : 通过XMLHttpRequest(异步HTTP请求对象)获取非本页内容,

jsonp的核心 : 动态添加<script>标签来调用外部js脚本。

jsonp只支持get请求,ajax支持get和post请求

jsonp要用json格式传递数据,而ajax不一定用json格式,可能是字符串、数组等。

6.事件绑定和普通事件有什么区别?

事件绑定就是将事件通过类似于addEventListener的方法绑定在dom元素上;

普通事件即为dom元素直接调用事件方法;

普通事件可以被第二次掉用的事件覆盖,而事件绑定不会被覆盖,而是会依次执行。

DOM之事件详解参考以下文章:

DOM之事件触发、冒泡、捕获详解

DOM之事件定义-Event类

7.js事件流(事件捕获、事件冒泡)

事件流:事件流是描述从页面中接收事件的顺序。也可以说是事件传播的顺序。

“DOM2事件流”规定的事件流包括三个阶段:

  1. 事件捕获阶段:提供了在事件没有送达目标之前查看事件的机会。与冒泡方向相反,由父向子逐级捕获。
  2. 处于目标阶段:目标对象(元素)本身的时间处理程序调用(执行)。
  3. 事件冒泡阶段。目标元素的事件大部分都会冒泡(传播)到DOM树根。有一些特殊的事件不会冒泡,比如mouseenter和mouseleave事件不会冒泡。若要冒泡,可以使用mouseover和mouseout替代。

addEventListener

事件处理程序的注册方法为addEventListener(),它接受3个参数(事件类型,事件回调函数,布尔值),其中最后一个参数默认值为false,将会把程序注册为冒泡事件处理程序。而其若为true,则为捕获事件处理程序。两者的区别是:若为冒泡,表示在冒泡阶段调用事件处理程序,逐渐冒泡到外层。而捕获恰好相反,在捕获阶段调用处理程序,执行顺序相反。在IE中只支持事件冒泡。移除事件使用removeEventListener(),传参如上。

IE8及以下浏览器不支持此方法,使用attachEvent(on+事件类型, 事件回调函数)方法,只有两个参数(不支持事件捕获)。移除事件使用detachEvent()方法,参数相同。

window.onclick

注册事件最简单的方法就是直接给元素加事件属性(DOM0级模型),将处理程序赋值给相应的事件属性(事件前面带on)。这种方式适用于所有浏览器。但是缺点是元素将最多只有一个事件处理程序。

如何实现先冒泡后捕获的效果

在DOM标准事件模型中,是先捕获后冒泡。但是如果要实现先冒泡后捕获的效果,对于同一事件,监听捕获和冒泡,分别对应不同的处理函数,监听到捕获事件,先暂缓执行其处理函数,直到冒泡事件被捕获后再执行捕获事件处理函数。

8.事件模型

js包含三种事件模型:DOM0事件模型(始事件模型),DOM2事件模型,IE事件模型

  1. DOM0级模型:事件不会流动传播。事件绑定监听函数方式:① html标签中添加属性如<div onclick="fun()"></div>;② js中获取dom元素后给元素加事件属性:document.onclick = fn,取消监听使用document.onclick = null
  2. IE事件模型:使用attachEvent("onclick", handler)detachEvent("onclick", handler)方法进行绑定事件和移除绑定。事件流只有2个阶段,没有事件捕获阶段。
  3. DOM2级模型:addEventListener()removeEventListener()进行监听和取消。事件流有3个阶段。

9.js事件委托

事件委托指的是,不在事件的目标对象(元素)上设置监听函数,而是在其父元素上设置监听函数。通过事件冒泡,父元素可以监听到子元素上事件的触发。通过判断事件目标元素的类型来做不同的操作。jQuery的on方法就是使用了事件委托。事件委托常用与动态元素的绑定,新添加的子元素也可以有监听函数,也可以触发事件回调。

举例:比如我们要实时监听li元素的事件,可以将事件注册到ul标签上。这样当li元素动态添加后也能捕获到添加的li元素事件。

10.js垃圾回收机制(GC)

js解释器当监测到一个对象(字符串、数组等)无用的时候,为了避免内存占用,就会把这一块的内存释放掉。这就是垃圾回收。GC执行时遍历所有对象,对不可访问的对象进行回收。

垃圾回收的方法是:

  1. 清除标记:垃圾回收器会给内存中的变量添加标记。当进入环境时标记“进入”,离开环境时标记“离开”。回收器会适时删除离开环境中的变量(当然被引用的变量不会被删除)。
  2. 引用计数:为每个引用类型的变量的引用次数计数,当被另一个变量引用时记为1,当引用其他变量时,这个变量计数-1,当回收器再次执行时就会清除计数为0的值。但如果相互引用的次数过多(循环引用),就不会触发回收器,造成内存泄漏。
  3. 若要回收一个变量,可以将变量手动赋值为null。

为什么需要垃圾回收?

由于字符串、对象、数组等数据没有固定的大小,只有当他们的大小已知时,才能对他们进行存储和分配。每次创建变量时js解释器都会分配内存来存储这个实体。如果内存不够用,将会造成内存溢出导致系统崩溃。所以必须要有一套内存的释放机制。这就是垃圾回收机制的必要性。

var a = "hello world"
var b = "hello"
var a = b //此时会释放掉"hello world"

11.js内存之堆和栈

堆(Heap)和栈(Stack)是程序内存的两种管理方式。其也是两种不同的数据结构。

栈内存:系统自动分配释放 ,用于存放基本类型的变量和对象的引用变量。栈内存存取速度比堆要快很多(栈使用一级缓存,堆使用二级缓存),仅次于寄存器(寄存器是cpu的内存单元)。满足先进后出的数据结构,栈数据可以共享。

堆内存:允许程序在运行时动态申请某个大小的内存空间。由开发人员分配和释放。结构类似于链表。相当于一棵树。

基本类型:String,Number,Boolean,Null,Undefined,symbol,这6种基本数据类型它们是直接按值存放的,所以可以直接访问。存于栈内存中。

引用类型:Function,Array,Object,当我们需要访问这三种引用类型的值时,首先得从栈中获得该对象的地址指针,然后再从堆内存中取得所需的数据。

函数被声明后在堆中以字符串形式存储,且申明函数作用域(脚本作用域)。被调用时在栈中开辟执行环境栈,作用是执行代码,而非保存代码。函数作用域(脚本作用域)中的变量在申明后会与全局区分开来,保障每个词法作用域的代码独立性。

内存溢出:

如果想要堆溢出,比较简单,可以循环创建对象或大的对象; 如果想要栈溢出,可以递归调用方法,这样随着栈深度的增加,JVM(虚拟机)维持着一条长长的方法调用轨迹(作用域链),直到内存不够分配,产生栈溢出。

相关知识点:深拷贝和浅拷贝

12.两个script标签中的代码,一处报错,另一处有影响吗?

两个script标签是两个代码块。报错不会影响另一处。但如果后面的代码块有引用前面的变量或对象,是会出错。