0%

JavaScript中的this指向有很多琢磨不透的地方,很多时候有种莫名其妙的感觉。这次想系统的理解一下js中的this。

this指向举例

this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁实际上this的最终指向的是那个调用它的对象

以下我将举出非常多的示例来理解this指向。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function a() {
let user = 'flura'
console.log(this.use) // undefined
console.log(this) // Window
}
a()
// 根据这个例子,我们按照上面说的this最终指向的是调用它的对象,这里的函数a实际是Window对象所点出来的。

以上代码等价于
function a() {
let user = 'flura'
console.log(this.use) // undefined
console.log(this) // Window
}
window.a()
1
2
3
4
5
6
7
8
let obj = {
user: 'flura',
method: function() {
console.log(this.user) // flura
console.log(this) // {user: "flura", method: ƒ} 这里this指向的是obj
}
}
obj.method()

这里的this是指向对象obj的,调用这个method是通过obj.method()执行的,所以自然指向的就是对象obj。this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁实际上this的最终指向的是那个调用它的对象

在来一个更加明确的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
let obj = {
user: 'flura',
method: function () {
console.log(this.user) // undefined
console.log(this) // window
}
}
let method2 = obj.method //在这里发现这个和上面的例子不等价。验证了谁调用指向谁。 这里函数method虽然被对象obj引用,但是在将method赋值给method2的时候并没有执行,所以最终指向的window。这和上面的例子不一样,上面的例子是直接执行了method。
method2() // window调用了method


// 插一点箭头函数的知识
let obj = {
user: 'flura',
method: () => {
console.log(this.user) // undefined
console.log(this) // window
}
}
obj.method() // 在这里可以发现this的指向被往上抛了一层,指向了obj的上一层 window。 这就是箭头函数的特殊之处。所以定义对象上的方法上时,不要使用箭头函数。

以下引用了追梦子的博客 彻底理解js中的thsi指向

1
2
3
4
5
6
7
var o = {
user:"追梦子",
fn:function(){
console.log(this.user); //追梦子
}
}
window.o.fn();

  这段代码和上面的那段代码几乎是一样的,但是这里的this为什么不是指向window,如果按照上面的理论,最终this指向的是调用它的对象,这里先说个而外话,window是js中的全局对象,我们创建的变量实际上是给window添加属性,所以这里可以用window点o对象。

  这里先不解释为什么上面的那段代码this为什么没有指向window,我们再来看一段代码。

1
2
3
4
5
6
7
8
9
10
var o = {
a:10,
b:{
a:12,
fn:function(){
console.log(this.a); //12
}
}
}
o.b.fn();

 这里同样也是对象o点出来的,但是同样this并没有执行它,那你肯定会说我一开始说的那些不就都是错误的吗?其实也不是,只是一开始说的不准确,接下来我将补充一句话,我相信你就可以彻底的理解this的指向的问题。

  • 情况1:如果一个函数中有this,但是它没有被上一级的对象所调用,那么this指向的就是window。

  • 情况2:如果一个函数中有this,这个函数有被上一级的对象所调用,那么this指向的就是上一级的对象。

  • 情况3:如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

构造函数的this:

1
2
3
4
5
function Fn(){
this.user = "追梦子";
}
var a = new Fn();
console.log(a.user); //追梦子

 这里之所以对象a可以点出函数Fn里面的user是因为new关键字可以改变this的指向,将这个this指向对象a,为什么我说a是对象,因为用了new关键字就是创建一个对象实例,理解这句话可以想想我们的例子3,我们这里用变量a创建了一个Fn的实例(相当于复制了一份Fn到对象a里面),此时仅仅只是创建,并没有执行,而调用这个函数Fn的是对象a,那么this指向的自然是对象a,那么为什么对象a中会有user,因为你已经复制了一份Fn函数到对象a中,用了new关键字就等同于复制了一份。

更新一个小问题当this碰到return时

1
2
3
4
5
6
7
function fn()  
{
this.user = '追梦子';
return {};
}
var a = new fn;
console.log(a.user); //undefined

再看一个

1
2
3
4
5
6
7
function fn()  
{
this.user = '追梦子';
return function(){};
}
var a = new fn;
console.log(a.user); //undefined

再来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function fn()  
{
this.user = '追梦子';
return 1;
}
var a = new fn;
console.log(a.user); //追梦子
function fn()
{
this.user = '追梦子';
return undefined;
}
var a = new fn;
console.log(a.user); //追梦子

什么意思呢?

  如果返回值是一个对象,那么this指向的就是那个返回的对象,如果返回值不是一个对象那么this还是指向函数的实例。

1
2
3
4
5
6
7
function fn()  
{
this.user = '追梦子';
return undefined;
}
var a = new fn;
console.log(a); //fn {user: "追梦子"}

  还有一点就是虽然null也是对象,但是在这里this还是指向那个函数的实例,因为null比较特殊。

1
2
3
4
5
6
7
function fn()  
{
this.user = '追梦子';
return null;
}
var a = new fn;
console.log(a.user); //追梦子

this绑定规则

默认绑定

默认绑定是函数针对的独立调用的时候,不带任何修饰的函数引用进行调用,非严格模式下 this 指向全局对象(浏览器下指向 Window,Node.js 环境是 Global )。

1
2
3
4
5
6
7
var a = 2
function foo() {
var a = 5
console.log(this.a) // 2
console.log(this) // window
}
foo()

这是最常用的函数调用类型:独立函数调用。可以把这条规则看作是无法应用其他规则时的默认规则。

怎么看出foo()是默认绑定? 在代码中,foo()是直接使用不带任何修饰的函数引用进行调用的,因此只能使用默认绑定。

默认绑定的另一种情况

在函数中以函数作为参数传递,例如setTimeOutsetInterval等,这些函数中传递的函数中的this指向,在非严格模式指向的是全局对象。

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var name = 'JavaScript';
var person = {
name: '程序员',
sayHi: sayHi
}
function sayHi(){
setTimeout(function(){
console.log('Hello,', this.name);
})
}
person.sayHi();
setTimeout(function(){
person.sayHi();
},200);
// 输出结果 Hello, JavaScript
// 输出结果 Hello, JavaScript

隐式绑定

判断 this 隐式绑定的基本标准:函数调用的时候是否在上下文中调用,或者说是否某个对象调用函数。

1
2
3
4
5
6
7
8
9
function foo() {
console.log(this.a)
}

var obj = {
a: 2,
foo: foo
}
obj.foo() // 2

foo 方法是作为对象的属性调用的,那么此时 foo 方法执行时,this 指向 obj 对象。使用上下文来调用函数,所以最后this绑定在这个上下文对象obj上。

隐式绑定的另一种情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function foo() {
console.log(this.a)
}

var obj2 = {
a: 22,
foo: foo
}

var obj1 = {
a: 11,
obj2: obj2
}
obj1.obj2.foo() // 22

对象属性引用链只有上一层或者说最后一层在调用位置中起作用。 如果一个函数中有this,这个函数中包含多个对象,尽管这个函数是被最外层的对象所调用,this指向的也只是它上一级的对象

显式绑定

显式绑定,通过函数call apply bind 可以修改函数this的指向。call 与 apply 方法都是挂载在 Function 原型下的方法,所有的函数都能使用。

它们的第一个参数都是一个对象,是给this准备的,接着在调用函数时将其绑定到this。因为可以直接指定this的绑定对象,因此我们称之为显式绑定。

1
2
3
4
5
6
7
function foo() {
console.log(this.a)
}
var obj = {
a : 2
}
foo.call(obj) // 2

call 和 apply 的区别

  1. call和apply的第一个参数会绑定到函数体的this上,如果不传参数,例如fun.call(),非严格模式,this默认还是绑定到全局对象
  2. call函数接收的是一个参数列表,apply函数接收的是一个参数数组。
1
2
unc.call(thisArg, arg1, arg2, ...)        // call 用法
func.apply(thisArg, [arg1, arg2, ...]) // apply 用法

call和apply的注意点

这两个方法在调用的时候,如果我们传入数字、布尔类型或者字符串来当做this的绑定对象,这两个方法会把传入的参数转成它的对象类型(也就是new Number(…)、 new Boolean(…)、new String(…) )。这通常被称为”装箱”。

bind函数

bind 方法 会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(定义内容来自于 MDN )

1
func.bind(thisArg[, arg1[, arg2[, ...]]])    // bind 用法

new 绑定

使用new调用函数的时候,或者说发生构造函数调用时,会自动执行下面几个操作:

  1. 创建一个全新的空对象
  2. 将空对象的 proto 指向原对象的 prototype
  3. 这个新对象会绑定到函数调用的this
  4. 如果函数没有返回其他对象,那么new表达式中的函数调用会自动返回这个新对象。

注意:如果创建新的对象,构造函数不传值的话,新对象中的属性不会有值,但是新的对象中会有这个属性。

手动实现一个new创建对象代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function New(func) {
var res = {};
if (func.prototype !== null) {
res.__proto__ = func.prototype;
}
var ret = func.apply(res, Array.prototype.slice.call(arguments, 1));
if ((typeof ret === "object" || typeof ret === "function") && ret !== null) {
return ret;
}
return res;
}
var obj = New(A, 1, 2);
// equals to
var obj = new A(1, 2);

this绑定优先级

上面介绍了 this 的四种绑定规则,但是一段代码有时候会同时应用多种规则,这时候 this 应该如何指向呢?其实它们也是有一个先后顺序的,具体规则如下:

new绑定 > 显式绑定 > 隐式绑定 > 默认绑定

判断this

我们可以根据优先级来判断函数在某个调用位置引用的是哪条规则。可以按照下面的顺序来进行判断:

  1. 函数是否在new中调用(new绑定)? 如果是的话this绑定的是新建的对象。

    var bar = new foo()

  2. 函数是否通过call、apply(显示绑定)调用?如果是的话,this绑定的是指定的对象.

    var bar = foo(obj2)

  3. 函数是否在某个上下文对象中调用(隐式绑定)?如果是的话,this绑定的是那个上下文对象。

    var bar = obj1.foo()

  4. 如果不是的话,使用默认绑定。

    var bar = foo()

参考文章

彻底理解js中this的指向

重学 this 关键字

参考书目: 你不知道的JavaScript

使用JavaScript执行大型运算时,经常会出现假死现象,这是因为JavaScript是单线程编程语言,运算能力比较弱。HTML5新增Web Workders能够创建一个不影响前台处理的后台进程,并且在这个后台线程中可以继续创建多个子线程,以帮助JavaScript实现多线程的能力。通过WebWork, 开发者可以将耗时较长的处理交给后台线程去运行,从而解决在执行大量运算造成进程阻塞而页面无响应的情况。

Read more »

在使用vue-bus进行兄弟组件的数据传递过程中,遇到的问题,及解决方案 问题:vue中eventbus被多次触发,在this.$on监听事件时,内部的this发生改变导致,无法在vue实例中添加数据。

Read more »

生命周期图示

每个 Vue 实例在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。

vue生命周期实例

Vue钩子详解

Vue所有的生命周期钩子自动绑定在this上下文到实例中,因此你可以访问数据,对属性和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法。这是因为箭头函数绑定了父上下文,因此this与你期待的Vue实例不同。
1、beforeCreate
  在实例初始化之后,数据观测和event/watcher时间配置之前被调用。
2、created
  实例已经创建完成之后被调用。在这一步,实例已经完成以下的配置:数据观测,属性和方法的运算,watch/event事件回调。然而,挂载阶段还没开始,$el属性目前不可见。
3、beforeMount
  在挂载开始之前被调用:相关的render函数首次被调用。
  该钩子在服务器端渲染期间不被调用。
4、mounted
  el被新创建的vm.$el替换,并挂在到实例上去之后调用该钩子函数。如果root实例挂载了一个文档内元素,当mounted被调用时vm.$el也在文档内。
  该钩子在服务端渲染期间不被调用。
5、beforeUpdate
  数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。
  你可以在这个钩子中进一步第更改状态,这不会触发附加的重渲染过程。
  该钩子在服务端渲染期间不被调用。
6、updated
  由于数据更改导致的虚拟DOM重新渲染和打补丁,在这之后会调用该钩子。
  当这个钩子被调用时,组件DOM已经更新,所以你现在可以执行依赖于DOM的操作。然而在大多数情况下,你应该避免在此期间更改状态,因为这可能会导致更新无限循环。
  该钩子在服务端渲染期间不被调用。
7、activated
  keep-alive组件激活时调用。
  该钩子在服务器端渲染期间不被调用。
8、deactivated
  keep-alive组件停用时调用。
  该钩子在服务端渲染期间不被调用。
9、beforeDestroy 【类似于React生命周期的componentWillUnmount】
  实例销毁之间调用。在这一步,实例仍然完全可用。
  该钩子在服务端渲染期间不被调用。
10、destroyed
  Vue实例销毁后调用。调用后,Vue实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
  该钩子在服务端渲染不会被调用。

组件周期示例

当父组件内嵌套两个子组件A,B时,子组件实例化时生命周期钩子会如何加载?

1567671373868

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<Achild/>
<Bchild/>
</div>
</template>

<script>
import Achild from './Achild';
import Bchild from './Bchild';
export default {
name: 'Home',
components: {
Achild,
Bchild
},
}
</script>

<style scoped>

</style>

打印父组件实例化

1567682945513

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
<template>
<div>
<Achild/>
<Bchild/>
<!-- <router-link to="A">to A</router-link>
<router-link to="B">to B</router-link> -->
</div>
</template>

<script>
import Achild from './Achild';
import Bchild from './Bchild';
export default {
name: 'Home',
components: {
Achild,
Bchild
},
data () {
return {

}
},
beforeCreate () {
console.group('%c%s', 'color:black', 'beforeCreate 创建前状态===============组件father》')
},
created () {
console.group('%c%s', 'color:black', 'created 创建完毕状态===============组件father》')
},
beforeMount () {
console.group('%c%s', 'color:black', 'beforeMount 挂载前状态===============组件father》')
},
mounted () {
console.group('%c%s', 'color:black', 'mounted 挂载状态===============组件father》')
},
beforeUpdate () {
console.group('%c%s', 'color:black', 'beforeUpdate 更新前状态===============组件father》')
},
updated () {
console.group('%c%s', 'color:black', 'updated 更新状态===============组件father》')
},
beforeDestroy () {
console.group('%c%s', 'color:black', 'beforeDestroy 破前状态===============组件father》')
},
destroyed () {
console.group('%c%s', 'color:black', 'destroyed 破坏状态===============组件father》')
}
}
</script>

从组件A跳转到组件B。每次进入一个组件,这个组件原来的实例都会被销毁,然后新的实例会被建立

1567685173570

使用keep-alive

  • <keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。
  • 当组件在 <keep-alive> 内被切换,它的 activateddeactivated 这两个生命周期钩子函数将会被对应执行。

在路由出口渲染组件时配置:

1
2
3
4
<keep-alive >
<router-view v-if="$route.meta.keepAlive"/>
</keep-alive>
<router-view v-if="!$route.meta.keepAlive"></router-view>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
path: '/A',
name: 'A',
component: () => import ('../components/Achild'),
meta: {
keepAlive: true // 需要缓存 通过keep-alive 保存该组件的状态
}
},
{
path: '/B',
name: 'B',
component: () => import('../components/Bchild'),
meta: {
keepAlive: true // 需要缓存 通过keep-alive 保存该组件的状态
}
}

在设置keep-alive缓存的组件中,首次进入组件,会一次调用组件的钩子函数:created –> mounted –>activated 再次进入时,只触发activated钩子函数。离开这个组件就只会触发deactivated这个钩子。

这里介绍了清除上次打包后的dist文件夹,CopyWebpackPlugin的作用,减小打包出来的bundle.js文件的大小,从bundle里抽离出css,可视化分析打包文件的webpack插件,还有webpack sourceMap定位压缩前代码错误,与配置本地代理实现开发环境跨域。

Read more »

1px实现的难点

我们知道,像素可以分为物理像素(CSS像素)和设备像素。由于现在手机大部分是Retina高清屏幕,所以在PC端和移动端存在设备像素比的概念。简单说就是你在pc端看到的1px和在移动端看到的1px是不一样的。

在PC端上,像素可以称为CSS像素,PC端上dpr为1。也就说你书写css样式是是多少在pc上就显示多少。而在移动端上,像素通常使用设备像素。往往PC端和移动端上在不做处理的情况下1px显示是不同的。

一个物理像素等于多少个设备像素取决于移动设备的屏幕特性(是否是Retina)和用户缩放比例。

如果是Retina高清屏幕,那么dpr的值可能为2或者3,那么当你在pc端上看到的1px时,在移动端上看到的就会是2px或者3px。

由于业务需求,我们需要一些方法来实现移动端上的1px。

scale

如果在一个元素上使用scale时会导致整个元素同时缩放,所以应该在该元素的伪元素下设置scale属性。

1
2
3
4
5
6
.scale::after {
display: block;
content: '';
border-bottom: 1px solid #000;
transform: scaleY(.5);
}

linear-gradient

通过线性渐变,也可以实现移动端1px的线。原理大致是使用渐变色,上部分为白色,下部分为黑色。这样就可以将线从视觉上看只有1px。

由于是通过背景颜色渐变实现的,所以这里要使用伪元素并且设置伪元素的高度。 当然,也可以不使用伪元素,但是就会增加一个没有任何意义的空标签了。

1
2
3
4
5
6
div.linear::after {
display: block;
content: '';
height: 1px;
background: linear-gradient(0, #fff, #000);
}

box-shadow

通过box-shaodow来实现1px也可以,实现原理是将纵坐标的shadow设置为0.5px即可。box-shadow属性在Chrome和Firefox下支持小数设置,但是在Safari下不支持。所以使用该方法设置移动端1px时应该慎重使用。

1
2
3
div.shadow {
box-shadow: 0 0.5px 0 0 #000;
}

svg

另外,可以使用可缩放矢量图形(svg)来实现。由于是矢量图形,因此在不同设备屏幕特性下具有伸缩性。

1
2
3
4
5
6
.svg::after {
display: block;
content: '';
height: 1px;
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="1px"><line x1="0" y1="0" x2="100%" y2="0" stroke="#000"></line></svg>');
}

在Chrome下可以显示移动端的1px,但是由于Firefox的background-image如果是svg的话,颜色的命名形式只支持英文书写方式,如’black, red’等,所以在Fire下没有1px的线。

解决Firefox下background-image的svg颜色命名问题很简单。可以使用black这种命名方式或者将其转换成base64.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
方法一:颜色不采用十六进制,而是用英文方式
.svg::after {
display: block;
content: '';
height: 1px;
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="1px"><line x1="0" y1="0" x2="100%" y2="0" stroke="black"></line></svg>');
}

方法二:将svg改为base64
.svg::after {
display: block;
content: '';
height: 1px;
background-image: url('data:image/svg+xml;utf-8,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMDAlIiBoZWlnaHQ9IjFweCI+PGxpbmUgeDE9IjAiIHkxPSIwIiB4Mj0iMTAwJSIgeTI9IjAiIHN0cm9rZT0iYmxhY2siPjwvbGluZT48L3N2Zz4=');
}

当然,以上的所有方案都是基于dpr=2的情况下实现的,此外还需要考虑dpr=3的情况。因此,可以根据media query来判断屏幕特性,根据不同的屏幕特性进行适当的设置。如下是根据不同的dpr设置scale属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@media all and (-webkit-min-device-pixel-ratio: 2) {
.scale::after {
display: block;
content: '';
border-bottom: 1px solid #000;
transform: scaleY(.5);
}
}

@media all and (-webkit-min-device-pixel-ratio: 3) {
.scale::after {
display: block;
content: '';
border-bottom: 1px solid #000;
transform: scaleY(.333);
}
}

适用于各种设备

接下来看看各种设备下的场景。首先使用JavaScript计算出scale的值:

1
var scale = 1 / window.devicePixelRation;

head中的meta标签设备:

1
<meta name="viewport" content="initial-scale=scale,maximum-scale=scale,minimum-scale=scale,user-scalable=no">

border0.5px

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.custom-border{
width:200px;
margin:10px auto;
height:100px;
border:1px solid #333;
background-color:#eee;
padding:10px;
}
.scale-border{
margin:10px auto;
height:100px;
position:relative;
padding:10px;
width: 200px;
}
.border{
-webkit-transform:scale(0.5);
transform:scale(0.5);
position:absolute;
border:1px solid #333;
top:-50%;
right:-50%;
bottom:-50%;
left:-50%;
border-radius: 10px;
background-color:#eee;
}
.content{
position:relative;
z-index:2;
}

<div class="custom-border border-color">边框宽度1px</div>
<div class="scale-border">
<div class="content">边框宽度0.5px</div>
<div class="border border-color"></div>
</div>

特性

  • 从浏览器中创建 XMLHttpRequests
  • 从 node.js 创建 http 请求
  • 支持 Promise API
  • 拦截请求和响应
  • 转换请求数据和响应数据
  • 取消请求
  • 自动转换 JSON 数据
  • 客户端支持防御 XSRF

使用

安装

使用 npm:

1
$ npm install axios

使用 cdn:

1
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

在vue项目中使用axios

  1. 挂载在vue的原型上 全局使用

    首先在主入口文件main.js中引用,之后挂在vue的原型链上

    1
    2
    import axios from 'axios'
    Vue.prototype.$axios = axios

    在vue组件中使用:

    1
    2
    3
    4
    5
    this.$axios.get('/api/getList').then((res) => {
    this.newList = res.data.data
    }).catch(err => {
    console.log(err)
    })
  2. 结合 vue-axios使用

    在main.js中引用

    1
    2
    3
    4
    import axios from 'axios'
    import VueAxios from 'vue-axios'

    Vue.use(VueAxios,axios);

axios API

axios.get(url[, config])

axios.delete(url[, config])

axios.post(url[, data[, config]])

axios.put(url[, data[, config]])

axios.patch(url[, data[, config]])

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
this.axios.get('/api/demo', {
params: {
id: 124,
name: 'jerry'
}
})
其实一般axios添加参数是
this.axios.get('/api/demo?id=124&&name='jerry)

this.axios.delete('/api/demo', {
data: {
id: 123,
name: 'Henry',
sex: 1,
phone: 13333333
}

this.axios.post('/api/demo', {
name: 'jack',
sex: 'man'
})

this.axios.put('/api/demo', {
name: 'jack',
sex: 'man'
})

在使用axios时,注意到配置选项中包含params和data两者,以为他们是相同的,实则不然。

因为params是添加到url的请求字符串中的,用于get请求。 参数是以id=124&name=jerry的形式附到url的后面

而data是添加到请求体(body)中的, 用于post请求。

POST请求提交时,使用的Content-Type是application/x-www-form-urlencoded,而使用原生AJAX的POST请求如果不指定请求头RequestHeader,默认使用的Content-Type是text/plain;charset=UTF-8。

执行多个并发请求

1
2
3
4
5
6
7
8
9
10
11
12
function getUserAccount() {
return axios.get('/user/12345');
}

function getUserPermissions() {
return axios.get('/user/12345/permissions');
}

axios.all([getUserAccount(), getUserPermissions()]).then(
axios.spread(function (acct, perms) {
// 两个请求现在都执行完成
}));

可以通过向 axios 传递相关配置来创建请求

axios(config)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 发送 POST 请求
axios({
method: 'post',
url: '/user/12345',
data: {
firstName: 'Fred',
lastName: 'Flintstone'
}
});
//等同于以下写法
axios.post('/user/12345', {
firstName: 'Fred',
lastName: 'Flintstone'
})

// 获取远端图片
axios({
method:'get',
url:'http://bit.ly/2mTM3nY',
responseType:'stream'
}).then(function(response) {
response.data.pipe(fs.createWriteStream('ada_lovelace.jpg'))
});

简单的请求配置(config)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
{
// `url` 是用于请求的服务器 URL
url: '/user',

// `method` 是创建请求时使用的方法
method: 'get', // default

// `baseURL` 将自动加在 `url` 前面,除非 `url` 是一个绝对 URL。
// 它可以通过设置一个 `baseURL` 便于为 axios 实例的方法传递相对 URL
baseURL: 'https://some-domain.com/api/',

// `headers` 是即将被发送的自定义请求头
headers: { Authorization: token }

// `params` 是即将与请求一起发送的 URL 参数 用于get请求
// 必须是一个无格式对象(plain object)或 URLSearchParams 对象
params: {
ID: 12345
},

// `data` 是作为请求主体被发送的数据
// 只适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
data: {
firstName: 'Fred'
},

// `timeout` 指定请求超时的毫秒数(0 表示无超时时间)
// 如果请求话费了超过 `timeout` 的时间,请求将被中断
timeout: 1000,

}

响应结构

某个请求的响应包含以下信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
// `data` 由服务器提供的响应
data: {},

// `status` 来自服务器响应的 HTTP 状态码
status: 200,

// `statusText` 来自服务器响应的 HTTP 状态信息
statusText: 'OK',

// `headers` 服务器响应的头
headers: {},

// `config` 是为请求提供的配置信息
config: {},
// 'request'
// `request` is the request that generated this response
// It is the last ClientRequest instance in node.js (in redirects)
// and an XMLHttpRequest instance the browser
request: {}
}

接收响应

1
2
3
4
5
6
7
8
9
axios支持Promise对象,所以获取响应值 用then
axios.get('/user/12345')
.then(function(res) {
console.log(rese.data);
console.log(res.status);
console.log(res.statusText);
console.log(res.headers);
console.log(res.config);
});

可以用catch做错误处理

全局配置axios默认值

1
2
3
4
5
axios.defaults.baseURL = "https://api.example.com";
axios.defaults.headers.common["Authorization"] = AUTH_TOKEN;
axios.defaults.headers.post["Content-Type"] = "application/x-www-form-urlencoded";
axios.defaults.headers.common['Authorization'] = ''; // 设置请求头为 Authorization
axios.defaults.timeout = 200; 在超时前,所有请求都会等待 2.5 秒

错误处理

1
2
3
4
5
6
7
8
9
10
11
12
13
axios.get("/user/12345")
.catch(function (error) {
if (error.response) {
// 请求已发出,但服务器响应的状态码不在 2xx 范围内
console.log(error.response.data);
console.log(error.response.status);
console.log(error.response.headers);
} else {
// Something happened in setting up the request that triggered an Error
console.log("Error", error.message);
}
console.log(error.config);
});

可以使用 validateStatus 配置选项定义一个自定义 HTTP 状态码的错误范围。

1
2
3
4
5
axios.get("/user/12345", {
validateStatus: function (status) {
return status < 500; // 状态码在大于或等于500时才会 reject
}
})

拦截器

可以自定义拦截器,在在请求或响应被 thencatch 处理前拦截它们。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 添加请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前做些什么
return config;
}, function (error) {
// 对请求错误做些什么
return Promise.reject(error);
});

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
// 对响应数据做点什么
return response;
}, function (error) {
// 对响应错误做点什么
return Promise.reject(error);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
//配置发送请求前的拦截器 可以设置token信息 
axios.interceptors.request.use(config => {
//loading开始 //加载进度条
loadingInstance.start();
let token = localStorage.getItem("x-auth-token"); // 给所有请求带上token
if (token) { // 判断是否存在token,如果存在的话,则每个http header都加上token
config.headers.token = `${token}`;
}
return config;
}, error => {
//出错,也要loading结束
loadingInstance.close();
return Promise.reject(error);
});


// 配置响应拦截器
axios.interceptors.response.use(res => {
//loading结束
loadingInstance.close();
//这里面写所需要的代码
if (res.data.code == '401') {
//全局登陆过滤,当判读token失效或者没有登录时 返回登陆页面
return false;
};
return Promise.resolve(res);
}, error => {
loadingInstance.close();
return Promise.reject(error);
})

注意事项

axios会默认序列化 JavaScript 对象为 JSON

axios中POST的默认请求体类型为Content-Type:application/json(JSON规范流行)这也是最常见的请求体类型,也就是说使用的是序列化后的json格式字符串来传递参数,如:{ "name" : "mike", "sex" : "male" };同时,后台必须要以支持@RequestBody的形式接收参数,否则会出现前台传参正确,后台接收不到的情况。
如果想要设置类型为Content-Type:application/x-www-form-urlencoded(浏览器原生支持),axios提供了两种方式,如下:

  1. URLSearchParams

    1
    2
    3
    4
    const params = new URLSearchParams();
    params.append('param1', 'value1');
    params.append('param2', 'value2');
    axios.post('/user', params);
  2. qs

    默认情况下在安装完axios后就可以使用qs库 选择使用qs库编码数据:

    1
    2
    const qs = require('qs');0
    axios.post('/foo', qs.stringify({ 'bar': 123 }));

官方文档

众所周知,直接用webpack-cli生成的vue项目目录极为臃肿,非常的不好看。所以打算自己配置webpack项目,这里我们从零开始搭建webpack4的vue项目

Read more »

概念

本质上,webpack 是一个现代 JavaScript 应用程序的*静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph)*,其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle

在了解webpack原理之前,需要掌握以下几个核心概念

  • Entry: 入口,webpack构建第一步从entry开始
  • module:模块,在webpack中一个模块对应一个文件。webpack会从entry开始,递归找出所有依赖的模块
  • Chunk:代码块,一个chunk由多个模块组合而成,用于代码合并与分割
  • Loader: 模块转换器,用于将模块的原内容按照需求转换成新内容
  • Plugin:拓展插件,在webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情

流程概述

webpack从启动到结束依次执行以下操作:

1
2
3
4
5
6
7
graph TD
初始化参数 --> 开始编译
开始编译 -->确定入口
确定入口 --> 编译模块
编译模块 --> 完成编译模块
完成编译模块 --> 输出资源
输出资源 --> 输出完成

各个阶段执行的操作如下:

  1. 初始化参数:从配置文件(默认webpack.config.js)和shell语句中读取与合并参数,得出最终的参数
  2. 开始编译(compile):用上一步得到的参数初始化Comiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译
  3. 确定入口:根据配置中的entry找出所有的入口文件
  4. 编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过处理
  5. 完成编译模块:经过第四步之后,得到了每个模块被翻译之后的最终内容以及他们之间的依赖关系
  6. 输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会
  7. 输出完成:在确定好输出内容后,根据配置(webpack.config.js && shell)确定输出的路径和文件名,将文件的内容写入文件系统中(fs)

简单看一下webpack配置文件(webpack.config.js):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');

module.exports = {
// 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。
entry: {
bundle: [
'webpack/hot/dev-server',
'webpack-dev-server/client?http://localhost:8080',
path.resolve(__dirname, 'app/app.js')
]
},
// 文件路径指向(可加快打包过程)。
resolve: {
alias: {
'react': pathToReact
}
},
// 生成文件,是模块构建的终点,包括输出文件与输出路径。
output: {
path: path.resolve(__dirname, 'build'),
filename: '[name].js'
},
// 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。
module: {
loaders: [
{
test: /\.js$/,
loader: 'babel',
query: {
presets: ['es2015', 'react']
}
}
],
noParse: [pathToReact]
},
// webpack 各插件对象,在 webpack 的事件流中执行对应的方法。
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};

入口(entry)

**入口起点(entry point)*指示 webpack 应该使用哪个模块,来作为构建其内部依赖图*的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。

每个依赖项随即被处理,最后输出到称之为 bundles 的文件中,可以通过在 webpack 配置中配置 entry 属性,来指定一个入口起点(或多个入口起点)。默认值为 ./src

接下来我们看一个 entry 配置的最简单例子:

webpack.config.js

1
2
3
module.exports = {
entry: './path/to/my/entry/file.js'
};

出口(output)

output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。基本上,整个应用程序结构,都会被编译到你指定的输出路径的文件夹中。你可以通过在配置中指定一个 output 字段,来配置这些处理过程:

webpack.config.js

1
2
3
4
5
6
7
8
9
const path = require('path');

module.exports = {
entry: './path/to/my/entry/file.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'my-first-webpack.bundle.js'
}
};

在上面的示例中,我们通过 output.filenameoutput.path 属性,来告诉 webpack bundle 的名称,以及我们想要 bundle 生成(emit)到哪里。可能你想要了解在代码最上面导入的 path 模块是什么,它是一个 Node.js 核心模块,用于操作文件路径

loader

loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。

本质上,webpack loader 将所有类型的文件,转换为应用程序的依赖图(和最终的 bundle)可以直接引用的模块。

注意,loader 能够 import 导入任何类型的模块(例如 .css 文件),这是 webpack 特有的功能,其他打包程序或任务执行器的可能并不支持。我们认为这种语言扩展是有很必要的,因为这可以使开发人员创建出更准确的依赖关系图。

在更高层面,在 webpack 的配置中 loader 有两个目标:

  1. test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。
  2. use 属性,表示进行转换时,应该使用哪个 loader。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const path = require('path');

const config = {
output: {
filename: 'my-first-webpack.bundle.js'
},
module: {
rules: [
{ test: /\.txt$/, use: 'raw-loader' }
]
}
};

module.exports = config;

以上配置中,对一个单独的 module 对象定义了 rules 属性,里面包含两个必须属性:testuse。这告诉 webpack 编译器(compiler) 如下信息:

“嘿,webpack 编译器,当你碰到「在 require()/import 语句中被解析为 ‘.txt’ 的路径」时,在你对它打包之前,先使用 raw-loader 转换一下。”

重要的是要记得,在 webpack 配置中定义 loader 时,要定义在 module.rules 中,而不是 rules。然而,在定义错误时 webpack 会给出严重的警告。为了使你受益于此,如果没有按照正确方式去做,webpack 会“给出严重的警告”

插件(plugins)

loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。

想要使用一个插件,你只需要 require() 它,然后把它添加到 plugins 数组中。多数插件可以通过选项(option)自定义。你也可以在一个配置文件中因为不同目的而多次使用同一个插件,这时需要通过使用 new 操作符来创建它的一个实例。

webpack.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const HtmlWebpackPlugin = require('html-webpack-plugin'); //installed via npm
const webpack = require('webpack'); //to access built-in plugins
const path = require('path');

module.exports = {
entry: './path/to/my/entry/file.js',
output: {
filename: 'my-first-webpack.bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.(js|jsx)$/,
use: 'babel-loader'
}
]
},
plugins: [
new webpack.ProgressPlugin(),
new HtmlWebpackPlugin({template: './src/index.html'})
]
};

对象介绍

js中包含两种不同数据类型的值: 基本类型值和引用类型值。 基本类型值指的是简单的数据段,而引用类型值指的是那些由可能由多个值构成的对象。

js对象都是引用类型,对象是某个特定引用类型的实例。对象是js的基本块。对象是属性的集合,属性是键值对。js中看到的大多数引用类型都是Object类型的实例,Object也是js中使用最多的一个类型。

按引用赋值

1
2
3
4
5
6
7
let obj1 = {
name : 'jack',
number: 10
}
let obj2 = obj1 // 将obj1的引用赋值给obj2 此时obj1、obj2指向同一块内存空间
obj2.name = 'tom'
console.log(obj1.name) // tom

但从一个变量像另一个变量复制引用类型的值时,同样也会将存储在变量对象中的值复制一份放到位新的变量分配的空间中。不同的是,这个值的副本实际是一个指针,而这个指针指向存储在堆中的一个对象。复制操作结束后,两个变量实际上将引用同一个对象。因此改变其中一个变量就会改变另一个变量。

对象的拷贝方式

1. 复制对象的原始方法是循环遍历原始对象,然后一个接一个地复制每个属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
let objCopy = {}; // objCopy 将存储 mainObj 的副本

function copy(mainObj) {
let key;

for (key in mainObj) {
objCopy[key] = mainObj[key]; // 将每个属性复制到objCopy对象
}
return objCopy;
}

const mainObj = {
a: 2,
b: 5,
c: {
x: 7,
y: 4,
},
}

console.log(copy(mainObj));
objCopy.c.x = 10086
console.log("mainObj.c.x", mainObj.c.x) // 10086

上面的代码只复制了 mainObj 的可枚举属性。

如果原始对象中的一个属性本身就是一个对象,那么副本和原始对象之间将共享这个对象,从而使其各自的属性指向同一个对象。

2.浅拷贝

浅拷贝是对象共用一个内存地址,对象的变化相互影响。比如常见的赋值引用就是浅拷贝:

1
2
3
4
5
6
7
let obj1 = {
name : 'jack',
number: 10
}
let obj2 = obj1 // 将obj1的引用赋值给obj2 此时obj1、obj2指向同一块内存空间
obj2.name = 'tom'
console.log(obj1.name) // tom
Object.assign()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
let obj = {
a: 1,
b: {
c: 2,
},
}
let newObj = Object.assign({}, obj);
console.log(newObj); // { a: 1, b: { c: 2} }

obj.a = 10;
console.log(obj); // { a: 10, b: { c: 2} }
console.log(newObj); // { a: 1, b: { c: 2} }

newObj.a = 20;
console.log(obj); // { a: 10, b: { c: 2} }
console.log(newObj); // { a: 20, b: { c: 2} }

newObj.b.c = 30;
console.log(obj); // { a: 10, b: { c: 30} }
console.log(newObj); // { a: 20, b: { c: 30} }

// 注意: newObj.b.c = 30; 为什么呢..

bject.assign 只是浅拷贝。 newObj.bobj.b 都引用同一个对象,没有单独拷贝,而是复制了对该对象的引用。任何对对象属性的更改都适用于使用该对象的所有引用。

注意:原型链上的属性和不可枚举的属性不能复制。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let someObj = {
a: 2,
}

let obj = Object.create(someObj, {
b: {
value: 2,
},
c: {
value: 3,
enumerable: true,
},
});

let objCopy = Object.assign({}, obj);
console.log(objCopy); // { c: 3 }

someObj 是在 obj 的原型链,所以它不会被复制。
属性 b 是不可枚举属性。
属性 c 具有 可枚举(enumerable) 属性描述符,所以它可以枚举。 这就是为什么它会被复制。

使用展开操作符(…)

ES6已经有了用于数组解构赋值的 rest 元素,和实现的数组字面展开的操作符。

1
2
3
4
5
6
7
const array = [
"a", "c", "d", { four: 4 },
];
const newArray = [...array];
console.log(newArray);
// 结果
// ["a", "c", "d", { four: 4 }]

只对浅拷贝有效

3.深拷贝

简单理解深拷贝是将对象放到一个新的内存中,两个对象的改变不会相互影响。

JSON.parse() 和 JSON.stringify()
1
2
3
4
5
6
srcObj = {'name': '明', grade: {'chi': '50', 'eng': '50'} };
// copyObj2 = Object.assign({}, srcObj);
copyObj2 = JSON.parse(JSON.stringify(srcObj));
copyObj2.name = '红';
copyObj2.grade.chi = '60';
console.log('JSON srcObj', srcObj); // { name: '明', grade: { chi: '50', eng: '50' } }

可以看到改变copyObj2并没有改变原始对象,实现了基本的深拷贝。
但是用JSON.parse()和JSON.stringify()会有一个问题。
JSON.parse()和JSON.stringify()能正确处理的对象只有Number、String、Array等能够被json表示的数据结构,因此函数这种不能被json表示的类型将不能被正确处理。比如

1
2
3
4
5
6
7
8
9
srcObj = {
'name': '明',
grade: {'chi': '50', 'eng': '50'},
'hello': function() {console.log('hello')}
};
copyObj2 = JSON.parse(JSON.stringify(srcObj));
copyObj2.name = '红';
copyObj2.grade.chi = '60';
console.log('JSON srcObj', copyObj2); //{ name: '红', grade: { chi: '60', eng: '50' } }

可以看出,经过转换之后,function丢失了,因此JSON.parse()和JSON.stringify()还是需要谨慎使用。

所以我们需要自己写一个可以进行深拷贝的代码。

自定义的深拷贝方法
1
2
3
4
5
6
7
8
9
10
11
function clone(target) {
if (typeof target === 'object') {
let newObject = {}
for (key in target) {
newObject[key] = clone(target[key])
}
return newObject
} else {
return target
}
}

因为在深拷贝里,我们不知道要拷贝的对象是多少深度的,所以我们用递归来解决。

  • 如果是基本类型,就无需拷贝直接返回
  • 如果是引用类型就创建一个新的对象,遍历需要拷贝的对象,如果有更深层次的对象就继续递归到其为基本数据类型。这样我们就完成了一个很简单的深拷贝。
考虑数组

上面的方法,我们只考虑了object,如果要拷贝数组的话,只要在新建对象那边加一个判断就行了。

1
2
3
4
5
6
7
8
9
10
11
function clone(target) {
if (typeof target === 'object') {
let newTarget = Array.isArray(target) ? [] : {} // 判断这个引用类型是不是数组,如果是新建一个数组,再往数组内添加属性。如果不是新建一个对象,往对象内添加属性
for (key in target) {
newTarget[key] = clone(target[key])
}
return newTarget
} else {
return target
}
}
如果考虑函数。。。
  • 箭头函数: 将它转为字符串,再将字符串转换为方法。

    1
    2
    constconst arrowFunction = () => {}
    eval(arrowFunction.toString)
  • 普通函数:可以使用正则来处理普通函数

    分别使用正则取出函数体和函数参数,然后使用new Function ([arg1[, arg2[, ...argN]],] functionBody)构造函数重新构造一个新的函数。

数组的深拷贝和浅拷贝

最后在补充一点数组的深拷贝和浅拷贝,同对象一样数组的浅拷贝也是改变其中一个会相互影响,比如:

1
2
3
4
let srcArr = [1, 2, 3];
let copyArr = srcArr;
copyArr[0] = '0';
console.log('srcArr', srcArr); // ['0', 2, 3]

但是数组的深拷贝方法要相对简单一些可以理解为数组方法中那些会改变原数组的方法,比如

  • concat
  • slice
  • es6 的Array.from