0%

运行时框架

作业调度框架

table模块架构

进程

TaskExecutor

  1. taskmanager.numberOfTaskSlots:指定每个TaskExecutor的任务槽数量。默认情况下,该属性设置为1,表示每个TaskExecutor只能执行一个任务。如果需要在同一节点上并行执行多个任务,则需要将该属性设置为大于1的值。
  2. taskmanager.memory.process.size:指定TaskExecutor进程的内存大小。默认情况下,该属性设置为1GB。可以根据节点的资源情况和作业的内存需求进行适当调整。
  3. taskmanager.cpu.cores:指定TaskExecutor可以使用的CPU核心数量。默认情况下,该属性设置为所有可用的CPU核心数。可以根据节点的CPU资源情况和作业的CPU需求进行适当调整。
  4. taskmanager.tmp.dirs:指定TaskExecutor使用的临时文件目录。默认情况下,该属性设置为系统的临时目录。可以根据节点的磁盘空间情况和作业的磁盘需求进行适当调整。

核心

“纸上得来终觉浅,绝知此事要躬行”。

学会透过现象看本质

1. 扎实的计算机基础

我们需要储备计算机组成原理、操作系统、网络协议以及数据库等基础知识。具体的性能问题往往还与传输、计算、存储数据等相关,那我们还需要储备数据结构、算法以及数学等基础知识。

2. 习惯透过源码了解技术本质

我们需要深入源码,通过分析来学习、总结一项技术的实现原理和优缺点,这样我们就能更客观地去学习一项技术,还能透过源码来学习牛人的思维方式,收获更好的编码实现方式。

3. 善于追问和总结

“知其然且知所以然”才是我们积累经验的关键。知道了一项技术背后的实现原理,我们才能在遇到性能问题时,做到触类旁通。

调优政策

  • 分析查找问题是一个复杂而又细致的过程,某个性能问题可能是一个原因导致的,也可能是几个原因共同导致的结果。我们分析查找问题可以采用自下而上的方式,而我们解决系统性能问题,则可以采用自上而下的方式逐级优化。

1. 优化代码

2. 优化设计

3. 优化算法

4. 时间换空间

5. 空间换时间

6. 参数调优

7. 兜底策略,确保系统稳定性

数据结构

数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为 O(1),但在数组中间以及头部插入数据时,需要复制移动后面的元素。

链表:一种在物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。由于链表不用必须按顺序存储,所以链表在插入的时候可以达到 O(1) 的复杂度,但查找一个结点或者访问特定编号的结点需要 O(n) 的时间。

哈希表:根据关键码值(Key value)直接进行访问的数据结构。通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做哈希函数,存放记录的数组就叫做哈希表。

:由 n(n≥1)个有限结点组成的一个具有层次关系的集合,就像是一棵倒挂的树。

字符串性能优化

MGD7wV.md.jpg

  • 一个 char 字符占 16 位,2 个字节。这个情况下,存储单字节编码内的字符(占一个字节的字符)就显得非常浪费。JDK1.9 的 String 类为了节约内存空间,于是使用了占 8 位,1 个字节的 byte 数组来存放字符串

  • String 对象的不可变性

  • 对象和对象引用

  • 使用 String.intern 来节省内存空间,从而优化 String 对象的存储

    具体做法就是,在每次赋值的时候使用 String 的 intern 方法,如果常量池中有相同值,就会重复使用该对象,返回对象引用,这样一开始的对象就可以被回收掉。这种方式可以使重复性非常高的地址信息存储大小从 20G 降到几百兆。

    如果调用 intern 方法,会去查看字符串常量池中是否有等于该对象的字符串,如果没有,就在常量池中新增该对象,并返回该对象引用;如果有,就返回常量池中的字符串引用。堆内存中原有的对象由于没有引用指向它,将会通过垃圾回收器回收。

    1
    2
    3
    4
    5
    6
    String a =new String("abc").intern();
    String b = new String("abc").intern();

    if(a==b) {// a 和 b 引用的是同一个对象。
    System.out.print("a==b");//正常输出
    }
  • 用 String.indexOf() 方法代替 Split() 方法完成字符串的分割

慎重使用正则表达式

MGDWWQ.md.jpg

正则表达式是一个用正则符号写出的公式,程序对这个公式进行语法分析,建立一个语法分析树,再根据这个分析树结合正则表达式的引擎生成执行程序(这个执行程序我们把它称作状态机,也叫状态自动机),用于字符匹配。

如果使用正则表达式能使你的代码简洁方便,那么在做好性能排查的前提下,可以去使用;如果不能,那么正则表达式能不用就不用,以此避免造成更多的性能问题。

ArrayList VS LinkedList

  • 由于 ArrayList 是数组实现的,而数组是一块连续的内存空间,在添加元素到数组头部的时候,需要对头部以后的数据进行复制重排,所以效率很低;而 LinkedList 是基于链表实现,在添加元素的时候,首先会通过循环查找到添加元素的位置,如果要添加的位置处于 List 的前半段,就从前往后找;若其位置处于后半段,就从后往前找。因此 LinkedList 添加元素到头部是非常高效的。
  • LinkedList 的 for 循环性能是最差的,而 ArrayList 的 for 循环性能是最好的。

Stream

MY0E6A.md.jpg

命令

  • 清空数据

    cache -clear -c=xxxxx

  • lostPart

    1
    2
    cache -rebalance -c=xxxx
    cache -rebalance 命令不需要依赖于 cache -stop 的操作。启动或重新启动分区重平衡可以在缓存运行期间进行,不需要停止缓存。
  • 停止多个缓存表

cache -stop --caches=myCache_*

  • 重新平衡多个缓存表

cache -rebalance --caches=myCache1,myCache2,myCache3

  • 停止所有缓存表,

    1
    2
    cache -stop --all 
    cache -start --all
  • 重新加载所有缓存表并重新平衡

    ``cache -l -r –all`

    cache -clear 命令只是清空缓存中的数据,但是缓存仍然处于活动状态,可以继续使用;而cache -stop 命令则停止了缓存,并且清空了所有数据,需要使用 cache -start 命令重新启动缓存,并重新加载数据。

​ ``cache -start命令用于启动一个已经停止的缓存。如果缓存之前已经存在并被停止,使用cache -start` 命令可以重新启动该缓存,并开始加载数据。如果该缓存之前不存在,则会创建一个新的缓存并开始加载数据。

  • 查看表的状态

使用 cache -state 命令查看指定缓存表的状态,包括缓存大小、分区数量、复制和备份数等信息。


设置复制和备份数量:
cache -rebalance -r -b
其中是要设置的复制数量,是要设置的备份数量。

设置备份数量为1,即每个分区都在一个不同的节点上保留1个备份数量
设置复制数量为2,即每个缓存表的分区都会在集群中的两个节点上复制

复制和备份都是为了提高数据可用性和可靠性,但复制是将同一份数据存储在多个节点上,而备份是将数据副本存储在集群中的不同节点上。在配置Ignite缓存表时,可以同时设置复制和备份的数量来确保数据的高可用性和可靠性。


如果有三个Ignite节点,可以按照以下建议设置缓存表的复制和备份数量:

复制因子建议设置为2,这样每个分区的数据都会在两个节点上进行备份,当一个节点故障时,可以保证数据的可靠性。
例如,使用ignitevisorcmd.sh命令可以这样设置:cache -set-replicas myCache 2
备份数量建议设置为1,这样每个分区的数据都会在一个节点上进行备份,当一个节点故障时,可以保证数据的可靠性,并且减少额外的网络开销。
例如,使用ignitevisorcmd.sh命令可以这样设置:cache -set-backups myCache 1

需要注意的是,设置复制和备份数量会增加网络和存储开销,因此需要根据具体场景进行权衡和调整。
此外,还需要根据数据大小、访问模式、节点配置等因素进行性能测试和优化。

知识点

ignite-2.7.0

  • 编译ignite-core模块的时候需要使用jdk8,因为会报jdk.internal.misc.SharedSecrets找不到的错误原因:估计是在GridUnsafe.java中的miscPackage方法以及javaNioAccessObject的Class<?> cls = Class.forName(pkgName + ".misc.SharedSecrets");出现的问题,jdk11中SharedSecrets出现的位置是jdk.internal.access.SharedSecrets

持久化

  • Ignite的原生持久化会在磁盘上存储一个数据的超集,以及根据容量在内存中存储一个子集。比如,如果有100个条目,然后内存只能存储20条,那么磁盘上会存储所有的100条,然后为了提高性能在内存中缓存20条。
  • 和纯内存的使用场景一样,当打开持久化时,每个独立的节点只会持久化数据的一个子集,不管是主还是备节点,都是只包括节点所属的分区的数据,总的来说,整个集群包括了完整的数据集。
  • 在开发应用时可能需要修改自定义对象字段的类型。例如假设对象A的字段类型A.rangeint类型,然后决定将A.range的类型修改为long类型。之后会发现集群或应用将无法重启,因为Ignite不支持字段/列类型的更改。
  • 无法变更枚举值的顺序,也无法在枚举值列表的开始或者中部添加新的常量,但是可以在列表的末尾添加新的常量。

ZooKeeper Discovery

ZooKeeper Discovery是为需要保持易扩展性和线性性能的大规模部署而设计的。然而,同时使用Ignite和ZooKeeper需要配置和管理两个分布式系统,这可能是一个挑战。因此,我们建议你只有在计划扩展到100个或1000个节点时才使用ZooKeeper Discovery。否则,最好使用TCP/IP发现。


目录结构

  • marshaller

    $IGNITE_HOME/work/db/marshaller 类classpath信息

  • binary_meta

    $IGNITE_HOME/work/db/binary_meta 类的元素信息

下面三个配置项是在配置文件中进行配置

  • storagePath

    $RDX_HOME/data/ignite/persistent 持久化文件

  • walPath

    $RDX_HOME/data/ignite/wal_store 持久化数据的元数据信息(类名、元素名、位置)

    • walPath和storagePath存储的数据的关联
  • walArchivePath

    $RDX_HOME/data/ignite/wal_archive

    • 和walPath存储的数据结构一样,二者的关系

Ignite and ZooKeeper Configuration Considerations

  • zookeeper中 tickTime和syncLimit参数的定义

When using ZooKeeper Discovery, you need to make sure that the configuration parameters of the ZooKeeper cluster and Ignite cluster match each other.

Consider a sample ZooKeeper configuration, as follows:

1
2
3
4
5
# The number of milliseconds of each tick
tickTime=2000

# The number of ticks that can pass between sending a request and getting an acknowledgement
syncLimit=5
  • 在zookeeper中 tickTime和syncLimit参数的作用

Configured this way, ZooKeeper server detects its own segmentation from the rest of the ZooKeeper cluster only after tickTime * syncLimit elapses. Until this event is detected at ZooKeeper level, all Ignite nodes connected to the segmented ZooKeeper server do not try to reconnect to the other ZooKeeper servers.

  • 在ignite中 sessionTimeout参数与zookeeper的tickTime和syncLimit参数的关联

On the other hand, there is a sessionTimeout parameter on the Ignite side that defines how soon ZooKeeper closes an Ignite node’s session if the node gets disconnected from the ZooKeeper cluster. If sessionTimeout is smaller than tickTime * syncLimit , then the Ignite node is notified by the segmented ZooKeeper server too late — its session expires before it tries to reconnect to other ZooKeeper servers.

To avoid this situation, sessionTimeout should be bigger than tickTime * syncLimit.

内存模型

jobmanager

  • JVM Heap大小设置对应配置文件flink-conf.yamljobmanager.heap.size

taskmanager

  • Total Flink Memory对应flink-conf.yamltaskmanager.memory.flink.size
  • JVM Metaspace对应flink-conf.yamltaskmanager.memory.jvm-metaspace.size
  • Total Process Memory对应flink-conf.yamltaskmanager.memory.process.size
  • 含义
    TaskManager进程占用的所有与Flink相关的内存(不包括JVM元空间和其他额外开销)。具体包含4大块:Flink框架内存(堆内、堆外)、托管内存(仅堆外)、网络缓存(仅堆外)、任务内存(堆内、堆外)。

  • 参数
    taskmanager.memory.flink.size:无默认值,需要用户指定。

Flink框架(Framework)内存

  • 含义
    Flink Runtime底层占用的内存,一般来讲相对固定,不需要更改。极特殊情况下才需要调大一些,比如非常高的算子并行度,或者与外部系统(如Hadoop)有密集交互等等。

  • 参数

    taskmanager.memory.framework.heap.size:堆内部分(Framework Heap),默认值128MB;

    taskmanager.memory.framework.off-heap.size:堆外部分(Framework Off-Heap),以直接内存形式分配,默认值128MB。

托管(Managed)内存

  • 含义
    纯堆外内存,由MemoryManager管理,用于中间结果缓存、排序、哈希表等,以及RocksDB状态后端。可见,RocksDB消耗的内存可以由用户显式控制了,不再像旧版本一样难以预测和调节。

  • 参数

    taskmanager.memory.managed.fraction:托管内存占Flink总内存taskmanager.memory.flink.size的比例,默认值0.4;taskmanager.memory.managed.size:托管内存的大小,无默认值,一般也不指定,而是依照上述比例来推定,更加灵活。

网络(Network)缓存

  • 含义
    纯堆外内存,用于TaskManager之间(shuffle、广播等)及与外部组件的数据传输,以直接内存形式分配。

  • 参数

    taskmanager.memory.network.min:网络缓存的最小值,默认64MB;taskmanager.memory.network.max:网络缓存的最大值,默认1GB;taskmanager.memory.network.fraction:网络缓存占Flink总内存taskmanager.memory.flink.size的比例,默认值0.1。若根据此比例算出的内存量比最小值小或比最大值大,就会限制到最小值或者最大值。

任务(Task)内存

  • 含义
    顾名思义,是算子逻辑和用户代码、自定义数据结构真正占用的内存。

  • 参数

    taskmanager.memory.task.heap.size:堆内部分(Task Heap),无默认值,一般不建议设置,会自动用Flink总内存减去框架、托管、网络三部分的内存推算得出。taskmanager.memory.task.off-heap.size:堆外部分(Task Off-Heap),以直接内存形式分配,默认值为0,即不使用。

TaskManager进程总内存(Total Process Memory)
  • 含义
    在容器化部署(on YARN/K8s/Mesos)环境下使用,是Flink总内存、JVM元空间与JVM额外内存开销的和,也就是容器本身的内存大小。

  • 参数
    taskmanager.memory.process.size:无默认值,需要用户指定。

JVM元空间(Metaspace)
  • 含义
    存放已被虚拟机加载的类信息、常量、静态变量,即编译器编译后的代码

  • 参数
    taskmanager.memory.jvm-metaspace.size:默认值256MB。

JVM额外开销(Overhead)

  • 含义
    为JVM预留的其他本地内存,用于线程栈、代码缓存等,作用有些类似于之前版本中为容器预留的截断(cutoff)内存。当然在1.10版本中,原先的containerized.heap-cutoff-ratio与containerized.heap-cutoff-min参数对TM就不再生效了。

  • 参数

    taskmanager.memory.jvm-overhead.min:JVM额外开销的最小值,默认192MB;taskmanager.memory.jvm-overhead.max:JVM额外开销的最大值,默认1GB;taskmanager.memory.jvm-overhead.fraction:JVM额外开销占TM进程总内存taskmanager.memory.process.size(注意不是Flink总内存)的比例,默认值0.1。若根据此比例算出的内存量比最小值小或比最大值大,就会限制到最小值或者最大值。

Flink内存参数与JVM参数的关系

上述内存参数会直接影响启动TaskManager时使用的JVM参数,使用相关工具可以观察到。

  • -Xmx/-Xms:对应堆内框架内存与任务内存之和;

  • -XX:MaxDirectMemorySize:对应三块直接内存,即堆外框架内存、任务内存与网络缓存之和;

  • -XX:MaxMetaspaceSize:对应JVM元空间设置。

配置优化

  • 节点平均分配
    1
    cluster.evenly-spread-out-slots: true

reference

Flink 1.10之改进的TaskManager内存模型与配置【附源码】_wx5c7a97e3804fd_51CTO博客

常用

  • ctrl + P
    列出参数列表(使用比较多 )
  • ctrl + shift + enter
    当在括号里输入完最后一个参数时候他会直接光标跳到最后并且添加分号,不需要自己按向左键移动光标。(使用比较多 )
    ctrl + enter 向下插入一行,光标位置不变
  • shift + enter
    向下插入一行。光标位置进入下一行(可以在一行的任何位置换行,而不必将光标移动到行尾,使用比较多)
  • ctrl+left/right
    在单词间跳动光标,基本在所有的编辑器中都是适用 (使用比较多 )
  • ctr+shitf+left/right
    快速选中,基本在所有的编辑器中都是适用(shift选中,ctrl+left/right在单词中快速跳动,使用比较多 )
  • Alt+left/right,切换代码视图(使用比较多 )
  • Alt+Up/Down,在方法间快速移动定位(使用比较多 )
  • F2 或 Shift+F2,高亮错误或警告快速定位(使用比较多)
  • Ctrl+Alt+T
    可以把代码包在一个块内,例如:try/catch(使用比较多 )
  • Ctrl+F,处于查找状态下按down向下查找,up向上查找(使用比较多)
  • Ctrl+F12,可以显示当前文件的结构(快速查找当前类中某个方法,使用比较多)
  • Ctrl+Shift+W,取消选择光标所在词(使用比较多)
  • Ctrl+[ OR ],可以跑到大括号的开头与结尾
  • Ctrl+Shift +[ OR ],选择大括号中的内容
  • alt + 1 同上(在工程目录和编辑页面间切换,使用比较多)
  • Ctrl+Alt+O,优化导入的类和包(去掉没有使用的import,使用比较多 )
  • Shift+F6,重构 - 重命名(使用比较多 )
  • Ctrl+G,定位行
  • Ctrl+H,显示类结构图(类的继承层次)
  • Ctrl+U,转到父类
  • Ctrl+”+/-“,当前方法展开、折叠
  • Ctrl+Shift+”+/-“,全部展开、折叠
  • Ctrl+Shift+J,整合两行
  • ctrl + shift + space
    当new 一个变量的时候,已经写了变量类型,打了等号之后按这个可以快速完成。

Compile and Run

nRX0JI.jpg

Debugging

nRXhYn.jpg

Editing

nRXwFA.jpg

General

nRX2wQ.jpg

Live Templates

nRXRoj.jpg

nRXBWt.jpg

Refactoring

nRXsQf.jpg

Search/Replace

nRXrSP.jpg

nRXyy8.jpg

VCS/Local History

nRX6OS.jpg

Docker 包括三个基本概念:

  • 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。
  • 容器(Container):镜像(Image)和容器(Container)的关系,就像是面向对象程序设计中的类和实例一样,镜像是静态的定义,容器是镜像运行时的实体。容器可以被创建、启动、停止、删除、暂停等。
  • 仓库(Repository):仓库可看着一个代码控制中心,用来保存镜像。

操作镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
查找
docker search xxx
拉取
docker pull xxx
运行(容器)
docker run xxx
删除
docker rmi xxx

docker images : 列出本地镜像。

语法
docker images [OPTIONS] [REPOSITORY[:TAG]]
OPTIONS说明:
-a :列出本地所有的镜像(含中间映像层,默认情况下,过滤掉中间映像层);
–digests :显示镜像的摘要信息;
-f :显示满足条件的镜像;
–format :指定返回值的模板文件;
–no-trunc :显示完整的镜像信息;
-q :只显示镜像ID。
  • 导出镜像

    1
    2
    docker save $REPOSITORY:$TAG > $PATH/$NAME.tar
    # 注意$后面的变量需要替换
  • 导入镜像

    1
    docker load --input $PATH/$NAME.tar
  • 在镜像中运行容器

    1
    2
    3
    4
    5
    6
    7
    8
    docker run -it -p 8899:8899 --name test $REPOSITORY:$TAG

    -it:表示交互式终端的容器,非启动后立刻结束的容器
    --name test:给容器取个名字,嫌麻烦可以省去
    $REPOSITORY:$TAG:容器是用哪个镜像启动的(一个容器,必须依赖一个镜像启动)
    -v 指定了宿主机上的目录用来作为docker仓库的存储位置,如果不指定的话,registry镜像重启之后会丢失已经存储在本地仓库的镜像文件
    -p hostPort:containerPort
    -v hostVolume:containerVolume 这两个地址如果不存在都会创建,一旦容器运行,两部分会完全同步

    volume

Docker Image可以理解成多个只读文件叠加而成,因此Docker Image是只读的。

当我们将其运行起来,就相当于在只读的Image外包裹了一层读写层变成了容器。

当你删除容器之后,使用这个镜像重新创建一个容器,此时的镜像的只读层还和原来的一样,但是你在读写层的修改全部都会丢失(没有使用原有volume的前提下)。

docker使用volume实现数据的持久化,不仅如此volume还能帮助容器和容器之间,容器和host之间共享数据。

  • 进入运行的容器

    1
    docker exec -it $CONTAINER_ID /bin/bash
    • 注意

      命令中的/bin/bash是根据 docker inspect $image_id,根据”Cmd”配置项决定的

把jar包程序制作成镜像

  • step1:

    编辑dockerfile

1
2
3
4
5
6
7
8
9
10
11
# 基础镜像
FROM openjdk:8-jre

# 指定路径
WORKDIR /data/justMicroservice/learning-cloudalibaba

ARG JAR_FILE=nacos-provider/target/nacos-provider-0.0.1-SNAPSHOT.jar
# 复制jar文件到路径
COPY ${JAR_FILE} nacos-provider.jar
# 启动网关服务
ENTRYPOINT ["java","-jar","nacos-provider.jar"]
  • Step2:

    在$path的一级目录执行(比如dockerfile位置/a/b/dockerfile,那命令在a目录下执行)

1
docker build -t $repository -f $path/dockerfile .

add git commit info to image tag

1
2
3
4
5
6
7
8
9
10
#!/bin/sh

IMAGE_NAME=your-image-name
echo "image name: $IMAGE_NAME"

GIT_BRANCH=$(git symbolic-ref --short HEAD)
LAST_COMMIT=$(git rev-parse HEAD)
echo "git commit info : $LAST_COMMIT"

docker build . -t $IMAGE_NAME:"$GIT_BRANCH-$LAST_COMMIT"

Manage Docker as a non-root user

  1. Create the docker group.

    1
    $ sudo groupadd docker
  2. Add your user to the docker group.

    1
    $ sudo usermod -aG docker $USER
  3. Log out and log back in so that your group membership is re-evaluated.

    If testing on a virtual machine, it may be necessary to restart the virtual machine for changes to take effect.

    On a desktop Linux environment such as X Windows, log out of your session completely and then log back in.

    On Linux, you can also run the following command to activate the changes to groups:

    1
    $ newgrp docker 
  4. Verify that you can run docker commands without sudo.

    1
    $ docker run hello-world

Failed to load listeners: no sockets found via socket activation: make sure the service was started by systemd

1
2
3
4
5
6
7
edit the docker’s systemd unit file and change the ExecStart:
sudo systemctl edit docker.service

edite The value ExecStart, change fd:// to unix://:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H unix://

Error creating default “bridge” network: cannot create network (docker0): conflicts with network (docker0): networks have same bridge name

1
2
sudo rm -rf /var/lib/docker/network
sudo systemctl start docker

文字转载自JS 中 this 在各个场景下的指向

1. this 的奥秘

很多时候, JS 中的 this 对于咱们的初学者很容易产生困惑不解。 this 的功能很强大,但需要一定付出才能慢慢理解它。

对Java、PHP或其他标准语言来看,this 表示类方法中当前对象的实例。大多数情况下,this不能在方法之外使用,这样就比较不会造成混淆。

在J要中情况就有所不同: this表示函数的当前执行上下文,JS 中函数调用主要有以下几种方式:

  • 函数调用: alert('Hello World!')
  • 方法调用: console.log('Hello World!')
  • 构造函数: new RegExp('\\d')
  • 隐式调用: alert.call(undefined, 'Hello World!')

每种调用类型以自己的方式定义上下文,所以就很容易产生混淆。

此外,严格模式也会影响执行上下文。

理解this关键是要清楚的知道函数调用及其如何影响上下文。

本文主要说明函数的调用方式及如何影响 this,并且说明执行上下文的常见陷阱。

在开始之前,先知道几个术语:

调用函数正在执行创建函数体的代码,或者只是调用函数。 例如,parseInt函数调用是parseInt(’15’)。

  • 函数调用:执行构成函数主体的代码:例如,parseInt函数调用是parseInt('15')
  • 调用的上下文:指 this 在函数体内的值。 例如,map.set('key', 'value')的调用上下文是 map
  • 函数的作用域:是在函数体中可访问的变量、对象和函数的集合。

2.函数调用

当一个表达式为函数接着一个(,一些用逗号分隔的参数以及一个时,函数调用被执行,例如parseInt('18')

函数调用表达式不能是属性方式的调用,如 obj.myFunc(),这种是创建一个方法调用。再如 [1,5].join(',')不是函数调用,而是方法调用,这种区别需要记住哈,很重要滴

函数调用的一个简单示例:

1
2
3
4
5
6
7
function hello(name) {
return 'Hello ' + name + '!';
}
// 函数调用
const message = hello('World');
console.log(message); // => 'Hello World!'
复制代码

hello('World')是函数调用: hello表达式等价于一个函数,跟在它后面的是一对括号以及'World'参数。

一个更高级的例子是IIFE(立即调用的函数表达式)

1
2
3
4
5
const message = (function(name) {
return 'Hello ' + name + '!';
})('World');
console.log(message) // => 'Hello World!'
复制代码

IIFE也是一个函数调用:第一对圆括号(function(name) {...})是一个表达式,它的计算结果是一个函数对象,后面跟着一对圆括号,圆括号的参数是“World”

2.1. 在函数调用中的this

this 在函数调用中是一个全局对象

局对象由执行环境决定。在浏览器中,thiswindow 对象。

img

在函数调用中,执行上下文是全局对象。

再来看看下面函数中的上下文又是什么鬼:

1
2
3
4
5
6
7
8
9
10
function sum(a, b) {
console.log(this === window); // => true
this.myNumber = 20; // 将'myNumber'属性添加到全局对象
return a + b;
}
// sum() is invoked as a function
// sum() 中的 `this` 是一个全局对象(window)
sum(15, 16); // => 31
window.myNumber; // => 20
复制代码

在调用sum(15,16)时,JS 自动将this设置为全局对象,在浏览器中该对象是window

this在任何函数作用域(最顶层作用域:全局执行上下文)之外使用,this 表示 window 对象

1
2
3
4
5
6
7
8
9
console.log(this === window); // => true
this.myString = 'Hello World!';
console.log(window.myString); // => 'Hello World!'

<!-- In an html file -->
<script type="text/javascript">
console.log(this === window); // => true
</script>
复制代码

2.2 严格模式下的函数调用 this 又是什么样的

this 在严格模式下的函数调用中为 undefined

严格模式是在 ECMAScript 5.1中引入的,它提供了更好的安全性和更强的错误检查。

要启用严格模式,函数头部写入use strict 即可。

启用后,严格模式会影响执行上下文,this 在常规函数调用中值为undefined。 与上述情况2.1相反,执行上下文不再是全局对象。

img

严格模式函数调用示例:

1
2
3
4
5
6
7
function multiply(a, b) {
'use strict'; // 启用严格模式
console.log(this === undefined); // => true
return a * b;
}
multiply(2, 5); // => 10
复制代码

multiply(2,5)作为函数调用时,thisundefined

严格模式不仅在当前作用域中有效,在内部作用域中也是有效的(对于在内部声明的所有函数):

1
2
3
4
5
6
7
8
9
10
11
12
13
function execute() {
'use strict'; // 开启严格模式
function concat(str1, str2) {
// 严格模式仍然有效
console.log(this === undefined); // => true
return str1 + str2;
}
// concat() 在严格模式下作为函数调用
// this in concat() is undefined
concat('Hello', ' World!'); // => "Hello World!"
}
execute();
复制代码

'use strict'被插入到执行体的顶部,在其作用域内启用严格模式。 因为函数concat是在执行的作用域中声明的,所以它继承了严格模式。

单个JS文件可能包含严格和非严格模式。 因此,对于相同的调用类型,可以在单个脚本中具有不同的上下文行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function nonStrictSum(a, b) {
// 非严格模式
console.log(this === window); // => true
return a + b;
}
function strictSum(a, b) {
'use strict';
// 启用严格模式
console.log(this === undefined); // => true
return a + b;
}

nonStrictSum(5, 6); // => 11
strictSum(8, 12); // => 20
复制代码

2.3 陷阱:this 在内部函数中的时候

函数调用的一个常见陷阱是,认为this在内部函数中的情况与外部函数中的情况相同。

正确地说,内部函数的上下文只依赖于它的调用类型,而不依赖于外部函数的上下文。

要将 this 设置为所需的值,可以通过 .call().apply()修改内部函数的上下文或使用.bind()创建绑定函数。

下面的例子是计算两个数的和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
console.log(this === numbers); // => false
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => NaN
复制代码

sum()是对象上的方法调用,所以sum中的上下文是numbers对象。calculate函数是在sum中定义的,你可能希望在calculate()this也表示number对象。

calculate()是一个函数调用(不是方法调用),它将this作为全局对象window(非严格模下)。即使外部函数sum将上下文作为number对象,它在calculate里面没有影响。

sum()的调用结果是NaN,不是预期的结果5 + 10 = 15,这都是因为没有正确调用calculate

为了解决这个问题,calculate函数中上下文应该与 sum 中的一样,以便可以访问numberAnumberB属性。

一种解决方案是通过调用calculator.call(this)手动将calculate上下文更改为所需的上下文。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
function calculate() {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
// 使用 .call() 方法修改上下文
return calculate.call(this);
}
};
numbers.sum(); // => 15
复制代码

call(this)像往常一样执行calculate函数,但 call 会把上下文修改为指定为第一个参数的值。

现在this.numberA + this.numberB相当于numbers.numberA + numbers.numberB。 该函数返回预期结果5 + 10 = 15

另一种就是使用箭头函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const numbers = {
numberA: 5,
numberB: 10,
sum: function() {
console.log(this === numbers); // => true
const calculate = () => {
console.log(this === numbers); // => true
return this.numberA + this.numberB;
}
return calculate();
}
};
numbers.sum(); // => 15
复制代码

3.方法调用

方法是存储在对象属性中的函数。例如

1
2
3
4
5
6
7
8
const myObject = {
// helloFunction 是一个方法
helloFunction: function() {
return 'Hello World!';
}
};
const message = myObject.helloFunction();
复制代码

helloFunctionmyObject的一个方法,要调用该方法,可以这样子调用 :myObject.helloFunction

当一个表达式以属性访问的形式执行时,执行的是方法调用,它相当于以个函数接着(,一组用逗号分隔的参数以及)。

利用前面的例子,myObject.helloFunction()是对象myObject上的一个helloFunction的方法调用。[1, 2].join(',')/\s/.test('beautiful world')也被认为是方法调用。

区分函数调用和方法调用非常重要,因为它们是不同的类型。主要区别在于方法调用需要一个属性访问器形式来调用函数(obj.myFunc()obj['myFunc']()),而函数调用不需要(myFunc())。

1
2
3
4
5
6
7
8
9
10
11
12
13
['Hello', 'World'].join(', '); // 方法调用
({ ten: function() { return 10; } }).ten(); // 方法调用
const obj = {};
obj.myFunction = function() {
return new Date().toString();
};
obj.myFunction(); // 方法调用

const otherFunction = obj.myFunction;
otherFunction(); // 函数调用
parseFloat('16.60'); // 函数调用
isNaN(0); // 函数调用
复制代码

理解函数调用和方法调用之间的区别有助于正确识别上下文。

3.1 方法调用中 this 是肿么样

在方法调用中,this是拥有这个方法的对象

当调用对象上的方法时,this就变成了对象本身。

img

创建一个对象,该对象有一个递增数字的方法

1
2
3
4
5
6
7
8
9
10
11
12
const calc = {
num: 0,
increment: function() {
console.log(this === calc); // => true
this.num += 1;
return this.num;
}
};
// method invocation. this is calc
calc.increment(); // => 1
calc.increment(); // => 2
复制代码

调用calc.increment()使increment函数的上下文成为calc对象。所以使用this.num来增加num属性是有效的。

再来看看另一个例子。JS对象从原型继承一个方法,当在对象上调用继承的方法时,调用的上下文仍然是对象本身

1
2
3
4
5
6
7
8
9
10
const myDog = Object.create({
sayName: function() {
console.log(this === myDog); // => true
return this.name;
}
});
myDog.name = 'Milo';
// 方法调用 this 指向 myDog
myDog.sayName(); // => 'Milo'
复制代码

Object.create()创建一个新对象myDog,并根据第一个参数设置其原型。myDog对象继承sayName方法。

执行myDog. sayname()时,myDog是调用的上下文。

在EC6 class 语法中,方法调用上下文也是实例本身

1
2
3
4
5
6
7
8
9
10
11
12
13
class Planet {
constructor(name) {
this.name = name;
}
getName() {
console.log(this === earth); // => true
return this.name;
}
}
var earth = new Planet('Earth');
// method invocation. the context is earth
earth.getName(); // => 'Earth'
复制代码

3.2 陷阱:将方法与其对象分离

方法可以从对象中提取到一个单独的变量const alone = myObj.myMethod。当方法单独调用时,与原始对象alone()分离,你可能认为当前的this就是定义方法的对象myObject

如果方法在没有对象的情况下调用,那么函数调用就会发生,此时的this指向全局对象window严格模式下是undefined

下面的示例定义了Animal构造函数并创建了它的一个实例:myCat。然后setTimout()在1秒后打印myCat对象信息

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => false
console.log('The ' + this.type + ' has ' + this.legs + ' legs');
}
}
const myCat = new Animal('Cat', 4);
// The undefined has undefined legs
setTimeout(myCat.logInfo, 1000);
复制代码

你可能认为setTimout调用myCat.loginfo()时,它应该打印关于myCat对象的信息。

不幸的是,方法在作为参数传递时与对象是分离,setTimout(myCat.logInfo)以下情况是等效的:

1
2
3
4
5
setTimout(myCat.logInfo);
// 等价于
const extractedLogInfo = myCat.logInfo;
setTimout(extractedLogInfo);
复制代码

将分离的logInfo作为函数调用时,this是全局 window,所以对象信息没有正确地打印。

函数可以使用.bind()方法与对象绑定,就可以解决 this 指向的问题。

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = function() {
console.log(this === myCat); // => true
console.log('The ' + this.type + ' has ' + this.legs + ' legs');
};
}
const myCat = new Animal('Cat', 4);
// logs "The Cat has 4 legs"
setTimeout(myCat.logInfo.bind(myCat), 1000);
复制代码

myCat.logInfo.bind(myCat)返回一个新函数,它的执行方式与logInfo完全相同,但是此时的 this 指向 myCat,即使在函数调用中也是如此。

另一种解决方案是将logInfo()方法定义为一个箭头函数:

1
2
3
4
5
6
7
8
9
10
11
12
function Animal(type, legs) {
this.type = type;
this.legs = legs;
this.logInfo = () => {
console.log(this === myCat); // => true
console.log('The ' + this.type + ' has ' + this.legs + ' legs');
};
}
const myCat = new Animal('Cat', 4);
// logs "The Cat has 4 legs"
setTimeout(myCat.logInfo, 1000);
复制代码

4. 构造函数调用

new关键词紧接着函数对象,(,一组逗号分隔的参数以及)时被调用,执行的是构造函数调用如new RegExp('\\d')

声明了一个Country函数,并且将它作为一个构造函数调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function Country(name, traveled) {
this.name = name ? name : 'United Kingdom';
this.traveled = Boolean(traveled);
}
Country.prototype.travel = function() {
this.traveled = true;
};
// 构造函数调用
const france = new Country('France', false);
// 构造函数调用
const unitedKingdom = new Country;

france.travel(); // Travel to France
复制代码

new Country('France', false)Country函数的构造函数调用。它的执行结果是一个name属性为'France'的新的对象。 如果这个构造函数调用时不需要参数,那么括号可以省略:new Country

从ES6开始,JS 允许用class关键词来定义构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
class City {
constructor(name, traveled) {
this.name = name;
this.traveled = false;
}
travel() {
this.traveled = true;
}
}
// Constructor invocation
const paris = new City('Paris', false);
paris.travel();
复制代码

new City('Paris')是构造函数调用。这个对象的初始化由这个类中一个特殊的方法constructor来处理。其中,this指向新创建的对象。

构造函数创建了一个新的空的对象,它从构造函数的原型继承了属性。构造函数的作用就是去初始化这个对象。 可能你已经知道了,在这种类型的调用中,上下文指向新创建的实例。

当属性访问myObject.myFunction前面有一个new关键词时,JS会执行构造函数调用而不是原来的方法调用。

例如new myObject.myFunction():它相当于先用属性访问把方法提取出来extractedFunction = myObject.myFunction,然后利用把它作为构造函数创建一个新的对象: new extractedFunction()

4.1. 构造函数中的 this

在构造函数调用中 this 指向新创建的对象

构造函数调用的上下文是新创建的对象。它利用构造函数的参数初始化新的对象,设定属性的初始值,添加事件处理函数等等。

img

来看看下面示例中的上下文

1
2
3
4
5
6
7
8
function Foo () {
console.log(this instanceof Foo); // => true
this.property = 'Default Value';
}
// Constructor invocation
const fooInstance = new Foo();
fooInstance.property; // => 'Default Value'
复制代码

new Foo() 正在进行构造函数调用,其中上下文是fooInstance。 在Foo内部初始化对象:this.property被赋值为默认值。

同样的情况在用class语法(从ES6起)时也会发生,唯一的区别是初始化在constructor方法中进行:

1
2
3
4
5
6
7
8
9
10
class Bar {
constructor() {
console.log(this instanceof Bar); // => true
this.property = 'Default Value';
}
}
// Constructor invocation
const barInstance = new Bar();
barInstance.property; // => 'Default Value'
复制代码

4.2. 陷阱: 忘了使用 new

有些JS函数不是只在作为构造函数调用的时候才创建新的对象,作为函数调用时也会,例如RegExp

1
2
3
4
5
6
7
var reg1 = new RegExp('\\w+');
var reg2 = RegExp('\\w+');

reg1 instanceof RegExp; // => true
reg2 instanceof RegExp; // => true
reg1.source === reg2.source; // => true
复制代码

当执行的 new RegExp('\\w+')RegExp('\\w+')时,JS 会创建等价的正则表达式对象。

使用函数调用来创建对象存在一个潜在的问题(不包括工厂模式),因为一些构造函数可能会忽略在缺少new关键字时初始化对象的逻辑。

下面的例子说明了这个问题:

1
2
3
4
5
6
7
8
9
10
11
function Vehicle(type, wheelsCount) {
this.type = type;
this.wheelsCount = wheelsCount;
return this;
}
// 忘记使用 new
const car = Vehicle('Car', 4);
car.type; // => 'Car'
car.wheelsCount // => 4
car === window // => true
复制代码

Vehicle是一个在上下文对象上设置typewheelsCount属性的函数。

当执行Vehicle('Car', 4)时,返回一个对象Car,它具有正确的属性:Car.typeCarCar.wheelsCount4,你可能认为它很适合创建和初始化新对象。

然而,在函数调用中,thiswindow对象 ,因此 Vehicle('Car',4)window 对象上设置属性。 显然这是错误,它并没有创建新对象。

当你希望调用构造函数时,确保你使用了new操作符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Vehicle(type, wheelsCount) {
if (!(this instanceof Vehicle)) {
throw Error('Error: Incorrect invocation');
}
this.type = type;
this.wheelsCount = wheelsCount;
return this;
}
// Constructor invocation
const car = new Vehicle('Car', 4);
car.type // => 'Car'
car.wheelsCount // => 4
car instanceof Vehicle // => true

// Function invocation. Throws an error.
const brokenCar = Vehicle('Broken Car', 3);
复制代码

new Vehicle('Car',4) 运行正常:创建并初始化一个新对象,因为构造函数调用中时使用了new关键字。

在构造函数里添加了一个验证this instanceof Vehicle来确保执行的上下文是正确的对象类型。如果this不是Vehicle,那么就会报错。这样,如果执行Vehicle('Broken Car', 3)(没有new),我们会得到一个异常:Error: Incorrect invocation

5. 隐式调用

使用myFun.call()myFun.apply()方法调用函数时,执行的是隐式调用。

JS中的函数是第一类对象,这意味着函数就是对象,对象的类型为Function。从函数对象的方法列表中,.call().apply()用于调用具有可配置上下文的函数。

  • 方法 .call(thisArg[, arg1[, arg2[, ...]]])将接受的第一个参数thisArg作为调用时的上下文,arg1, arg2, ...这些则作为参数传入被调用的函数。
  • 方法.apply(thisArg, [args])将接受的第一个参数thisArg作为调用时的上下文,并且接受另一个类似数组的对象[arg1, arg2, ...]作为被调用函数的参数传入。

下面是隐式调用的例子

1
2
3
4
5
6
function increment(number) {
return ++number;
}
increment.call(undefined, 10); // => 11
increment.apply(undefined, [10]); // => 11
复制代码

increment.call()increment.apply()都用参数10调用了这个自增函数。

两者的区别是.call()接受一组参数,例如myFunction.call(thisValue, 'value1', 'value2')。而.apply()接受的一组参数必须是一个类似数组的对象,例如myFunction.apply(thisValue, ['value1', 'value2'])。

5.1. 隐式调用中的this

在隐式调用.call()或.apply()中,this是第一个参数

很明显,在隐式调用中,this作为第一个参数传递给.call().apply()

1
2
3
4
5
6
7
8
var rabbit = { name: 'White Rabbit' };
function concatName(string) {
console.log(this === rabbit); // => true
return string + this.name;
}
concatName.call(rabbit, 'Hello '); // => 'Hello White Rabbit'
concatName.apply(rabbit, ['Bye ']); // => 'Bye White Rabbit'
复制代码

当应该使用特定上下文执行函数时,隐式调用非常有用。例如为了解决方法调用时,this总是window或严格模式下的undefined的上下文问题。隐式调用可以用于模拟在一个对象上调用某个方法。

1
2
3
4
5
6
7
8
9
10
11
12
function Runner(name) {
console.log(this instanceof Rabbit); // => true
this.name = name;
}
function Rabbit(name, countLegs) {
console.log(this instanceof Rabbit); // => true
Runner.call(this, name);
this.countLegs = countLegs;
}
const myRabbit = new Rabbit('White Rabbit', 4);
myRabbit; // { name: 'White Rabbit', countLegs: 4 }
复制代码

Rabbit中的Runner.call(this, name)隐式调用了父类的函数来初始化这个对象。

6. 绑定函数

绑定函数是与对象连接的函数。通常使用.bind()方法从原始函数创建。原始函数和绑定函数共享相同的代码和作用域,但执行时上下文不同。

方法 myFunc.bind(thisArg[, arg1[, arg2[, ...]]])接受第一个参数thisArg作为绑定函数执行时的上下文,并且它接受一组可选的参数 arg1, arg2, ...作为被调用函数的参数。它返回一个绑定了thisArg的新函数。

1
2
3
4
5
6
7
8
9
function multiply(number) {
'use strict';
return this * number;
}
const double = multiply.bind(2);

double(3); // => 6
double(10); // => 20
复制代码

bind(2)返回一个新的函数对象doubledouble 绑定了数字2multiplydouble具有相同的代码和作用域。

.apply().call() 方法相反,它不会立即调用该函数,.bind()方法只返回一个新函数,在之后被调用,只是this已经被提前设置好了。

6.1. 绑定函数中的this

在调用绑定函数时,this.bind()的第一个参数。

.bind()的作用是创建一个新函数,调用该函数时,将上下文作为传递给.bind()的第一个参数。它是一种强大的技术,使咱们可以创建一个定义了this值的函数。

img

来看看,如何在如何在绑定函数设置 this

1
2
3
4
5
6
7
8
9
10
11
12
const numbers = {
array: [3, 5, 10],
getNumbers: function() {
return this.array;
}
};
const boundGetNumbers = numbers.getNumbers.bind(numbers);
boundGetNumbers(); // => [3, 5, 10]
// Extract method from object
const simpleGetNumbers = numbers.getNumbers;
simpleGetNumbers(); // => undefined (严格模式下报错)
复制代码

numbers.getNumbers.bind(numbers)返回绑定numbers对象boundGetNumbers函数。boundGetNumbers()调用时的thisnumber对象,并能够返回正确的数组对象。

可以将函数numbers.getNumbers提取到变量simpleGetNumbers中而不进行绑定。在之后的函数调用中simpleGetNumbers()thiswindow(严格模式下为undefined),不是number对象。在这个情况下,simpleGetNumbers()不会正确返回数组。

6.2 紧密的上下文绑定

.bind()创建一个永久的上下文链接,并始终保持它。 一个绑定函数不能通过.call()或者.apply()来改变它的上下文,甚至是再次绑定也不会有什么作用。

只有绑定函数的构造函数调用才能更改已经绑定的上下文,但是很不推荐的做法(构造函数调用必须使用常规的非绑定函数)。

下面示例创建一个绑定函数,然后尝试更改其已预先定义好的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getThis() {
'use strict';
return this;
}
const one = getThis.bind(1);
// 绑定函数调用
one(); // => 1
// 使用带有.apply()和.call()的绑定函数
one.call(2); // => 1
one.apply(2); // => 1
// 再次绑定
one.bind(2)(); // => 1
// 以构造函数的形式调用绑定函数
new one(); // => Object
复制代码

只有new one()改变了绑定函数的上下文,其他方式的调用中this总是等于1。

7. 箭头函数

箭头函数用于以更短的形式声明函数,并在词法上绑定上下文。它可以这样使用

1
2
3
4
5
6
7
const hello = (name) => {
return 'Hello ' + name;
};
hello('World'); // => 'Hello World'
// Keep only even numbers
[1, 2, 5, 6].filter(item => item % 2 === 0); // => [2, 6]
复制代码

箭头函数语法简单,没有冗长的function 关键字。当箭头函数只有一条语句时,甚至可以省略return关键字。

箭头函数是匿名的,这意味着name属性是一个空字符串''。这样它就没有词法上函数名(函数名对于递归、分离事件处理程序非常有用)

同时,跟常规函数相反,它也不提供arguments对象。但是,这在ES6中通过rest parameters修复了:

1
2
3
4
5
6
7
const sumArguments = (...args) => {
console.log(typeof arguments); // => 'undefined'
return args.reduce((result, item) => result + item);
};
sumArguments.name // => ''
sumArguments(5, 5, 6); // => 16
复制代码

7.1. 箭头函数中的this

this 定义箭头函数的封闭上下文

箭头函数不会创建自己的执行上下文,而是从定义它的外部函数中获取 this。 换句话说,箭头函数在词汇上绑定 this

img

下面的例子说明了这个上下文透明的特性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
log() {
console.log(this === myPoint); // => true
setTimeout(()=> {
console.log(this === myPoint); // => true
console.log(this.x + ':' + this.y); // => '95:165'
}, 1000);
}
}
const myPoint = new Point(95, 165);
myPoint.log();
复制代码

setTimeout使用与log()方法相同的上下文(myPoint对象)调用箭头函数。正如所见,箭头函数从定义它的函数继承上下文。

如果在这个例子里尝试用常规函数,它创建自己的上下文(window或严格模式下的undefined)。因此,要使相同的代码正确地使用函数表达式,需要手动绑定上下文:setTimeout(function(){…}.bind(this))。这很冗长,使用箭头函数是一种更简洁、更短的解决方案。

如果箭头函数在最顶层的作用域中定义(在任何函数之外),则上下文始终是全局对象(浏览器中的 window):

1
2
3
4
5
6
onst getContext = () => {
console.log(this === window); // => true
return this;
};
console.log(getContext() === window); // => true
复制代码

箭头函数一劳永逸地与词汇上下文绑定。 即使修改上下文,this也不能被改变:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const numbers = [1, 2];
(function() {
const get = () => {
console.log(this === numbers); // => true
return this;
};
console.log(this === numbers); // => true
get(); // => [1, 2]
// Use arrow function with .apply() and .call()
get.call([0]); // => [1, 2]
get.apply([0]); // => [1, 2]
// Bind
get.bind([0])(); // => [1, 2]
}).call(numbers);
复制代码

无论如何调用箭头函数get,它总是保留词汇上下文numbers。 用其他上下文的隐式调用(通过 get.call([0])get.apply([0]))或者重新绑定(通过.bind())都不会起作用。

箭头函数不能用作构造函数。 将它作为构造函数调用(new get())会抛出一个错误:TypeError: get is not a constructor

7.2. 陷阱: 用箭头函数定义方法

你可能希望使用箭头函数来声明一个对象上的方法。箭头函数的定义相比于函数表达式短得多:(param) => {...} instead of function(param) {..}

来看看例子,用箭头函数在Period类上定义了format()方法:

1
2
3
4
5
6
7
8
9
10
11
function Period (hours, minutes) {  
this.hours = hours;
this.minutes = minutes;
}
Period.prototype.format = () => {
console.log(this === window); // => true
return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(2, 30);
walkPeriod.format(); // => 'undefined hours and undefined minutes'
复制代码

由于format是一个箭头函数,并且在全局上下文(最顶层的作用域)中定义,因此 this 指向window对象。

即使format作为方法在一个对象上被调用如walkPeriod.format()window仍然是这次调用的上下文。之所以会这样是因为箭头函数有静态的上下文,并不会随着调用方式的改变而改变。

该方法返回'undefined hours和undefined minutes',这不是咱们想要的结果。

函数表达式解决了这个问题,因为常规函数确实能根据实际调用改变它的上下文:

1
2
3
4
5
6
7
8
9
10
11
function Period (hours, minutes) {  
this.hours = hours;
this.minutes = minutes;
}
Period.prototype.format = function() {
console.log(this === walkPeriod); // => true
return this.hours + ' hours and ' + this.minutes + ' minutes';
};
const walkPeriod = new Period(2, 30);
walkPeriod.format(); // => '2 hours and 30 minutes'
复制代码

walkPeriod.format()是一个对象上的方法调用,它的上下文是walkPeriod对象。this.hours等于2this.minutes等于30,所以这个方法返回了正确的结果:'2 hours and 30 minutes'

原文:dmitripavlutin.com/gentle-expl…

代码部署后可能存在的BUG没法实时知道,事后为了解决这些BUG,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug。

总结

为函数调用对this影响最大,从现在开始不要问自己:

this 是从哪里来的?

而是要看看

函数是怎么被调用的?

对于箭头函数,需要想想

在这个箭头函数被定义的地方,this是什么?

这是处理this时的正确想法,它们可以让你免于头痛。

1
2
3
4
5
config.json (挖矿配置)、
sysupdate (XMR 挖矿软件)、
update.sh (本脚本)、
networkservice(scanner扫描并入侵其他的主机)、
sysguard(watchdog 用于监控并保证病毒的正常运行以及更新) 并保证他们以 root 权限运行。
  1. 查看病毒的 PID 号

  2. 获取绝对路径

    1
    ls -l /proc/{pid 号}/exe
  3. 删除/tmp文件夹的所有文件

  4. 检查下定时任务

    1
    crontab -l 或者 cat /var/spool/cron/will

    上面的will为当前用户名

    查看定时任务的日志

    1
    more /var/log/cron log
  5. 删除定时任务

    1
    rm /var/spool/cron/root 或者 crontab -r 删除定时任务
  6. kill 命令将相关进程干掉

    1
    2
    3
    sysupdate
    networkservice
    sysguard

1
流包括字符流和字节流,流从概念上来说是一个连续的数据流。当程序需要读数据的时候就需要使用输入流读取数据,当需要往外写数据的时候就需要输出流
  • 字节流:字节流主要用来处理字节或二进制对象InputStream、OutputStream,

    在使用字节流的时候,InputStream和OutputStream都是抽象类,我们实例化的都是他们的子类,每一个子类都有自己的作用范围

  • 字符流:字符流用来处理字符文本或字符串Reader、Writer

    在使用字符流的时候也是,Reader和Writer都是抽象类,我们实例化的都是他们的子类,每一个子类都有自己的作用范围

  • 字节流转换为字符流

    使用InputStreamReader、OutputStreamWriter可以将输入字节流转化为输入字符流

    1
    2
    Reader reader  =  new InputStreamReader(inputStream);
    Writer writer = new OutputStreamWriter(outputStream)

输入字节流:InputStream

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception{
File file = new File("D:/a.txt");
InputStream inputStream = new FileInputStream(file);
byte[] bytes = new byte[(int) file.length()];
inputStream.read(bytes);
System.out.println(new String(bytes));
inputStream.close();
}

输入字符流:Reader

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception{
File file = new File("D:/a.txt");
Reader reader = new FileReader(file);
char[] bytes = new char[(int) file.length()];
reader.read(bytes);
System.out.println(new String(bytes));
reader.close();
}

输出字节流:OutputStream

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception{
String var = "hai this is a test";
File file = new File("D:/b.txt");
OutputStream outputStream = new FileOutputStream(file);
outputStream.write(var.getBytes());
outputStream.close();
}

输出字符流:Writer

1
2
3
4
5
6
7
public static void main(String[] args) throws Exception{
String var = "hai this is a test";
File file = new File("D:/b.txt");
Writer writer = new FileWriter(file);
writer.write(var);
writer.close();
}

BufferedInputStream

在使用InputStream的时候,都是一个字节一个字节的读或写,而BufferedInputStream为输入字节流提供了缓冲区,读数据的时候会一次读取一块数据放到缓冲区里,当缓冲区里的数据被读完之后,输入流会再次填充数据缓冲区,直到输入流被读完,有了缓冲区就能够提高很多io速度

1
2
3
4
5
/**
* inputStream 输入流
* 1024 内部缓冲区大小为1024byte
*/
BufferedInputStream bufferedInputStream = new BufferedInputStream(inputStream,1024);

BufferedOutputStream

BufferedOutputStream可以为输出字节流提供缓冲区,作用与BufferedInputStream类似,使用方式将输出流包装到BufferedOutputStream中

1
2
3
4
5
/**
* outputStream 输出流
* 1024 内部缓冲区大小为1024byte
*/
BufferedOutputStream bufferedOutputStream = new BufferedOutputStream(outputStream,1024);

BufferedReader

为输入字符流提供缓冲区

1
BufferedReader bufferedReader = new BufferedReader(reader,1024);

BufferedWriter

为输出字符流提供缓冲区

1
BufferedWriter bufferedWriter = new BufferedWriter(writer,1024);

io模型

bio

同步阻塞IO模型

JDK 1.4版本以前

在这种模式中通常用一个线程去接受请求,然后用一个线程池去处理请求,用这种方式并发管理多个Socket客户端连接

nio

同步非阻塞IO模型

JDK 1.4版本以后

成熟的框架,比如Netty

aio

异步非阻塞IO模型

JDK1.7升级了Nio类库,成为Nio2.0,最主要的是提供了异步文件的IO操作,以及事件驱动IO,AIO的异步套接字通道是真正的异步非阻塞IO


参考:

一文看懂java io系统