深入理解JavaScript系列—什么是作用域

几乎所有编程语言最基本的功能之一,简单的说,作用域控制着变量与函数的可见性以及生命周期。那么问题来了:

  1. 变量存储在哪?
  2. 怎么找到变量?
    这些问题就需要一套良好的规则来存储和查找变量,而这种规则被称之为作用域。
    但是,究竟在哪里并且怎么设置这些作用域的规则呢?

理解作用域

我们学习作用域的方式就是将这个过程模拟成几个人物之间的对话。那么,由谁进行这场对话呢?

演员表

  • 引擎

从头到尾负责整个Javascript程序的编译及执行过程。

  • 编译器

负责语法分析及代码生成等脏活累活。

  • 作用域

负责收集并维护由所有声明的标识符(变量)组成的一系列查询,并实施一套非常严格的规则,确定当前执行的代码对这些标识符的访问权限。

为了完全理解javascript的工作原理,你需要从引擎开始一样思考,从它们的角度提出问题,并从它们的角度回答这些问题。

对话

当你看见 var a = 2;这段程序时,很可能认为这是一句声明,但javascript实际上会将其看成两个完全不同的声明:var a和a=2,第一个定义声明是在编译阶段进行,第二个是赋值声明,会被留在原地等待执行阶段。编译器处理过程如下
image
总结:变量的复制操作会执行两个动作,首先编译器会在当前作用域种声明一个变量(如果之前没有声明过),然后再运行时引擎会在作用域种查找该变量,如果能够找到就会对它赋值。

Ubuntu 16.04 LTS快速更新阿里源

新手在使用Ubuntu的时候可能在升级时感觉很慢,如果这样他就需要换一个适合自己的源了。
下面我就简单的说一下怎样换源。
在终端里输入 sudo cp /etc/apt/sources.list /etc/apt/sources.list_backup (表示备份列表)
再输入 sudo gedit /etc/apt/sources.list
你就能看到源列表了,把你看到的都删除然后粘贴上适合你的源
源,自己找一个适合自己的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
sudo vi /etc/apt/sources.list
deb http://mirrors.aliyun.com/ubuntu/ xenial main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse
##测试版源
deb http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse
# 源码
deb-src http://mirrors.aliyun.com/ubuntu/ xenial main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-backports main restricted universe multiverse
##测试版源
deb-src http://mirrors.aliyun.com/ubuntu/ xenial-proposed main restricted universe multiverse
# Canonical 合作伙伴和附加
deb http://archive.canonical.com/ubuntu/ xenial partner
deb http://extras.ubuntu.com/ubuntu/ xenial main

深入浅出基于依赖收集的响应式原理


每当问到VueJS响应式原理,大家可能都会脱口而出“Vue通过Object.defineProperty方法把data对象的全部属性转化成getter/setter,当属性被访问或修改时通知变化”。然而,其内部深层的响应式原理可能很多人都没有完全理解,网络上关于其响应式原理的文章质量也是参差不齐,大多是贴个代码加段注释了事。本文将会从一个非常简单的例子出发,一步一步分析响应式原理的具体实现思路。

使数据对象变得“可观测”

首先,我们定义一个数据对象,就以王者荣耀里面的其中一个英雄为例子:

1
2
3
4
const hero = {
health: 3000,
IQ: 150
}

我们定义了这个英雄的生命值为3000,IQ为150。但是现在还不知道他是谁,不过这不重要,只需要知道这个英雄将会贯穿我们整篇文章,而我们的目的就是通过这个英雄的属性,知道这个英雄是谁。

现在我们可以通过hero.health和hero.IQ直接读写这个英雄对应的属性值。但是,当这个英雄的属性被读取或修改时,我们并不知情。那么应该如何做才能够让英雄主动告诉我们,他的属性被修改了呢?这时候就需要借助Object.defineProperty的力量了。

关于Object.defineProperty的介绍,MDN上是这么说的:

1
codeObject.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

在本文中,我们只使用这个方法使对象变得“可观测”,更多关于这个方法的具体内容,请参考https://developer.mozilla.org...,就不再赘述了。

1
2
3
4
5
6
7
console.log(hero.health)

// -> 3000
// -> 我的health属性被读取了!

hero.health = 5000
// -> 我的health属性被修改了

可以看到,英雄已经可以主动告诉我们其属性的读写情况了,这也意味着,这个英雄的数据对象已经是“可观测”的了。为了把英雄的所有属性都变得可观测,我们可以想一个办法:

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
/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
get () {
// 触发getter
console.log(`我的${key}属性被读取了!`)
return val
},
set (newVal) {
// 触发setter
console.log(`我的${key}属性被修改了!`)
val = newVal
}
})
}

/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable (obj) {
const keys = Object.keys(obj)
keys.forEach((key) => {
defineReactive(obj, key, obj[key])
})
return obj
}

现在我们可以把英雄这么定义:

1
2
3
4
const hero = observable({
health: 3000,
IQ: 150
})

读者们可以在控制台自行尝试读写英雄的属性,看看它是不是已经变得可观测的。

计算属性

现在,英雄已经变得可观测,任何的读写操作他都会主动告诉我们,但也仅此而已,我们仍然不知道他是谁。如果我们希望在修改英雄的生命值和IQ之后,他能够主动告诉他的其他信息,这应该怎样才能办到呢?假设可以这样:

1
2
3
watcher(hero, 'type', () => {
return hero.health > 4000 ? '坦克' : '脆皮'
})

我们定义了一个watcher作为“监听器”,它监听了hero的type属性。这个type属性的值取决于hero.health,换句话来说,当hero.health发生变化时,hero.type也应该发生变化,前者是后者的依赖。我们可以把这个hero.type称为“计算属性”。

那么,我们应该怎样才能正确构造这个监听器呢?可以看到,在设想当中,监听器接收三个参数,分别是被监听的对象、被监听的属性以及回调函数,回调函数返回一个该被监听属性的值。顺着这个思路,我们尝试着编写一段代码:

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
/**
* 当计算属性的值被更新时调用
* @param { Any } val 计算属性的值
*/
function onComputedUpdate (val) {
console.log(`我的类型是:${val}`);
}

/**
* 观测者
* @param { Object } obj 被观测对象
* @param { String } key 被观测对象的key
* @param { Function } cb 回调函数,返回“计算属性”的值
*/
function watcher (obj, key, cb) {
Object.defineProperty(obj, key, {
get () {
const val = cb()
onComputedUpdate(val)
return val
},
set () {
console.error('计算属性无法被赋值!')
}
})
}

现在我们可以把英雄放在监听器里面,尝试跑一下上面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
watcher(hero, 'type', () => {
return hero.health > 4000 ? '坦克' : '脆皮'
})

hero.type

hero.health = 5000

hero.type

// -> 我的health属性被读取了!
// -> 我的类型是:脆皮
// -> 我的health属性被修改了!
// -> 我的health属性被读取了!
// -> 我的类型是:坦克

现在看起来没毛病,一切都运行良好,是不是就这样结束了呢?别忘了,我们现在是通过手动读取hero.type来获取这个英雄的类型,并不是他主动告诉我们的。如果我们希望让英雄能够在health属性被修改后,第一时间主动发起通知,又该怎么做呢?这就涉及到本文的核心知识点——依赖收集。

依赖收集

我们知道,当一个可观测对象的属性被读写时,会触发它的getter/setter方法。换个思路,如果我们可以在可观测对象的getter/setter里面,去执行监听器里面的onComputedUpdate()方法,是不是就能够实现让对象主动发出通知的功能呢?

由于监听器内的onComputedUpdate()方法需要接收回调函数的值作为参数,而可观测对象内并没有这个回调函数,所以我们需要借助一个第三方来帮助我们把监听器和可观测对象连接起来。

这个第三方就做一件事情——收集监听器内的回调函数的值以及onComputedUpdate()方法。

现在我们把这个第三方命名为“依赖收集器”,一起来看看应该怎么写:

1
2
3
const Dep = {
target: null
}

就是这么简单。依赖收集器的target就是用来存放监听器里面的onComputedUpdate()方法的。

定义完依赖收集器,我们回到监听器里,看看应该在什么地方把onComputedUpdate()方法赋值给Dep.target:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function watcher (obj, key, cb) {
// 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
const onDepUpdated = () => {
const val = cb()
onComputedUpdate(val)
}

Object.defineProperty(obj, key, {
get () {
Dep.target = onDepUpdated
// 执行cb()的过程中会用到Dep.target,
// 当cb()执行完了就重置Dep.target为null
const val = cb()
Dep.target = null
return val
},
set () {
console.error('计算属性无法被赋值!')
}
})
}

我们在监听器内部定义了一个新的onDepUpdated()方法,这个方法很简单,就是把监听器回调函数的值以及onComputedUpdate()给打包到一块,然后赋值给Dep.target。这一步非常关键,通过这样的操作,依赖收集器就获得了监听器的回调值以及onComputedUpdate()方法。作为全局变量,Dep.target理所当然的能够被可观测对象的getter/setter所使用。

重新看一下我们的watcher实例:

1
2
3
watcher(hero, 'type', () => {
return hero.health > 4000 ? '坦克' : '脆皮'
})

在它的回调函数中,调用了英雄的health属性,也就是触发了对应的getter函数。理清楚这一点很重要,因为接下来我们需要回到定义可观测对象的defineReactive()方法当中,对它进行改写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function defineReactive (obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get () {
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set (newVal) {
val = newVal
deps.forEach((dep) => {
dep()
})
}
})
}

可以看到,在这个方法里面我们定义了一个空数组deps,当getter被触发的时候,就会往里面添加一个Dep.target。回到关键知识点Dep.target等于监听器的onComputedUpdate()方法,这个时候可观测对象已经和监听器捆绑到一块。任何时候当可观测对象的setter被触发时,就会调用数组中所保存的Dep.target方法,也就是自动触发监听器内部的onComputedUpdate()方法。

至于为什么这里的deps是一个数组而不是一个变量,是因为可能同一个属性会被多个计算属性所依赖,也就是存在多个Dep.target。定义deps为数组,若当前属性的setter被触发,就可以批量调用多个计算属性的onComputedUpdate()方法了。
完成了这些步骤,基本上我们整个响应式系统就已经搭建完成,下面贴上完整的代码:

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
/**
* 定义一个“依赖收集器”
*/
const Dep = {
target: null
}

/**
* 使一个对象转化成可观测对象
* @param { Object } obj 对象
* @param { String } key 对象的key
* @param { Any } val 对象的某个key的值
*/
function defineReactive (obj, key, val) {
const deps = []
Object.defineProperty(obj, key, {
get () {
console.log(`我的${key}属性被读取了!`)
if (Dep.target && deps.indexOf(Dep.target) === -1) {
deps.push(Dep.target)
}
return val
},
set (newVal) {
console.log(`我的${key}属性被修改了!`)
val = newVal
deps.forEach((dep) => {
dep()
})
}
})
}

/**
* 把一个对象的每一项都转化成可观测对象
* @param { Object } obj 对象
*/
function observable (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
return obj
}

/**
* 当计算属性的值被更新时调用
* @param { Any } val 计算属性的值
*/
function onComputedUpdate (val) {
console.log(`我的类型是:${val}`)
}

/**
* 观测者
* @param { Object } obj 被观测对象
* @param { String } key 被观测对象的key
* @param { Function } cb 回调函数,返回“计算属性”的值
*/
function watcher (obj, key, cb) {
// 定义一个被动触发函数,当这个“被观测对象”的依赖更新时调用
const onDepUpdated = () => {
const val = cb()
onComputedUpdate(val)
}

Object.defineProperty(obj, key, {
get () {
Dep.target = onDepUpdated
// 执行cb()的过程中会用到Dep.target,
// 当cb()执行完了就重置Dep.target为null
const val = cb()
Dep.target = null
return val
},
set () {
console.error('计算属性无法被赋值!')
}
})
}

const hero = observable({
health: 3000,
IQ: 150
})

watcher(hero, 'type', () => {
return hero.health > 4000 ? '坦克' : '脆皮'
})

console.log(`英雄初始类型:${hero.type}`)

hero.health = 5000

// -> 我的health属性被读取了!
// -> 英雄初始类型:脆皮
// -> 我的health属性被修改了!
// -> 我的health属性被读取了!
// -> 我的类型是:坦克

上述代码可以直接在code pen点击预览或者浏览器控制台上执行。

代码优化

在上面的例子中,依赖收集器只是一个简单的对象,其实在defineReactive()内部的deps数组等和依赖收集有关的功能,都应该集成在Dep实例当中,所以我们可以把依赖收集器改写一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Dep {
constructor () {
this.deps = []
}

depend () {
if (Dep.target && this.deps.indexOf(Dep.target) === -1) {
this.deps.push(Dep.target)
}
}

notify () {
this.deps.forEach((dep) => {
dep()
})
}
}

Dep.target = null

同样的道理,我们对observable和watcher都进行一定的封装与优化,使这个响应式系统变得模块化:

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
class Observable {
constructor (obj) {
return this.walk(obj)
}

walk (obj) {
const keys = Object.keys(obj)
keys.forEach((key) => {
this.defineReactive(obj, key, obj[key])
})
return obj
}

defineReactive (obj, key, val) {
const dep = new Dep()
Object.defineProperty(obj, key, {
get () {
dep.depend()
return val
},
set (newVal) {
val = newVal
dep.notify()
}
})
}
}

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
class Watcher {
constructor (obj, key, cb, onComputedUpdate) {
this.obj = obj
this.key = key
this.cb = cb
this.onComputedUpdate = onComputedUpdate
return this.defineComputed()
}

defineComputed () {
const self = this
const onDepUpdated = () => {
const val = self.cb()
this.onComputedUpdate(val)
}

Object.defineProperty(self.obj, self.key, {
get () {
Dep.target = onDepUpdated
const val = self.cb()
Dep.target = null
return val
},
set () {
console.error('计算属性无法被赋值!')
}
})
}
}

然后我们来跑一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const hero = new Observable({
health: 3000,
IQ: 150
})

new Watcher(hero, 'type', () => {
return hero.health > 4000 ? '坦克' : '脆皮'
}, (val) => {
console.log(`我的类型是:${val}`)
})

console.log(`英雄初始类型:${hero.type}`)

hero.health = 5000

// -> 英雄初始类型:脆皮
// -> 我的类型是:坦克

尾声

看到上述的代码,是不是发现和VueJS源码里面的很像?其实VueJS的思路和原理也是类似的,只不过它做了更多的事情,但核心还是在这里边。

在学习VueJS源码的时候,曾经被响应式原理弄得头昏脑涨,并非一下子就看懂了。后在不断的思考与尝试下,同时参考了许多其他人的思路,才总算把这一块的知识点完全掌握。希望这篇文章对大家有帮助,如果发现有任何错漏的地方,也欢迎向我指出,谢谢大家~

javascript实用技巧和写法建议

前言

从大学到现在,接触前端已经有几年了,感想方面,就是对于程序员而言,想要提高自己的技术水平和编写易于阅读和维护的代码,我觉得不能每天都是平庸的写代码,更要去推敲,去摸索和优化代码,总结当中的技巧,积极听取别人的建议,这样自己的技术水平会提高的更快。那么今天,我在这里就分享一下关于javascript方面的写作的实用技巧和建议,这些技巧和建议是我平常在开发项目上会用到的,希望能让大家学到知识,更希望能起到一个交流意见的作用,也就是说大家有什么好的技巧或者建议,欢迎分享,或者觉得我的想法存在什么问题,欢迎指出!

更短的数组去重写法

1
2
3
[...new Set([2,"12",2,12,1,2,1,6,12,13,6])]
//[2, "12", 12, 1, 6, 13]
//es6的新特性

对象深浅拷贝

关于对象的深浅拷贝,我个人见解就是有一下几点:

  1. 深拷贝和浅拷贝只针对像Object, Array这样的引用类型数据。

  2. 浅拷贝是对对象引用地址进行拷贝,并没有开辟新的栈,也就是拷贝后的结果是两个对象指向同一个引用地址,修改其中一个对象的属性,则另一个对象的属性也会改变。

  3. 深拷贝则是开启一个新的栈,两个对象对应两个不同的引用地址,修改一个对象的属性,不会改变另一个对象的属性。

浅拷贝

1
var myInfo={name:'守候',sex:'男'};

1
var newInfo=myInfo;


1
newInfo.sex='女';


1
console.log(myInfo)   //{name: "守候", sex: "女"}

假-深拷贝

假-深拷贝这个是自己随性命名的,大家看看就好,别当真!

1
var myInfo={name:'守候',sex:'男'};


1
var newInfo=Object.assign({},myInfo)


1
newInfo.sex='女';


1
2
console.log(myInfo)   //{name: "守候", sex: "男"}
console.log(myInfo) //{name: "守候", sex: "女"}

真-深拷贝

真-深拷贝这个是自己随性命名的,大家看看就好,别当真!

看着深浅拷贝,区别写法很简单,但是那个上面的深拷贝写法是有问题的。看下面案例

1
2
3
4
5
6
7
8
9
10
var arr=[{a:1,b:2},{a:3,b:4}]
var newArr=Object.assign([],arr)
//截断数组
newArr.length=1
console.log(newArr)//[{a:1,b:2}]
console.log(arr)//[{a:1,b:2},{a:3,b:4}]
//操作newArr,这里看着对arr没影响,实际上已经挖了一个坑,下面就跳进去
newArr[0].a=123
//修改newArr[0]这个对象,也是影响了arr[0]这个对象
console.log(arr[0])//{a: 123, b: 2}

为什么会这样呢,因为Object.assign并不是深拷贝,是披着深拷贝外衣的浅拷贝。最多也是Object.assign会课拷贝第一层的值,对于第一层的值都是深拷贝,而到第二层的时候就是 复制引用。类似的情况还有,slice方法和contact方法等。
要解决这个问题,就得自己封装方法!如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//利用递归来实现深拷贝,如果对象属性的值是引用类型(Array,Object),那么对该属性进行深拷贝,直到遍历到属性的值是基本类型为止。
function deepClone(obj){
if(!obj&& typeof obj!== 'object'){
return;
}
var newObj= obj.constructor === Array ? [] : {};
for(var key in obj){
if(obj[key]){
if(obj[key] && typeof obj[key] === 'object'){
newObj[key] = obj[key].constructor === Array ? [] : {};
//递归
newObj[key] = deepClone(obj[key]);
}else{
newObj[key] = obj[key];
}
}
}
return newObj;
}
var arr=[{a:1,b:2},{a:3,b:4}]
var newArr=deepClone(arr)
console.log(arr[0])//{a:1,b:2}
newArr[0].a=123
console.log(arr[0])//{a:1,b:2}

还有一个方法就是简单粗暴法,我现在在用的一个方法!原理很简单,就是先把对象转成字符串,再把字符串转成对象!也能实现同样效果

1
2
3
4
var newArr2=JSON.parse(JSON.stringify(arr));
console.log(arr[0])//{a:1,b:2}
newArr2[0].a=123
console.log(arr[0])//{a:1,b:2}

上面所说的浅拷贝,真假深拷贝(自己随性命名的),这几种情况,在开发上都有可能要用到,至于要使用哪一种方式,视情况而定!

使用事件委托

一个简单的需求,比如想给ul下面的li加上点击事件,点击哪个li,就显示那个li的innerHTML。这个貌似很简单!代码如下!

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<ul id="ul-test">
<li>0</li>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
<li>6</li>
<li>7</li>
<li>8</li>
<li>9</li>
</ul>
</body>
<script type="text/javascript">
var oUl=document.getElementById("ul-test");
var oLi=oUl.getElementsByTagName("li");
for(var i=0,len=oLi.length;i<len;i++){
oLi[i].addEventListener("click",function(){
alert(this.innerHTML)
})
}
</script>
</html>

很简单,这样就实现了,实际上这里有坑,也待优化!

  1. for循环,循环的是li,10个li就循环10次,绑定10次事件,100个就循环了100次,绑定100次事件!
  2. 如果li不是本来就在页面上的,是未来元素,是页面加载了,再通过js动态加载进来了,上面的写法是无效的,点击li是没有反应的!
    所以就者需要用事件委托(即使不考虑上面的第二种情况,也是建议使用事件委托)!代码如下
    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
    <!DOCTYPE html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title></title>
    </head>
    <body>
    <ul id="ul-test">
    <li>0</li>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
    <li>6</li>
    <li>7</li>
    <li>8</li>
    <li>9</li>
    </ul>
    </body>
    <script type="text/javascript">
    var oUl=document.getElementById("ul-test");
    oUl.addEventListener("click",function(ev){
    var ev=ev||window.event;
    var target=ev.target||ev.srcElement;
    //如果点击的最底层是li元素
    if(target.tagName.toLowerCase()==='li'){
    alert(target.innerHTML)
    }
    })
    </script>
    </html>

这样写,即使是动态添加进来的li点击也有反应,还有一个就是ul只有一个,事件绑定在ul上,无论li多少个,都是添加一次事件!但是也是可能会有问题,如果li下面还有子元素,那么点击的时候,target可能不是li,而是鼠标点击那个位置的最底层元素!如下图,如果鼠标点击白色区域,那个target就是body元素,鼠标点击绿色区域target就是div元素,鼠标点击蓝色区域target就是ul,点击橙色就是li。

使用对象作为函数参数

大家试想下这样一个函数–函数接受几个参数,但是这几个参数都不是必填的,函数该怎么处理?是不是下面这样

1
2
3
4
5
function personInfo(name,phone,card){
...
}
//以上函数,可以任意传参数。比如我想传card等于1472586326。这下是不是这样写
personInfo('','','1472586326')

有没有觉得上面写法奇怪,不太优雅?下面这里看着舒服一点!

1
2
3
4
function personInfo(opt){
...
}
personInfo({card:'1472586326'})

再想一下,如果一个函数,参数很多,怎么处理?

1
2
3
function test(arg1,arg2,arg3,arg4,arg5,arg6,arg7){
...
}

密集恐惧症复发没有复发?下面这样看着会舒服一点!

1
2
3
function personInfo(opt){
...
}

最后再想一下,如果需求改了,操作函数也要改!函数也要增加一个参数。

1
2
3
4
5
6
7
8
//原来函数
function personInfo(name,phone,card){
...
}
//修改后
function personInfo(name,age,phone,card){
...
}

这样就是参数修改一次,函数的参数就要修改一次!如果是用对象,就不会出现这样问题!

1
2
3
4
//修改前后都是这样,变得是函数的操作内容和调用时候的传参!
function personInfo(opt){
...
}

看了上面的几个栗子,总结来说,就是当函数的参数不固定的时候,参数多(三个或者三个以上)的时候,建议用一个对象记录参数,这样会比较方便,也为以后如果参数要改留了条后路!

使用push和apply合并数组

合并数组这个已经是老生常谈的话题了,方法也是多种多样!

contact

1
2
3
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1=arr1.concat(arr2)
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

concat会一个全新的数组,表示arr1和arr2两个数组的组合,并让arr1和arr2不变。简单吧?
但如果arr1和arr2的长度都很长,那就产生了一个很长很长的数组,内存又被占用了那么多。

for

1
2
3
4
5
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
for(var i=0,len=arr2.length;i<len;i++){
arr1.push(arr2[i])
}
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

这里是往arr1循环添加arr2的元素,但是有一个情况,arr1的长度远小于arr2的长度,是不是循环arr1性能更好,循环次数更少。处理这个很简单,但是万一不知道arr1和arr2到底哪个长度更少呢?而且,for循环不够优雅!

reduce

1
2
3
4
5
6
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1 = arr2.reduce( function(coll,item){
coll.push( item );
return coll;
}, arr1 );
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

逼格高了一点,而且用ES6的箭头函数还可以减少一些代码量,但它仍然需要一个函数,每个元素都需要调用一次。

push.apply

1
2
3
var arr1=[1,2,3,4,5],arr2=[6,7,8,9,10];
arr1.push.apply(arr1,arr2);
console.log(arr1)//[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

这个是建议写法,逼格看着高,代码少,也不会产生新的数组,也不难理解,就是调用arr1.push这个函数实例的apply方法,同时把arr2当作参数传入,这样arr1.push这个方法就会遍历arr2数组的所有元素,达到合并的效果。相当于arr1.push.apply(arr1,[6,7,8,9,10]);,最后相当于arr1.push(6,7,8,9,10)

toFixed保留整数

在开发上,经常会遇到最多保留多少位小数或者类似的问题,针对这个,使用toFixed可以很简单的解决问题,但是如果数据是要和后台交互的,而且后台存储的数据一般是保存数字类型,而使用toFixed后生成的是一个字符串,这下,就需要把toFixed生成的是一个字符串转成数字类型,转发很多。今天我说一个最简单–+。代码如下

1
2
3
4
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=+a
console.log(a)//123.37

PS:a=a|0和~~a也可以实现,但是生成的是一个整数,如下

1
2
3
4
5
6
7
8
9
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=a|0
console.log(a)//123
//---------------------------------分割线
var a=123.36896335.toFixed(2)
console.log(a)//'123.37'
a=~~a
console.log(a)//123

其它类型数据转布尔数据

其它类型数据转布尔数据

1
2
3
4
5
6
7
8
9
10
11
12
console.log(!!'123')
//true
!!12
//true
!!-1
//true
!![]
//true
!!''
//false
!!null
//false

缓存变量

for循环缓存length

1
2
3
4
5
6
7
8
9
var arr=[1,2,3,4,5,6]
for(var i=0,i<arr.length;i++){
...
}
//------------------------分割线
var arr=[1,2,3,4,5,6]
for(var i=0,len=arr.length;i<len;i++){
...
}

第一段就是每一次循环的时候,都要查询一次arr.length。第二段代码就是缓存了arr.length,每次对比len就好,理论上是第二段代码的写法比较好,性能比较高!但是随着浏览器的发展,这个细节的性能上的影响貌似远远小于预期,现在还是建议缓存!我写了下面的测试用例(谷歌浏览器测试)!

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
var arr100=[], arr10000=[];
for(var i=0;i<100;i++){
arr100.push(i)
}
for(var i=0;i<10000;i++){
arr10000.push(i)
}
//缓存情况
function testCache(arr){
console.time();
for(var i=0;len=arr.length;i<len;i++){

}
console.timeEnd()
}
//不缓存情况
function testNoCache(arr){
console.time();
for(var i=0;len=arr.length;i<len;i++){

}
console.timeEnd()
}
testCache(arr100)//default: 0.007ms
testCache(arr10000)//default: 0.035ms
testNoCache(arr100)//default: 0.012ms
testNoCache(arr10000)//default: 0.109ms
//这只是一个最简单的数组,如果遍历的是一个nodeList(元素列表),效果可能会更明显。

元素事件

这里我用jquery来讲解,比较容易理解,原生js也是这个道理!如下代码

1
2
3
4
5
6
7
8
$('.div1').click(function(){
...
})
//--------------------------分割线
var $div1=$('.div1');
$div1.click(function(){
...
})

上面的代码,改变的也是缓存了$(‘.div1’),但是这里就建议是第二种写法了,因为第一种点击一次就要查询一次.div1,Dom的操作还是能减少就减少!

使用innerHTML添加元素

比如有一个需求,往ul里面添加10个li,两种方法,如下代码

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
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title></title>
</head>
<body>
<ul id="ul-test">

</ul>
</body>
<script type="text/javascript">
var oUl=document.getElementById("ul-test");
//createElement方式
console.time();
for(var i=0;i<10;i++){
var oLi=document.createElement('li');
oLi.innerHTML=i;
oUl.appendChild(oLi);
}
console.timeEnd();
//innerHTML方式
console.time();
var _html='';
for(var i=0;i<10;i++){
_html+='<li>'+i+'</li>'
}
oUl.innerHTML=_html;
console.timeEnd();
</script>
</html>

大家把代码用浏览器打开,发现基本是第二种方式更快,第8点也说了,DOM操作能少就少!第一种要操作10次DOM,第二种只需要操作1次DOM。还有一个就是,这个只是很简单的li,如果是下面的列表呢?用第一种方式,得createElement多少次,innerHTML多少次,appendChild多少次?代码多,各个节点的逻辑和嵌套关系也乱!用第二种方式就是一个拼接字符串的操作,比第一种方式好多了,如果用es6的模板字符串,就更简单了!

将参数转成数组

函数里的arguments,虽然拥有length属性,但是arguments不是一个数组,是一个类数组,没有push,slice等方法。有些时候,需要把arguments转成数组,转的方法也不止一个,推荐的是是下面的写法!

1
var _arguments=Array.prototype.slice.apply(arguments)

函数节流

这里拿一个栗子说,比如mousemove,onscroll,onresize这些事件触发的时候,可能已经触发了60次事件,这样很消耗性能,而且实际上,我们并不需要这么频繁的触发,只要大约100毫秒触发一次就好!那么这样就需要函数节流了!

普通写法

1
2
3
4
5
6
7
8
var count = 0;
function beginCount() {
count++;
console.log(count);
}
document.onmousemove = function () {
beginCount();
};

效果

节流写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var count = 0;
function beginCount() {
count++;
console.log(count);
}
function delayFn(method, thisArg) {
clearTimeout(method.props);
method.props = setTimeout(function () {
method.call(thisArg)
},100)
}
document.onmousemove = function () {
delayFn(beginCount)
};

效果


这种方式,其实是有问题的,在不断触发停下来等待100ms才开始执行,中间操作得太快直接无视。于是在网上找到下面这种方案!

第二种节流写法

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
function delayFn2 (fn, delay, mustDelay){
var timer = null;
var t_start;
return function(){
var context = this, args = arguments, t_cur = +new Date();
//先清理上一次的调用触发(上一次调用触发事件不执行)
clearTimeout(timer);
//如果不存触发时间,那么当前的时间就是触发时间
if(!t_start){
t_start = t_cur;
}
//如果当前时间-触发时间大于最大的间隔时间(mustDelay),触发一次函数运行函数
if(t_cur - t_start >= mustDelay){
fn.apply(context, args);
t_start = t_cur;
}
//否则延迟执行
else {
timer = setTimeout(function(){
fn.apply(context, args);
}, delay);
}
};
}
var count=0;
function fn1(){
count++;
console.log(count)
}
//100ms内连续触发的调用,后一个调用会把前一个调用的等待处理掉,但每隔200ms至少执行一次
document.onmousemove=delayFn2(fn1,100,200)

小结

好了,关于我自己总结的一些实用技巧和建议,就到这里了!关于javascript的技巧和建议,这点大家还是要多看网上的资源,也要自己多总结,毕竟我自己总结的只是我自己发现的,只是冰山一角。但还是希望这篇文章能帮到大家,让大家学习到知识。当然,更希望的是能起到一个交流意见的作用。如果大家有什么建议,技巧。也欢迎分享。觉得我哪里写错了,写得不够好,也欢迎指出!让大家一起互相帮助,互相学习!

新起点

做开发也有三年了,从代码小白到今天一直是顺风顺水,今年6月份转战来到西安,自己天真的以为在这里可以大刀阔斧的大干一番,没想到坎坷才刚刚开始。
面试了好多家都被没能通过,理论基础差导致要么公司开的薪水达不到自己的要求,要么直接没音讯,这让我郁闷了好几天,自己在那琢磨在上家公司的时候,既能开发web,又能开发app,对于新技术也能迅速上手,怎么就差的连工作都找不上,真的是说不出的失落……
一个成功的男人背后都有一个伟大的女人这句话说的一点都没错,反正我是信了。我们家灰灰在我郁闷的那几天就一直开导我。确实是有舍必有得,有高峰就有低谷,现在我需要的是补充我的基础知识,在基础知识中挖掘编程的核心思想,是时候该重新学习!真正的学习!领悟技术!接纳技术!
在这里感谢,那些拒绝我的面试官,给了我重新认识自己的机会。感谢我们家灰灰,在我最无助的时候支持我、鼓励我。