开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Python 代码规范

发表于 2019-01-18

觉得有用的 Github 给颗星补贴家用呀 .⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄ !

翻译自: gist.github.com/sloria/7001… (有些翻译的不太好就只能对照一下英文原文来理解了 ʅ(‾◡◝)ʃ )

通用的标准

价值观

  • “给别人写的工具要达到自己也愿意用的标准。” - Kenneth Reitz
  • “简单比功能更重要。” - Pieter Hintjens
  • “适合90%的用例,忽略那些说话的人。” - Kenneth Reitz
  • “优美的总比丑的好。” - PEP 20
  • 为开源贡献 (为不开源的项目也贡献) 。

通用的开发指导

  • “明确好于不明确。” - PEP 20
  • “可读性很重要。” - PEP 20
  • “代码写完以后任何人都能来解决任何问题。” - Khan Academy Development Docs
  • 发现 broken window(破窗理论那个破窗) (坏的设计, 不对的决定, 或者不好的代码) 就马上解决。
  • “现在着手去做比不做要强。” - PEP 20
  • 用全力去测试,为新特性写文档。
  • 测试驱动的开发–人驱动的开发,是很重要的。
  • 这些指导方针会,并且很可能会,改变。

关于 Python 的标准

风格

聪明的孩子都遵循 PEP 8。

命名

(命名我觉得还是英文更清晰你说是吧)

  • Variables(变量), functions, methods, packages, modules
    • lower_case_with_underscores
  • Classes and Exceptions
    • CapWords
  • Protected methods and internal functions
    • _single_leading_underscore(self, ...)
  • Private methods
    • __double_leading_underscore(self, ...)
  • Constants(常量)
    • ALL_CAPS_WITH_UNDERSCORES
通用命名指南

避免单个字母的变量 (尤其是, l, O, I)。

除非: 在非常短的代码块中,变量意义上下文直接可见

下面的代码就是可以的

1
2
复制代码for e in elements:
e.mutate()

避免变量名冗余。

正确

1
2
3
4
复制代码import audio

core = audio.Core()
controller = audio.Controller()

错误

1
2
3
4
复制代码import audio

core = audio.AudioCore()
controller = audio.AudioController()

最好要 “反向标记”。

正确

1
2
3
复制代码elements = ...
elements_active = ...
elements_defunct = ...

错误

1
2
3
复制代码elements = ...
active_elements = ...
defunct_elements ...

避免用 getter, setter 方法

正确

1
复制代码person.age = 42

错误

1
复制代码person.set_age(42)

缩进排版

都说那么多次了,用 4 下空格键 – 别用 tab。

Imports 方法

导入整个 module,别导入其中一个或几个 symbol。例如, 对于 module canteen 来说有文件 canteen/sessions.py,

正确

1
2
3
复制代码import canteen
import canteen.sessions
from canteen import sessions

错误

1
2
复制代码from canteen import get_user  # Symbol from canteen/__init__.py
from canteen.sessions import get_session # Symbol from canteen/sessions.py

例外: 有的第三方代码文档中说明要直接导入单个 symbol的。

原因: 避免循环导入。 看 这里。

把所有的 imports 放在代码最上面,并且分成三类用空格隔开,顺序如下

  1. System imports(系统自带的)
  2. Third-party imports (第三方的)
  3. Local source tree imports (本地的)

原因:开发者能更清晰地读出 models 的来源。

注释

遵循 PEP 257 的注释准则。 reStructured Text 和 Sphinx 能帮助你更好地执行这些标准。

用一行来注释显而易见的方法

1
复制代码"""Return the pathname of ``foo``."""

多行注释应该包括

  • 概要
  • 用例(如果可以的话)
  • Args (参数)
  • 返回的类型
1
2
3
4
5
6
7
8
9
10
11
复制代码"""Train a model to classify Foos and Bars.

Usage::

>>> import klassify
>>> data = [("green", "foo"), ("orange", "bar")]
>>> classifier = klassify.train(data)

:param train_data: A list of tuples of the form ``(color, label)``.
:rtype: A :class:`Classifier <Classifier>`
"""

Notes

  • 用动词 (“Return”) 而不是用名词 (“Returns”).
  • 注释 class 中的 __init__ 方法。
1
2
3
4
5
6
7
8
9
复制代码class Person(object):
"""A simple representation of a human being.

:param name: A string, the person's name.
:param age: An int, the person's age.
"""
def __init__(self, name, age):
self.name = name
self.age = age
注解

减少使用注解,一般来说多用一些 small methods 比注解更有效。

错误

1
2
3
复制代码# If the sign is a stop sign
if sign.color == 'red' and sign.sides == 8:
stop()

正确

1
2
3
4
5
复制代码def is_stop_sign(sign):
return sign.color == 'red' and sign.sides == 8

if is_stop_sign(sign):
stop()

当你写注解时, 想想: “Strunk and White apply.”(我也不知道这是啥,摊手) - PEP 8

每一行的长度

莫慌。 80-100 个字符差不多。

可以把长的字串加上小括号。

1
2
3
4
5
6
复制代码wiki = (
"The Colt Python is a .357 Magnum caliber revolver formerly manufactured "
"by Colt's Manufacturing Company of Hartford, Connecticut. It is sometimes "
'referred to as a "Combat Magnum". It was first introduced in 1955, the '
"same year as Smith & Wesson's M29 .44 Magnum."
)

测试

争取 100% 的覆盖率。不过也别太钻牛角尖。

通用测试指南

  • 可以用长的,描述性的命名方式。这样可以不需要对 test methods 注释。
  • 测试是独立的。别用真实的网络或数据库,可以用分离的用模拟数据的测试数据库。
  • Prefer factories to fixtures. (感觉是一个用于测试的 model)
  • 别让没完成的测试通过,不然你很可能把它忘了。此外,你应该加上一个 placeholder: assert False, "TODO: finish me"。

单元测试

  • 专注于一小部分功能。
  • 最好要快,不过即使慢也不能不测试。
  • 对于单个 class 或 model 可以提供一个专门的测试用例。
1
2
3
4
5
6
7
8
9
复制代码import unittest
import factories

class PersonTest(unittest.TestCase):
def setUp(self):
self.person = factories.PersonFactory()

def test_has_age_in_dog_years(self):
self.assertEqual(self.person.dog_years, self.person.age / 7)

功能测试

功能测试是更高一个级别的测试,更贴近用户操作你的应用的场景。一般用于 web 和 GUI 的应用。

  • 把测试写的场景化。测试用例和测试方法名称应该对一个场景的描述。
  • 在代码前用注释写出这句代码的故事(功能)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码import unittest

class TestAUser(unittest.TestCase):

def test_can_write_a_blog_post(self):
# Goes to the her dashboard
...
# Clicks "New Post"
...
# Fills out the post form
...
# Clicks "Submit"
...
# Can see the new post
...

注意测试用例以及方法名连起来读就是: “Test A User can write a blog post”,是不是很好用。

Inspired by…

  • PEP 20 (The Zen of Python)
  • PEP 8 (Style Guide for Python)
  • The Hitchiker’s Guide to Python
  • Khan Academy Development Docs
  • Python Best Practice Patterns
  • Pythonic Sensibilities
  • The Pragmatic Programmer
  • and many other bits and bytes

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java基础之多线程编程

发表于 2019-01-14

回顾

在上一篇 Java基础之线程那些事 我们介绍了关于线程和进程的相关概念,还留下了个题目:即三个窗口同时卖100张票的问题,那么今天就来说说多线程编程的实现。

构建多线程主要有继承和实现两种方法

多线程创建

1.继承Thread类

继承的方法即构建一个类继承于Thread类,并实现run方法,具体如下

1
2
3
4
5
6
7
8
复制代码class SubThread extends  Thread{
@Override
public void run() {
//具体需要完成的业务代码
}
}
SubThread subThread=new SubThread();//创建实例
subThread.start();//线程启动
  • start方法启动线程并执行对应的run方法
  • 需要将需要实现的业务代码放在run方法内

2.实现Runnable接口

实现java.lang.Runnable接口

1
2
3
4
5
6
7
8
9
复制代码class SubThread implements Runnable{//创建一个实现Runnable接口的类
@Override
public void run() {//实现run方法
//你的业务代码
}
}
SubThread subThread1=new SubThread();//创建一个实例
Thread t1=new Thread(subThread1);//把实例当参数传递给Thread构造方法 //得到一个线程实例
t1.start();//调用线程实例的start方法

几个重要的步骤

  • 1 构建一个类实现Runnable接口
  • 2 重写run方法 run里写你的业务代码
  • 3 构建这个类实例并当参数传递到Thread类构造方法中得到线程实例
  • 4 线程实例调用start方法

3.Thred类的一些基础方法

1
2
3
4
5
6
7
8
9
10
复制代码Thread.currentThread() //获取当前线程
//以下都是线程实例上的方法
setName(); //设置线程名字
getName(); //获取线程名字
yield();//显示释放cpu的执行权
join();//在一个线程执行中调用另一个目标线程的join方法,意味着立马执行目标线程,且执行完毕才回到原线程
isAlive();//判断当前线程是否还存活
sleep();//显示的让线程睡眠
setPriority() //设置当前线程的优先级
getPriority()//获取当前线程的优先级

4.对比继承与实现

都与Runnable接口相关

业务目标代码放在run方法里

鉴于java单继的特性 实现接口都方式更具有通用性

如果多个线程操作同一份资源 更适合使用实现都方式

卖票程序

在上一篇中我们留个问题:就是三个窗口卖票,票一共有100张,我们来实现它

1.继承的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码class Window extends  Thread{//继承
static int ticket=100;
@Override
public void run() {
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"售票:票号为:"+ ticket--);
}else{
break;
}
}
}
}
//创建三个线程实例
Window w1=new Window();
Window w2=new Window();
Window w3=new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();

由于是一共卖100张票,故需要设ticket 为static ,三个线程共享这个数据

2.实现的方法

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
复制代码class Window implements Runnable{//实现接口
int ticket=100;
@Override
public void run() {
while (true){
if(ticket>0){
System.out.println(Thread.currentThread().getName()+"售票:票号为:"+ ticket--);
}else{
break;
}
}
}
}

//构建三个线程实例
Window w=new Window();
Thread t1=new Thread(w);
Thread t2=new Thread(w);
Thread t3=new Thread(w);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();

注:由于这里只有一个Window实例 所有只有一份ticket=100,不需要static修饰

多线程程序的优缺点

在单核的时代,多线程会带来线程切换的损耗,但是即使是这样多线程对图形化界面更有意义,它可增强用户体验。(例如你在单核机器上边写代码 边听歌) 实际上多个线程在cpu上来回切换,给你一种并行的假象。

在多核心时代,例如当前的8代i7处理器,已经是6核12线程了,可以保持多个线程并行运算,极大的提高性能,用多线程编程
反而能提高计算机cpu的利用率

另外多线程编程能改善程序结构,可将长又复杂的进程分割成多个线程,独立运行,利于理解和修改

总结

今天主要讲解如何实现多线程,以及线程实例上有哪些方法,需要重点掌握实现的方式,因为实现的方式避开了java单继承的缺点。

但是本文的样例代码并不是线程安全的,关于线程安全问题,我们下一篇讲解。

喜欢本文的朋友们,欢迎长按下图关注订阅号”我的编程笔记”,收看更多精彩内容~~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如何高效读写百万级的Excel?

发表于 2019-01-14

高效读取百万级数据

接上一篇介绍的高效写文件之后,最近抽时间研究了下Excel文件的读取。概括来讲,poi读取excel有两种方式:用户模式和事件模式。

然而很多业务场景中的读取Excel仍然采用用户模式,但是这种模式需要创建大量对象,对大文件的支持非常不友好,非常容易OOM。但是对于事件模式而言,往往需要自己实现listener,并且需要根据自己需要解析不同的event,所以用起来比较复杂。

基于此,EasyExcel封装了常用的Excel格式文档的事件解析,并且提供了接口供开发小哥扩展定制化,实现让你解析Excel不再费神的目的。

Talk is cheap, show me the code.

使用姿势

pom

1
2
3
复制代码    <groupId>com.github.Dorae132</groupId>
<artifactId>easyutil.easyexcel</artifactId>
<version>1.1.0</version>

普通姿势

看看下边的姿势,是不是觉得只需要关心业务逻辑了?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码ExcelUtils.excelRead(ExcelProperties.produceReadProperties("C:\\Users\\Dorae\\Desktop\\ttt\\",
"append_0745704108fa42ffb656aef983229955.xlsx"), new IRowConsumer<String>() {
@Override
public void consume(List<String> row) {
System.out.println(row);
count.incrementAndGet();
try {
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}, new IReadDoneCallBack<Void>() {
@Override
public Void call() {
System.out.println(
"end, count: " + count.get() + "\ntime: " + (System.currentTimeMillis() - start));
return null;
}
}, 3, true);

定制姿势

什么?你想定制context,添加handler?请看下边!你只需要实现一个Abstract03RecordHandler然后regist到context(关注ExcelVersionEnums中的factory)就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码public static void excelRead(IHandlerContext context, IRowConsumer rowConsumer, IReadDoneCallBack callBack,
int threadCount, boolean syncCurrentThread) throws Exception {
// synchronized main thread
CyclicBarrier cyclicBarrier = null;
threadCount = syncCurrentThread ? ++threadCount : threadCount;
if (callBack != null) {
cyclicBarrier = new CyclicBarrier(threadCount, () -> {
callBack.call();
});
} else {
cyclicBarrier = new CyclicBarrier(threadCount);
}
for (int i = 0; i < threadCount; i++) {
THREADPOOL.execute(new ConsumeRowThread(context, rowConsumer, cyclicBarrier));
}
context.process();
if (syncCurrentThread) {
cyclicBarrier.await();
}
}

框架结构

如图,为整个EasyExcel的结构,其中(如果了解过设计模式,或者读过相关源码,应该会很容易理解):

  1. 绿色为可扩展接口,
  2. 上半部分为写文件部分,下办部分为读文件。

图 1-1

总结

至此,EasyExcel的基本功能算是晚上了,欢迎各路大神提Issue过来。🍗

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

python性能优化之函数执行时间分析

发表于 2019-01-13

最近发现项目API请求比较慢,通过抓包发现主要是response时间太长,于是就开始进行优化工作。优化工作的关键一步是定位出问题的瓶颈,对于优化速度来说,从优化函数执行时间这个维度去切入是一个不错的选择。

本文侧重分析,不展开如何优化

利器

工欲善其事,必先利其器,我们需要一套方便高效的工具记录函数运行时间。说是一套工具,但对于一个简单项目或者日常开发来说,实现一个工具类足矣,由于实现比较简单,直接上代码:

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
复制代码from functools import wraps

import cProfile
from line_profiler import LineProfiler

import time


def func_time(f):
"""
简单记录执行时间
:param f:
:return:
"""

@wraps(f)
def wrapper(*args, **kwargs):
start = time.time()
result = f(*args, **kwargs)
end = time.time()
print f.__name__, 'took', end - start, 'seconds'
return result

return wrapper


def func_cprofile(f):
"""
内建分析器
"""

@wraps(f)
def wrapper(*args, **kwargs):
profile = cProfile.Profile()
try:
profile.enable()
result = f(*args, **kwargs)
profile.disable()
return result
finally:
profile.print_stats(sort='time')

return wrapper



try:
from line_profiler import LineProfiler


def func_line_time(follow=[]):
"""
每行代码执行时间详细报告
:param follow: 内部调用方法
:return:
"""
def decorate(func):
@wraps(func)
def profiled_func(*args, **kwargs):
try:
profiler = LineProfiler()
profiler.add_function(func)
for f in follow:
profiler.add_function(f)
profiler.enable_by_count()
return func(*args, **kwargs)
finally:
profiler.print_stats()

return profiled_func

return decorate

except ImportError:
def func_line_time(follow=[]):
"Helpful if you accidentally leave in production!"
def decorate(func):
@wraps(func)
def nothing(*args, **kwargs):
return func(*args, **kwargs)

return nothing

return decorate

原始代码可以参考gist

如下,实现了3个装饰器函数func_time,func_cprofile,func_line_time,分别对应

  1. 简单输出函数的执行时间
  2. 利用python自带的内置分析包cProfile 分析,它主要统计函数调用以及每个函数所占的cpu时间
  3. 利用line_profiler开源项目,它可以统计每行代码的执行次数和执行时间。

使用说明

我们以一个简单的循环例子来说明一下,

1
2
3
复制代码def test():
for x in range(10000000):
print x
  • func_time

关于func_time我觉得没什么好说的,就是简单输出下函数调用时间,这个在我们粗略统计函数执行时间的时候可以使用

如下:

1
2
3
4
5
6
复制代码@func_time
def test():
for x in range(10000000):
print x
# 输出
test took 6.10190296173 seconds
  • func_cprofile

cProfile是python内置包,基于lsprof的用C语言实现的扩展应用,运行开销比较合理,没有外部依赖,适合做快速的概要测试

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码@func_cprofile
def test():
for x in range(10000000):
print x
# 输出
3 function calls in 6.249 seconds

Ordered by: internal time

ncalls tottime percall cumtime percall filename:lineno(function)
1 6.022 6.022 6.249 6.249 test.py:41(test)
1 0.227 0.227 0.227 0.227 {range}
1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects}

输出说明:

单位为秒

  1. 第一行告诉我们一共有3个函数被调用。

正常开发过程,第一行更多是输出类似194 function calls (189 primiive calls) in 0.249 seconds,(189 primiive calls)表示189个是原生(primitive)调用,表明这些调用不涉及递归
2. ncalls表示函数的调用次数,如果这一列有两个数值,表示有递归调用,第一个是总调用次数,第二个是原生调用次数。
3. tottime是函数内部消耗的总时间(不包括调用其他函数的时间)。
4. percall是tottime除以ncalls,表示每次调用平均消耗时间。
5. cumtime是之前所有子函数消耗时间的累积和。
6. percall是cumtime除以原生调用的数量,表示该函数调用时,每个原生调用的平均消耗时间。
7. filename:lineno(function)为被分析函数所在文件名、行号、函数名。

  • func_line_time

line_profiler可以生成非常直接和详细的报告,但它系统开销很大,会比实际执行时间慢一个数量级

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码@func_line_time()
def test():
for x in range(10000000):
print x
# 输出
Timer unit: 1e-06 s

Total time: 14.4183 s
File: /xx/test.py
Function: test at line 41

Line # Hits Time Per Hit % Time Line Contents
==============================================================
41 @func_line_time()
42 def test():
43 10000001 4031936.0 0.4 28.0 for x in range(10000000):
44 10000000 10386347.0 1.0 72.0 print x

输出说明:

单位为微秒

  1. Total Time:测试代码的总运行时间
  2. Line:代码行号
  3. Hits:表示每行代码运行的次数
  4. Time:每行代码运行的总时间
  5. Per Hits:每行代码运行一次的时间
  6. % Time:每行代码运行时间的百分比

总结

日常开发中,可以使用func_time,func_cprofile做基本检查,定位大致问题,使用func_line_time进行更细致的深入检查。

注:func_line_time 还可以检查函数内部调用的函数执行时间,通过follow参数指定对应的内部调用的函数声明即可,该参数是个数组,也就是说可以检查多个内部调用的函数

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

PostgreSQL构建通用标签系统

发表于 2019-01-11

对资源打标签在建站过程中是很常见的需求,有些时候我们需要给文章打标签,有些时候我们需要给用户打标签。实现一个标签系统其实并不难,其本质就是一个多对多的关系-我可以对同一篇博客打多个标签,同时也可以把一个标签打到不同的博客身上。这篇文章主要通过分析标签系统的原理,并用PostgreSQL来实现一个能够为多种资源打标签的标签系统。

  1. 单一资源标签系统

先从单一资源开始,所谓单一资源便是,我们只给一种数据资源打标签。假设我们需要给博客文章打标签,那么我们需要构建以下几个表:

  1. 文章表posts,用于存储文章的基本信息。
  2. 标签表tags,用于存储标签的基本信息。
  3. 标签-文章表tags_posts,存储双方的id并形成多对多的关系。

表设计图大概是

Model Design for Simple Tag System

先进入数据库引擎并创建对应的数据库

1
2
3
4
5
复制代码postgres=# create database blog;
CREATE DATABASE

postgres=# \c blog;
blog=#

通过SQL语句创建上面所提到的数据表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码CREATE TABLE posts (
id SERIAL,
body text,
title varchar(80)
);

CREATE TABLE tags (
id SERIAL,
name varchar(80)
);

CREATE TABLE tags_posts (
id SERIAL,
tag_id integer,
post_id integer
);

每个表都只是包含了该资源最基础的字段, 到这一步为止其实已经构建好了一个最简单的标签系统了。接下来则是填充数据,我的策略是添加两篇文章,五个标签,给标题为Ruby的文章打上language标签,给标题为Docker的文章打上container的标签,两篇文章都要打上tech标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码-- 填充文章数据
INSERT INTO posts (body, title) VALUES ('Hello Ruby', 'Ruby');
INSERT INTO posts (body, title) VALUES ('Hello Docker', 'Docker');

-- 填充标签数据
INSERT INTO tags (name) VALUES ('language');
INSERT INTO tags (name) VALUES ('container');
INSERT INTO tags (name) VALUES ('tech');

-- 为相关资源打上标签
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'container'), (SELECT id FROM posts WHERE title = 'Docker'));
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'tech'), (SELECT id FROM posts WHERE title = 'Docker'));
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'tech'), (SELECT id FROM posts WHERE title = 'Ruby'));
INSERT INTO tags_posts (tag_id, post_id) VALUES ((SELECT id FROM tags WHERE name = 'language'), (SELECT id FROM posts WHERE title = 'Ruby'));

然后分别查询两篇文章都被打上了什么标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码blog=# SELECT tags.name FROM tags, posts, tags_posts WHERE tags.id = tags_posts.tag_id AND posts.id = tags_posts.post_id AND posts.title = 'Ruby';
name
----------
language
tech
(2 rows)

blog=# SELECT tags.name FROM tags, posts, tags_posts WHERE tags.id = tags_posts.tag_id AND posts.id = tags_posts.post_id AND posts.title = 'Docker';
name
-----------
container
tech
(2 rows)

两篇文章都被打上期望的标签了,相关的语句有点长,一般生产线上不会这样直接操作数据库。各种编程语言的社区一般都对这种数据库操作进行了封装,这为编写业务代码带来了不少的便利性。

  1. 为多种资源打标签

如果只需要对一个数据表打标签的话,依照上面的逻辑来设计表已经足够了。但是现实世界往往没那么简单,假设除了要给博客文章打标签之外,还需要给用户表打标签呢?我们需要把表设计得更灵活一些。如果继续用tags表来存标签数据,为了给用户打标签还得另外建一个名为tags_users的表来存储标签与用户数据之间的关系。

但更好的做法应该是采用名为多态的设计。创建关联表taggings,这个关联表除了会存储关联的两个id之外,还会存储被打上标签的资源类型,我们根据类型来区分被打标签的到底是哪种资源,这会在每条记录上多存了类型数据,不过好处就是可以少建表,所有的标签关系都通过一个表来存储。

Ruby比较流行的标签系统ActsAsTaggableOn 就沿用了这个设计,不过它的类型字段直接存的是对应资源的类名,或许是为了更方便编程吧,数据大概如下:

1
2
3
4
5
6
复制代码naive_development=# select id, tag_id, taggable_type, taggable_id from taggings;
id | tag_id | taggable_type | taggable_id
----+--------+----------------------+-------------
1 | 1 | Refinery::Blog::Post | 1
2 | 2 | Refinery::Blog::Post | 1
3 | 3 | Refinery::Blog::Post | 1

先通过taggable_type获取类名,然后再利用taggable_id的数据就能准确获取相关的资源了。

a. 修改原表

表设计图大概如下

Model Design for multi

这里我不重新建表了,而直接修改原有的表,并进行数据迁移

  1. 增加type字段用于存储资源类型。
  2. 把原来的数据表改名为更通用的名字taggings。
  3. 把原来的post_id字段改成更通用的名字taggable_id。
  4. 给原有的资源填充数据,type字段统一填数据post。
1
2
3
4
复制代码ALTER TABLE tags_posts ADD COLUMN type varchar(80);
ALTER TABLE tags_posts RENAME TO taggings;
ALTER TABLE taggings RENAME COLUMN post_id TO taggable_id;
UPDATE taggings SET type='post';

b. 添加用户

在给用户打标签之前先创建用户表,并填充数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码-- 创建简单的用户表
CREATE TABLE users (
id SERIAL,
username varchar(80),
age integer
);


-- 添加一个名为lan的用户,并添加两个相关的标签

INSERT INTO users (username, age) values ('lan', 26);

INSERT INTO tags (name) VALUES ('student');
INSERT INTO tags (name) VALUES ('programmer');

c. 给用户打标签

接下来需要给用户lan打上标签,对原有的SQL语句做一些调整,并在打标签的时候把type字段填充为user。

1
2
3
复制代码INSERT INTO taggings (tag_id, taggable_id, type) VALUES ((SELECT id FROM tags WHERE name = 'student'), (SELECT id FROM users WHERE username = 'lan'), 'user');

INSERT INTO taggings (tag_id, taggable_id, type) VALUES ((SELECT id FROM tags WHERE name = 'programmer'), (SELECT id FROM users WHERE username = 'lan'), 'user');

上述的SQL语句为用户打上了student以及programmer两个标签。

d. 查看标签情况

为了完成这个任务我们依然要联合三张表进行查询,同时还要约束type的类型

  • 用户名为lan的用户被打上的所有标签
1
2
3
4
5
6
7
复制代码blog=# SELECT tags.name FROM tags, users, taggings WHERE tags.id = taggings.tag_id AND users.id = taggings.taggable_id AND taggings.type = 'user' AND users.username = 'lan';

name
------------
student
programmer
(2 rows)
  • 标题为Ruby的文章被打上的所有标签
1
2
3
4
5
6
复制代码blog=# SELECT tags.name FROM tags, posts, taggings WHERE tags.id = taggings.tag_id AND posts.id = taggings.taggable_id AND taggings.type = 'post' AND posts.title = 'Ruby';

name
----------
language
tech

OK,都跟预期一样,现在的标签系统就比较通用了。

总结

本文通过PostgreSQL的基础语句来构建了一个标签系统。实现了一个标签系统其实并不难,各个语言的社区应该都有相关的集成。本人也就是想抛开编程语言,从数据库层面来剖析一个标签系统的基本原理。

PS: 另外推荐一个比较好用的Model Design工具dbdiagram,可以用文本的方式对数据表进行设计,边设计边预览。最后还能以PNG,PDF甚至SQL源文件的形式导出。本文的数据表配图均由用该软件制作。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go语言(golang)的错误(error)处理的推荐方案

发表于 2019-01-08

原文链接:www.flysnow.org/2019/01/01/…
微信公众号:flysnow_org(飞雪无情)

对于Go语言(golang)的错误设计,相信很多人已经体验过了,它是通过返回值的方式,来强迫调用者对错误进行处理,要么你忽略,要么你处理(处理也可以是继续返回给调用者),对于golang这种设计方式,我们会在代码中写大量的if判断,以便做出决定。

1
2
3
4
5
6
7
8
复制代码func main() {
conent,err:=ioutil.ReadFile("filepath")
if err !=nil{
//错误处理
}else {
fmt.Println(string(conent))
}
}

这类代码,在我们编码中是非常的,大部分情况下error都是nil,也就是没有任何错误,但是非nil的时候,意味着错误就出现了,我们需要对他进行处理。

error 接口

error其实一个接口,内置的,我们看下它的定义

1
2
3
4
5
复制代码// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}

它只有一个方法 Error,只要实现了这个方法,就是实现了error。现在我们自己定义一个错误试试。

1
2
3
4
5
6
复制代码type fileError struct {
}

func (fe *fileError) Error() string {
return "文件错误"
}

自定义 error

自定义了一个fileError类型,实现了error接口。现在测试下看看效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码func main() {
conent, err := openFile()
if err != nil {
fmt.Println(err)
} else {
fmt.Println(string(conent))
}
}

//只是模拟一个错误
func openFile() ([]byte, error) {
return nil, &fileError{}
}

我们运行模拟的代码,可以看到文件错误的通知。

在实际的使用过程中,我们可能遇到很多错误,他们的区别是错误信息不一样,一种做法是每种错误都类似上面一样定义一个错误类型,但是这样太麻烦了。我们发现Error返回的其实是个字符串,我们可以修改下,让这个字符串可以设置就可以了。

1
2
3
4
5
6
7
复制代码type fileError struct {
s string
}

func (fe *fileError) Error() string {
return fe.s
}

恩,这样改造后,我们就可以在声明fileError的时候,设置好要提示的错误文字,就可以满足我们不同的需要了。

1
2
3
4
复制代码//只是模拟一个错误
func openFile() ([]byte, error) {
return nil, &fileError{"文件错误,自定义"}
}

恩,可以了,已经达到了我们的目的。现在我们可以把它变的更通用一些,比如修改fileError的名字,再创建一个辅助函数,便于我们创建不同的错误类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码//blog:www.flysnow.org
//wechat:flysnow_org
func New(text string) error {
return &errorString{text}
}

type errorString struct {
s string
}

func (e *errorString) Error() string {
return e.s
}

变成以上这样,我们就可以通过New函数,辅助我们创建不同的错误了,这其实就是我们经常用到的errors.New函数,被我们一步步剖析演化而来,现在大家对Go语言(golang)内置的错误error有了一个清晰的认知了。

存在的问题

虽然Go语言对错误的设计非常简洁,但是对于我们开发者来说,很明显是不足的,比如我们需要知道出错的更多信息,在什么文件的,哪一行代码?只有这样我们才更容易的定位问题。

还有比如,我们想对返回的error附加更多的信息后再返回,比如以上的例子,我们怎么做呢?我们只能先通过Error方法,取出原来的错误信息,然后自己再拼接,再使用errors.New函数生成新错误返回。

如果我们以前做过java开发,我们知道Java的异常是可以嵌套的,也就是说,通过这个,我们很容易知道错误的根本原因,因为Java的异常,是一层层的嵌套返回的,不管中间经历了多少包装,我们可以通过cause找到根本错误的原因。

解决问题

如果要解决以上的问题,那么首先我们必须再继续扩充我们的errorString,再增加一些字段来存储更多的信息。比如我们要记录堆栈信息。

1
2
3
4
5
复制代码type stack []uintptr
type errorString struct {
s string
*stack
}

欢迎关注微信公众号flysnow_org或者博客网站 www.flysnow.org/ 查看更多原创文章。

有了存储堆栈信息的stack字段,我们在生成错误的时候,就可以把调用的堆栈信息存储在这个字段里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码//blog:www.flysnow.org
//wechat:flysnow_org

func callers() *stack {
const depth = 32
var pcs [depth]uintptr
n := runtime.Callers(3, pcs[:])
var st stack = pcs[0:n]
return &st
}

func New(text string) error {
return &errorString{
s: text,
stack: callers(),
}
}

完美解决,现在如果再解决,对现有的错误附加一些信息的问题呢?相信大家应该有思路了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码type withMessage struct {
cause error
msg string
}

func WithMessage(err error, message string) error {
if err == nil {
return nil
}
return &withMessage{
cause: err,
msg: message,
}
}

使用WithMessage函数,对原来的error包装下,就可以生成一个新的带有包装信息的错误了。

推荐的方案

以上我们在解决问题是,采取的方法是不是比较熟悉?尤其是看源代码,没错,这就是github.com/pkg/errors这个错误处理库的源代码。

因为Go语言提供的错误太简单了,以至于简单的我们无法更好的处理问题,甚至不能为我们处理错误,提供更有用的信息,所以诞生了很多对错误处理的库,github.com/pkg/errors是比较简洁的一样,并且功能非常强大,受到了大量开发者的欢迎,使用者很多。

它的使用非常简单,如果我们要新生成一个错误,可以使用New函数,生成的错误,自带调用堆栈信息。

1
复制代码func New(message string) error

如果有一个现成的error,我们需要对他进行再次包装处理,这时候有三个函数可以选择。

1
2
3
4
5
6
7
8
复制代码//只附加新的信息
func WithMessage(err error, message string) error

//只附加调用堆栈信息
func WithStack(err error) error

//同时附加堆栈和信息
func Wrap(err error, message string) error

其实上面的包装,很类似于Java的异常包装,被包装的error,其实就是Cause,在前面的章节提到错误的根本原因,就是这个Cause。所以这个错误处理库为我们提供了Cause函数让我们可以获得最根本的错误原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码func Cause(err error) error {
type causer interface {
Cause() error
}

for err != nil {
cause, ok := err.(causer)
if !ok {
break
}
err = cause.Cause()
}
return err
}

使用for循环一直找到最根本(最底层)的那个error。

以上的错误我们都包装好了,也收集好了,那么怎么把他们里面存储的堆栈、错误原因等这些信息打印出来呢?其实,这个错误处理库的错误类型,都实现了Formatter接口,我们可以通过fmt.Printf函数输出对应的错误信息。

1
2
3
复制代码%s,%v //功能一样,输出错误信息,不包含堆栈
%q //输出的错误信息带引号,不包含堆栈
%+v //输出错误信息和堆栈

以上如果有循环包装错误类型的话,会递归的把这些错误都会输出。

小结

通过使用这个 github.com/pkg/errors 错误库,我们可以收集更多的信息,可以让我们更容易的定位问题。

我们收集的这些信息不止可以输出到控制台,也可以当做日志,使用输出到相应的Log日志里,便于分析问题。

据说这个库,会被加入到Golang 标准 SDK 里,期待着,如果加入的话,应该就是补充现在标准库里的errors 这个package了。

本文为原创文章,转载注明出处,欢迎扫码关注公众号flysnow_org或者网站asf www.flysnow.org/ ,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「好看」,感谢支持。

扫码关注

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

动画:什么是散列表?

发表于 2019-01-07

散列表

散列表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做散列函数,存放记录的数组称做散列表。

散列函数

散列函数,顾名思义,它是一个函数。如果把它定义成 hash(key) ,其中 key 表示元素的键值,则 hash(key) 的值表示经过散列函数计算得到的散列值。

散列函数的特点:

1.确定性

如果两个散列值是不相同的(根据同一函数),那么这两个散列值的原始输入也是不相同的。

2.散列碰撞(collision)

散列函数的输入和输出不是唯一对应关系的,如果两个散列值相同,两个输入值很可能是相同的,但也可能不同。

3.不可逆性

一个哈希值对应无数个明文,理论上你并不知道哪个是。

“船长,如果一样东西你知道在哪里,还算不算丢了。”

“不算。”

“好的,那您的酒壶没有丢。”

4.混淆特性

输入一些数据计算出散列值,然后部分改变输入值,一个具有强混淆特性的散列函数会产生一个完全不同的散列值。

常见的散列函数

1. MD5

MD5 即 Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致。是计算机广泛使用的杂凑算法之一,主流编程语言普遍已有 MD5 实现。

将数据(如汉字)运算为另一固定长度值,是杂凑算法的基础原理,MD5 的前身有 MD2 、MD3 和 MD4 。

MD5 是输入不定长度信息,输出固定长度 128-bits 的算法。经过程序流程,生成四个32位数据,最后联合起来成为一个 128-bits 散列。

基本方式为,求余、取余、调整长度、与链接变量进行循环运算,得出结果。

MD5 计算广泛应用于错误检查。在一些 BitTorrent 下载中,软件通过计算 MD5 来检验下载到的碎片的完整性。

MD5 校验

2. SHA-1

SHA-1(英语:Secure Hash Algorithm 1,中文名:安全散列算法1)是一种密码散列函数,SHA-1可以生成一个被称为消息摘要的160位(20字节)散列值,散列值通常的呈现形式为40个十六进制数。

SHA-1 曾经在许多安全协议中广为使用,包括TLS和SSL、PGP、SSH、S/MIME和IPsec,曾被视为是MD5的后继者。

散列冲突

理想中的一个散列函数,希望达到

如果 key1 ≠ key2,那 hash(key1) ≠ hash(key2)

这种效果,然而在真实的情况下,要想找到一个不同的 key 对应的散列值都不一样的散列函数,几乎是不可能的,即使是 MD5 或者 由美国国家安全局设计的 SHA-1 算法也无法实现。

事实上,再好的散列函数都无法避免散列冲突。

为什么呢?

这涉及到数学中比较好理解的一个原理:抽屉原理。

抽屉原理:桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面至少放两个苹果。这一现象就是我们所说的“抽屉原理”。

抽屉原理

对于散列表而言,无论设置的存储区域(n)有多大,当需要存储的数据大于 n 时,那么必然会存在哈希值相同的情况。这就是所谓的散列冲突。

散列冲突

那应该如何解决散列冲突问题呢?

常用的散列冲突解决方法有两类,开放寻址法(open addressing)和链表法(chaining)。

开放寻址法

定义:将散列函数扩展定义成探查序列,即每个关键字有一个探查序列h(k,0)、h(k,1)、…、h(k,m-1),这个探查序列一定是0….m-1的一个排列(一定要包含散列表全部的下标,不然可能会发生虽然散列表没满,但是元素不能插入的情况),如果给定一个关键字k,首先会看h(k,0)是否为空,如果为空,则插入;如果不为空,则看h(k,1)是否为空,以此类推。

开放寻址法是一种解决碰撞的方法,对于开放寻址冲突解决方法,比较经典的有线性探测方法(Linear Probing)、二次探测(Quadratic probing)和 双重散列(Double hashing)等方法。

线性探测方法

开放寻址法之线性探测方法

当我们往散列表中插入数据时,如果某个数据经过散列函数散列之后,存储位置已经被占用了,我们就从当前位置开始,依次往后查找,看是否有空闲位置,直到找到为止。

以上图为例,散列表的大小为 8 ,黄色区域表示空闲位置,橙色区域表示已经存储了数据。目前散列表中已经存储了 4 个元素。此时元素 7777777 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。

于是按顺序地往后一个一个找,看有没有空闲的位置,此时,运气很好正巧在下一个位置就有空闲位置,将其插入,完成了数据存储。

线性探测法一个很大的弊端就是当散列表中插入的数据越来越多时,散列冲突发生的可能性就会越来越大,空闲位置会越来越少,线性探测的时间就会越来越久。极端情况下,需要从头到尾探测整个散列表,所以最坏情况下的时间复杂度为 O(n)。

开放寻址法之线性探测方法的弊端

二次探测方法

二次探测是二次方探测法的简称。顾名思义,使用二次探测进行探测的步长变成了原来的“二次方”,也就是说,它探测的下标序列为 hash(key)+0,hash(key)+1^2或[hash(key)-1^2],hash(key)+2^2或[hash(key)-2^2]。

二次探测方法

以上图为例,散列表的大小为 8 ,黄色区域表示空闲位置,橙色区域表示已经存储了数据。目前散列表中已经存储了 7 个元素。此时元素 7777777 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。

按照二次探测方法的操作,有冲突就先 + 1^2,8 这个位置有值,冲突;变为 - 1^2,6 这个位置有值,还是有冲突;于是 - 2^2, 3 这个位置是空闲的,插入。

双重散列方法

所谓双重散列,意思就是不仅要使用一个散列函数,而是使用一组散列函数 hash1(key),hash2(key),hash3(key)。。。。。。先用第一个散列函数,如果计算得到的存储位置已经被占用,再用第二个散列函数,依次类推,直到找到空闲的存储位置。

双重散列方法

以上图为例,散列表的大小为 8 ,黄色区域表示空闲位置,橙色区域表示已经存储了数据。目前散列表中已经存储了 7 个元素。此时元素 7777777 经过 Hash 算法之后,被散列到位置下标为 7 的位置,但是这个位置已经有数据了,所以就产生了冲突。

此时,再将数据进行一次哈希算法处理,经过另外的 Hash 算法之后,被散列到位置下标为 3 的位置,完成操作。

事实上,不管采用哪种探测方法,只要当散列表中空闲位置不多的时候,散列冲突的概率就会大大提高。为了尽可能保证散列表的操作效率,一般情况下,需要尽可能保证散列表中有一定比例的空闲槽位。

一般使用加载因子(load factor)来表示空位的多少。

加载因子是表示 Hsah 表中元素的填满的程度,若加载因子越大,则填满的元素越多,这样的好处是:空间利用率高了,但冲突的机会加大了。反之,加载因子越小,填满的元素越少,好处是冲突的机会减小了,但空间浪费多了。

链表法

链表法是一种更加常用的散列冲突解决办法,相比开放寻址法,它要简单很多。如下动图所示,在散列表中,每个位置对应一条链表,所有散列值相同的元素都放到相同位置对应的链表中。

链表法

今日问题

请问可以对链表法进行怎样的改造,去实现一个更加高效的散列表?

欢迎关注这个会做动画的程序员👆

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

DOM4J 解析 XML 之忽略转义字符

发表于 2019-01-04

项目经验,如需转载,请注明作者:Yuloran (t.cn/EGU6c76)

背景

项目开发需要手动合入几十种语言的翻译到 string.xml 中,这是一件非常痛苦的事情:Copy、Paste,Copy、Paste,Copy、Paste… 人都快疯了!被逼无奈写了个自动替换翻译的工具。原理很简单:解析 Excel中的翻译,替换到 Xml 中。Excel 解析用 jxl.jar,Xml 解析与修改用 DOM,一顿操作,一天就写完了!正高兴呢,赶紧使用 git diff 查看修改对比,一看坏事了:“坑爹呢!一点也不完美啊!原字符串中的转义字符全被转义了好嘛!难道还要手动还回去嘛!像我这样优(懒)秀(惰)的人根本无法容忍好嘛!” 所以,本文记录如何使用 DOM4J(上面不是说用 DOM 解析吗?这里怎么又成 DOM4J 了?忽悠谁呢!) 解析 XML 并让其忽略转义字符。

为什么不用 DOM

谁说我没用 DOM,我一上来就用的 DOM 好嘛!毕竟 JDK 自带的啊!但是用了后,用户体验贼差好嘛!稍微贴下使用方法:

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
复制代码package com.yuloran;

import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;

public class Main {

public static void main(String[] args) throws ParserConfigurationException, IOException, SAXException, TransformerException {

// 1. 解析
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = factory.newDocumentBuilder();
Document document = documentBuilder.parse(new InputSource(new InputStreamReader(new FileInputStream("strings.xml"), "UTF-8")));

// 2. 遍历
NodeList strings = document.getElementsByTagName("string");
for (int i = 0; i < strings.getLength(); i++) {
Node item = strings.item(i);
System.out.print(String.format("Element:[tag:%s, content:%s] ", item.getNodeName(), item.getTextContent()));
NamedNodeMap attributes = item.getAttributes();
for (int j = 0; j < attributes.getLength(); j++) {
Node attr = attributes.item(j);
System.out.println(String.format("Attr:[key:%s, value:%s]", attr.getNodeName(), attr.getNodeValue()));
}
}

// 3. 保存
Transformer transformer = TransformerFactory.newInstance().newTransformer();
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
transformer.transform(new DOMSource(document), new StreamResult("strings_copy.xml"));
}

}

strings.xml:

strings.xml

解析日志:

解析日志.png

strings_copy.xml:

strings_copy.png

DOM 解析保存 XML 文件的问题:

  • XML 文档声明中自动添加 standalone=”no”,可以通过 document.setXmlStandalone(true); 去除,但是这样的话,缩进就会失效!
  • 文件换行符自动更换为操作系统所在的换行符!

所以,我不用 DOM,而是用 DOM4J。用了 DOM4J 这些问题都将成为浮云!

DOM4J 解析

太简单啦!直接阅读官方指南即可!我从未见过如此简洁明了的 API 文档!

DOM4J jar 包及依赖下载:

  • 点我下载 dom4j-2.1.0.jar

注意看截图:

DOM4J依赖.png

如果工程中 dom4j 是 maven 依赖,就不需要手动下载 jaxen.jar。如果是 jar 依赖,还需要下载 jaxen.jar,否则编译时会找不到类。

  • 点我下载 jaxen-1.1.6.jar

DOM4J 使用示例

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
复制代码package com.yuloran;

import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.xml.sax.InputSource;

import java.io.*;
import java.util.List;

public class Main {

public static void main(String[] args) throws DocumentException, IOException {
// 1. 解析
SAXReader reader = new SAXReader();
Document document = reader.read(new InputSource(new InputStreamReader(new FileInputStream("strings.xml"), "UTF-8")));

// 2. 遍历
List<Node> list = document.selectNodes("/resources/string[@name]");
for (Node node : list) {
System.out.print(String.format("Element:[tag:%s, content:%s] ", node.getName(), node.getText()));
System.out.println(String.format("Attr:[name@%s]", node.valueOf("@name")));
}

// 3.保存
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("strings_dom4j.xml"), "UTF-8");
XMLWriter xmlWriter = new XMLWriter(writer);
// 忽略 Element 对象中的转义字符
xmlWriter.setEscapeText(false);
xmlWriter.write(document);
xmlWriter.close();
}

}

strings_dom4j.xml:

strings_dom4j.xml

怎么样?看看这输出,一点毛病没有!

忽略转义字符

其实这是 SAX(Simple Application Interface For Xml) 解析的问题,SAX 解析 XML 时,会自动将元素文本中的转义字符转义,导致最后将 Document 对象保存为文件时,无法将原转义字符写回:

原文件:

带转义字符的xml.png

DOM4J 解析再写回:

DOM4J 解析再写回.png

所以,我们需要实现一个过滤器,每当 SAX 解析一个转义字符,我们就将其原样写回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码        reader.setXMLFilter(new XMLFilterImpl() {
@Override
public void characters(char[] ch, int start, int length) throws SAXException {
String text = new String(ch, start, length);
System.out.println("text is: " + text);

if (length == 1) {
if ((int) ch[0] == 160) {
char[] escape = "&#160;".toCharArray();
super.characters(escape, 0, escape.length);
return;
}
}

super.characters(ch, start, length);
}
});

再配合 xmlWriter.setEscapeText(false); 即可原样输出原 Xml 文件中的转义字符:

1
2
3
4
5
6
复制代码        // 3.保存
OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream("strings_dom4j.xml"), "UTF-8");
XMLWriter xmlWriter = new XMLWriter(writer);
xmlWriter.setEscapeText(false);
xmlWriter.write(document);
xmlWriter.close();

测试结果:

原Xml.png

日志:

日志.png

strings_dom4j.xml:

strings_dom4j.xml

其它的转义字符也是同样的处理方法。可以将要忽略的转义字符放到配置文件中,做工具的时候从配置中读取要忽略的转义字符,这样更灵活。

总结

本文只写了最终的解决方案,实际上探索这个解决方案的过程还是比较复杂的。需求小众,没什么资料,只能看源码,猜接口,反正我是不相信这样的解析框架是没有暴露用户自行处理字符串的接口的。果然还是可以通过 characters() 方法,只是 SAXReader 没有暴露 ContentHandler 接口,内部封装成了 SAXContentHandler,characters() 方法则暴露到了 XMLFilter 接口中,哈,一番好找。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Fragment生命周期

发表于 2019-01-04

文中的源代码基于Support包27.1.1版本

1 Fragment生命周期

大家都知道Fragment的生命周期,以及其对应的一些生命周期函数:

Fragment的生命周期函数很多,但其实Fragment中只定义了6种状态

1
2
3
4
5
6
复制代码static final int INITIALIZING = 0;     // Not yet created.
static final int CREATED = 1; // Created.
static final int ACTIVITY_CREATED = 2; // The activity has finished its creation.
static final int STOPPED = 3; // Fully created, not started.
static final int STARTED = 4; // Created and started, not resumed.
static final int RESUMED = 5; // Created started and resumed.

Fragment的整个生命周期一直在这6个状态中流转,调用对应的生命周期方法然后进入下一个状态,如下图

1.1 Fragment与Activity

Fragment的生命周期与Activity的生命周期密切相关
Activity管理Fragment生命周期的方式是在Activity的生命周期方法中调用FragmentManager的对应方法,通过FragmentManager将现有的Fragment迁移至下一个状态,同时触发相应的生命周期函数

Activity生命周期函数 FragmentManager触发的函数 Fragment状态迁移 Fragment生命周期回调
onCreate dispatchCreate INITIALIZING->CREATED onAttach、onCreate
onStart dispatchStart CREATED->ACTIVITY_CREATED->STOPPED->STARTED onCreateView、onActivityCreated、onStart
onResume(准确来讲是onPostResume) dispatchResume STARTED->RESUMED onResume
onPause dispatchPause RESUMED->STARTED onPause
onStop dispatchStop STARTED->STOPPED onStop
onDestroy dispatchDestroy STOPPED->ACTIVITY_CREATED->CREATED->INITIALIZING onDestroyView、onDestroy、onDetach

上个图更加清晰:

1.2 Fragment与FragmentTransaction

我们经常使用FragmentTransaction中的add、remove、replace、attach、detach、hide、show等方法对Fragment进行操作,这些方法都会使Fragment的状态发生变化,触发对应的生命周期函数

(假设此时Activity处于RESUME状态)

FragmentTransaction中的方法 Fragment触发的生命周期函数
add onAttach->onCreate->onCreateView->onActivityCreated->onStart->onResume
remove onPause->onStop->onDestoryView->onDestory->onDetach
replace replace可拆分为add和remove,
detach (在调用detach之前需要先通过add添加Fragment)onPause->onStop->onDestoryView
attach (调用attach之前需要先调用detach)onCreateView->onActivityCreated->onStarted->onResumed
hide 不会触发任何生命周期函数
show 不会触发任何生命周期函数

通过对Fragment生命周期的变化的观察,我们可以很容易发现,add/remove操作会引起Fragment在INITIALIZING和RESUMED这两个状态之间迁移。
而attach/detach操作会引起Fragment在CREATED和RESUMED这两个状态之间迁移。

注:add函数这里有一个需要注意的点,如果当前Activity处于STARTED状态,Fragment是无法进入RESUMED状态的,只有当Activity进入RESUME状态,然后触发onResume->FragmentManager.dispatchStateChange(Fragment.RESUMED),然后调用Fragment.onResume函数之后Fragment才会进入RESUMED状态。

1.3 Fragment与ViewPager

通过FragmentPagerAdapter我们可以将Fragment与ViewPager结合起来使用,那么ViewPager中的Fragment的生命周期又是怎样的呢?

其实也简单,FragmentPagerAdapter内部其实就是通过FragmentTransaction对Fragment进行操作的,主要涉及add、detach、attach这三个方法。

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
复制代码@SuppressWarnings("ReferenceEquality")
@Override
public Object instantiateItem(ViewGroup container, int position) {
//...
final long itemId = getItemId(position);

// Do we already have this fragment?
String name = makeFragmentName(container.getId(), itemId);
Fragment fragment = mFragmentManager.findFragmentByTag(name);
if (fragment != null) {
//如果已经存在Fragment实例
//那么使用attach操作进行添加
mCurTransaction.attach(fragment);
} else {
//Fragment实例还没创建,通过getItem创建一个实例
//然后通过add操作添加
fragment = getItem(position);
mCurTransaction.add(container.getId(), fragment,
makeFragmentName(container.getId(), itemId));
}
//...
return fragment;
}

@Override
public void destroyItem(ViewGroup container, int position, Object object) {
if (mCurTransaction == null) {
mCurTransaction = mFragmentManager.beginTransaction();
}
//...
//使用detach销毁Fragment
mCurTransaction.detach((Fragment)object);
}

通过上述源码可知,FragmentPagerAdapter通过FragmentTransaction.add方法添加Fragment,后续通过attach和detach来操作。这些方法对应的生命周期我们可以参照上面的图即可。
我们举例来模拟一下看看,假设有ViewPager有5个页面,以及offscreenPageLimit为1,

  1. 第一次加载时,第一第二页通过add函数被加载,处在RESUMED状态
  2. 滑动到第二页,第三页被加载,也是通过add函数被加载的,处在RESUMED状态
  3. 继续滑动到第三页,此时第一页通过detach函数被回收,处在CREATED状态,同时第四页通过add被加载处于RESUMED状态
  4. 滑动到第二页,此时第一页通过attach被加载,处于RESUMED状态,第四页被detach处于CREATED状态

总结:ViewPager中当前页与当前页左右两页都处于RESUMED状态,其他页面要么未被创建,要么处于CREATED状态,滑动过程中Fragment的生命周期变化我们可以通过上面这个例子得到。

1.4 Fragment与DialogFragment

在使用DialogFragment的时候我们习惯使用它提供的show、hide方法进行显示或者隐藏。这两方法内部其实使用了FragmentTransaction的add、remove方法,这些方法对应的生命周期我们已经讲过了就不在赘述了。

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
复制代码public void show(FragmentManager manager, String tag) {
mDismissed = false;
mShownByMe = true;
FragmentTransaction ft = manager.beginTransaction();
//核心操作
ft.add(this, tag);
ft.commit();
}


void dismissInternal(boolean allowStateLoss) {
//...
if (mBackStackId >= 0) {
//...
} else {
FragmentTransaction ft = getFragmentManager().beginTransaction();
//核心操作
ft.remove(this);
if (allowStateLoss) {
ft.commitAllowingStateLoss();
} else {
ft.commit();
}
}
}

DialogFragment比较特别的是内部还维护了一个Dialog,DialogFragment设计之初就是使用FragmentManager来管理Dialog,主要使用了Dialog的show、hide、dismiss这三个方法。对应关系如下

Fragment生命周期函数 对应的Dialog的方法
onStart show
onStop hide
onDestoryView dismiss

2 不同的添加方式对Fragment的生命周期有什么影响

Fragment的添加方式有两种:

  1. 通过在xml文件中使用fragment标签添加
  2. 在代码中使用FragmentTransaction添加

这里我们就来聊聊,这两种不同的添加方式对于Fragment的生命周期回调会产生什么样的影响。

2.1 使用fragment标签添加

xml中的Fragment的实例创建最终会交由FragmentManager负责,方法为onCreateView

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
复制代码//FragmentManager.java
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
//判断是否是Fragment标签
if (!"fragment".equals(name)) {
return null;
}

//下面这些代码是获取xml中定义的
//Fragment的一些信息
//如类名(全路径)、id、tag
String fname = attrs.getAttributeValue(null, "class");
TypedArray a = context.obtainStyledAttributes(attrs, FragmentTag.Fragment);
if (fname == null) {
fname = a.getString(FragmentTag.Fragment_name);
}
int id = a.getResourceId(FragmentTag.Fragment_id, View.NO_ID);
String tag = a.getString(FragmentTag.Fragment_tag);
a.recycle();

//检查指定的Fragment类是否派生子Fragment
if (!Fragment.isSupportFragmentClass(mHost.getContext(), fname)) {
return null;
}

//必须满足id不为空或者tag不为空或者包裹Fragment的Container的id不为空
//否则抛出异常
int containerId = parent != null ? parent.getId() : 0;
if (containerId == View.NO_ID && id == View.NO_ID && tag == null) {
throw new IllegalArgumentException(attrs.getPositionDescription()
+ ": Must specify unique android:id, android:tag, or have a parent with an id for " + fname);
}

// If we restored from a previous state, we may already have
// instantiated this fragment from the state and should use
// that instance instead of making a new one.
Fragment fragment = id != View.NO_ID ? findFragmentById(id) : null;
if (fragment == null && tag != null) {
fragment = findFragmentByTag(tag);
}
if (fragment == null && containerId != View.NO_ID) {
fragment = findFragmentById(containerId);
}

//log...

//通过反射创建Fragment实例
if (fragment == null) {
fragment = Fragment.instantiate(context, fname);
//这个字段标志该Fragment实例是来自于xml文件
fragment.mFromLayout = true;
fragment.mFragmentId = id != 0 ? id : containerId;
fragment.mContainerId = containerId;
fragment.mTag = tag;
fragment.mInLayout = true;
fragment.mFragmentManager = this;
fragment.mHost = mHost;
fragment.onInflate(mHost.getContext(), attrs, fragment.mSavedFragmentState);
//重点方法
//第二个参数名为moveToStateNow
//此处为true,因此该Fragment将会立即
//迁移到当前FragmentManager所记录的状态
//通常我们在onCreate方法中设置layout
//因此通常来讲此时FragmentManager
//处于CREATED状态
addFragment(fragment, true);

} else if (fragment.mInLayout) {
//...
} else {
//...
}

if (mCurState < Fragment.CREATED && fragment.mFromLayout) {
//如果当前FragmentManager处于INITIALIZING状态
//那么强制将该Fragment迁移至CREATED状态
moveToState(fragment, Fragment.CREATED, 0, 0, false);
} else {
//如果此时FragmentManager的状态大于CREATED
//那么将该Fragment迁移至对应的状态
moveToState(fragment);
}

//...
return fragment.mView;
}

onCreateView的工作基本上就是创建Fragment实例并将其迁移至指定状态了,我们以一个Activity正常启动的流程作为分析的场景,那么此时Fragment将最终进入CREATED状态。

在前面学习Fragment生命周期的时候,我们有提到过Activity进入onCreate之后会触发Fragment的onAttach和onCreate的生命周期回调。但在当前这种场景下,Fragment会提前触发onCreateView来创建视图,这一点可以在moveToState的源码中得到印证:

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
复制代码void moveToState(Fragment f, int newState, int transit, int transitionStyle,
boolean keepActive) {

//...
switch (f.mState) {
case Fragment.INITIALIZING:
//...
case Fragment.CREATED:
//...
//下面这个if语句来自于ensureInflatedFragmentView方法
//为了方便,这里直接贴上了该方法的代码
//如果该Fragment来自于布局文件
//那么触发onCreateView创建试图实例
if (f.mFromLayout && !f.mPerformedCreateView) {
f.mView = f.performCreateView(f.performGetLayoutInflater(
f.mSavedFragmentState), null, f.mSavedFragmentState);
if (f.mView != null) {
f.mInnerView = f.mView;
f.mView.setSaveFromParentEnabled(false);
if (f.mHidden) f.mView.setVisibility(View.GONE);
f.onViewCreated(f.mView, f.mSavedFragmentState);
dispatchOnFragmentViewCreated(f, f.mView, f.mSavedFragmentState, false);
} else {
f.mInnerView = null;
}
}
if (newState > Fragment.CREATED) {
//...
}
//...
}
//...

}

2.2 在代码中使用FragmentTransaction添加

此处我们以在Activity.onCreate方法中add一个Fragment作为分析场景

1
2
3
4
5
6
7
8
9
复制代码public class DemoActivity extends FragmentActivity{
protected void onCreate(Bundle savedInstanceState){
super.onCreate(savedInstanceState);
setContentView(R.layout.demo);
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.add(R.id.container, new DemoFragment());
ft.commit();
}
}

先不管add里面进行了什么操作,我们知道如果不调用commit方法,那么add操作是不会起效的的。
commit方法会经历以下调用链
commit->
commitInternal->
FragmentManager.enqueueAction

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码//FragmentTransaction的实现类为BackStackRecord
//action的实际类型是BackStackRecord
public void enqueueAction(OpGenerator action, boolean allowStateLoss) {
if (!allowStateLoss) {
checkStateLoss();
}
synchronized (this) {
//...
mPendingActions.add(action);
synchronized (this) {
boolean postponeReady =
mPostponedTransactions != null && !mPostponedTransactions.isEmpty();
boolean pendingReady = mPendingActions != null && mPendingActions.size() == 1;
if (postponeReady || pendingReady) {
//重点
//getHandler拿到的是一个主线程的Handler
//这里没有直接调用moveToState,而是抛了一个
//消息至消息队列,这将导致Fragment的状态迁移被延后
mHost.getHandler().removeCallbacks(mExecCommit);
mHost.getHandler().post(mExecCommit);
}
}
}
}

当mExecCommit被触发就会经历下面的调用链
FragmentManager.execPendingActions->
BackStackRecord.generateOps->
…->
BackStackRecord.executeOps->
FragmentManager.xxxFragment->
FragmentManager.moveToState
最终发生了Fragment的状态迁移

那么mExecCommit是否真的就老老实实待在消息队列中等待被执行呢?答案是否定的。
我们来看看FragmentActivity.onStart方法

1
2
3
4
5
6
7
8
9
10
11
12
复制代码protected void onStart() {
super.onStart();
//...

//敲黑板
mFragments.execPendingActions();

//...

mFragments.dispatchStart();
//...
}

可以看到,execPendingActions被提前触发了,再搭配下面的dispatchStart,那么Fragment将从INITIALIZING一下子迁移至STARTED(execPendingActions方法触发后会将mExecCommit从消息队列中移除)。
FragmentActivity在onStart、onResume和onPostResume生命周期回调中都会调用FragmentManager.execPendingActions,因此当我们在Activity.onStart、Activity.onResume中通过代码添加Fragment时,Fragment的状态迁移分别会发生在Activity.onResume、Activity.onPostResume之后。
那么在onPostResume之后再添加Fragment会发生什么呢?
此时由于onPostResume方法中的FragmentManager.execPendingActions已经在super中调用过了,因此mExecCommit将会被触发,这里有一个最大的不同点就是Fragment的生命周期变化与Activity的生命周期变化不处于同一个消息周期。

2.3 总结

我们以一张图对本节内容进行总结:

28.0.0版本的support包中移除了STOPPED状态,但是经过测试,其生命变化与上图保持一致

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

面试官问:JS的this指向

发表于 2018-12-25

前言

你好,我是若川。这是面试官问系列的第四篇,旨在帮助读者提升JS基础知识,包含new、call、apply、this、继承相关知识。

面试官问系列文章如下:感兴趣的读者可以点击阅读。

1.面试官问:能否模拟实现JS的new操作符

2.面试官问:能否模拟实现JS的bind方法

3.面试官问:能否模拟实现JS的call和apply方法

4.面试官问:JS的this指向

5.面试官问:JS的继承

面试官出很多考题,基本都会变着方式来考察this指向,看候选人对JS基础知识是否扎实。
读者可以先拉到底部看总结,再谷歌(或各技术平台)搜索几篇类似文章,看笔者写的文章和别人有什么不同(欢迎在评论区评论不同之处),对比来看,验证与自己现有知识是否有盲点,多看几篇,自然就会完善自身知识。

附上之前写文章写过的一段话:已经有很多关于this的文章,为什么自己还要写一遍呢。学习就好比是座大山,人们沿着不同的路登山,分享着自己看到的风景。你不一定能看到别人看到的风景,体会到别人的心情。只有自己去登山,才能看到不一样的风景,体会才更加深刻。

函数的this在调用时绑定的,完全取决于函数的调用位置(也就是函数的调用方法)。为了搞清楚this的指向是什么,必须知道相关函数是如何调用的。

全局上下文

非严格模式和严格模式中this都是指向顶层对象(浏览器中是window)。

1
2
3
4
5
javascript复制代码this === window // true
'use strict'
this === window;
this.name = '若川';
console.log(this.name); // 若川

函数上下文

普通函数调用模式

1
2
3
4
5
6
javascript复制代码// 非严格模式
var name = 'window';
var doSth = function(){
console.log(this.name);
}
doSth(); // 'window'

你可能会误以为window.doSth()是调用的,所以是指向window。虽然本例中window.doSth确实等于doSth。name等于window.name。上面代码中这是因为在ES5中,全局变量是挂载在顶层对象(浏览器是window)中。
事实上,并不是如此。

1
2
3
4
5
6
7
javascript复制代码// 非严格模式
let name2 = 'window2';
let doSth2 = function(){
console.log(this === window);
console.log(this.name2);
}
doSth2() // true, undefined

这个例子中let没有给顶层对象中(浏览器是window)添加属性,window.name2和window.doSth都是undefined。

严格模式中,普通函数中的this则表现不同,表现为undefined。

1
2
3
4
5
6
7
8
javascript复制代码// 严格模式
'use strict'
var name = 'window';
var doSth = function(){
console.log(typeof this === 'undefined');
console.log(this.name);
}
doSth(); // true,// 报错,因为this是undefined

看过的《你不知道的JavaScript》上卷的读者,应该知道书上将这种叫做默认绑定。
对call,apply熟悉的读者会类比为:

1
2
ini复制代码doSth.call(undefined);
doSth.apply(undefined);

效果是一样的,call,apply作用之一就是用来修改函数中的this指向为第一个参数的。
第一个参数是undefined或者null,非严格模式下,是指向window。严格模式下,就是指向第一个参数。后文详细解释。

经常有这类代码(回调函数),其实也是普通函数调用模式。

1
2
3
4
5
6
7
8
9
javascript复制代码var name = '若川';
setTimeout(function(){
console.log(this.name);
}, 0);
// 语法
setTimeout(fn | code, 0, arg1, arg2, ...)
// 也可以是一串代码。也可以传递其他函数
// 类比 setTimeout函数内部调用fn或者执行代码`code`。
fn.call(undefined, arg1, arg2, ...);

对象中的函数(方法)调用模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
javascript复制代码var name = 'window';
var doSth = function(){
console.log(this.name);
}
var student = {
name: '若川',
doSth: doSth,
other: {
name: 'other',
doSth: doSth,
}
}
student.doSth(); // '若川'
student.other.doSth(); // 'other'
// 用call类比则为:
student.doSth.call(student);
// 用call类比则为:
student.other.doSth.call(student.other);

但往往会有以下场景,把对象中的函数赋值成一个变量了。
这样其实又变成普通函数了,所以使用普通函数的规则(默认绑定)。

1
2
3
4
ini复制代码var studentDoSth = student.doSth;
studentDoSth(); // 'window'
// 用call类比则为:
studentDoSth.call(undefined);

call、apply、bind 调用模式

上文提到call、apply,这里详细解读一下。先通过MDN认识下call和apply
MDN 文档:Function.prototype.call()

语法

1
kotlin复制代码fun.call(thisArg, arg1, arg2, ...)

thisArg

在fun函数运行时指定的this值。需要注意的是,指定的this值并不一定是该函数执行时真正的this值,如果这个函数处于非严格模式下,则指定为null和undefined的this值会自动指向全局对象(浏览器中就是window对象),同时值为原始值(数字,字符串,布尔值)的this会指向该原始值的自动包装对象。

arg1, arg2, …

指定的参数列表

返回值

返回值是你调用的方法的返回值,若该方法没有返回值,则返回undefined。

apply和call类似。只是参数不一样。它的参数是数组(或者类数组)。

根据参数thisArg的描述,可以知道,call就是改变函数中的this指向为thisArg,并且执行这个函数,这也就使JS灵活很多。严格模式下,thisArg是原始值是值类型,也就是原始值。不会被包装成对象。举个例子:

1
2
3
4
5
6
7
8
9
10
11
javascript复制代码var doSth = function(name){
console.log(this);
console.log(name);
}
doSth.call(2, '若川'); // Number{2}, '若川'
var doSth2 = function(name){
'use strict';
console.log(this);
console.log(name);
}
doSth2.call(2, '若川'); // 2, '若川'

虽然一般不会把thisArg参数写成值类型。但还是需要知道这个知识。
之前写过一篇文章:面试官问:能否模拟实现JS的call和apply方法
就是利用对象上的函数this指向这个对象,来模拟实现call和apply的。感兴趣的读者思考如何实现,再去看看笔者的实现。

bind和call和apply类似,第一个参数也是修改this指向,只不过返回值是新函数,新函数也能当做构造函数(new)调用。
MDN Function.prototype.bind

bind()方法创建一个新的函数, 当这个新函数被调用时this键值为其提供的值,其参数列表前几项值为创建时指定的参数序列。

语法:
fun.bind(thisArg[, arg1[, arg2[, …]]])

参数:
thisArg
调用绑定函数时作为this参数传递给目标函数的值。 如果使用new运算符构造绑定函数,则忽略该值。当使用bind在setTimeout中创建一个函数(作为回调提供)时,作为thisArg传递的任何原始值都将转换为object。如果没有提供绑定的参数,则执行作用域的this被视为新函数的thisArg。
arg1, arg2, …
当绑定函数被调用时,这些参数将置于实参之前传递给被绑定的方法。
返回值
返回由指定的this值和初始化参数改造的原函数拷贝。

之前也写过一篇文章:面试官问:能否模拟实现JS的bind方法
就是利用call和apply指向这个thisArg参数,来模拟实现bind的。感兴趣的读者思考如何实现,再去看看笔者的实现。

构造函数调用模式

1
2
3
4
5
6
7
javascript复制代码function Student(name){
this.name = name;
console.log(this); // {name: '若川'}
// 相当于返回了
// return this;
}
var result = new Student('若川');

使用new操作符调用函数,会自动执行以下步骤。

  1. 创建了一个全新的对象。
  2. 这个对象会被执行[[Prototype]](也就是__proto__)链接。
  3. 生成的新对象会绑定到函数调用的this。
  4. 通过new创建的每个对象将最终被[[Prototype]]链接到这个函数的prototype对象上。
  5. 如果函数没有返回对象类型Object(包含Functoin, Array, Date, RegExg, Error),那么new表达式中的函数调用会自动返回这个新的对象。

由此可以知道:new操作符调用时,this指向生成的新对象。
特别提醒一下,new调用时的返回值,如果没有显式返回对象或者函数,才是返回生成的新对象。

1
2
3
4
5
6
7
8
sql复制代码function Student(name){
this.name = name;
// return function f(){};
// return {};
}
var result = new Student('若川');
console.log(result); {name: '若川'}
// 如果返回函数f,则result是函数f,如果是对象{},则result是对象{}

很多人或者文章都忽略了这一点,直接简单用typeof判断对象。虽然实际使用时不会显示返回,但面试官会问到。

之前也写了一篇文章面试官问:能否模拟实现JS的new操作符,是使用apply来把this指向到生成的新生成的对象上。感兴趣的读者思考如何实现,再去看看笔者的实现。

原型链中的调用模式

1
2
3
4
5
6
7
8
javascript复制代码function Student(name){
this.name = name;
}
var s1 = new Student('若川');
Student.prototype.doSth = function(){
console.log(this.name);
}
s1.doSth(); // '若川'

会发现这个似曾相识。这就是对象上的方法调用模式。自然是指向生成的新对象。
如果该对象继承自其它对象。同样会通过原型链查找。
上面代码使用
ES6中class写法则是:

1
2
3
4
5
6
7
8
9
10
javascript复制代码class Student{
constructor(name){
this.name = name;
}
doSth(){
console.log(this.name);
}
}
let s1 = new Student('若川');
s1.doSth();

babel es6转换成es5的结果,可以去babeljs网站转换测试自行试试。

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
javascript复制代码'use strict';

var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

var Student = function () {
function Student(name) {
_classCallCheck(this, Student);

this.name = name;
}

_createClass(Student, [{
key: 'doSth',
value: function doSth() {
console.log(this.name);
}
}]);

return Student;
}();

var s1 = new Student('若川');
s1.doSth();

由此看出,ES6的class也是通过构造函数模拟实现的,是一种语法糖。

箭头函数调用模式

先看箭头函数和普通函数的重要区别:

1、没有自己的this、super、arguments和new.target绑定。
2、不能使用new来调用。
3、没有原型对象。
4、不可以改变this的绑定。
5、形参名称不能重复。

箭头函数中没有this绑定,必须通过查找作用域链来决定其值。
如果箭头函数被非箭头函数包含,则this绑定的是最近一层非箭头函数的this,否则this的值则被设置为全局对象。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ini复制代码var name = 'window';
var student = {
name: '若川',
doSth: function(){
// var self = this;
var arrowDoSth = () => {
// console.log(self.name);
console.log(this.name);
}
arrowDoSth();
},
arrowDoSth2: () => {
console.log(this.name);
}
}
student.doSth(); // '若川'
student.arrowDoSth2(); // 'window'

其实就是相当于箭头函数外的this是缓存的该箭头函数上层的普通函数的this。如果没有普通函数,则是全局对象(浏览器中则是window)。
也就是说无法通过call、apply、bind绑定箭头函数的this(它自身没有this)。而call、apply、bind可以绑定缓存箭头函数上层的普通函数的this。
比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
javascript复制代码var student = {
name: '若川',
doSth: function(){
console.log(this.name);
return () => {
console.log('arrowFn:', this.name);
}
}
}
var person = {
name: 'person',
}
student.doSth().call(person); // '若川' 'arrowFn:' '若川'
student.doSth.call(person)(); // 'person' 'arrowFn:' 'person'

DOM事件处理函数调用

addEventerListener、attachEvent、onclick

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
xml复制代码<button class="button">onclick</button>
<ul class="list">
<li>1</li>
<li>2</li>
<li>3</li>
</ul>
<script>
var button = document.querySelector('button');
button.onclick = function(ev){
console.log(this);
console.log(this === ev.currentTarget); // true
}
var list = document.querySelector('.list');
list.addEventListener('click', function(ev){
console.log(this === list); // true
console.log(this === ev.currentTarget); // true
console.log(this);
console.log(ev.target);
}, false);
</script>

onclick和addEventerListener是指向绑定事件的元素。
一些浏览器,比如IE6~IE8下使用attachEvent,this指向是window。
顺便提下:面试官也经常考察ev.currentTarget和ev.target的区别。
ev.currentTarget是绑定事件的元素,而ev.target是当前触发事件的元素。比如这里的分别是ul和li。
但也可能点击的是ul,这时ev.currentTarget和ev.target就相等了。

内联事件处理函数调用

1
2
ini复制代码<button class="btn1" onclick="console.log(this === document.querySelector('.btn1'))">点我呀</button>
<button onclick="console.log((function(){return this})());">再点我呀</button>

第一个是button本身,所以是true,第二个是window。这里跟严格模式没有关系。
当然我们现在不会这样用了,但有时不小心写成了这样,也需要了解。

其实this的使用场景还有挺多,比如对象object中的getter、setter的this,new Function()、eval。
但掌握以上几种,去分析其他的,就自然迎刃而解了。
使用比较多的还是普通函数调用、对象的函数调用、new调用、call、apply、bind调用、箭头函数调用。
那么他们的优先级是怎样的呢。

优先级

而箭头函数的this是上层普通函数的this或者是全局对象(浏览器中是window),所以排除,不算优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
javascript复制代码var name = 'window';
var person = {
name: 'person',
}
var doSth = function(){
console.log(this.name);
return function(){
console.log('return:', this.name);
}
}
var Student = {
name: '若川',
doSth: doSth,
}
// 普通函数调用
doSth(); // window
// 对象上的函数调用
Student.doSth(); // '若川'
// call、apply 调用
Student.doSth.call(person); // 'person'
new Student.doSth.call(person);

试想一下,如果是Student.doSth.call(person)先执行的情况下,那new执行一个函数。是没有问题的。
然而事实上,这代码是报错的。运算符优先级是new比点号低,所以是执行new (Student.doSth.call)(person)
而Function.prototype.call,虽然是一个函数(apply、bind也是函数),跟箭头函数一样,不能用new调用。所以报错了。

1
kotlin复制代码Uncaught TypeError: Student.doSth.call is not a constructor

这是因为函数内部有两个不同的方法:[[Call]]和[[Constructor]]。
当使用普通函数调用时,[[Call]]会被执行。当使用构造函数调用时,[[Constructor]]会被执行。call、apply、bind和箭头函数内部没有[[Constructor]]方法。

从上面的例子可以看出普通函数调用优先级最低,其次是对象上的函数。
call(apply、bind)调用方式和new调用方式的优先级,在《你不知道的JavaScript》是对比bind和new,引用了mdn的bind的ployfill实现,new调用时bind之后的函数,会忽略bind绑定的第一个参数,(mdn的实现其实还有一些问题,感兴趣的读者,可以看我之前的文章:面试官问:能否模拟实现JS的bind方法),说明new的调用的优先级最高。
所以它们的优先级是new 调用 > call、apply、bind 调用 > 对象上的函数调用 > 普通函数调用。

总结

如果要判断一个运行中函数的 this 绑定, 就需要找到这个函数的直接调用位置。 找到之后
就可以顺序应用下面这四条规则来判断 this 的绑定对象。

  1. new 调用:绑定到新创建的对象,注意:显示return函数或对象,返回值不是新创建的对象,而是显式返回的函数或对象。
  2. call 或者 apply( 或者 bind) 调用:严格模式下,绑定到指定的第一个参数。非严格模式下,null和undefined,指向全局对象(浏览器中是window),其余值指向被new Object()包装的对象。
  3. 对象上的函数调用:绑定到那个对象。
  4. 普通函数调用: 在严格模式下绑定到 undefined,否则绑定到全局对象。

ES6 中的箭头函数:不会使用上文的四条标准的绑定规则, 而是根据当前的词法作用域来决定this, 具体来说, 箭头函数会继承外层函数,调用的 this 绑定( 无论 this 绑定到什么),没有外层函数,则是绑定到全局对象(浏览器中是window)。 这其实和 ES6 之前代码中的 self = this 机制一样。

DOM事件函数:一般指向绑定事件的DOM元素,但有些情况绑定到全局对象(比如IE6~IE8的attachEvent)。

一定要注意,有些调用可能在无意中使用普通函数绑定规则。 如果想“ 更安全” 地忽略 this 绑
定, 你可以使用一个对象, 比如 ø = Object.create(null), 以保护全局对象。

面试官考察this指向就可以考察new、call、apply、bind,箭头函数等用法。从而扩展到作用域、闭包、原型链、继承、严格模式等。这就是面试官乐此不疲的原因。

读者发现有不妥或可改善之处,欢迎指出。另外觉得写得不错,可以点个赞,也是对笔者的一种支持。

考题

this指向考题经常结合一些运算符等来考察。看完本文,不妨通过以下两篇面试题测试一下。
小小沧海:一道常被人轻视的前端JS面试题

从这两套题,重新认识JS的this、作用域、闭包、对象

扩展阅读

你不知道的JavaScript 上卷

冴羽:JavaScript深入之从ECMAScript规范解读this

这波能反杀:前端基础进阶(五):全方位解读this

笔者精选文章

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK

学习 lodash 源码整体架构,打造属于自己的函数式编程类库

学习 underscore 源码整体架构,打造属于自己的函数式编程类库

学习 jQuery 源码整体架构,打造属于自己的 js 类库

面试官问:JS的继承

面试官问:JS的this指向

面试官问:能否模拟实现JS的call和apply方法

面试官问:能否模拟实现JS的bind方法

面试官问:能否模拟实现JS的new操作符

前端使用puppeteer 爬虫生成《React.js 小书》PDF并合并

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。

个人博客

segmentfault前端视野专栏,开通了前端视野专栏,欢迎关注~

掘金专栏,欢迎关注~

知乎前端视野专栏,开通了前端视野专栏,欢迎关注~

github blog,求个star^_^~

微信公众号 若川视野

可能比较有趣的微信公众号,长按扫码关注。也可以加微信 ruochuan12,注明来源,拉您进【前端视野交流群】。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…880881882…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%