首页 文章 精选 留言 我的

精选列表

搜索[教程],共8922篇文章
优秀的个人博客,低调大师

SpringBoot2全家桶,快速入门学习开发网站教程

说明 本系列文章带大家玩转SpringBoot2。 但是学习是一个循序渐进的过程,所以该文章将分为几个小章节讲述。 并且在学习SpringBoot之前需要一定的基础知识 SpringMVC、Spring、MyBatis基础知识 Maven MySQL、Redis 本文章代码建立的环境基础 注:由于环境不同可能会导致代码运行效果不同,请同步环境 开发环境 名称 版本 JDK jdk-12.0.2 IDE Eclipse2019.09 Databases MySQL_v8.0.18 SpringBoot v2.2.2 章节 1.SpringBoot2初体验 2.SpringBoot2编写第一个Controller 3.SpringBoot2整合MyBatis 4.Springboot2将默认的数据库连接池hikari替换为druid 5.SpringBoot2整合Redis 6.SpringBoot2配置默认Tomcat服务器 7.SpringBoot2更换Tomcat为Jetty 8.SpringBoot2整合Thymeleaf

优秀的个人博客,低调大师

Django 教程

MVC:models.py views.py urls.py books.html # models.py (the database tables) from django.db import models class Book(models.Model): name = models.CharField(max_length=50) pub_date = models.DateField() # views.py (the business logic) from django.shortcuts import render_to_response from models import Book def latest_books(request): book_list = Book.objects.order_by('-pub_date')[:10] return render_to_response('latest_books.html', {'book_list': book_list}) # urls.py (the URL configuration) from django.conf.urls.defaults import * import views urlpatterns = patterns('', (r'^latest/$', views.latest_books), ) # latest_books.html (the template) Books Books {% for book in book_list %} {{ book.name }} {% endfor %} django.contrib 运行pythonmanage.pysyncdb。这一步将生成管理界面使用的额外数据库表。 当你把'django.contrib.auth'加进INSTALLED_APPS后,第一次运行syncdb命令时, 系统会请你创建一个超级用户。 如果你不这么作,你需要运行pythonmanage.pycreatesuperuser来另外创建一个admin的用户帐号,否则你将不能登入admin (提醒一句: 只有当INSTALLED_APPS包含'django.contrib.auth'时,pythonmanage.pycreatesuperuser这个命令才可用.) 将自己的模型加入默认DB from django.contrib import admin from mysite.books.models import Publisher, Author, Book admin.site.register(Publisher) admin.site.register(Author) admin.site.register(Book) model class Book(models.Model): title = models.CharField(max_length=100) authors = models.ManyToManyField(Author) publisher = models.ForeignKey(Publisher) publication_date = models.DateField() def __unicode__(self): return self.title model允许默认值: class Author(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=40) email = models.EmailField(**blank=True** ) class Book(models.Model): title = models.CharField(max_length=100) authors = models.ManyToManyField(Author) publisher = models.ForeignKey(Publisher) publication_date = models.DateField(**blank=True, null=True** ) 修改model上显示的标签:verbose_name class Author(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=40) email = models.EmailField(blank=True, **verbose_name='e-mail'** ) 自定义列表: class Author(models.Model): first_name = models.CharField(max_length=30) last_name = models.CharField(max_length=40) email = models.EmailField(blank=True, verbose_name='e-mail') **def __unicode__(self):** **return u'%s %s' % (self.first_name, self.last_name)**

优秀的个人博客,低调大师

WCF 教程

冬季寒冷,无心出门。研究了下早想看看的WCF。左右看了不少,无奈文笔不是很好。所以就翻译老外的一个系列。 补足或者补充他的不足之处放在博客园给大家看。有不足之处欢迎指正! 目录: 1. 快速开始 2. 有关书籍 3. 基础知识 4. WCF架构 5.WCF托管 6.WCF绑定 7. 元数据交换 8. 契约 9. 实例管理 10. Instance Deactivation 11. Durable Service 12. Throattling 13. 操作 14. 事件 15. 传输模型 16. 事务 17. Silverlight中的异常处理 18. 定制消息头 19. WCF4.0 介绍 欢迎加群互相学习,共同进步。QQ群:iOS: 58099570 | Android: 572064792 | Nodejs:329118122 做人要厚道,转载请注明出处! 本文转自张昺华-sky博客园博客,原文链接:http://www.cnblogs.com/sunshine-anycall/archive/2011/01/22/1942141.html ,如需转载请自行联系原作者

优秀的个人博客,低调大师

Spark教程

Spark官方网站 http://spark.apache.org Spark通用性解释 Spark的特点 官方网站都有。目前支持Java,Scala, Python,R。推荐使用Java和Scala,spark2中对python的支持不够好。 Spark的数据源 HDFS, HBase, Cassandra(类似于hbase数据库,国外用的多) Hive, Tachyon(基于内存的分布式的文件系统, 阿里出的http://www.alluxio.org/,比较重要) Spark的四种部署模式 hadoop 模式(spark on yarn)。用yarn来管理spark的资源,也是国内用的最多的模式。 Mesos模式。Mesos一个类似于yarn的资源管理器,国内用的少,国外多。 Standalone模式。上面两种都不是自己管理资源,使用第三方来管理,而standalone模式是spark自己来管理资源,多内用的比较多 部署到云端 验证集群是否好用 验证spark集群是否可用 ./bin/spark-submit --class org.apache.spark.examples.SparkPi --master yarn --num-executors 1 --driver-memory 500m --executor-memory 500m --executor-cores 1 lib/spark-examples-1.5.2-hadoop2.5*.jar 10 验证结果如下: Pi is roughly 3.1411 用scala编写完简单的运算后,可以在localhost:4040上查看 运行spark有两种: 本地运行 bin/spark-submit --master local --class org.apache.spark.examples.SparkPi lib/spark-examples-1.5.2-hadoop2.6.0-cdh5.4.4.jar 5 本地运行会直接输出结果 分布式运行 bin/spark-submit --master yarn-cluster --class org.apache.spark.examples.SparkPi lib/spark-examples-1.5.2-hadoop2.6.0-cdh5.4.4.jar 5 而分布式运行的结果在 cd $HADOOP_HOME/logs/userlogs/ scala 运行例子 val lines = sc.textFile("hdfs://master:9000/input0917/qqFriend.txt") val lineLengths = lines.map(s => s.length) val totalLength = lineLengths.reduce((a, b) => a + b) 输出: totalLength: Int = 188 RDD Spark架构 Spark安装前准备 安装好JAVA 安装好Hadoop 安装好scala 并配置好上述的的环境变量 Spark任务提交 http://spark.apache.org/docs/2.1.0/submitting-applications.html SparkHistoryServer配置 关于sparkshell的使用 http://blog.csdn.net/sunflower_cao/article/details/26708797 创见RDD文件 第一种方式是通过读取本地或者hdfs上的文件创建RDD sc.textFile("hdfs://") 第二种方式是通过并行化的方式创建RDD. 其实就是通过我们自己取模拟数据 val str=Array("you jump","I jump") val list = Array(1,2,3,4,5,6) val listadd = sc.parallelize(list) 可以看到返回值就是RDD,当然可以调用RDD中的函数,比如reduce算子等 listadd.reduce(_+_) 大多数方式是使用第一种 transformation 和action 原理 Spark支持两种RDD操作: transformation and action. transformation 会针对已有的RDD创建一个新的RDD,而action则主要对RDD进行最后的操作。transformation只是记录了对RDD的操作,并不会触发spark程序的执行,只有当transform之后接着一个action操作,那么所有的transformation才会执行。比如 val file=sc.textFile("hdfs://hadoop1:9000/hello.txt").flatMap(line => line.split("\t")) 回车之后并没有触发spark的执行,因为flatMap等属于transformation操作 等到file.collect()后会看到spark的执行,collect是action操作 具体可以参考编程指南:http://spark.apache.org/docs/2.1.0/programming-guide.html 那为什么要有transformation和action呢?Spark可以通过这种lazy属性,来进行底层的spark应用程序的优化,避免过多的中间结果。 Spark算子 http://spark.apache.org/docs/latest/rdd-programming-guide.html#transformations 练习如下,注意eclipse的使用。 - 将鼠标放在方法上,ctrl+1 来选择返回值类型 - 鼠标选中区域, ctrl+shift+/ 来进行多行注释 - 当实现某个方法,查看错误信息,提示添加内部方法,添加即可 map和flatmap的区别: map与mappartitions的区别: map迭代的是RDD中的每一个元素,而mappartiontions迭代的一个分区。如果在映射过程中频繁创建额外的对象,那么mappartitions比map高效。但也需要注意内存,因为内存可能不够用,比分区小。 比如在迭代元素的时候要将10000条结果存到mysql数据库,如果用map需要创建10000个额外的connection,用map partitions可能只需要创建10个, 对每个分区建立连接。 下面是不同算子做实验的代码示例: repartition coalesce 重新进行分区 宽以来 窄依赖 shuffle 那什么时候用repartition呢?比如: filter之后,partition的量会减少,比如之前是100 partitions,对应的需要起100 个tasks,现在filter后变成了50个,此时要是100个tasks就浪费了,可以repartition到50。repartition分区会进行shuffle操作 package com.ecaoyng.spark; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import org.apache.cassandra.cli.CliParser.newColumnFamily_return; import org.apache.cassandra.thrift.Cassandra.system_add_column_family_args; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaPairRDD; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; import org.apache.spark.api.java.function.FlatMapFunction; import org.apache.spark.api.java.function.Function; import org.apache.spark.api.java.function.Function2; import org.apache.spark.api.java.function.VoidFunction; import org.stringtemplate.v4.compiler.STParser.mapExpr_return; import scala.Tuple2; public class Trans { /** * repartition will do shuffle */ public static void repartition(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("repartition"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3,4,5,6); JavaRDD<Integer> list1RDD = sc.parallelize(list1); JavaRDD<Integer> repartition = list1RDD.repartition(2); repartition.foreach(new VoidFunction<Integer>() { public void call(Integer arg0) throws Exception { System.out.println(arg0); } }); } /** * output: * Hello1 Hello2 Hello3 Hello4 Hello5 Hello6 */ public static void mapPartitions(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("mapParititons"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3,4,5,6); JavaRDD<Integer> list1RDD = sc.parallelize(list1, 2); /** * return value is string */ JavaRDD<String> mapPartitions = list1RDD.mapPartitions(new FlatMapFunction<Iterator<Integer>, String>() { /* * process each partition each time * partition1: 123 * partition2: 456 * */ public Iterable<String> call(Iterator<Integer> t) throws Exception { ArrayList<String> arrayList = new ArrayList<String>(); while (t.hasNext()) { Integer i = (Integer) t.next(); arrayList.add("Hello" + i); } return arrayList; } }); mapPartitions.foreach(new VoidFunction<String>() { public void call(String arg0) throws Exception { System.out.println(arg0); } }); } /* * di ka er ji * output:1 a 1 b 1 c 2 a 2 b 2 c 3 a 3 b 3 c */ public static void cartesian(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("cartesian"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3); List<String> list2 = Arrays.asList("a","b","c"); JavaRDD<Integer> list1RDD = sc.parallelize(list1); JavaRDD<String> list2RDD = sc.parallelize(list2); JavaPairRDD<Integer, String> cartesian = list1RDD.cartesian(list2RDD); cartesian.foreach(new VoidFunction<Tuple2<Integer,String>>() { public void call(Tuple2<Integer, String> t) throws Exception { System.out.println(t._1 + " " + t._2); } }); } /* qu chong * output:4 1 6 3 5 2 */ public static void distinct(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("distinct"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3,4,4,5,5,6,6); JavaRDD<Integer> listRDD = sc.parallelize(list1); JavaRDD<Integer> distinct = listRDD.distinct(); distinct.foreach(new VoidFunction<Integer>() { public void call(Integer t) throws Exception { System.out.println(t); } }); } /* * jiao ji * output: 4 3 */ public static void interSection() { SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("union"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3,4); List<Integer> list2 = Arrays.asList(3,4,5,6); JavaRDD<Integer> list1RDD = sc.parallelize(list1); JavaRDD<Integer> list2RDD = sc.parallelize(list2); JavaRDD<Integer> intersection = list1RDD.intersection(list2RDD); intersection.foreach(new VoidFunction<Integer>() { public void call(Integer t) throws Exception { System.out.println(t); } }); } /* * bingji, bu qu chong */ public static void union() { SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("union"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3,4); List<Integer> list2 = Arrays.asList(3,4,5,6); JavaRDD<Integer> list1RDD = sc.parallelize(list1); JavaRDD<Integer> list2RDD = sc.parallelize(list2); JavaRDD<Integer> union = list1RDD.union(list2RDD); union.foreach(new VoidFunction<Integer>() { public void call(Integer t) throws Exception { System.out.println(t); } }); } /* * ID: 1 Name: [Tom, Tom] Score: [30, 70] ID: 3 Name: [Chirs] Score: [60, 90] ID: 2 Name: [Jerry] Score: [40, 60] * */ public static void cogroup() { SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("cogroup"); JavaSparkContext sc = new JavaSparkContext(conf); List<Tuple2<Integer, String>> stu = Arrays.asList( new Tuple2<Integer, String>(1, "Tom"), new Tuple2<Integer, String>(2, "Jerry"), new Tuple2<Integer, String>(3, "Chirs"), new Tuple2<Integer, String>(1, "Tom") ); List<Tuple2<Integer, Integer>> score = Arrays.asList( new Tuple2<Integer, Integer>(1, 30), new Tuple2<Integer, Integer>(2, 40), new Tuple2<Integer, Integer>(3, 60), new Tuple2<Integer, Integer>(1, 70), new Tuple2<Integer, Integer>(2, 60), new Tuple2<Integer, Integer>(3, 90) ); JavaPairRDD<Integer, String> stuRDD = sc.parallelizePairs(stu); JavaPairRDD<Integer, Integer> scoreRDD = sc.parallelizePairs(score); // <2,tuple<Jerry,{40,60}>> JavaPairRDD<Integer, Tuple2<Iterable<String>, Iterable<Integer>>> cogroupRDD = stuRDD.cogroup(scoreRDD); cogroupRDD.foreach(new VoidFunction<Tuple2<Integer,Tuple2<Iterable<String>,Iterable<Integer>>>>() { public void call( Tuple2<Integer, Tuple2<Iterable<String>, Iterable<Integer>>> t) throws Exception { System.out.println("ID: " + t._1); System.out.println("Name: " + t._2._1); System.out.println("Score: " + t._2._2); } }); } /* * output: ID: 1 Name: Tom Score: 30 ==================> ID: 3 Name: Chirs Score: 60 ==================> ID: 2 Name: Jerry Score: 40 ==================> */ public static void join(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("join"); JavaSparkContext sc = new JavaSparkContext(conf); List<Tuple2<Integer, String>> stu = Arrays.asList( new Tuple2<Integer, String>(1, "Tom"), new Tuple2<Integer, String>(2, "Jerry"), new Tuple2<Integer, String>(3, "Chirs") ); List<Tuple2<Integer, Integer>> score = Arrays.asList( new Tuple2<Integer, Integer>(1, 30), new Tuple2<Integer, Integer>(2, 40), new Tuple2<Integer, Integer>(3, 60) ); JavaPairRDD<Integer, String> stuRDD = sc.parallelizePairs(stu); JavaPairRDD<Integer, Integer> scoreRDD = sc.parallelizePairs(score); // Integer:id, Tuple2<String, Integer>: name,score JavaPairRDD<Integer, Tuple2<String, Integer>> joined = stuRDD.join(scoreRDD); joined.foreach(new VoidFunction<Tuple2<Integer,Tuple2<String,Integer>>>() { public void call(Tuple2<Integer, Tuple2<String, Integer>> t) throws Exception { System.out.println("ID: " + t._1); System.out.println("Name: " + t._2._1); System.out.println("Score: " + t._2._2); System.out.println("==================>"); } }); } public static void sortByKey(){ } /* calculate sum by each key * output: PBCS:95 TDCS:120 */ public static void reduceByKey(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("reduceByKey"); JavaSparkContext sc = new JavaSparkContext(conf); List<Tuple2<String, Integer>> asList = Arrays.asList( new Tuple2<String, Integer>("TDCS", 30), new Tuple2<String, Integer>("TDCS", 40), new Tuple2<String, Integer>("TDCS", 50), new Tuple2<String, Integer>("PBCS", 40), new Tuple2<String, Integer>("PBCS", 55) ); JavaPairRDD<String, Integer> parallelizePairsRDD = sc.parallelizePairs(asList); /* * input: Integer, Integer * output: sum:Integer * scala.reduceByKey(_+_) */ parallelizePairsRDD.reduceByKey(new Function2<Integer, Integer, Integer>() { public Integer call(Integer v1, Integer v2) throws Exception { return v1 + v2; } }).foreach(new VoidFunction<Tuple2<String,Integer>>() { public void call(Tuple2<String, Integer> sum) throws Exception { System.out.println(sum._1 + ":" + sum._2); } }); } /* * src: key-value * Tuple2 in scala is similar with map in Java * output: PBCS Terry Shawn ======================================> TDCS Tom Jerry Chris * */ public static void groupByKey(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("groupByKey"); JavaSparkContext sc = new JavaSparkContext(conf); List<Tuple2<String, String>> list = Arrays.asList( new Tuple2<String, String>("TDCS", "Tom"), new Tuple2<String, String>("TDCS", "Jerry"), new Tuple2<String, String>("TDCS", "Chris"), new Tuple2<String, String>("PBCS", "Terry"), new Tuple2<String, String>("PBCS", "Shawn") ); // due to key-value, it should be use parallelizePairs JavaPairRDD<String, String> parallelizePairsRDD = sc.parallelizePairs(list); JavaPairRDD<String, Iterable<String>> groupByKey = parallelizePairsRDD.groupByKey(); groupByKey.foreach(new VoidFunction<Tuple2<String,Iterable<String>>>() { public void call(Tuple2<String, Iterable<String>> t) throws Exception { System.out.println(t._1); Iterator<String> iterator = t._2.iterator(); while (iterator.hasNext()) { System.out.println(iterator.next()); } System.out.println("======================================>"); } }); } /* * flatmap: {You jump I jump} * split each word into a sigle line */ public static void flatMap(){ SparkConf conf = new SparkConf(); // if no set on conf.setMaster, the default setting will use distributed mode. conf.setMaster("local"); conf.setAppName("flatmap"); JavaSparkContext sc = new JavaSparkContext(conf); List<String> list = Arrays.asList("you jump","I jump"); JavaRDD<String> listRDD = sc.parallelize(list); // U refers to the return value of method: FlatMapFunction JavaRDD<String> flatMap = listRDD.flatMap(new FlatMapFunction<String, String>() { public Iterable<String> call(String t) throws Exception { return Arrays.asList(t.split(" ")); } }); flatMap.foreach(new VoidFunction<String>() { public void call(String line) throws Exception { System.out.println(line); } }); } /* * Map() test: say hello to each items within a array */ public static void map() { SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("map"); JavaSparkContext sc = new JavaSparkContext(conf); List<String> list = Arrays.asList("Tom","Jerry","USA","China"); JavaRDD<String> listRDD = sc.parallelize(list); // R refers to the retuen value of new Function JavaRDD<String> map = listRDD.map(new Function<String, String>() { public String call(String str) throws Exception { return "hello " + str; } }); // action opts map.foreach(new VoidFunction<String>() { public void call(String str) throws Exception { System.out.println(str); } }); } /* * fliter(): get odds from dataset */ public static void fliter(){ SparkConf conf = new SparkConf(); conf.setMaster("local"); conf.setAppName("map"); JavaSparkContext sc_fliter = new JavaSparkContext(conf); List<Integer> fliter_list = Arrays.asList(1,2,3,4,5,6,7,8,9); JavaRDD<Integer> fliterRDD = sc_fliter.parallelize(fliter_list); JavaRDD<Integer> filter = fliterRDD.filter(new Function<Integer, Boolean>() { public Boolean call(Integer num) throws Exception { return num %2 ==0; } }); filter.foreach(new VoidFunction<Integer>() { public void call(Integer num) throws Exception { System.out.println(num); } }); } public static void main(String[] args) { // map(); // fliter(); // flatMap(); // groupByKey(); // reduceByKey(); // join(); // cogroup(); // union(); // interSection(); // distinct(); // cartesian(); // mapPartitions(); repartition(); } } RDD持久化(RDD Persistence) http://spark.apache.org/docs/latest/rdd-programming-guide.html#rdd-persistence 当你缓存了一个RDD,每个节点都缓存了RDD的所有分区,这样就可以在内存中进行计算,速度更快(提高10倍)。 可以对希望缓存的RDD进行cache或者persist方法进行标记。她通过动作操作第一次在Rdd上进行计算后,它会被缓存在节点的内存中。Spark的缓存具有容错性,如果RDD的某个分区丢失,它会自动使用最初创建RDD的转换操作进行重新计算。 建议: 优先选择MEMORY_ONLY(=cache),纯内存计算,速度快 MEMORY_ONLY 缓存不了所有的数据,MEMORY_ONLY_SER 将数据序列化后进行存储,需要耗费一些CPU资源,并需要反序列化。 可以选在带2的方式。恢复的时候可以使用备份,不需要重新计算 能不是用disk的就不使用disk,有时候从磁盘读还不如重新计算 之外,OFF_HEAP (experimental) 是将RDD存到tachyon上。 那什么是tachyon 一个开源的基于jvm的内存分布式文件系统,介于计算层和存储层之间,简单的理解为存储层在内存内的缓存系统。 为何会出现tachyon?以内存替换磁盘,就能明显的减少延时,所以涌现出很多基于内存的计算工具,比如spark计算框架。 spark运行在jvm中,spark的任务会将数据存入jvm的堆中,随着计算的迭代,jvm堆中存放的数据量迅速增大,对于spark而言,spark的计算引擎和存储引擎处在同一个jvm中,所以会有重复的gc方面的开销,增大了系统的延时 当jvm崩溃时,缓存在jvm堆中的数据也会消失,这个时候spark不得不根据rdd的血缘关系重新计算数据。 如果spark需要和其他的计算框架,比如mapreduce,此时就需要通过第三方共享,比如hdfs。需要额外的开销,比如磁盘的IO开销 因基于内存的分布式计算框架有以上的问题,那么就促使了内存分布式文件系统的诞生, 比如tachyon。 共享变量(广播变量,累加变量) 广播变量 举例: 之前 val a的时候需要将a拷贝好多份到每个task上,如果a大道1个G,就浪费好多空间。现在有了broadcast var 就将变量拷到 累加变量 按照之前的习惯写sum会出错,一种新的声明方式 Spark on Yarn 只需要在spark下的conf目录下的spark-env.sh 中配置Hadoop_conf_dir(hadoop的位置)即可。让spark通过hadoop的配置文件找到yarn. 之前用standalone模式需要启动master和worker,现在用yarn模式不需要启动master和worker,只需要启动hdfs和yarn即可 start-dfs.sh, jps -> 3个服务 start-yarn.sh 之后spark的开发和之前一样,只需要在提交代码的时候指定运行的模式 具体请参考官方文档: http://spark.apache.org/docs/latest/submitting-applications.html # Run on a YARN cluster export HADOOP_CONF_DIR=XXX ./bin/spark-submit \ --class org.apache.spark.examples.SparkPi \ --master yarn \ --deploy-mode cluster \ # can be client for client mode --executor-memory 20G \ --num-executors 50 \ /path/to/examples.jar \ 1000 yarn提交也分为client和cluster模式 cluster 注意executor会向driver(application master) client: 建议: 调试的时候使用client模式。使用client模式的时候打印出来的信息非常详细,有利于我们调试程序。 调试完成以后,建议使用cluster模式提交任务。 举例,先在本地运行如下代码,成功在本地生成union的并集: package com.ecaoyng.spark; import java.util.Arrays; import java.util.List; import org.apache.spark.SparkConf; import org.apache.spark.api.java.JavaRDD; import org.apache.spark.api.java.JavaSparkContext; public class SaveAsText { public static void Save(){ SparkConf conf = new SparkConf(); conf.setMaster("local[3]"); // 因为是本地运行 conf.setAppName("SaveAsTextFile"); JavaSparkContext sc = new JavaSparkContext(conf); List<Integer> list1 = Arrays.asList(1,2,3,4,5); List<Integer> list2 = Arrays.asList(4,5,6,7,8); JavaRDD<Integer> list1RDD = sc.parallelize(list1); JavaRDD<Integer> list2RDD = sc.parallelize(list2); JavaRDD<Integer> unionRDD = list1RDD.union(list2RDD); unionRDD.repartition(1).saveAsTextFile("/home/zkpk/union"); # 注意是在本地磁盘的目录下生成union文件夹 } public static void main(String[] args) { Save(); } } 测试通过后,因为要放到cluster上运行,所以将conf.setMaster注释掉。之后用eclipse将jar包导出 下面是提交的代码: /home/zkpk/spark-1.5.2-bin-2.5.2/bin/spark-submit \ --class com.ecaoyng.spark.SaveAsText \ --master yarn \ --deploy-mode cluster \ --executor-memory 100M \ --num-executors 1 \ /home/zkpk/output/SaveUnion.jar \ 注意里面不要出现注释,否则可能会出错。如果发现参数错误可以spark-submit –help 查看哪些是可以运行在cluster上的参数。 输出的结果放在hdfs上。当然事先需要设置export SPARK_CONF_DIR=/home/zkpk/hadoop-2.5.2 或者在spark的配置中的spark-env.sh中配置 也可以在web中查看http://master:18088/ 提交的进度 宽依赖和窄依赖 窄依赖,父RDD的每个分区只被一个子RDD的分区依赖。 宽依赖,父RDD的分区被多个子RDD的分区所依赖。 shuffle的原理 本例中前面三个RDD都不涉及到其他的节点,但是真实环境是需要其他节点的参与的。shuffle阶段需要大量的磁盘IO,序列化与反序列化,网络数据的传输,所以spark很大的性能损耗都在shuffle上,所以有必要进行调优。 shuffle的发展经历了三个阶段:1.2,1.3, 1.5之后。分别取名: 未经优化的HashshuffleManager 优化后的HashshuffleManager SortShuffleManager 普通机制 bypass机制 [未经优化的HashshuffleManager] 每一个map task生成的buffer和file和 reduce的数量有关,可以联想MR中的partition,可以看到生成的碎文件太多,产生的中间文件数量如图 [经过优化的HashshuffleManager] [现在在使用的是sort-based shuffle manage ] shuffle 配置参数地址:http://spark.apache.org/docs/2.0.0/configuration.html 正常情况使用的是普通运行机制,但是当shuffle read task小于200(默认值)时就启用bypass机制 因为合并成了一个磁盘文件,所以中间文件将被删除 bypass模式 未完待续

优秀的个人博客,低调大师

Dockerfile使用教程

一、什么是Dockerfile Dockerfile是创建自定义Docker镜像的描述文件,我们可以在Dockerfile中写出一条条指令来控制镜像需要执行什么操作,下面具体讲讲Dockerfile中的指令。 二、Dockerfile四大功能分区 1. 基础镜像 声明自定义镜像的基础镜像 FROM centos 2. 描述信息 此处可以声明镜像的负责人、版本、构建时间、描述等一系列信息,此处可以声明任何信息 LABEL maintainer=tingtiandadi@360.cn LABEL version="1.0" LABEL date="2020/03/08" LABEL description="RPM软件包制作" 3. 安装镜像执行指令 3.1 USER 执行Dockerfile中shell指令时指定的用户,包括:RUN、CMD、ENTRYPOINT USER <user>[:<usergroup>] USER root 3.2 EXPOSE 指定容器运行时的服务端口 EXPOSE 80 443 3.3 WORKDIR 为Dockerfile中执行的指令设置工作目录,包括:ADD、COPY、RUN、CMD、ENTRYPOINT WORKDIR /root 3.4 ADD 拷贝文件/目录到镜像中 ADD <src>...<dest> ADD rpmbuild.tar.gz /root/ ADD https://xxx.com/rpmbuild.tar.gz /root/ 3.5 COPY 拷贝文件/目录到镜像中,用法与ADD相同,但是不支持自动下载和解压 ADD /home/mysql_cron /etc/cron.d/ 3.6 VOLUME 把主机的目录挂载到docker中,类似nfs文件共享,一般很少用,大多在docker run中用-v来进行挂载 VOLUME ["/usr/local/"] 3.7 ARG 在构建镜像时指定的参数 ARG user USER $user 只需要在docker build时带上user参数即可 docker build --build-arg user=root -f dockerfile_rpmbuild . 3.8 RUN 构建镜像时运行的指令 RUN ["yum", "install", "net-tools", "-y"] RUN yum install net-tools -y 3.9 ENV 设置当前系统内环境变量 ENV JAVA_HOME /usr/local/jdk1.8.0_231 3.10 HEALTHCHECK 检查容器是否在仍在工作 HEALTHCHECK --interval=5m --timeout=3s --retries=3 \ CMD curl -f http:/localhost/ || exit 1 选项说明: --interval=DURATION (default: 30s):每隔多长时间探测一次,默认30秒 -- timeout= DURATION (default: 30s):服务响应超时时长,默认30秒 --start-period= DURATION (default: 0s):服务启动多久后开始探测,默认0秒 --retries=N (default: 3):认为检测失败几次为宕机,默认3次 返回值说明: 0:容器成功是健康的,随时可以使用 1:不健康的容器无法正常工作 2:保留不使用此退出代码 4. 容器启动执行指令 4.1 CMD方式 可以编辑多条,每条指令按顺序执行。 CMD ["-C", "/start.sh"] CMD ["/usr/sbin/sshd", "-D"] CMD /usr/sbin/sshd -D 4.2 ENTRYPOINT方式 同CMD类似,但这种方式执行的命令不会被docker run执行的参数所覆盖,同时docker run执行的参数会传到此方式执行的脚本中。 ENTRYPOINT [ "rpmbuild", "-bb" , "/root/rpmbuild/SPEC/mysql-install.spec"] ENTRYPOINT [ "dpkg", "-b", "mysql-install", "mysql-install.deb"] 重要提示:ENTRYPOINT可以有多行,但是执行时只有最后一行生效,切记! 三、Dockerfile实例 # 基础镜像 FROM centos # 描述信息 LABEL maintainer=tingtiandadi@360.cn LABEL version="1.0" LABEL date="2020/03/08" LABEL description="RPM软件包制作" # 安装镜像执行指令 WORKDIR /root USER root RUN rpm -ivh net-tools-2.0-0.51.20160912git.el8.x86_64.rpm RUN yum install rpm-build-4.14.2-25.el8.x86_64 -y # 启动容器执行指令 ENTRYPOINT [ "rpmbuild", "-bb", "rpmbuild/SPEC/*.spec"]

优秀的个人博客,低调大师

javaScript教程(三)

浏览器模型 浏览器环境概述 JavaScript 是浏览器的内置脚本语言。也就是说,浏览器内置了 JavaScript 引擎,并且提供各种接口,让 JavaScript 脚本可以控制浏览器的各种功能。一旦网页内嵌了 JavaScript 脚本,浏览器加载网页,就会去执行脚本,从而达到操作浏览器的目的,实现网页的各种动态效果。 这里开始介绍浏览器提供的各种 JavaScript 接口。首先,介绍 JavaScript 代码嵌入网页的方法 代码嵌入网页的方法 网页中嵌入 JavaScript 代码,主要有四种方法: script 元素嵌入代码 <script> var x = 1 + 5; console.log(x); </script> 1.text/javascript:这是默认值,也是历史上一贯设定的值。如果你省略type属性,默认就是这个值。对于老式浏览器,设为这个值比较好 2.application/javascript:对于较新的浏览器,建议设为这个值 由于 <script id="mydata" type="x-custom-data"> console.log('Hello World'); </script> 浏览器不会执行也不会显示内容,因为不认识它的type属性;但是,这个 document.getElementById('mydata').text // console.log('Hello World'); script 元素加载外部脚本 <script src="https://www.example.com/script.js"></script> 如果脚本文件使用了非英语字符,还应该注明字符的编码 <script charset="utf-8" src="https://www.example.com/script.js"></script> 所加载的脚本必须是纯的 JavaScript 代码,不能有HTML代码和 <script charset="utf-8" src="example.js"> console.log('Hello World!'); </script> 为了防止攻击者篡改外部脚本,script标签允许设置一个integrity属性,写入该外部脚本的 Hash 签名,用来验证脚本的一致性 <script src="/assets/application.js" integrity="sha256-TvVUHzSfftWg1rcfL6TIJ0XKEGrgLyEq6lEpcmrG9qs="></script> 上面代码中,script标签有一个integrity属性,指定了外部脚本/assets/application.js的 SHA256 签名。一旦有人改了这个脚本,导致 SHA256 签名不匹配,浏览器就会拒绝加载 事件属性 网页元素的事件属性(比如onclick和onmouseover),可以写入 JavaScript 代码。当指定事件发生时,就会调用这些代码 <button id="myBtn" onclick="console.log(this.id)">点击</button> //如果有多个语句,使用分号分隔即可 URL 协议 URL 支持javascript:协议,即在 URL 的位置写入代码,使用这个 URL 的时候就会执行 JavaScript 代码 <a href="javascript:console.log('Hello')">点击</a> 浏览器的地址栏也可以执行javascript:协议。将javascript:console.log('Hello')放入地址栏,按回车键也会执行这段代码;如果 JavaScript 代码返回一个字符串,浏览器就会新建一个文档,展示这个字符串的内容,原有文档的内容都会消失 <a href="javascript: new Date().toLocaleTimeString();">点击</a> 上面代码中,用户点击链接以后,会打开一个新文档,里面有当前时间;如果返回的不是字符串,那么浏览器不会新建文档,也不会跳转 <a href="javascript: console.log(new Date().toLocaleTimeString())">点击</a> javascript:协议的常见用途是书签脚本 Bookmarklet。由于浏览器的书签保存的是一个网址,所以javascript:网址也可以保存在里面,用户选择这个书签的时候,就会在当前页面执行这个脚本。为了防止书签替换掉当前文档,可以在脚本前加上void,或者在脚本最后加上void 0 <a href="javascript: void new Date().toLocaleTimeString();">点击</a> <a href="javascript: new Date().toLocaleTimeString();void 0;">点击</a> script 元素 工作原理 浏览器加载 JavaScript 脚本,主要通过 1.浏览器一边下载 HTML 网页,一边开始解析;也就是说,不等到下载完,就开始解析 2.解析过程中,浏览器发现 3.如果 4.JavaScript 引擎执行完毕,控制权交还渲染引擎,恢复往下解析 HTML 网页 加载外部脚本时,浏览器会暂停页面渲染,等待脚本下载并执行完成后再继续渲染;原因是 JavaScript 代码可以修改 DOM,所以必须把控制权让给它,否则会导致复杂的线程竞赛的问题。如果外部脚本加载时间很长(一直无法完成下载),那么浏览器就会一直等待脚本下载完成,造成网页长时间失去响应,浏览器就会呈现“假死”状态,这被称为“阻塞效应”。为了避免这种情况,较好的做法是将 <head> <script> console.log(document.body.innerHTML); </script> </head> <body></body> 上面代码执行时会报错,因为此时document.body元素还未生成 一种解决方法是设定DOMContentLoaded事件的回调函数 <head> <script> document.addEventListener( 'DOMContentLoaded', function (event) { console.log(document.body.innerHTML); } ); </script> </head> DOMContentLoaded事件只有在 DOM 结构生成之后才会触发 另一种解决方法是使用 <script src="jquery.min.js" onload="console.log(document.body.innerHTML)"></script> 但是,如果将脚本放在页面底部,就可以完全按照正常的方式写,上面两种方式都不需要。如果有多个script标签,比如下面这样: <script src="a.js"></script> <script src="b.js"></script> 浏览器会同时并行下载a.js和b.js,但是,执行时会保证先执行a.js,然后再执行b.js,即使后者先下载完成,也是如此。也就是说,脚本的执行顺序由它们在页面中的出现顺序决定,这是为了保证脚本之间的依赖关系不受到破坏。当然,加载这两个脚本都会产生“阻塞效应”,必须等到它们都加载完成,浏览器才会继续页面渲染。解析和执行 CSS,也会产生阻塞。Firefox 浏览器会等到脚本前面的所有样式表,都下载并解析完,再执行脚本;Webkit则是一旦发现脚本引用了样式,就会暂停执行脚本,等到样式表下载并解析完,再恢复执行。 此外,对于来自同一个域名的资源,比如脚本文件、样式表文件、图片文件等,浏览器一般有限制,同时最多下载6~20个资源,即最多同时打开的 TCP 连接有限制,这是为了防止对服务器造成太大压力。如果是来自不同域名的资源,就没有这个限制。所以,通常把静态文件放在不同的域名之下,以加快下载速度 defer 属性 为了解决脚本文件下载阻塞网页渲染的问题,一个方法是对 <script src="a.js" defer></script> <script src="b.js" defer></script> 上面代码中,只有等到 DOM 加载完成后,才会执行a.js和b.js defer属性的运行流程如下: 1.浏览器开始解析 HTML 网页 2.解析过程中,发现带有defer属性的 3.浏览器继续往下解析 HTML 网页,同时并行下载 4.浏览器完成解析 HTML 网页,此时再回过头执行已经下载完成的脚本 有了defer属性,浏览器下载脚本文件的时候,不会阻塞页面渲染。下载的脚本文件在DOMContentLoaded事件触发前执行(即刚刚读取完

优秀的个人博客,低调大师

JavaScript 教程(二)

面向对象编程 实例对象与 new 命令 JavaScript 语言具有很强的面向对象编程能力,这里介绍 JavaScript 面向对象编程的基础知识 对象是什么 面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。那么,“对象”(object)到底是什么?我们从两个层次来理解 对象是单个实物的抽象 一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程 对象是一个容器,封装了属性(property)和方法(method) 属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等) 构造函数 面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。构造函数就是一个普通的函数,但是有自己的特征和用法 var Vehicle = function () { this.price = 1000; }; 上面代码中,Vehicle就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写 构造函数的特点有两个。 1.函数体内部使用了this关键字,代表了所要生成的对象实例 2.生成对象的时候,必须使用new命令 new 命令 基本用法 new命令的作用,就是执行构造函数,返回一个实例对象 var Vehicle = function () { this.price = 1000; }; var v = new Vehicle(); v.price // 1000 上面代码通过new命令,让构造函数Vehicle生成一个实例对象,保存在变量v中。这个新生成的实例对象,从构造函数Vehicle得到了price属性。new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.price表示实例对象有一个price属性,值是1000。使用new命令时,根据需要构造函数也可以接受参数 var Vehicle = function (p) { this.price = p; }; var v = new Vehicle(500); new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号 var v = new Vehicle(); // 推荐的写法 var v = new Vehicle; // 不推荐的写法 一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果 var Vehicle = function (){ this.price = 1000; }; var v = Vehicle(); v // undefined price // 1000 上面代码中,调用Vehicle构造函数时,忘了加上new命令。结果,变量v变成了undefined,而price属性变成了全局变量。因此,应该非常小心,避免不使用new命令、直接调用构造函数。为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错 function Fubar(foo, bar){ 'use strict'; this._foo = foo; this._bar = bar; } Fubar() // TypeError: Cannot set property '_foo' of undefined 上面代码的Fubar为构造函数,use strict命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript 不允许对undefined添加属性)。另一个解决办法,构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象 function Fubar(foo, bar) { if (!(this instanceof Fubar)) { return new Fubar(foo, bar); } this._foo = foo; this._bar = bar; } Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1 上面代码中的构造函数,不管加不加new命令,都会得到同样的结果 new 命令的原理 使用new命令时,它后面的函数依次执行下面的步骤 1.创建一个空对象,作为将要返回的对象实例 2.将这个空对象的原型,指向构造函数的prototype属性 3.将这个空对象赋值给函数内部的this关键字 4.开始执行构造函数内部的代码 也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象 var Vehicle = function () { this.price = 1000; return 1000; }; (new Vehicle()) === 1000 // false 上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意 var Vehicle = function (){ this.price = 1000; return { price: 2000 }; }; (new Vehicle()).price // 2000 上面代码中,构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象 function getMessage() { return 'this is a message'; } var msg = new getMessage(); msg // {} typeof msg // "object" 上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句。new命令简化的内部流程,可以用下面的代码表示 function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) { var args = [].slice.call(arguments); // 将 arguments 对象转为数组 var constructor = args.shift(); // 取出构造函数 var context = Object.create(constructor.prototype); // 创建一个空对象,继承构造函数的 prototype 属性 var result = constructor.apply(context, args); // 执行构造函数 return (typeof result === 'object' && result != null) ? result : context; // 如果返回结果是对象,就直接返回,否则返回 context 对象 } var actor = _new(Person, '张三', 28); // 实例 new.target 函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined function f() { console.log(new.target === f); } f() // false new f() // true 使用这个属性,可以判断函数调用的时候,是否使用new命令 function f() { if (!new.target) { throw new Error('请使用 new 命令调用!'); } // ... } f() // Uncaught Error: 请使用 new 命令调用! Object.create() 创建实例对象 构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法 var person1 = { name: '张三', age: 38, greeting: function() { console.log('Hi! I\'m ' + this.name + '.'); } }; var person2 = Object.create(person1); person2.name // 张三 person2.greeting() // Hi! I'm 张三. 上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法 this 关键字 涵义 this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义大部分开发任务都无法完成。this可以用在构造函数之中,表示实例对象;除此之外,this还可以用在别的场合。但不管是什么场合,this都有一个共同点:它总是返回一个对象;简单说,this就是属性或方法“当前”所在的对象 var person = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; person.describe() // "姓名:张三" 上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向person,this.name就是person.name。由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的 var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var B = { name: '李四' }; B.describe = A.describe; B.describe() // "姓名:李四" 上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象是B,所以this.name就指向B.name。稍稍重构这个例子,this的动态指向就能看得更清楚 function f() { return '姓名:'+ this.name; } var A = { name: '张三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名:张三" B.describe() // "姓名:李四" 只要函数被赋给另一个变量,this的指向就会变 var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; var f = A.describe; f() // "姓名:李四" 上面代码中,A.describe被赋值给变量f,内部的this就会指向f运行时所在的对象(本例是顶层对象)。再看一个网页编程的例子 <input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){ if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } </script> 上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),然后就可以从this.value上面读到用户的输入值 总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方 实质 JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系 var obj = { foo: 5 }; 上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的 { foo: { [[value]]: 5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } } 注意,foo属性的值保存在属性描述对象的value属性里面。这样的结构是很清晰的,问题在于属性的值可能是一个函数;这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性 var obj = { foo: function () {} }; 由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行 var f = function () {}; var obj = { f: f }; f() // 单独执行 obj.f() // obj 环境执行 JavaScript 允许在函数体内部,引用当前环境的其他变量 var f = function () { console.log(x); }; 上面代码中,函数体里面使用了变量x,该变量由运行环境提供。现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境 var f = function () { console.log(this.x);} var x = 1; var obj = { f: f, x: 2}; // 单独执行 f() // 1 // obj 环境执行 obj.f() // 2 上面代码中,函数f在全局环境执行,this.x指向全局环境的x;在obj环境执行,this.x指向obj.x 使用场合 this主要有以下几个使用场合 全局环境 全局环境使用this,它指的就是顶层对象window this === window // true function f() { console.log(this === window); } f() // true 上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this就是指顶层对象window 构造函数 构造函数中的this,指的是实例对象 var Obj = function (p) { this.p = p; }; var o = new Obj('Hello World!'); o.p // "Hello World!" 上面代码定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性 对象的方法 如果对象的方法里面包含this,this的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。但是,这条规则很不容易把握。请看下面的代码 var obj ={ foo: function () { console.log(this); } }; obj.foo() // obj 上面代码中,obj.foo方法执行时,它内部的this指向obj。但是,下面这几种用法,都会改变this的指向 // 情况一 (obj.foo = obj.foo)() // window // 情况二 (false || obj.foo)() // window // 情况三 (1, obj.foo)() // window 上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj。可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码 // 情况一 (obj.foo = function () { console.log(this);})() // 等同于 (function () { console.log(this);})() // 情况二 (false || function () { console.log(this);})() // 情况三 (1, function () { console.log(this);})() 如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层 var a = { p: 'Hello', b: { m: function() { console.log(this.p); } } }; a.b.m() // undefined 上面代码中,a.b.m方法在a对象的第二层,该方法内部的this不是指向a,而是指向a.b,因为实际执行的是下面的代码 var b = { m: function() { console.log(this.p); } }; var a = { p: 'Hello', b: b }; (a.b).m() // 等同于 b.m() 如果要达到预期效果,只有写成下面这样 var a = { b: { m: function() { console.log(this.p); }, p: 'Hello' } }; 如果这时将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象 var a = { b: { m: function() { console.log(this.p); }, p: 'Hello' } }; var hello = a.b.m; hello() // undefined 上面代码中,m是多层对象内部的一个方法。为求简便,将其赋值给hello变量,结果调用时,this指向了顶层对象。为了避免这个问题,可以只将m所在的对象赋值给hello,这样调用时,this的指向就不会变 var hello = a.b; hello.m() // Hello 使用注意点 避免多层 this 由于this的指向是不确定的,所以切勿在函数中包含多层的this var o = { f1: function () { console.log(this); var f2 = function () { console.log(this); }(); } } o.f1() // Object // Window 上面代码包含两层this,结果运行后,第一层指向对象o,第二层指向全局对象,因为实际执行的是下面的代码 var temp = function () { console.log(this); }; var o = { f1: function () { console.log(this); var f2 = temp(); } } 一个解决方法是在第二层改用一个指向外层this的变量 var o = { f1: function() { console.log(this); var that = this; var f2 = function() { console.log(that); }(); } } o.f1() // Object // Object 上面代码定义了变量that,固定指向外层的this,然后在内层使用that,就不会发生this指向的改变。事实上,使用一个变量固定this的值,然后内层函数调用这个变量,是非常常见的做法。JavaScript 提供了严格模式,也可以硬性避免这种问题。严格模式下,如果函数内部的this指向顶层对象,就会报错 var counter = { count: 0 }; counter.inc = function () { 'use strict'; this.count++ }; var f = counter.inc; f() // TypeError: Cannot read property 'count' of undefined 上面代码中,inc方法通过'use strict'声明采用严格模式,这时内部的this一旦指向顶层对象,就会报错 避免数组处理方法中的 this 数组的map和foreach方法,允许提供一个函数作为参数。这个函数内部不应该使用this var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }); } } o.f() // undefined a1 // undefined a2 上面代码中,foreach方法的回调函数中的this,其实是指向window对象,因此取不到o.v的值。原因跟上一段的多层this是一样的,就是内层的this不指向外部,而指向顶层对象。解决这个问题的一种方法,就是前面提到的,使用中间变量固定this var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { var that = this; this.p.forEach(function (item) { console.log(that.v+' '+item); }); } } o.f() // hello a1 // hello a2 另一种方法是将this当作foreach方法的第二个参数,固定它的运行环境 var o = { v: 'hello', p: [ 'a1', 'a2' ], f: function f() { this.p.forEach(function (item) { console.log(this.v + ' ' + item); }, this); } } o.f() // hello a1 // hello a2 避免回调函数中的 this 回调函数中的this往往会改变指向,最好避免使用 var o = new Object(); o.f = function () { console.log(this === o);} $('#button').on('click', o.f); // jQuery 的写法 上面代码中,点击按钮以后,控制台会显示false。原因是此时this不再指向o对象,而是指向按钮的 DOM 对象,因为f方法是在按钮对象的环境中被调用的。这种细微的差别,很容易在编程中忽视,导致难以察觉的错误。为了解决这个问题,可以采用下面的一些方法对this进行绑定,也就是使得this固定指向某个对象,减少不确定性 绑定 this 的方法 this的动态切换,固然为 JavaScript 创造了巨大的灵活性,但也使得编程变得困难和模糊。有时,需要把this固定下来,避免出现意想不到的情况。JavaScript 提供了call、apply、bind这三个方法,来切换/固定this的指向 Function.prototype.call() 函数实例的call方法,可以指定函数内部this的指向(即函数执行时所在的作用域),然后在所指定的作用域中,调用该函数 var obj = {}; var f = function () { return this; }; f() === window // true f.call(obj) === obj // true 上面代码中,全局环境运行函数f时,this指向全局环境(浏览器为window对象);call方法可以改变this的指向,指定this指向对象obj,然后在对象obj的作用域中运行函数f。call方法的参数,应该是一个对象。如果参数为空、null和undefined,则默认传入全局对象 var n = 123; var obj = { n: 456 }; function a() { console.log(this.n);} a.call() // 123 a.call(null) // 123 a.call(undefined) // 123 a.call(window) // 123 a.call(obj) // 456 上面代码中,a函数中的this关键字,如果指向全局对象,返回结果为123。如果使用call方法将this关键字指向obj对象,返回结果为456。可以看到,如果call方法没有参数,或者参数为null或undefined,则等同于指向全局对象。如果call方法的参数是一个原始值,那么这个原始值会自动转成对应的包装对象,然后传入call方法 var f = function () { return this; }; f.call(5) // Number {[[PrimitiveValue]]: 5} 上面代码中,call的参数为5,不是对象,会被自动转成包装对象(Number的实例),绑定f内部的this。call方法还可以接受多个参数 func.call(thisValue, arg1, arg2, ...) call的第一个参数就是this所要指向的那个对象,后面的参数则是函数调用时所需的参数 function add(a, b) { return a + b;} add.call(this, 1, 2) // 3 上面代码中,call方法指定函数add内部的this绑定当前环境(对象),并且参数为1和2,因此函数add运行后得到3。call方法的一个应用是调用对象的原生方法 var obj = {}; // object的hasOwnProperty()方法返回一个布尔值,判断对象是否包含特定的自身(非继承)属性 obj.hasOwnProperty('toString') // false // 覆盖掉继承的 hasOwnProperty 方法 obj.hasOwnProperty = function () { return true;}; obj.hasOwnProperty('toString') // true Object.prototype.hasOwnProperty.call(obj, 'toString') // false 上面代码中,hasOwnProperty是obj对象继承的方法,如果这个方法一旦被覆盖,就不会得到正确结果。call方法可以解决这个问题,它将hasOwnProperty方法的原始定义放到obj对象上执行,这样无论obj上有没有同名方法,都不会影响结果 Function.prototype.apply() apply方法的作用与call方法类似,也是改变this指向,然后再调用该函数。唯一的区别就是,它接收一个数组作为函数执行时的参数,使用格式如下: func.apply(thisValue, [arg1, arg2, ...]) apply方法的第一个参数也是this所要指向的那个对象,如果设为null或undefined,则等同于指定全局对象。第二个参数则是一个数组,该数组的所有成员依次作为参数,传入原函数。原函数的参数,在call方法中必须一个个添加,但是在apply方法中,必须以数组形式添加 function f(x, y){ console.log(x + y);} f.call(null, 1, 1) // 2 f.apply(null, [1, 1]) // 2 上面代码中,f函数本来接受两个参数,使用apply方法以后,就变成可以接受一个数组作为参数。利用这一点,可以做一些有趣的应用 找出数组最大元素 JavaScript 不提供找出数组最大元素的函数。结合使用apply方法和Math.max方法,就可以返回数组的最大元素 var a = [10, 2, 4, 15, 9]; Math.max.apply(null, a) // 15 将数组的空元素变为undefined 通过apply方法,利用Array构造函数将数组的空元素变成undefined Array.apply(null, ['a', ,'b']) // [ 'a', undefined, 'b' ] 空元素与undefined的差别在于,数组的forEach方法会跳过空元素,但是不会跳过undefined。因此,遍历内部元素的时候,会得到不同的结果 var a = ['a', , 'b']; function print(i) { console.log(i);} a.forEach(print) // a // b Array.apply(null, a).forEach(print) // a // undefined // b 转换类似数组的对象 另外,利用数组对象的slice方法,可以将一个类似数组的对象(比如arguments对象)转为真正的数组 Array.prototype.slice.apply({0: 1, length: 1}) // [1] Array.prototype.slice.apply({0: 1}) // [] Array.prototype.slice.apply({0: 1, length: 2}) // [1, undefined] Array.prototype.slice.apply({length: 1}) // [undefined] 上面代码的apply方法的参数都是对象,但是返回结果都是数组,这就起到了将对象转成数组的目的。从上面代码可以看到,这个方法起作用的前提是,被处理的对象必须有length属性,以及相对应的数字键 绑定回调函数的对象 前面的按钮点击事件的例子,可以改写如下: var o = new Object(); o.f = function () { console.log(this === o);} var f = function (){ o.f.apply(o); // 或者 o.f.call(o); }; $('#button').on('click', f); // jQuery 的写法 上面代码中,点击按钮以后,控制台将会显示true。由于apply方法(或者call方法)不仅绑定函数执行时所在的对象,还会立即执行函数,因此不得不把绑定语句写在一个函数体内。更简洁的写法是采用下面介绍的bind方法 Function.prototype.bind() bind方法用于将函数体内的this绑定到某个对象,然后返回一个新函数 var d = new Date(); d.getTime() // 1553063951536 var print = d.getTime; print() // Uncaught TypeError: this is not a Date object. 上面代码中,我们将d.getTime方法赋给变量print,然后调用print就报错了。这是因为getTime方法内部的this,绑定Date对象的实例,赋给变量print以后,内部的this已经不指向Date对象的实例了。bind方法可以解决这个问题 var print = d.getTime.bind(d); print() // 1553063951536 上面代码中,bind方法将getTime方法内部的this绑定到d对象,这时就可以安全地将这个方法赋值给其他变量了。bind方法的参数就是所要绑定this的对象,下面是一个更清晰的例子 var counter = { count: 0, inc: function () { this.count++; } }; var func = counter.inc.bind(counter); func(); counter.count // 1 上面代码中,counter.inc方法被赋值给变量func。这时必须用bind方法将inc内部的this,绑定到counter,否则就会出错。this绑定到其他对象也是可以的 var counter = { count: 0, inc: function () { this.count++; } }; var obj = { count: 100 }; var func = counter.inc.bind(obj); func(); obj.count // 101 上面代码中,bind方法将inc方法内部的this,绑定到obj对象。结果调用func函数以后,递增的就是obj内部的count属性。bind还可以接受更多的参数,将这些参数绑定原函数的参数 var add = function (x, y) { return x * this.m + y * this.n;} var obj = { m: 2, n: 3}; var newAdd = add.bind(obj, 5); newAdd(6) // 28 上面代码中,bind方法除了绑定this对象,还将add函数的第一个参数x绑定成5,然后返回一个新函数newAdd,这个函数只要再接受一个参数y就能运行了。如果bind方法的第一个参数是null或undefined,等于将this绑定到全局对象,函数运行时this指向顶层对象(浏览器为window) function add(x, y) { return x + y;} var plus = add.bind(null, 5); plus(10) // 15 上面代码中,函数add内部并没有this,使用bind方法的主要目的是绑定参数x,以后每次运行新函数plus,就只需要提供另一个参数y就够了。而且因为add内部没有this,所以bind的第一个参数是null,不过这里如果是其他对象,也没有影响。bind方法有一些使用注意点 每一次返回一个新函数 bind方法每运行一次,就返回一个新函数,这会产生一些问题。比如,监听事件的时候,不能写成下面这样 element.addEventListener('click', o.m.bind(o)); 上面代码中,click事件绑定bind方法生成的一个匿名函数。这样会导致无法取消绑定,所以,下面的代码是无效的 element.removeEventListener('click', o.m.bind(o)); 正确的方法是写成下面这样: var listener = o.m.bind(o); element.addEventListener('click', listener); // ... element.removeEventListener('click', listener); 结合回调函数使用 回调函数是 JavaScript 最常用的模式之一,但是一个常见的错误是,将包含this的方法直接当作回调函数。解决方法就是使用bind方法,将counter.inc绑定counter var counter = { count: 0, inc: function () {'use strict'; this.count++; } }; function callIt(callback) { callback();} callIt(counter.inc.bind(counter)); counter.count // 1 上面代码中,callIt方法会调用回调函数。这时如果直接把counter.inc传入,调用时counter.inc内部的this就会指向全局对象。使用bind方法将counter.inc绑定counter以后,就不会有这个问题,this总是指向counter。还有一种情况比较隐蔽,就是某些数组方法可以接受一个函数当作参数。这些函数内部的this指向,很可能也会出错 var obj = { name: '张三', times: [1, 2, 3], print: function () { this.times.forEach(function (n) { console.log(this.name); }); } }; obj.print() // 没有任何输出 上面代码中,obj.print内部this.times的this是指向obj的,这个没有问题。但是,forEach方法的回调函数内部的this.name却是指向全局对象,导致没有办法取到值。稍微改动一下,就可以看得更清楚 obj.print = function () { this.times.forEach(function (n) { console.log(this === window); }); }; obj.print() // true // true // true 解决这个问题,也是通过bind方法绑定this obj.print = function () { this.times.forEach(function (n) { console.log(this.name); }.bind(this)); }; obj.print() // 张三 // 张三 // 张三 结合call方法使用 利用bind方法,可以改写一些 JavaScript 原生方法的使用形式,以数组的slice方法为例 [1, 2, 3].slice(0, 1) // [1] // 等同于 Array.prototype.slice.call([1, 2, 3], 0, 1) // [1] 上面的代码中,数组的slice方法从[1, 2, 3]里面,按照指定位置和长度切分出另一个数组。这样做的本质是在[1, 2, 3]上面调用Array.prototype.slice方法,因此可以用call方法表达这个过程,得到同样的结果。call方法实质上是调用Function.prototype.call方法,因此上面的表达式可以用bind方法改写 var slice = Function.prototype.call.bind(Array.prototype.slice); slice([1, 2, 3], 0, 1) // [1] 上面代码的含义就是,将Array.prototype.slice变成Function.prototype.call方法所在的对象,调用时就变成了Array.prototype.slice.call。类似的写法还可以用于其他数组方法 var push = Function.prototype.call.bind(Array.prototype.push); var pop = Function.prototype.call.bind(Array.prototype.pop); var a = [1 ,2 ,3]; push(a, 4) a // [1, 2, 3, 4] pop(a) a // [1, 2, 3] 如果再进一步,将Function.prototype.call方法绑定到Function.prototype.bind对象,就意味着bind的调用形式也可以被改写 function f() { console.log(this.v);} var o = { v: 123 }; var bind = Function.prototype.call.bind(Function.prototype.bind); bind(f, o)() // 123 上面代码的含义就是,将Function.prototype.bind方法绑定在Function.prototype.call上面,所以bind方法就可以直接使用,不需要在函数实例上使用 对象的继承 面向对象编程很重要的一个方面,就是对象的继承。A 对象通过继承 B 对象,就能直接拥有 B 对象的所有属性和方法。这对于代码的复用是非常有用的。大部分面向对象的编程语言,都是通过“类”(class)实现对象的继承。传统上,JavaScript 语言的继承不通过 class,而是通过“原型对象”(prototype)实现,这里主要介绍 JavaScript 的原型链继承。ES6 引入了 class 语法,基于 class 的继承暂不在这里介绍 原型对象概述 构造函数的缺点 JavaScript 通过构造函数生成新对象,因此构造函数可以视为对象的模板。实例对象的属性和方法,可以定义在构造函数内部 function Cat (name, color) { this.name = name; this.color = color;} var cat1 = new Cat('大毛', '白色'); cat1.name // '大毛' cat1.color // '白色' 上面代码中,Cat函数是一个构造函数,函数内部定义了name属性和color属性,所有实例对象(上例是cat1)都会生成这两个属性,即这两个属性会定义在实例对象上面。通过构造函数为实例对象定义属性,虽然很方便,但是有一个缺点。同一个构造函数的多个实例之间,无法共享属性,从而造成对系统资源的浪费 function Cat(name, color) { this.name = name; this.color = color; this.meow = function () { console.log('喵喵'); }; } var cat1 = new Cat('大毛', '白色'); var cat2 = new Cat('二毛', '黑色'); cat1.meow === cat2.meow // false 上面代码中,cat1和cat2是同一个构造函数的两个实例,它们都具有meow方法。由于meow方法是生成在每个实例对象上面,所以两个实例就生成了两次。也就是说,每新建一个实例,就会新建一个meow方法。这既没有必要,又浪费系统资源,因为所有meow方法都是同样的行为,完全应该共享。这个问题的解决方法,就是 JavaScript 的原型对象(prototype) prototype 属性的作用 JavaScript 继承机制的设计思想就是,原型对象的所有属性和方法,都能被实例对象共享。也就是说,如果属性和方法定义在原型上,那么所有实例对象就能共享,不仅节省了内存,还体现了实例对象之间的联系。下面,先看怎么为对象指定原型。JavaScript 规定,每个函数都有一个prototype属性,指向一个对象 function f() {} typeof f.prototype // "object" 上面代码中,函数f默认具有prototype属性,指向一个对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型 function f() {} typeof f.prototype // "object" 上面代码中,函数f默认具有prototype属性,指向一个对象。对于普通函数来说,该属性基本无用。但是,对于构造函数来说,生成实例的时候,该属性会自动成为实例对象的原型 function Animal(name) { this.name = name;} Animal.prototype.color = 'white'; var cat1 = new Animal('大毛'); var cat2 = new Animal('二毛'); cat1.color // 'white' cat2.color // 'white' 上面代码中,构造函数Animal的prototype属性,就是实例对象cat1和cat2的原型对象。原型对象上添加一个color属性,结果,实例对象都共享了该属性。原型对象的属性不是实例对象自身的属性。只要修改原型对象,变动就立刻会体现在所有实例对象上。也就是说,当实例对象本身没有某个属性或方法的时候,它会到原型对象去寻找该属性或方法。这就是原型对象的特殊之处。如果实例对象自身就有某个属性或方法,它就不会再去原型对象寻找这个属性或方法 Animal.prototype.color = 'yellow'; cat1.color = 'black'; cat1.color // 'black' cat2.color // 'yellow' Animal.prototype.color // 'yellow'; 总结一下,原型对象的作用,就是定义所有实例对象共享的属性和方法。这也是它被称为原型对象的原因,而实例对象可以视作从原型对象衍生出来的子对象 Animal.prototype.walk = function () { console.log(this.name + ' is walking'); }; 上面代码中,Animal.prototype对象上面定义了一个walk方法,这个方法将可以在所有Animal实例对象上面调用 原型链 JavaScript 规定,所有对象都有自己的原型对象(prototype)。一方面,任何一个对象,都可以充当其他对象的原型;另一方面,由于原型对象也是对象,所以它也有自己的原型。因此,就会形成一个“原型链”(prototype chain):对象到原型,再到原型的原型……。如果一层层地上溯,所有对象的原型最终都可以上溯到Object.prototype,即Object构造函数的prototype属性。也就是说,所有对象都继承了Object.prototype的属性。这就是所有对象都有valueOf和toString方法的原因,因为这是从Object.prototype继承的。那么,Object.prototype对象有没有它的原型呢?回答是Object.prototype的原型是null。null没有任何属性和方法,也没有自己的原型。因此,原型链的尽头就是null Object.getPrototypeOf(Object.prototype) // null 上面代码表示,Object.prototype对象的原型是null,由于null没有任何属性,所以原型链到此为止。Object.getPrototypeOf方法返回参数对象的原型。读取对象的某个属性时,JavaScript 引擎先寻找对象本身的属性,如果找不到,就到它的原型去找,如果还是找不到,就到原型的原型去找。如果直到最顶层的Object.prototype还是找不到,则返回undefined。如果对象自身和它的原型,都定义了一个同名属性,那么优先读取对象自身的属性,这叫做“覆盖”(overriding) 注意,一级级向上,在整个原型链上寻找某个属性,对性能是有影响的。所寻找的属性在越上层的原型对象,对性能的影响越大。如果寻找某个不存在的属性,将会遍历整个原型链。举例来说,如果让构造函数的prototype属性指向一个数组,就意味着实例对象可以调用数组方法 var MyArray = function () {}; MyArray.prototype = new Array(); MyArray.prototype.constructor = MyArray; var mine = new MyArray(); mine.push(1, 2, 3); mine.length // 3 mine instanceof Array // true 上面代码中,mine是构造函数MyArray的实例对象,由于MyArray.prototype指向一个数组实例,使得mine可以调用数组方法(这些方法定义在数组实例的prototype对象上面)。最后那行instanceof表达式,用来比较一个对象是否为某个构造函数的实例,结果就是证明mine为Array的实例。上面代码还出现了原型对象的constructor属性 constructor 属性 prototype对象有一个constructor属性,默认指向prototype对象所在的构造函数 function P() {} P.prototype.constructor === P // true 由于constructor属性定义在prototype对象上面,意味着可以被所有实例对象继承 function P() {} var p = new P(); p.constructor === P // true p.constructor === P.prototype.constructor // true p.hasOwnProperty('constructor') // false P.prototype.hasOwnProperty('constructor') // true 上面代码中,p是构造函数P的实例对象,但是p自身没有constructor属性,该属性其实是读取原型链上面的P.prototype.constructor属性。constructor属性的作用是,可以得知某个实例对象,到底是哪一个构造函数产生的 function F() {}; var f = new F(); f.constructor === F // true f.constructor === RegExp // false 上面代码中,constructor属性确定了实例对象f的构造函数是F,而不是RegExp。另一方面,有了constructor属性,就可以从一个实例对象新建另一个实例 function Constr() {} var x = new Constr(); var y = new x.constructor(); y instanceof Constr // true 上面代码中,x是构造函数Constr的实例,可以从x.constructor间接调用构造函数。这使得在实例方法中,调用自身的构造函数成为可能 Constr.prototype.createCopy = function () { return new this.constructor(); }; 上面代码中,createCopy方法调用构造函数,新建另一个实例。constructor属性表示原型对象与构造函数之间的关联关系,如果修改了原型对象,一般会同时修改constructor属性,防止引用的时候出错 function Person(name) { this.name = name;} Person.prototype.constructor === Person // true Person.prototype = { method: function () {} }; Person.prototype.constructor === Person // false Person.prototype.constructor === Object // true 上面代码中,构造函数Person的原型对象改掉了,但是没有修改constructor属性,导致这个属性不再指向Person。由于Person的新原型是一个普通对象,而普通对象的constructor属性指向Object构造函数,导致Person.prototype.constructor变成了Object。所以,修改原型对象时,一般要同时修改constructor属性的指向 // 坏的写法 C.prototype = { method1: function (...) { ... }, }; // 好的写法 C.prototype = { constructor: C, method1: function (...) { ... }, }; // 更好的写法 C.prototype.method1 = function (...) { ... }; 上面代码中,要么将constructor属性重新指向原来的构造函数,要么只在原型对象上添加方法,这样可以保证instanceof运算符不会失真。如果不能确定constructor属性是什么函数,还有一个办法:通过name属性,从实例得到构造函数的名称 function Foo() {} var f = new Foo(); f.constructor.name // "Foo" instanceof 运算符 instanceof运算符返回一个布尔值,表示对象是否为某个构造函数的实例 var v = new Vehicle(); v instanceof Vehicle // true 上面代码中,对象v是构造函数Vehicle的实例,所以返回true。instanceof运算符的左边是实例对象,右边是构造函数。它会检查右边构建函数的原型对象(prototype),是否在左边对象的原型链上。因此,下面两种写法是等价的 v instanceof Vehicle // 等同于 Vehicle.prototype.isPrototypeOf(v) 由于instanceof检查整个原型链,因此同一个实例对象,可能会对多个构造函数都返回true var d = new Date(); d instanceof Date // true d instanceof Object // true 上面代码中,d同时是Date和Object的实例,因此对这两个构造函数都返回true。instanceof的原理是检查右边构造函数的prototype属性,是否在左边对象的原型链上。有一种特殊情况,就是左边对象的原型链上,只有null对象。这时,instanceof判断会失真 var obj = Object.create(null); typeof obj // "object" Object.create(null) instanceof Object // false 上面代码中,Object.create(null)返回一个新对象obj,它的原型是null。右边的构造函数Object的prototype属性,不在左边的原型链上,因此instanceof就认为obj不是Object的实例。但是,只要一个对象的原型不是null,instanceof运算符的判断就不会失真。instanceof运算符的一个用处,是判断值的类型 var x = [1, 2, 3]; var y = {}; x instanceof Array // true y instanceof Object // true 注意,instanceof运算符只能用于对象,不适用原始类型的值。此外,对于undefined和null,instanceOf运算符总是返回false var s = 'hello'; s instanceof String // false undefined instanceof Object // false null instanceof Object // false 利用instanceof运算符,还可以巧妙地解决,调用构造函数时,忘了加new命令的问题 function Fubar (foo, bar) { if (this instanceof Fubar) { this._foo = foo; this._bar = bar; } else { return new Fubar(foo, bar); } } 上面代码使用instanceof运算符,在函数体内部判断this关键字是否为构造函数Fubar的实例。如果不是,就表明忘了加new命令 构造函数的继承 让一个构造函数继承另一个构造函数,是非常常见的需求。这可以分成两步实现 第一步:在子类的构造函数中,调用父类的构造函数 function Sub(value) { Super.call(this); this.prop = value; } 上面代码中,Sub是子类的构造函数,this是子类的实例。在实例上调用父类的构造函数Super,就会让子类实例具有父类实例的属性。 第二步,是让子类的原型指向父类的原型,这样子类就可以继承父类原型 Sub.prototype = Object.create(Super.prototype); Sub.prototype.constructor = Sub; Sub.prototype.method = '...'; 上面代码中,Sub.prototype是子类的原型,要将它赋值为Object.create(Super.prototype),而不是直接等于Super.prototype。否则后面两行对Sub.prototype的操作,会连父类的原型Super.prototype一起修改掉。 另外一种写法是Sub.prototype等于一个父类实例 Sub.prototype = new Super(); 上面这种写法也有继承的效果,但是子类会具有父类实例的方法。有时,这可能不是我们需要的,所以不推荐使用这种写法。举例来说,下面是一个Shape构造函数 function Shape() { this.x = 0; this.y = 0;} Shape.prototype.move = function (x, y) { this.x += x; this.y += y; console.info('Shape moved.'); }; 我们需要让Rectangle构造函数继承Shape // 第一步,子类继承父类的实例 function Rectangle() { Shape.call(this); // 调用父类构造函数 } // 另一种写法 function Rectangle() { this.base = Shape; this.base(); } // 第二步,子类继承父类的原型 Rectangle.prototype = Object.create(Shape.prototype); Rectangle.prototype.constructor = Rectangle; 采用这样的写法以后,instanceof运算符会对子类和父类的构造函数,都返回true var rect = new Rectangle(); rect instanceof Rectangle // true rect instanceof Shape // true 上面代码中,子类是整体继承父类。有时只需要单个方法的继承,这时可以采用下面的写法 ClassB.prototype.print = function() { ClassA.prototype.print.call(this); // some code } 上面代码中,子类B的print方法先调用父类A的print方法,再部署自己的代码。这就等于继承了父类A的print方法 多重继承 JavaScript 不提供多重继承功能,即不允许一个对象同时继承多个对象。但是,可以通过变通方法,实现这个功能 function M1() { this.hello = 'hello';} function M2() { this.world = 'world';} function S() { M1.call(this); M2.call(this);} S.prototype = Object.create(M1.prototype); // 继承 M1 Object.assign(S.prototype, M2.prototype); // 继承链上加入 M2 S.prototype.constructor = S; // 指定构造函数 var s = new S(); s.hello // 'hello' s.world // 'world' 上面代码中,子类S同时继承了父类M1和M2。这种模式又称为 Mixin(混入) 模块 随着网站逐渐变成“互联网应用程序”,嵌入网页的 JavaScript 代码越来越庞大,越来越复杂。网页越来越像桌面程序,需要一个团队分工协作、进度管理、单元测试等等……开发者必须使用软件工程的方法,管理网页的业务逻辑。JavaScript 模块化编程,已经成为一个迫切的需求。理想情况下,开发者只需要实现核心的业务逻辑,其他都可以加载别人已经写好的模块。但是,JavaScript 不是一种模块化编程语言,ES6 才开始支持“类”和“模块”。下面介绍传统的做法,如何利用对象实现模块的效果 基本的实现方法 模块是实现特定功能的一组属性和方法的封装。简单的做法是把模块写成一个对象,所有的模块成员都放到这个对象里面 var module1 = new Object({ _count : 0, m1 : function (){ //... }, m2 : function (){ //... } }); 上面的函数m1和m2,都封装在module1对象里。使用的时候,就是调用这个对象的属性 module1.m1(); 但是,这样的写法会暴露所有模块成员,内部状态可以被外部改写。比如,外部代码可以直接改变内部计数器的值 module1._count = 5; 封装私有变量:构造函数的写法 我们可以利用构造函数,封装私有变量 function StringBuilder() { var buffer = []; this.add = function (str) { buffer.push(str); }; this.toString = function () { return buffer.join(''); }; } 上面代码中,buffer是模块的私有变量。一旦生成实例对象,外部是无法直接访问buffer的。但是,这种方法将私有变量封装在构造函数中,导致构造函数与实例对象是一体的,总是存在于内存之中,无法在使用完成后清除。这意味着,构造函数有双重作用,既用来塑造实例对象,又用来保存实例对象的数据,违背了构造函数与实例对象在数据上相分离的原则(即实例对象的数据,不应该保存在实例对象以外)。同时,非常耗费内存 function StringBuilder() { this._buffer = []; } StringBuilder.prototype = { constructor: StringBuilder, add: function (str) { this._buffer.push(str); }, toString: function () { return this._buffer.join(''); } }; 这种方法将私有变量放入实例对象中,好处是看上去更自然,但是它的私有变量可以从外部读写,不是很安全 封装私有变量:立即执行函数的写法 另一种做法是使用“立即执行函数”(Immediately-Invoked Function Expression,IIFE),将相关的属性和方法封装在一个函数作用域里面,可以达到不暴露私有成员的目的 var module1 = (function () { var _count = 0; var m1 = function () { //... }; var m2 = function () { //... }; return {m1 : m1, m2 : m2}; })(); 使用上面的写法,外部代码无法读取内部的_count变量 console.info(module1._count); //undefined 上面的module1就是 JavaScript 模块的基本写法。下面,再对这种写法进行加工 模块的放大模式 如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用“放大模式”(augmentation) var module1 = (function (mod){ mod.m3 = function () { //... }; return mod; })(module1); 上面的代码为module1模块添加了一个新方法m3(),然后返回新的module1模块。在浏览器环境中,模块的各个部分通常都是从网上获取的,有时无法知道哪个部分会先加载。如果采用上面的写法,第一个执行的部分有可能加载一个不存在空对象,这时就要采用"宽放大模式"(Loose augmentation) var module1 = (function (mod) { //... return mod; })(window.module1 || {}); 与"放大模式"相比,“宽放大模式”就是“立即执行函数”的参数可以是空对象 输入全局变量 独立性是模块的重要特点,模块内部最好不与程序的其他部分直接交互。为了在模块内部调用全局变量,必须显式地将其他变量输入模块 var module1 = (function ($, YAHOO) { //... })(jQuery, YAHOO); 上面的module1模块需要使用 jQuery 库和 YUI 库,就把这两个库(其实是两个模块)当作参数输入module1。这样做除了保证模块的独立性,还使得模块之间的依赖关系变得明显。立即执行函数还可以起到命名空间的作用 (function($, window, document) { function go(num) {} function handleEvents() {} function initialize() {} function dieCarouselDie() {} //attach to the global scope window.finalCarousel = { init : initialize, destroy : dieCarouselDie } })( jQuery, window, document ); 上面代码中,finalCarousel对象输出到全局,对外暴露init和destroy接口,内部方法go、handleEvents、initialize、dieCarouselDie都是外部无法调用的 Object 对象的相关方法 JavaScript 在Object对象上面,提供了很多相关方法,处理面向对象编程的相关操作 Object.getPrototypeOf() Object.getPrototypeOf方法返回参数对象的原型。这是获取原型对象的标准方法 var F = function () {}; var f = new F(); Object.getPrototypeOf(f) === F.prototype // true 上面代码中,实例对象f的原型是F.prototype。下面是几种特殊对象的原型 // 空对象的原型是 Object.prototype Object.getPrototypeOf({}) === Object.prototype // true // Object.prototype 的原型是 null Object.getPrototypeOf(Object.prototype) === null // true // 函数的原型是 Function.prototype function f() {} Object.getPrototypeOf(f) === Function.prototype // true Object.setPrototypeOf() Object.setPrototypeOf方法为参数对象设置原型,返回该参数对象。它接受两个参数,第一个是现有对象,第二个是原型对象 var a = {}; var b = {x: 1}; Object.setPrototypeOf(a, b); Object.getPrototypeOf(a) === b // true a.x // 1 上面代码中,Object.setPrototypeOf方法将对象a的原型,设置为对象b,因此a可以共享b的属性。new命令可以使用Object.setPrototypeOf方法模拟 var F = function () { this.foo = 'bar'; }; var f = new F(); // 等同于 var f = Object.setPrototypeOf({}, F.prototype); F.call(f); 上面代码中,new命令新建实例对象,其实可以分成两步。第一步,将一个空对象的原型设为构造函数的prototype属性(上例是F.prototype);第二步,将构造函数内部的this绑定这个空对象,然后执行构造函数,使得定义在this上面的方法和属性(上例是this.foo),都转移到这个空对象上 Object.create() 生成实例对象的常用方法是使用new命令让构造函数返回一个实例。但是很多时候只能拿到一个实例对象,它可能根本不是由构建函数生成的,那么能不能从一个实例对象生成另一个实例对象呢?JavaScript 提供了Object.create方法,用来满足这种需求。该方法接受一个对象作为参数,然后以它为原型,返回一个实例对象。该实例完全继承原型对象的属性 // 原型对象 var A = { print: function () { console.log('hello'); } }; // 实例对象 var B = Object.create(A); Object.getPrototypeOf(B) === A // true B.print() // hello B.print === A.print // true 上面代码中,Object.create方法以A对象为原型,生成了B对象。B继承了A的所有属性和方法。实际上,Object.create方法可以用下面的代码代替 if (typeof Object.create !== 'function') { Object.create = function (obj) { function F() {} F.prototype = obj; return new F(); }; } 上面代码表明,Object.create方法的实质是新建一个空的构造函数F,然后让F.prototype属性指向参数对象obj,最后返回一个F的实例,从而实现让该实例继承obj的属性。下面三种方式生成的新对象是等价的 var obj1 = Object.create({}); var obj2 = Object.create(Object.prototype); var obj3 = new Object(); 如果想要生成一个不继承任何属性(比如没有toString和valueOf方法)的对象,可以将Object.create的参数设为null var obj = Object.create(null); obj.valueOf() // TypeError: Object [object Object] has no method 'valueOf' 上面代码中,对象obj的原型是null,它就不具备一些定义在Object.prototype对象上面的属性,比如valueOf方法。使用Object.create方法的时候,必须提供对象原型,即参数不能为空,或者不是对象,否则会报错 Object.create() // TypeError: Object prototype may only be an Object or null Object.create(123) // TypeError: Object prototype may only be an Object or null Object.create方法生成的新对象,动态继承了原型。在原型上添加或修改任何方法,会立刻反映在新对象之上 var obj1 = { p: 1 }; var obj2 = Object.create(obj1); obj1.p = 2; obj2.p // 2 上面代码中,修改对象原型obj1会影响到实例对象obj2。除了对象的原型,Object.create方法还可以接受第二个参数。该参数是一个属性描述对象,它所描述的对象属性,会添加到实例对象,作为该对象自身的属性 var obj = Object.create({}, { p1: { value: 123, enumerable: true, configurable: true, writable: true,}, p2: { value: 'abc', enumerable: true, configurable: true, writable: true,} }); // 等同于 var obj = Object.create({}); obj.p1 = 123; obj.p2 = 'abc'; Object.create方法生成的对象,继承了它的原型对象的构造函数 function A() {} var a = new A(); var b = Object.create(a); b.constructor === A // true b instanceof A // true 上面代码中,b对象的原型是a对象,因此继承了a对象的构造函数A Object.prototype.isPrototypeOf() 实例对象的isPrototypeOf方法,用来判断该对象是否为参数对象的原型 var o1 = {}; var o2 = Object.create(o1); var o3 = Object.create(o2); o2.isPrototypeOf(o3) // true o1.isPrototypeOf(o3) // true 上面代码中,o1和o2都是o3的原型。这表明只要实例对象处在参数对象的原型链上,isPrototypeOf方法都返回true Object.prototype.isPrototypeOf({}) // true Object.prototype.isPrototypeOf([]) // true Object.prototype.isPrototypeOf(/xyz/) // true Object.prototype.isPrototypeOf(Object.create(null)) // false 上面代码中,由于Object.prototype处于原型链的最顶端,所以对各种实例都返回true,只有直接继承自null的对象除外 Object.prototype.__proto__ 实例对象的__proto__属性(前后各两个下划线),返回该对象的原型。该属性可读写 var obj = {}; var p = {}; obj.__proto__ = p; Object.getPrototypeOf(obj) === p // true 上面代码通过__proto__属性,将p对象设为obj对象的原型。根据语言标准,__proto__属性只有浏览器才需要部署,其他环境可以没有这个属性。它前后的两根下划线,表明它本质是一个内部属性,不应该对使用者暴露;因此,应该尽量少用这个属性,而是用Object.getPrototypeOf()和Object.setPrototypeOf(),进行原型对象的读写操作。原型链可以用__proto__很直观地表示 var A = { name: '张三'}; var B = { name: '李四'}; var proto = { print: function () { console.log(this.name); } }; A.__proto__ = proto; B.__proto__ = proto; A.print() // 张三 B.print() // 李四 A.print === B.print // true A.print === proto.print // true B.print === proto.print // true 上面代码中,A对象和B对象的原型都是proto对象,它们都共享proto对象的print方法。也就是说,A和B的print方法,都是在调用proto对象的print方法 获取原型对象方法的比较 如前所述,__proto__属性指向当前对象的原型对象,即构造函数的prototype属性 var obj = new Object(); obj.__proto__ === Object.prototype // true obj.__proto__ === obj.constructor.prototype // true 上面代码首先新建了一个对象obj,它的__proto__属性,指向构造函数(Object或obj.constructor)的prototype属性;因此,获取实例对象obj的原型对象,有三种方法 obj.__proto__ obj.constructor.prototype Object.getPrototypeOf(obj) 上面三种方法之中,前两种都不是很可靠。__proto__属性只有浏览器才需要部署,其他环境可以不部署。而obj.constructor.prototype在手动改变原型对象时,可能会失效 var P = function () {}; var p = new P(); var C = function () {}; C.prototype = p; var c = new C(); c.constructor.prototype === p // false 上面代码中,构造函数C的原型对象被改成了p,但是实例对象的c.constructor.prototype却没有指向p。所以,在改变原型对象时,一般要同时设置constructor属性 C.prototype = p; C.prototype.constructor = C; var c = new C(); c.constructor.prototype === p // true 因此,推荐使用第三种Object.getPrototypeOf方法,获取原型对象 Object.getOwnPropertyNames() Object.getOwnPropertyNames方法返回一个数组,成员是参数对象本身的所有属性的键名,不包含继承的属性键名 Object.getOwnPropertyNames(Date) // ["length", "name", "prototype", "now", "parse", "UTC"] 上面代码中,Object.getOwnPropertyNames方法返回Date所有自身的属性名。对象本身的属性之中,有的是可以遍历的(enumerable)有的是不可以遍历的。Object.getOwnPropertyNames方法返回所有键名,不管是否可以遍历。只获取那些可以遍历的属性,使用Object.keys方法 Object.keys(Date) // [] 上面代码表明,Date对象所有自身的属性,都是不可以遍历的 Object.prototype.hasOwnProperty() 对象实例的hasOwnProperty方法返回一个布尔值,用于判断某个属性定义在对象自身,还是定义在原型链上 Date.hasOwnProperty('length') // true Date.hasOwnProperty('toString') // false 上面代码表明,Date.length(构造函数Date可以接受多少个参数)是Date自身的属性,Date.toString是继承的属性。另外,hasOwnProperty方法是 JavaScript 之中唯一一个处理对象属性时,不会遍历原型链的方法 in 运算符和 for...in 循环 in运算符返回一个布尔值,表示一个对象是否具有某个属性。它不区分该属性是对象自身的属性,还是继承的属性 'length' in Date // true 'toString' in Date // true in运算符常用于检查一个属性是否存在。获得对象的所有可遍历属性(不管是自身的还是继承的),可以使用for...in循环 var o1 = { p1: 123 }; var o2 = Object.create(o1, { p2: { value: "abc", enumerable: true } }); for (p in o2) { console.info(p);} // p2 // p1 上面代码中,对象o2的p2属性是自身的,p1属性是继承的。这两个属性都会被for...in循环遍历。为了在for...in循环中获得对象自身的属性,可以采用hasOwnProperty方法判断一下 for ( var name in object ) { if ( object.hasOwnProperty(name) ) { /* loop code */ } } 获得对象的所有属性(不管是自身的还是继承的,也不管是否可枚举),可以使用下面的函数 function inheritedPropertyNames(obj) { var props = {}; while(obj) { Object.getOwnPropertyNames(obj).forEach(function(p) { props[p] = true;}); obj = Object.getPrototypeOf(obj); } return Object.getOwnPropertyNames(props); } 上面代码依次获取obj对象的每一级原型对象“自身”的属性,从而获取obj对象的“所有”属性,不管是否可遍历。下面是一个例子,列出Date对象的所有属性 inheritedPropertyNames(Date) // ["caller","constructor","toString","UTC",...] 对象的拷贝 如果要拷贝一个对象,需要做到下面两件事情。 1.确保拷贝后的对象,与原对象具有同样的原型。 2.确保拷贝后的对象,与原对象具有同样的实例属性。 下面就是根据上面两点,实现的对象拷贝函数 function copyObject(orig) { var copy = Object.create(Object.getPrototypeOf(orig)); copyOwnPropertiesFrom(copy, orig); return copy; } function copyOwnPropertiesFrom(target, source) { Object.getOwnPropertyNames(source).forEach(function (propKey) { var desc = Object.getOwnPropertyDescriptor(source, propKey); Object.defineProperty(target, propKey, desc); }); return target; } 另一种更简单的写法,是利用 ES2017 才引入标准的Object.getOwnPropertyDescriptors方法 function copyObject(orig) { return Object.create( Object.getPrototypeOf(orig), Object.getOwnPropertyDescriptors(orig) ); } 严格模式 除了正常的运行模式,JavaScript 还有第二种运行模式:严格模式(strict mode)。顾名思义,这种模式采用更加严格的 JavaScript 语法。同样的代码,在正常模式和严格模式中,可能会有不一样的运行结果。一些在正常模式下可以运行的语句,在严格模式下将不能运行 设计目的 早期的 JavaScript 语言有很多设计不合理的地方,但是为了兼容以前的代码,又不能改变老的语法,只能不断添加新的语法,引导程序员使用新语法。 严格模式是从 ES5 进入标准的,主要目的有以下几个: 1.明确禁止一些不合理、不严谨的语法,减少 JavaScript 语言的一些怪异行为 2.增加更多报错的场合,消除代码运行的一些不安全之处,保证代码运行的安全 3.提高编译器效率,增加运行速度 4.为未来新版本的 JavaScript 语法做好铺垫 总之,严格模式体现了 JavaScript 更合理、更安全、更严谨的发展方向 启用方法 进入严格模式的标志,是一行字符串'use strict' 老版本的引擎会把它当作一行普通字符串,加以忽略;新版本的引擎就会进入严格模式。严格模式可以用于整个脚本,也可以只用于单个函数 整个脚本文件 use strict放在脚本文件的第一行,整个脚本都将以严格模式运行;如果这行语句不在第一行就无效,整个脚本会以正常模式运行。(严格地说,只要前面不是产生实际运行结果的语句,use strict可以不在第一行,比如直接跟在一个空的分号后面,或者跟在注释后面) <script> 'use strict'; console.log('这是严格模式'); </script> <script> console.log('这是正常模式'); </script> 上面代码中,一个网页文件依次有两段 JavaScript 代码。前一个 <script> console.log('这是正常模式'); 'use strict'; </script> 单个函数 use strict放在函数体的第一行,则整个函数以严格模式运行 function strict() { 'use strict'; return '这是严格模式'; } function strict2() { 'use strict'; function f() { return '这也是严格模式'; } return f(); } function notStrict() { return '这是正常模式'; } 有时需要把不同脚本合并在一个文件里面。如果一个脚本是严格模式,另一个脚本不是,它们的合并就可能出错。严格模式的脚本在前,则合并后的脚本都是严格模式;如果正常模式的脚本在前,则合并后的脚本都是正常模式。这两种情况下,合并后的结果都是不正确的。这时可以考虑把整个脚本文件放在一个立即执行的匿名函数之中 (function () { 'use strict'; // some code here })(); 显式报错 严格模式使得 JavaScript 的语法变得更严格,更多的操作会显式报错。其中有些操作,在正常模式下只会默默地失败,不会报错 只读属性不可写 严格模式下,设置字符串的length属性,会报错 'use strict'; 'abc'.length = 5; // TypeError: Cannot assign to read only property 'length' of string 'abc' 上面代码报错,因为length是只读属性,严格模式下不可写。正常模式下,改变length属性是无效的,但不会报错。严格模式下,对只读属性赋值,或者删除不可配置(non-configurable)属性都会报错 // 对只读属性赋值会报错 'use strict'; Object.defineProperty({}, 'a', { value: 37, writable: false }); obj.a = 123; // TypeError: Cannot assign to read only property 'a' of object #<Object> // 删除不可配置的属性会报错 'use strict'; var obj = Object.defineProperty({}, 'p', { value: 1, configurable: false }); delete obj.p // TypeError: Cannot delete property 'p' of #<Object> 只设置了取值器的属性不可写 严格模式下,对一个只有取值器(getter)、没有存值器(setter)的属性赋值,会报错 'use strict'; var obj = { get v() { return 1; } }; obj.v = 2; // Uncaught TypeError: Cannot set property v of #<Object> which has only a getter 禁止扩展的对象不可扩展 严格模式下,对禁止扩展的对象添加新属性,会报错 'use strict'; var obj = {}; Object.preventExtensions(obj); obj.v = 1; // Uncaught TypeError: Cannot add property v, object is not extensible 上面代码中,obj对象禁止扩展,添加属性就会报错 eval、arguments 不可用作标识名 严格模式下,使用eval或者arguments作为标识名,将会报错。下面的语句都会报错 'use strict'; var eval = 17; var arguments = 17; var obj = { set p(arguments) { } }; try { } catch (arguments) { } function x(eval) { } function arguments() { } var y = function eval() { }; var f = new Function('arguments', "'use strict'; return 17;"); // SyntaxError: Unexpected eval or arguments in strict mode 函数不能有重名的参数 正常模式下,如果函数有多个重名的参数,可以用arguments[i]读取。严格模式下,这属于语法错误 function f(a, a, b) { 'use strict'; return a + b; } // Uncaught SyntaxError: Duplicate parameter name not allowed in this context 禁止八进制的前缀0表示法 正常模式下,整数的第一位如果是0,表示这是八进制数,比如0100等于十进制的64。严格模式禁止这种表示法,整数第一位为0,将报错 增强的安全措施 严格模式增强了安全保护,从语法上防止了一些不小心会出现的错误 全局变量显式声明 正常模式中,如果一个变量没有声明就赋值,默认是全局变量。严格模式禁止这种用法,全局变量必须显式声明 'use strict'; v = 1; // 报错,v未声明 for (i = 0; i < 2; i++) { // 报错,i 未声明 // ... } function f() { x = 123; } f() // 报错,未声明就创建一个全局变量 因此,严格模式下,变量都必须先声明,然后再使用 禁止 this 关键字指向全局对象 正常模式下,函数内部的this可能会指向全局对象,严格模式禁止这种用法,避免无意间创造全局变量 // 正常模式 function f() { console.log(this === window); } f() // true // 严格模式 function f() { 'use strict'; console.log(this === undefined); } f() // true 上面代码中,严格模式的函数体内部this是undefined,这种限制对于构造函数尤其有用。使用构造函数时,有时忘了加new,这时this不再指向全局对象,而是报错 function f() { 'use strict'; this.a = 1; }; f();// 报错,this 未定义 严格模式下,函数直接调用时(不使用new调用),函数内部的this表示undefined(未定义),因此可以用call、apply和bind方法,将任意值绑定在this上面。正常模式下,this指向全局对象,如果绑定的值是非对象,将被自动转为对象再绑定上去,而null和undefined这两个无法转成对象的值,将被忽略 // 正常模式 function fun() { return this; } fun() // window fun.call(2) // Number {2} fun.call(true) // Boolean {true} fun.call(null) // window fun.call(undefined) // window // 严格模式 'use strict'; function fun() { return this; } fun() //undefined fun.call(2) // 2 fun.call(true) // true fun.call(null) // null fun.call(undefined) // undefined 上面代码中,可以把任意类型的值,绑定在this上面 禁止使用 fn.callee、fn.caller 函数内部不得使用fn.caller、fn.arguments,否则会报错。这意味着不能在函数内部得到调用栈了 function f1() { 'use strict'; f1.caller; // 报错 f1.arguments; // 报错 } f1(); 禁止使用 arguments.callee、arguments.caller arguments.callee和arguments.caller是两个历史遗留的变量,从来没有标准化过,现在已经取消了。正常模式下调用它们没有什么作用,但是不会报错。严格模式明确规定,函数内部使用arguments.callee、arguments.caller将会报错 'use strict'; var f = function () { return arguments.callee; }; f(); // 报错 禁止删除变量 严格模式下无法删除变量,如果使用delete命令删除一个变量,会报错。只有对象的属性,且属性的描述对象的configurable属性设置为true,才能被delete命令删除 'use strict'; var x; delete x; // 语法错误 var obj = Object.create(null, { x: { value: 1, configurable: true } }); delete obj.x; // 删除成功 静态绑定 JavaScript 语言的一个特点,就是允许“动态绑定”,即某些属性和方法到底属于哪一个对象,不是在编译时确定的,而是在运行时(runtime)确定的。严格模式对动态绑定做了一些限制;某些情况下,只允许静态绑定;也就是说,属性和方法到底归属哪个对象,必须在编译阶段就确定。这样做有利于编译效率的提高,也使得代码更容易阅读,更少出现意外。具体来说,涉及以下几个方面: 禁止使用 with 语句 严格模式下,使用with语句将报错;因为with语句无法在编译时就确定,某个属性到底归属哪个对象,从而影响了编译效果 'use strict'; var v = 1; var obj = {}; with (obj) { v = 2; } // Uncaught SyntaxError: Strict mode code may not include a with statement 创设 eval 作用域 正常模式下,JavaScript 语言有两种变量作用域(scope):全局作用域和函数作用域。严格模式创设了第三种作用域:eval作用域。正常模式下,eval语句的作用域取决于它处于全局作用域还是函数作用域。严格模式下,eval语句本身就是一个作用域,不再能够在其所运行的作用域创设新的变量了,也就是说,eval所生成的变量只能用于eval内部 (function () { 'use strict'; var x = 2; console.log(eval('var x = 5; x')) // 5 console.log(x) // 2 })() 上面代码中,由于eval语句内部是一个独立作用域,所以内部的变量x不会泄露到外部。注意,如果希望eval语句也使用严格模式,有两种方式 // 方式一 function f1(str){ 'use strict'; return eval(str); } f1('undeclared_variable = 1'); // 报错 // 方式二 function f2(str){ return eval(str); } f2('"use strict";undeclared_variable = 1') // 报错 上面两种写法,eval内部使用的都是严格模式 arguments 不再追踪参数的变化 变量arguments代表函数的参数。严格模式下,函数内部改变参数与arguments的联系被切断了,两者不再存在联动关系 function f(a) { a = 2; return [a, arguments[0]]; } f(1); // 正常模式为[2, 2] function f(a) { 'use strict'; a = 2; return [a, arguments[0]]; } f(1); // 严格模式为[2, 1] 上面代码中,改变函数的参数,不会反应到arguments对象上来 向下一个版本的 JavaScript 过渡 JavaScript 语言的下一个版本是 ECMAScript 6,为了平稳过渡,严格模式引入了一些 ES6 语法 非函数代码块不得声明函数 ES6 会引入块级作用域。为了与新版本接轨,ES5 的严格模式只允许在全局作用域或函数作用域声明函数。也就是说,不允许在非函数的代码块内声明函数 'use strict'; if (true) { function f1() { } // 语法错误 } for (var i = 0; i < 5; i++) { function f2() { } // 语法错误 } 上面代码在if代码块和for代码块中声明了函数,ES5 环境会报错 注意,如果是 ES6 环境,上面的代码不会报错,因为 ES6 允许在代码块之中声明函数 保留字 为了向将来 JavaScript 的新版本过渡,严格模式新增了一些保留字(implements、interface、let、package、private、protected、public、static、yield等)。使用这些词作为变量名将会报错 function package(protected) { // 语法错误 'use strict'; var implements; // 语法错误 } 异步操作 异步操作概述 单线程模型 单线程模型指的是,JavaScript 只在一个线程上运行;也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待;注意,JavaScript 只在一个线程上运行,不代表 JavaScript 引擎只有一个线程。事实上,JavaScript 引擎有多个线程,单个脚本只能在一个线程上运行(称为主线程),其他线程都是在后台配合 JavaScript 之所以采用单线程,而不是多线程,跟历史有关系;JavaScript 从诞生起就是单线程,原因是不想让浏览器变得太复杂,因为多线程需要共享资源、且有可能修改彼此的运行结果,对于一种网页脚本语言来说就太复杂了。如果 JavaScript 同时有两个线程,一个线程在网页 DOM 节点上添加内容,另一个线程删除了这个节点,这时浏览器应该以哪个线程为准?是不是还要有锁机制?所以,为了避免复杂性,JavaScript 一开始就是单线程,这已经成了这门语言的核心特征,将来也不会改变 这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等着,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。JavaScript 语言本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果;这个时候,如果对方服务器迟迟没有响应,或者网络不通畅,就会导致脚本的长时间停滞 如果排队是因为计算量大,CPU 忙不过来,倒也算了,但是很多时候 CPU 是闲着的,因为 IO 操作(输入输出)很慢(比如 Ajax 操作从网络读取数据),不得不等着结果出来,再往下执行。JavaScript 语言的设计者意识到,这时 CPU 完全可以不管 IO 操作,挂起处于等待中的任务,先运行排在后面的任务。等到 IO 操作返回了结果,再回过头,把挂起的任务继续执行下去。这种机制就是 JavaScript 内部采用的“事件循环”机制(Event Loop) 单线程模型虽然对 JavaScript 构成了很大的限制,但也因此使它具备了其他语言不具备的优势。如果用得好,JavaScript 程序是不会出现堵塞的,这就是为什么 Node 可以用很少的资源,应付大流量访问的原因。为了利用多核 CPU 的计算能力,HTML5 提出 Web Worker 标准,允许 JavaScript 脚本创建多个线程,但是子线程完全受主线程控制,且不得操作 DOM。所以,这个新标准并没有改变 JavaScript 单线程的本质 同步任务和异步任务 程序里面所有的任务,可以分成两类:同步任务(synchronous)和异步任务(asynchronous)。 同步任务是那些没有被引擎挂起、在主线程上排队执行的任务;只有前一个任务执行完毕,才能执行后一个任务。异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。 举例来说,Ajax 操作可以当作同步任务处理,也可以当作异步任务处理,由开发者决定。如果是同步任务,主线程就等着 Ajax 操作返回结果,再往下执行;如果是异步任务,主线程在发出 Ajax 请求以后,就直接往下执行,等到 Ajax 操作有了结果,主线程再执行对应的回调函数 任务队列和事件循环 JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。(实际上,根据异步任务的类型,存在多个任务队列。为了方便理解,这里假设只存在一个队列) 首先,主线程会去执行所有的同步任务;等到同步任务全部执行完,就会去看任务队列里面的异步任务;如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。 异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。 JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件(a programming construct that waits for and dispatches events or messages in a program)” 异步操作的模式 下面总结一下异步操作的几种模式 回调函数 回调函数是异步操作最基本的方法。下面是两个函数f1和f2,编程的意图是f2必须等到f1执行完成,才能执行 function f1() { // ... } function f2() { // ... } f1(); f2(); 上面代码的问题在于,如果f1是异步操作,f2会立即执行,不会等到f1结束再执行。这时,可以考虑改写f1,把f2写成f1的回调函数 function f1(callback) { // ... callback(); } function f2() { // ... } f1(f2); 回调函数的优点是简单、容易理解和实现,缺点是不利于代码的阅读和维护,各个部分之间高度耦合(coupling),使得程序结构混乱、流程难以追踪(尤其是多个回调函数嵌套的情况),而且每个任务只能指定一个回调函数 事件监听 另一种思路是采用事件驱动模式。异步任务的执行不取决于代码的顺序,而取决于某个事件是否发生。还是以f1和f2为例;首先,为f1绑定一个事件 f1.on('done', f2); 上面这行代码的意思是,当f1发生done事件,就执行f2。然后,对f1进行改写: function f1() { setTimeout(function () { // ... f1.trigger('done'); }, 1000); } 上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2。这种方法的优点是比较容易理解,可以绑定多个事件,每个事件可以指定多个回调函数,而且可以“去耦合”(decoupling),有利于实现模块化。缺点是整个程序都要变成事件驱动型,运行流程会变得很不清晰。阅读代码的时候,很难看出主流程 发布/订阅 事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行;这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。 这个模式有多种实现,下面采用的是 Ben Alman 的 Tiny Pub/Sub,这是 jQuery 的一个插件。 首先,f2向信号中心jQuery订阅done信号 jQuery.subscribe('done', f2); 然后,f1进行如下改写 function f1() { setTimeout(function () { // ... jQuery.publish('done'); }, 1000); } 上面代码中,jQuery.publish('done')的意思是,f1执行完成后,向信号中心jQuery发布done信号,从而引发f2的执行;f2完成执行后,可以取消订阅(unsubscribe) jQuery.unsubscribe('done', f2); 这种方法的性质与“事件监听”类似,但是明显优于后者。因为可以通过查看“消息中心”,了解存在多少信号、每个信号有多少订阅者,从而监控程序的运行 异步操作的流程控制 如果有多个异步操作,就存在一个流程控制的问题:如何确定异步操作执行的顺序,以及如何保证遵守这种顺序 function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } 上面代码的async函数是一个异步任务,非常耗时,每次执行需要1秒才能完成,然后再调用回调函数;如果有六个这样的异步任务,需要全部完成后,才能执行最后的final函数。请问应该如何安排操作流程? function final(value) { console.log('完成: ', value); } async(1, function (value) { async(2, function (value) { async(3, function (value) { async(4, function (value) { async(5, function (value) { async(6, final); }); }); }); }); }); // 参数为 1 , 1秒后返回结果 // 参数为 2 , 1秒后返回结果 // 参数为 3 , 1秒后返回结果 // 参数为 4 , 1秒后返回结果 // 参数为 5 , 1秒后返回结果 // 参数为 6 , 1秒后返回结果 // 完成: 12 上面代码中,六个回调函数的嵌套,不仅写起来麻烦,容易出错,而且难以维护 串行执行 我们可以编写一个流程控制函数,让它来控制异步任务,一个任务完成以后,再执行另一个。这就叫串行执行 var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } function series(item) { if(item) { async( item, function(result) { results.push(result); return series(items.shift()); }); } else { return final(results[results.length - 1]); } } series(items.shift()); 上面代码中,函数series就是串行函数,它会依次执行异步任务,所有任务都完成后,才会执行final函数。items数组保存每一个异步任务的参数,results数组保存每一个异步任务的运行结果。注意,上面的写法需要六秒,才能完成整个脚本 并行执行 流程控制函数也可以是并行执行,即所有异步任务同时执行,等到全部完成以后,才执行final函数 var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } items.forEach(function(item) { async(item, function(result){ results.push(result); if(results.length === items.length) { final(results[results.length - 1]); } }) }); 上面代码中,forEach方法会同时发起六个异步任务,等到它们全部完成以后,才会执行final函数。相比而言,上面的写法只要一秒就能完成整个脚本;这就是说,并行执行的效率较高,比起串行执行一次只能执行一个任务,较为节约时间;但是问题在于如果并行的任务较多,很容易耗尽系统资源,拖慢运行速度。因此有了第三种流程控制方式 并行与串行的结合 所谓并行与串行的结合,就是设置一个门槛,每次最多只能并行执行n个异步任务,这样就避免了过分占用系统资源 var items = [ 1, 2, 3, 4, 5, 6 ]; var results = []; var running = 0; var limit = 2; function async(arg, callback) { console.log('参数为 ' + arg +' , 1秒后返回结果'); setTimeout(function () { callback(arg * 2); }, 1000); } function final(value) { console.log('完成: ', value); } function launcher() { while(running < limit && items.length > 0) { var item = items.shift(); async(item, function(result) { results.push(result); running--; if(items.length > 0) { launcher(); } else if(running == 0) { final(results); } }); running++; } } launcher(); 上面代码中,最多只能同时运行两个异步任务。变量running记录当前正在运行的任务数,只要低于门槛值,就再启动一个新的任务,如果等于0,就表示所有任务都执行完了,这时就执行final函数。 这段代码需要三秒完成整个脚本,处在串行执行和并行执行之间。通过调节limit变量,达到效率和资源的最佳平衡 定时器 JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()和setInterval()这两个函数来完成。它们向任务队列添加定时任务 setTimeout() setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器 var timerId = setTimeout(func|code, delay); 上面代码中,setTimeout函数接受两个参数,第一个参数func|code是将要推迟执行的函数名或者一段代码,第二个参数delay是推迟执行的毫秒数 console.log(1); setTimeout('console.log(2)',1000); console.log(3); // 1 // 3 // 2 上面代码会先输出1和3,然后等待1000毫秒再输出2。注意,console.log(2)必须以字符串的形式,作为setTimeout的参数。如果推迟执行的是函数,就直接将函数名,作为setTimeout的参数 function f() { console.log(2); } setTimeout(f, 1000); setTimeout的第二个参数如果省略,则默认为0 setTimeout(f) // 等同于 setTimeout(f, 0) 除了前两个参数,setTimeout还允许更多的参数。它们将依次传入推迟执行的函数(回调函数) setTimeout(function (a,b) { console.log(a + b); }, 1000, 1, 1); 上面代码中,setTimeout共有4个参数。最后两个参数,将在1000毫秒之后回调函数执行时作为回调函数的参数。还有一个需要注意的地方,如果回调函数是对象的方法,那么setTimeout使得方法内部的this关键字指向全局环境,而不是定义时所在的那个对象 var x = 1; var obj = { x: 2, y: function () { console.log(this.x); } }; setTimeout(obj.y, 1000) // 1 上面代码输出的是1,而不是2。因为当obj.y在1000毫秒后运行时,this所指向的已经不是obj了,而是全局环境。为了防止出现这个问题,一种解决方法是将obj.y放入一个函数 var x = 1; var obj = { x: 2, y: function () { console.log(this.x); } }; setTimeout(function () { obj.y(); }, 1000); // 2 上面代码中,obj.y放在一个匿名函数之中,这使得obj.y在obj的作用域执行,而不是在全局作用域内执行,所以能够显示正确的值;另一种解决方法是,使用bind方法,将obj.y这个方法绑定在obj上面 var x = 1; var obj = { x: 2, y: function () { console.log(this.x); } }; setTimeout(obj.y.bind(obj), 1000) // 2 setInterval() setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行 var i = 1 var timer = setInterval(function() { console.log(2); }, 1000) 上面代码中,每隔1000毫秒就输出一个2,会无限运行下去,直到关闭当前窗口;与setTimeout一样,除了前两个参数,setInterval方法还可以接受更多的参数,它们会传入回调函数。下面是一个通过setInterval方法实现网页动画的例子 var div = document.getElementById('someDiv'); var opacity = 1; var fader = setInterval(function() { opacity -= 0.1; if (opacity >= 0) { div.style.opacity = opacity; } else { clearInterval(fader); } }, 100); 上面代码每隔100毫秒,设置一次div元素的透明度,直至其完全透明为止。setInterval的一个常见用途是实现轮询。下面是一个轮询 URL 的 Hash 值是否发生变化的例子 var hash = window.location.hash; var hashWatcher = setInterval(function() { if (window.location.hash != hash) { updatePage(); } }, 1000); setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。为了确保两次执行之间有固定的间隔,可以不用setInterval,而是每次执行结束后,使用setTimeout指定下一次执行的具体时间 var i = 1; var timer = setTimeout(function f() { // ... timer = setTimeout(f, 2000); }, 2000); 上面代码可以确保,下一次执行总是在本次执行结束之后的2000毫秒开始 clearTimeout(),clearInterval() setTimeout和setInterval函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeout和clearInterval函数,就可以取消对应的定时器 var id1 = setTimeout(f, 1000); var id2 = setInterval(f, 1000); clearTimeout(id1); clearInterval(id2); 上面代码中,回调函数f不会再执行了,因为两个定时器都被取消了。setTimeout和setInterval返回的整数值是连续的,也就是说,第二个setTimeout方法返回的整数值,将比第一个的整数值大1 function f() {} setTimeout(f, 1000) // 10 setTimeout(f, 1000) // 11 setTimeout(f, 1000) // 12 上面代码中,连续调用三次setTimeout,返回值都比上一次大了1。利用这一点,可以写一个函数,取消当前所有的setTimeout定时器 (function() { // 每轮事件循环检查一次 var gid = setInterval(clearAllTimeouts, 0); function clearAllTimeouts() { var id = setTimeout(function() {}, 0); while (id > 0) { if (id !== gid) { clearTimeout(id); } id--; } } })(); 上面代码中,先调用setTimeout,得到一个计算器编号,然后把编号比它小的计数器全部取消 实例:debounce 函数 有时,我们不希望回调函数被频繁调用。比如,用户填入网页输入框的内容,希望通过 Ajax 方法传回服务器,jQuery 的写法如下: $('textarea').on('keydown', ajaxAction); 这样写有一个很大的缺点,就是如果用户连续击键,就会连续触发keydown事件,造成大量的 Ajax 通信。这是不必要的,而且很可能产生性能问题。正确的做法应该是,设置一个门槛值,表示两次 Ajax 通信的最小间隔时间。如果在间隔时间内,发生新的keydown事件,则不触发 Ajax 通信,并且重新开始计时。如果过了指定时间,没有发生新的keydown事件,再将数据发送出去。 这种做法叫做 debounce(防抖动)。假定两次 Ajax 通信的间隔不得小于2500毫秒,上面的代码可以改写成下面这样 $('textarea').on('keydown', debounce(ajaxAction, 2500)); function debounce(fn, delay){ var timer = null; // 声明计时器 return function() { var context = this; var args = arguments; clearTimeout(timer); timer = setTimeout(function () { fn.apply(context, args); }, delay); }; } 上面代码中,只要在2500毫秒之内,用户再次击键,就会取消上一次的定时器,然后再新建一个定时器。这样就保证了回调函数之间的调用间隔,至少是2500毫秒 运行机制 setTimeout和setInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。这意味着,setTimeout和setInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeout和setInterval指定的任务,一定会按照预定时间执行 setTimeout(someTask, 100); veryLongTask(); 上面代码的setTimeout,指定100毫秒以后运行一个任务。但是,如果后面的veryLongTask函数(同步任务)运行时间非常长,过了100毫秒还无法结束,那么被推迟运行的someTask就只有等着,等到veryLongTask运行结束,才轮到它执行。再看一个setInterval的例子 setInterval(function () { console.log(2); }, 1000); sleep(3000); function sleep(ms) { var start = Date.now(); while ((Date.now() - start) < ms) {} } setTimeout(f, 0) 含义 setTimeout的作用是将代码推迟到指定时间执行,如果指定时间为0,即setTimeout(f, 0),那么会立刻执行吗? 答案是不会。因为上一节说过,必须要等到当前脚本的同步任务,全部处理完以后,才会执行setTimeout指定的回调函数f。也就是说,setTimeout(f, 0)会在下一轮事件循环一开始就执行 setTimeout(function () { console.log(1); }, 0); console.log(2); // 2 // 1 上面代码先输出2,再输出1。因为2是同步任务,在本轮事件循环执行,而1是下一轮事件循环执行。总之,setTimeout(f, 0)这种写法的目的是,尽可能早地执行f,但是并不能保证立刻就执行f。实际上,setTimeout(f, 0)不会真的在0毫秒之后运行,不同的浏览器有不同的实现。以 Edge 浏览器为例,会等到4毫秒之后运行。如果电脑正在使用电池供电,会等到16毫秒之后运行;如果网页不在当前 Tab 页,会推迟到1000毫秒(1秒)之后运行。这样是为了节省系统资源 应用 setTimeout(f, 0)有几个非常重要的用途。它的一大应用是,可以调整事件的发生顺序。比如,网页开发中,某个事件先发生在子元素,然后冒泡到父元素,即子元素的事件回调函数,会早于父元素的事件回调函数触发。如果,想让父元素的事件回调函数先发生,就要用到setTimeout(f, 0) <input type="button" id="myButton" value="click"> <script> var input = document.getElementById('myButton'); input.onclick = function A() { setTimeout(function B() { input.value +=' input'; }, 0) }; document.body.onclick = function C() { input.value += ' body' }; </script> 上面代码在点击按钮后,先触发回调函数A,然后触发函数C。函数A中,setTimeout将函数B推迟到下一轮事件循环执行,这样就起到了,先触发父元素的回调函数C的目的了。 另一个应用是,用户自定义的回调函数,通常在浏览器的默认动作之前触发。比如,用户在输入框输入文本,keypress事件会在浏览器接收文本之前触发。因此,下面的回调函数是达不到目的的 <input type="text" id="input-box"> document.getElementById('input-box').onkeypress = function (event) { this.value = this.value.toUpperCase(); } 上面代码想在用户每次输入文本后,立即将字符转为大写。但是实际上,它只能将本次输入前的字符转为大写,因为浏览器此时还没接收到新的文本,所以this.value取不到最新输入的那个字符。只有用setTimeout改写,上面的代码才能发挥作用 document.getElementById('input-box').onkeypress = function() { var self = this; setTimeout(function() { self.value = self.value.toUpperCase(); }, 0); } 由于setTimeout(f, 0)实际上意味着,将任务放到浏览器最早可得的空闲时段执行,所以那些计算量大、耗时长的任务,常常会被放到几个小部分,分别放到setTimeout(f, 0)里面执行 var div = document.getElementsByTagName('div')[0]; // 写法一 for (var i = 0xA00000; i < 0xFFFFFF; i++) { div.style.backgroundColor = '#' + i.toString(16); } // 写法二 var timer; var i=0x100000; function func() { timer = setTimeout(func, 0); div.style.backgroundColor = '#' + i.toString(16); if (i++ == 0xFFFFFF) clearTimeout(timer); } timer = setTimeout(func, 0); 上面代码有两种写法,都是改变一个网页元素的背景色。写法一会造成浏览器“堵塞”,因为 JavaScript 执行速度远高于 DOM,会造成大量 DOM 操作“堆积”,而写法二就不会,这就是setTimeout(f, 0)的好处。 另一个使用这种技巧的例子是代码高亮的处理。如果代码块很大,一次性处理,可能会对性能造成很大的压力,那么将其分成一个个小块,一次处理一块,比如写成setTimeout(highlightNext, 50)的样子,性能压力就会减轻。 Promise 对象 概述 Promise 对象是 JavaScript 的异步操作解决方案,为异步操作提供统一接口。它起到代理作用(proxy),充当异步操作与回调函数之间的中介,使得异步操作具备同步操作的接口。Promise 可以让异步操作写起来,就像在写同步操作的流程,而不必一层层地嵌套回调函数。 首先,Promise 是一个对象,也是一个构造函数 function f1(resolve, reject) { // 异步代码... } var p1 = new Promise(f1); 上面代码中,Promise构造函数接受一个回调函数f1作为参数,f1里面是异步操作的代码。然后,返回的p1就是一个 Promise 实例。Promise 的设计思想是,所有异步任务都返回一个 Promise 实例。Promise 实例有一个then方法,用来指定下一步的回调函数 var p1 = new Promise(f1); p1.then(f2); 上面代码中,f1的异步操作执行完成,就会执行f2。传统的写法可能需要把f2作为回调函数传入f1,比如写成f1(f2),异步操作完成后,在f1内部调用f2。Promise 使得f1和f2变成了链式写法。不仅改善了可读性,而且对于多层嵌套的回调函数尤其方便 // 传统写法 step1(function (value1) { step2(value1, function(value2) { step3(value2, function(value3) { step4(value3, function(value4) { // ... }); }); }); }); // Promise 的写法 (new Promise(step1)).then(step2).then(step3).then(step4); 从上面代码可以看到,采用 Promise 以后,程序流程变得非常清楚,十分易读。总的来说,传统的回调函数写法使得代码混成一团,变得横向发展而不是向下发展。Promise 就是解决这个问题,使得异步流程可以写成同步流程。Promise 原本只是社区提出的一个构想,一些函数库率先实现了这个功能。ECMAScript 6 将其写入语言标准,目前 JavaScript 原生支持 Promise 对象 Promise 对象的状态 Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态:异步操作未完成(pending)/异步操作成功(fulfilled)/异步操作失败(rejected);上面三种状态里面,fulfilled和rejected合在一起称为resolved(已定型)。 这三种的状态的变化途径只有两种:从“未完成”到“成功” / 从“未完成”到“失败”。一旦状态发生变化就凝固了,不会再有新的状态变化。这也是 Promise 这个名字的由来,它的英语意思是“承诺”,一旦承诺成效,就不能再改变了。这也意味着,Promise 实例的状态变化只可能发生一次。因此,Promise 的最终结果只有两种: 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected Promise 构造函数 JavaScript 提供原生的Promise构造函数,用来生成 Promise 实例 var promise = new Promise(function (resolve, reject) { // ... if (/* 异步操作成功 */){ resolve(value); } else { /* 异步操作失败 */ reject(new Error()); } }); 上面代码中,Promise构造函数接受一个函数作为参数,该函数的两个参数分别是resolve和reject。它们是两个函数,由 JavaScript 引擎提供,不用自己实现。resolve函数的作用是,将Promise实例的状态从“未完成”变为“成功”(即从pending变为fulfilled),在异步操作成功时调用,并将异步操作的结果作为参数传递出去。reject函数的作用是,将Promise实例的状态从“未完成”变为“失败”(即从pending变为rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去 function timeout(ms) { return new Promise((resolve, reject) => { setTimeout(resolve, ms, 'done'); }); } timeout(100) 上面代码中,timeout(100)返回一个 Promise 实例。100毫秒以后,该实例的状态会变为fulfilled Promise.prototype.then() Promise 实例的then方法,用来添加回调函数。then方法可以接受两个回调函数,第一个是异步操作成功时(变为fulfilled状态)的回调函数,第二个是异步操作失败(变为rejected)时的回调函数(该参数可以省略)。一旦状态改变,就调用相应的回调函数 var p1 = new Promise(function (resolve, reject) { resolve('成功'); }); p1.then(console.log, console.error); // "成功" var p2 = new Promise(function (resolve, reject) { reject(new Error('失败')); }); p2.then(console.log, console.error); // Error: 失败 上面代码中,p1和p2都是Promise 实例,它们的then方法绑定两个回调函数:成功时的回调函数console.log,失败时的回调函数console.error(可以省略)。p1的状态变为成功,p2的状态变为失败,对应的回调函数会收到异步操作传回的值,然后在控制台输出。then方法可以链式使用 p1.then(step1).then(step2).then(step3).then( console.log, console.error); 上面代码中,p1后面有四个then,意味着依次有四个回调函数。只要前一步的状态变为fulfilled,就会依次执行紧跟在后面的回调函数。最后一个then方法,回调函数是console.log和console.error,用法上有一点重要的区别;console.log只显示step3的返回值,而console.error可以显示p1、step1、step2、step3之中任意一个发生的错误。举例来说,如果step1的状态变为rejected,那么step2和step3都不会执行了(因为它们是resolved的回调函数)。Promise 开始寻找,接下来第一个为rejected的回调函数,在上面代码中是console.error。这就是说,Promise 对象的报错具有传递性 then() 用法辨析 Promise 的用法,简单说就是一句话:使用then方法添加回调函数。但是,不同的写法有一些细微的差别,请看下面四种写法: // 写法一 f1().then(function () { return f2(); }); // 写法二 f1().then(function () { f2(); }); // 写法三 f1().then(f2()); // 写法四 f1().then(f2); 为了便于讲解,下面这四种写法都再用then方法接一个回调函数f3。写法一的f3回调函数的参数,是f2函数的运行结果 f1().then(function () { return f2(); }).then(f3); 写法二的f3回调函数的参数是undefined f1().then(function () { f2(); return; }).then(f3); 写法三的f3回调函数的参数,是f2函数返回的函数的运行结果 f1().then(f2()).then(f3); 写法四与写法一只有一个差别,那就是f2会接收到f1()返回的结果 f1().then(f2).then(f3); 实例:图片加载 下面是使用 Promise 完成图片的加载 var preloadImage = function (path) { return new Promise(function (resolve, reject) { var image = new Image(); image.onload = resolve; image.onerror = reject; image.src = path; }); }; 上面的preloadImage函数用法如下 preloadImage('https://example.com/my.jpg').then(function (e) { document.body.append(e.target) }).then(function () { console.log('加载成功') }) 小结 Promise 的优点在于,让回调函数变成了规范的链式写法,程序流程可以看得很清楚。它有一整套接口,可以实现许多强大的功能,比如同时执行多个异步操作,等到它们的状态都改变以后,再执行一个回调函数;再比如,为多个回调函数中抛出的错误,统一指定处理方法等等。而且,Promise 还有一个传统写法没有的好处:它的状态一旦改变,无论何时查询,都能得到这个状态。这意味着,无论何时为 Promise 实例添加回调函数,该函数都能正确执行。所以,你不用担心是否错过了某个事件或信号。如果是传统写法,通过监听事件来执行回调函数,一旦错过了事件,再添加回调函数是不会执行的。 Promise 的缺点是,编写的难度比传统写法高,而且阅读代码也不是一眼可以看懂。你只会看到一堆then,必须自己在then的回调函数里面理清逻辑 微任务 Promise 的回调函数属于异步任务,会在同步任务之后执行 new Promise(function (resolve, reject) { resolve(1); }).then(console.log); console.log(2); // 2 // 1 上面代码会先输出2,再输出1。因为console.log(2)是同步任务,而then的回调函数属于异步任务,一定晚于同步任务执行;但是,Promise 的回调函数不是正常的异步任务,而是微任务(microtask)。它们的区别在于,正常任务追加到下一轮事件循环,微任务追加到本轮事件循环。这意味着,微任务的执行时间一定早于正常任务 setTimeout(function() { console.log(1); }, 0); new Promise(function (resolve, reject) { resolve(2); }).then(console.log); console.log(3); // 3 // 2 // 1 上面代码的输出结果是321。这说明then的回调函数的执行时间,早于setTimeout(fn, 0)。因为then是本轮事件循环执行,setTimeout(fn, 0)在下一轮事件循环开始时执行 DOM DOM 概述 DOM DOM 是 JavaScript 操作网页的接口,全称为“文档对象模型”(Document Object Model)。它的作用是将网页转为一个 JavaScript 对象,从而可以用脚本进行各种操作(比如增删内容)。浏览器会根据 DOM 模型,将结构化文档(比如 HTML 和 XML)解析成一系列的节点,再由这些节点组成一个树状结构(DOM Tree)。所有的节点和最终的树状结构,都有规范的对外接口 DOM 只是一个接口规范,可以用各种语言实现。所以严格地说,DOM 不是 JavaScript 语法的一部分,但是 DOM 操作是 JavaScript 最常见的任务,离开了 DOM,JavaScript 就无法控制网页。另一方面,JavaScript 也是最常用于 DOM 操作的语言 节点 DOM 的最小组成单位叫做节点(node)。文档的树形结构(DOM 树),就是由各种不同类型的节点组成。每个节点可以看作是文档树的一片叶子 节点的类型有七种。 1.Document:整个文档树的顶层节点 2.DocumentType:doctype标签(比如) 3.Element:网页的各种HTML标签(比如、 等) 4.Attribute:网页元素的属性(比如class="right") 5.Text:标签之间或标签包含的文本 6.Comment:注释 7.DocumentFragment:文档的片段 浏览器提供一个原生的节点对象Node,上面这七种节点都继承了Node,因此具有一些共同的属性和方法 节点树 一个文档的所有节点,按照所在的层级,可以抽象成一种树状结构;这种树状结构就是 DOM 树。它有一个顶层节点,下一层都是顶层节点的子节点,然后子节点又有自己的子节点,就这样层层衍生出一个金字塔结构,倒过来就像一棵树。浏览器原生提供document节点,代表整个文档 document // 整个文档树 文档的第一层只有一个节点,就是 HTML 网页的第一个标签,它构成了树结构的根节点(root node),其他 HTML 标签节点都是它的下级节点;除了根节点,其他节点都有三种层级关系: 1.父节点关系(parentNode):直接的那个上级节点 2.子节点关系(childNodes):直接的下级节点3.同级节点关系(sibling):拥有同一个父节点的节点 DOM 提供操作接口,用来获取这三种关系的节点。比如,子节点接口包括firstChild(第一个子节点)和lastChild(最后一个子节点)等属性,同级节点接口包括nextSibling(紧邻在后的那个同级节点)和previousSibling(紧邻在前的那个同级节点)属性 Node 接口 所有 DOM 节点对象都继承了 Node 接口,拥有一些共同的属性和方法。这是 DOM 操作的基础 属性 Node.prototype.nodeType nodeType属性返回一个整数值,表示节点的类型 document.nodeType // 9 上面代码中,文档节点的类型值为9。Node 对象定义了几个常量,对应这些类型值 document.nodeType === Node.DOCUMENT_NODE // true 上面代码中,文档节点的nodeType属性等于常量Node.DOCUMENT_NODE;不同节点的nodeType属性值和对应的常量如下: 1.文档节点(document):9,对应常量Node.DOCUMENT_NODE 2.元素节点(element):1,对应常量Node.ELEMENT_NODE 3.属性节点(attr):2,对应常量Node.ATTRIBUTE_NODE 4.文本节点(text):3,对应常量Node.TEXT_NODE 5.文档片断节点(DocumentFragment):11,对应常量Node.DOCUMENT_FRAGMENT_NODE 6.文档类型节点(DocumentType):10,对应常量Node.DOCUMENT_TYPE_NODE 7.注释节点(Comment):8,对应常量Node.COMMENT_NODE 确定节点类型时,使用nodeType属性是常用方法 var node = document.documentElement.firstChild; if (node.nodeType === Node.ELEMENT_NODE) { console.log('该节点是元素节点'); } Node.prototype.nodeName nodeName属性返回节点的名称 <div id="d1">hello world</div> <script> var div = document.getElementById('d1'); div.nodeName // "DIV" </script> 上面代码中,元素节点 的nodeName属性就是大写的标签名DIV;不同节点的nodeName属性值如下: 1.文档节点(document):#document 2.元素节点(element):大写的标签名 3.属性节点(attr):属性的名称 4.文本节点(text):#text 5.文档片断节点(DocumentFragment):#document-fragment 6.文档类型节点(DocumentType):文档的类型 7.注释节点(Comment):#comment Node.prototype.nodeValue nodeValue属性返回一个字符串,表示当前节点本身的文本值,该属性可读写;只有文本节点(text)、注释节点(comment)和属性节点(attr)有文本值,因此这三类节点的nodeValue可以返回结果,其他类型的节点一律返回null。同样的,也只有这三类节点可以设置nodeValue属性的值,其他类型的节点设置无效 <div id="d1">hello world</div> <script> var div = document.getElementById('d1'); div.nodeValue // null div.firstChild.nodeValue // "hello world" </script> 上面代码中,div是元素节点,nodeValue属性返回null。div.firstChild是文本节点,所以可以返回文本值 Node.prototype.textContent textContent属性返回当前节点和它的所有后代节点的文本内容 <div id="divA">This is <span>some</span> text</div> document.getElementById('divA').textContent // This is some text textContent属性自动忽略当前节点内部的 HTML 标签,返回所有文本内容;该属性是可读写的,设置该属性的值,会用一个新的文本节点,替换所有原来的子节点。它还有一个好处,就是自动对 HTML 标签转义。这很适合用于用户提供的内容 document.getElementById('foo').textContent = '<p>GoodBye!</p>' 上面代码在插入文本时,会将 标签解释为文本,而不会当作标签处理。 对于文本节点(text)、注释节点(comment)和属性节点(attr),textContent属性的值与nodeValue属性相同。对于其他类型的节点,该属性会将每个子节点(不包括注释节点)的内容连接在一起返回。如果一个节点没有子节点,则返回空字符串。文档节点(document)和文档类型节点(doctype)的textContent属性为null。如果要读取整个文档的内容,可以使用document.documentElement.textContent Node.prototype.baseURI baseURI属性返回一个字符串,表示当前网页的绝对路径。浏览器根据这个属性,计算网页上的相对路径的 URL。该属性为只读 document.baseURI 如果无法读到网页的 URL,baseURI属性返回null;该属性的值一般由当前网址的 URL(即window.location属性)决定,但是可以使用 HTML 的标签,改变该属性的值 <base href="https://yq.aliyun.com/go/articleRenderRedirect?url=http%3A%2F%2Fwww.example.com%2Fpage.html"> 设置了以后,baseURI属性就返回标签设置的值 Node.prototype.ownerDocument Node.ownerDocument属性返回当前节点所在的顶层文档对象,即document对象 var d = p.ownerDocument; d === document // true document对象本身的ownerDocument属性,返回null Node.prototype.nextSibling Node.nextSibling属性返回紧跟在当前节点后面的第一个同级节点;如果当前节点后面没有同级节点,则返回null <div id="d1">hello</div><div id="d2">world</div> var d1 = document.getElementById('d1'); var d2 = document.getElementById('d2'); d1.nextSibling === d2 // true 注意,该属性还包括文本节点和注释节点( )。因此如果当前节点后面有空格,该属性会返回一个文本节点,内容为空格;nextSibling属性可以用来遍历所有子节点 var el = document.getElementById('div1').firstChild; while (el !== null) { console.log(el.nodeName); el = el.nextSibling; } 上面代码遍历div1节点的所有子节点 Node.prototype.previousSibling previousSibling属性返回当前节点前面的、距离最近的一个同级节点。如果当前节点前面没有同级节点,则返回null <div id="d1">hello</div><div id="d2">world</div> var d1 = document.getElementById('d1'); var d2 = document.getElementById('d2'); d2.previousSibling === d1 // true 上面代码中,d2.previousSibling就是d2前面的同级节点d1。注意,该属性还包括文本节点和注释节点。因此如果当前节点前面有空格,该属性会返回一个文本节点,内容为空格 Node.prototype.parentNode parentNode属性返回当前节点的父节点。对于一个节点来说,它的父节点只可能是三种类型:元素节点(element)、文档节点(document)和文档片段节点(documentfragment) if (node.parentNode) { node.parentNode.removeChild(node); } 上面代码中,通过node.parentNode属性将node节点从文档里面移除;文档节点(document)和文档片段节点(documentfragment)的父节点都是null。另外,对于那些生成后还没插入 DOM 树的节点,父节点也是null Node.prototype.parentElement parentElement属性返回当前节点的父元素节点;如果当前节点没有父节点,或者父节点类型不是元素节点,则返回null if (node.parentElement) { node.parentElement.style.color = 'red'; } 上面代码中,父元素节点的样式设定了红色;由于父节点只可能是三种类型:元素节点、文档节点(document)和文档片段节点(documentfragment);parentElement属性相当于把后两种父节点都排除了 Node.prototype.firstChild,Node.prototype.lastChild firstChild属性返回当前节点的第一个子节点,如果当前节点没有子节点,则返回null <p id="p1"><span>First span</span></p> var p1 = document.getElementById('p1'); p1.firstChild.nodeName // "SPAN" 注意,firstChild返回的除了元素节点,还可能是文本节点或注释节点 <p id="p1"><span>First span</span></p> var p1 = document.getElementById('p1'); p1.firstChild.nodeName // "#text" lastChild属性返回当前节点的最后一个子节点,如果当前节点没有子节点,则返回null。用法与firstChild属性相同 Node.prototype.childNodes childNodes属性返回一个类似数组的对象(NodeList集合),成员包括当前节点的所有子节点 var children = document.querySelector('ul').childNodes; 上面代码中,children就是ul元素的所有子节点;使用该属性,可以遍历某个节点的所有子节点 var div = document.getElementById('div1'); var children = div.childNodes; for (var i = 0; i < children.length; i++) { // ... } 文档节点(document)就有两个子节点:文档类型节点(docType)和 HTML 根元素节点 var children = document.childNodes; for (var i = 0; i < children.length; i++) { console.log(children[i].nodeType); } // 10 // 1 上面代码中,文档节点的第一个子节点的类型是10(即文档类型节点),第二个子节点的类型是1(即元素节点) 注意,除了元素节点,childNodes属性的返回值还包括文本节点和注释节点;如果当前节点不包括任何子节点,则返回一个空的NodeList集合。由于NodeList对象是一个动态集合,一旦子节点发生变化,立刻会反映在返回结果之中 Node.prototype.isConnected isConnected属性返回一个布尔值,表示当前节点是否在文档之中 var test = document.createElement('p'); test.isConnected // false document.body.appendChild(test); test.isConnected // true 上面代码中,test节点是脚本生成的节点,没有插入文档之前,isConnected属性返回false,插入之后返回true 方法 Node.prototype.appendChild() appendChild()方法接受一个节点对象作为参数,将其作为最后一个子节点,插入当前节点。该方法的返回值就是插入文档的子节点 var p = document.createElement('p'); document.body.appendChild(p); 上面代码新建一个 节点,将其插入document.body的尾部;如果参数节点是 DOM 已经存在的节点,appendChild()方法会将其从原来的位置,移动到新位置 var div = document.getElementById('myDiv'); document.body.appendChild(div); 上面代码中,插入的是一个已经存在的节点myDiv,结果就是该节点会从原来的位置,移动到document.body的尾部;如果appendChild()方法的参数是DocumentFragment节点,那么插入的是DocumentFragment的所有子节点,而不是DocumentFragment节点本身。返回值是一个空的DocumentFragment节点 Node.prototype.hasChildNodes() hasChildNodes方法返回一个布尔值,表示当前节点是否有子节点 var foo = document.getElementById('foo'); if (foo.hasChildNodes()) { foo.removeChild(foo.childNodes[0]); } 上面代码表示,如果foo节点有子节点,就移除第一个子节点;注意,子节点包括所有类型的节点,并不仅仅是元素节点。哪怕节点只包含一个空格,hasChildNodes方法也会返回true。判断一个节点有没有子节点,有许多种方法,下面是其中的三种: 1.node.hasChildNodes() 2.node.firstChild !== null 3.node.childNodes && node.childNodes.length > 0 hasChildNodes方法结合firstChild属性和nextSibling属性,可以遍历当前节点的所有后代节点 function DOMComb(parent, callback) { if (parent.hasChildNodes()) { for (var node = parent.firstChild; node; node = node.nextSibling) { DOMComb(node, callback); } } callback(parent); } DOMComb(document.body, console.log) // 用法 上面代码中,DOMComb函数的第一个参数是某个指定的节点,第二个参数是回调函数。这个回调函数会依次作用于指定节点,以及指定节点的所有后代节点 Node.prototype.cloneNode() cloneNode方法用于克隆一个节点。它接受一个布尔值作为参数,表示是否同时克隆子节点。它的返回值是一个克隆出来的新节点 var cloneUL = document.querySelector('ul').cloneNode(true); 该方法有一些使用注意点: 1.克隆一个节点,会拷贝该节点的所有属性,但是会丧失addEventListener方法和on-属性(即node.onclick = fn),添加在这个节点上的事件回调函数 2.该方法返回的节点不在文档之中,即没有任何父节点,必须使用诸如Node.appendChild这样的方法添加到文档之中。 3.克隆一个节点之后,DOM 有可能出现两个有相同id属性(即id="xxx")的网页元素,这时应该修改其中一个元素的id属性。如果原节点有name属性,可能也需要修改 Node.prototype.insertBefore() insertBefore方法用于将某个节点插入父节点内部的指定位置 var insertedNode = parentNode.insertBefore(newNode, referenceNode); insertBefore方法接受两个参数,第一个参数是所要插入的节点newNode,第二个参数是父节点parentNode内部的一个子节点referenceNode。newNode将插在referenceNode这个子节点的前面。返回值是插入的新节点newNode var p = document.createElement('p'); document.body.insertBefore(p, document.body.firstChild); 上面代码中,新建一个 节点,插在document.body.firstChild的前面,也就是成为document.body的第一个子节点。如果insertBefore方法的第二个参数为null,则新节点将插在当前节点内部的最后位置,即变成最后一个子节点 var p = document.createElement('p'); document.body.insertBefore(p, null); 上面代码中,p将成为document.body的最后一个子节点。这也说明insertBefore的第二个参数不能省略。 注意,如果所要插入的节点是当前 DOM 现有的节点,则该节点将从原有的位置移除,插入新的位置。 由于不存在insertAfter方法,如果新节点要插在父节点的某个子节点后面,可以用insertBefore方法结合nextSibling属性模拟 parent.insertBefore(s1, s2.nextSibling); 上面代码中,parent是父节点,s1是一个全新的节点,s2是可以将s1节点,插在s2节点的后面。如果s2是当前节点的最后一个子节点,则s2.nextSibling返回null,这时s1节点会插在当前节点的最后,变成当前节点的最后一个子节点,等于紧跟在s2的后面。如果要插入的节点是DocumentFragment类型,那么插入的将是DocumentFragment的所有子节点,而不是DocumentFragment节点本身。返回值将是一个空的DocumentFragment节点 Node.prototype.removeChild() removeChild方法接受一个子节点作为参数,用于从当前节点移除该子节点。返回值是移除的子节点 var divA = document.getElementById('A'); divA.parentNode.removeChild(divA); 上面代码移除了divA节点。注意,这个方法是在divA的父节点上调用的,不是在divA上调用的。下面是如何移除当前节点的所有子节点 var element = document.getElementById('top'); while (element.firstChild) { element.removeChild(element.firstChild); } 被移除的节点依然存在于内存之中,但不再是 DOM 的一部分。所以,一个节点移除以后,依然可以使用它,比如插入到另一个节点下面。如果参数节点不是当前节点的子节点,removeChild方法将报错 Node.prototype.replaceChild() replaceChild方法用于将一个新的节点,替换当前节点的某一个子节点 var replacedNode = parentNode.replaceChild(newChild, oldChild); 上面代码中,replaceChild方法接受两个参数,第一个参数newChild是用来替换的新节点,第二个参数oldChild是将要替换走的子节点。返回值是替换走的那个节点oldChild var divA = document.getElementById('divA'); var newSpan = document.createElement('span'); newSpan.textContent = 'Hello World!'; divA.parentNode.replaceChild(newSpan, divA); Node.prototype.contains() contains方法返回一个布尔值,表示参数节点是否满足以下三个条件之一: 1.参数节点为当前节点 2.参数节点为当前节点的子节点 3.参数节点为当前节点的后代节点 document.body.contains(node) 上面代码检查参数节点node,是否包含在当前文档之中 注意,当前节点传入contains方法,返回true nodeA.contains(nodeA) // true Node.prototype.compareDocumentPosition() compareDocumentPosition方法的用法,与contains方法完全一致,返回一个六个比特位的二进制值,表示参数节点与当前节点的关系 二进制值十进制值含义0000000两个节点相同0000011两个节点不在同一个文档(即有一个节点不在当前文档)0000102参数节点在当前节点的前面0001004参数节点在当前节点的后面0010008参数节点包含当前节点01000016当前节点包含参数节点10000032浏览器内部使用 <div id="mydiv"> <form><input id="test" /></form> </div> <script> var div = document.getElementById('mydiv'); var input = document.getElementById('test'); div.compareDocumentPosition(input) // 20 input.compareDocumentPosition(div) // 10 </script> 上面代码中,节点div包含节点input(二进制010000),而且节点input在节点div的后面(二进制000100),所以第一个compareDocumentPosition方法返回20(二进制010100,即010000 + 000100),第二个compareDocumentPosition方法返回10(二进制001010)。由于compareDocumentPosition返回值的含义,定义在每一个比特位上,所以如果要检查某一种特定的含义,就需要使用比特位运算符 var head = document.head; var body = document.body; if (head.compareDocumentPosition(body) & 4) { console.log('文档结构正确'); } else { console.log('<body> 不能在 <head> 前面'); } 上面代码中,compareDocumentPosition的返回值与4(又称掩码)进行与运算(&),得到一个布尔值,表示 是否在前面 Node.prototype.isEqualNode(),Node.prototype.isSameNode() isEqualNode方法返回一个布尔值,用于检查两个节点是否相等。所谓相等的节点,指的是两个节点的类型相同、属性相同、子节点相同 var p1 = document.createElement('p'); var p2 = document.createElement('p'); p1.isEqualNode(p2) // true isSameNode方法返回一个布尔值,表示两个节点是否为同一个节点 var p1 = document.createElement('p'); var p2 = document.createElement('p'); p1.isSameNode(p2) // false p1.isSameNode(p1) // true Node.prototype.normalize() normalize方法用于清理当前节点内部的所有文本节点(text)。它会去除空的文本节点,并且将毗邻的文本节点合并成一个,也就是说不存在空的文本节点,以及毗邻的文本节点 var wrapper = document.createElement('div'); wrapper.appendChild(document.createTextNode('Part 1 ')); wrapper.appendChild(document.createTextNode('Part 2 ')); wrapper.childNodes.length // 2 wrapper.normalize(); wrapper.childNodes.length // 1 上面代码使用normalize方法之前,wrapper节点有两个毗邻的文本子节点。使用normalize方法之后,两个文本子节点被合并成一个;该方法是Text.splitText的逆方法 Node.prototype.getRootNode() getRootNode()方法返回当前节点所在文档的根节点document,与ownerDocument属性的作用相同 document.body.firstChild.getRootNode() === document // true document.body.firstChild.getRootNode() === document.body.firstChild.ownerDocument // true 该方法可用于document节点自身,这一点与document.ownerDocument不同 document.getRootNode() // document document.ownerDocument // null NodeList 接口,HTMLCollection 接口 节点都是单个对象,有时需要一种数据结构,能够容纳多个节点。DOM 提供两种节点集合,用于容纳多个节点:NodeList和HTMLCollection。这两种集合都属于接口规范;许多 DOM 属性和方法,返回的结果是NodeList实例或HTMLCollection实例。主要区别是,NodeList可以包含各种类型的节点,HTMLCollection只能包含 HTML 元素节点 NodeList 接口 概述 NodeList实例是一个类似数组的对象,它的成员是节点对象。通过以下方法可以得到NodeList实例: 1.Node.childNodes 2.document.querySelectorAll()等节点搜索方法 document.body.childNodes instanceof NodeList // true NodeList实例很像数组,可以使用length属性和forEach方法。但是,它不是数组,不能使用pop或push之类数组特有的方法 var children = document.body.childNodes; Array.isArray(children) // false children.length // 34 children.forEach(console.log) 上面代码中,NodeList 实例children不是数组,但是具有length属性和forEach方法。如果NodeList实例要使用数组方法,可以将其转为真正的数组 var children = document.body.childNodes; var nodeArr = Array.prototype.slice.call(children); 除了使用forEach方法遍历 NodeList 实例,还可以使用for循环 var children = document.body.childNodes; for (var i = 0; i < children.length; i++) { var item = children[i]; } 注意,NodeList 实例可能是动态集合,也可能是静态集合。所谓动态集合就是一个活的集合,DOM 删除或新增一个相关节点,都会立刻反映在 NodeList 实例。目前,只有Node.childNodes返回的是一个动态集合,其他的 NodeList 都是静态集合 var children = document.body.childNodes; children.length // 18 document.body.appendChild(document.createElement('p')); children.length // 19 上面代码中,文档增加一个子节点,NodeList 实例children的length属性就增加了1 NodeList.prototype.length length属性返回 NodeList 实例包含的节点数量 document.querySelectorAll('xxx').length // 0 上面代码中,document.querySelectorAll返回一个 NodeList 集合。对于那些不存在的 HTML 标签,length属性返回0 NodeList.prototype.forEach() forEach方法用于遍历 NodeList 的所有成员。它接受一个回调函数作为参数,每一轮遍历就执行一次这个回调函数,用法与数组实例的forEach方法完全一致 var children = document.body.childNodes; children.forEach(function f(item, i, list) { // ... }, this); 上面代码中,回调函数f的三个参数依次是当前成员、位置和当前 NodeList 实例。forEach方法的第二个参数,用于绑定回调函数内部的this,该参数可省略 NodeList.prototype.item() item方法接受一个整数值作为参数,表示成员的位置,返回该位置上的成员 document.body.childNodes.item(0) 上面代码中,item(0)返回第一个成员;如果参数值大于实际长度,或者索引不合法(比如负数),item方法返回null;如果省略参数,item方法会报错;所有类似数组的对象,都可以使用方括号运算符取出成员;一般情况下,都是使用方括号运算符,而不使用item方法 document.body.childNodes[0] NodeList.prototype.keys(),NodeList.prototype.values(),NodeList.prototype.entries() 这三个方法都返回一个 ES6 的遍历器对象,可以通过for...of循环遍历获取每一个成员的信息。区别在于,keys()返回键名的遍历器,values()返回键值的遍历器,entries()返回的遍历器同时包含键名和键值的信息 var children = document.body.childNodes; for (var key of children.keys()) { console.log(key); } // 0 // 1 // 2 // ... for (var value of children.values()) { console.log(value); } // #text // <script> // ... for (var entry of children.entries()) { console.log(entry); } // Array [ 0, #text ] // Array [ 1, <script> ] // ... HTMLCollection 接口 概述 HTMLCollection是一个节点对象的集合,只能包含元素节点(element),不能包含其他类型的节点。它的返回值是一个类似数组的对象,但是与NodeList接口不同,HTMLCollection没有forEach方法,只能使用for循环遍历。返回HTMLCollection实例的,主要是一些Document对象的集合属性,比如document.links、document.forms、document.images等 document.links instanceof HTMLCollection // true HTMLCollection实例都是动态集合,节点的变化会实时反映在集合中;如果元素节点有id或name属性,那么HTMLCollection实例上面,可以使用id属性或name属性引用该节点元素。如果没有对应的节点,则返回null <img id="pic" src="http://example.com/foo.jpg"> var pic = document.getElementById('pic'); document.images.pic === pic // true 上面代码中,document.images是一个HTMLCollection实例,可以通过元素的id属性值,从HTMLCollection实例上取到这个元素 HTMLCollection.prototype.length length属性返回HTMLCollection实例包含的成员数量 document.links.length // 18 HTMLCollection.prototype.item() item方法接受一个整数值作为参数,表示成员的位置,返回该位置上的成员 var c = document.images; var img0 = c.item(0); 上面代码中,item(0)表示返回0号位置的成员。由于方括号运算符也具有同样作用,而且使用更方便,所以一般情况下,总是使用方括号运算符;如果参数值超出成员数量或者不合法(比如小于0),那么item方法返回null HTMLCollection.prototype.namedItem() namedItem方法的参数是一个字符串,表示id属性或name属性的值,返回对应的元素节点。如果没有对应的节点,则返回null <img id="pic" src="http://example.com/foo.jpg"> var pic = document.getElementById('pic'); document.images.namedItem('pic') === pic // true ParentNode 接口,ChildNode 接口 节点对象除了继承 Node 接口以外,还会继承其他接口。ParentNode接口表示当前节点是一个父节点,提供一些处理子节点的方法;ChildNode接口表示当前节点是一个子节点,提供一些相关方法 ParentNode 接口 如果当前节点是父节点,就会继承ParentNode接口。由于只有元素节点(element)、文档节点(document)和文档片段节点(documentFragment)拥有子节点,因此只有这三类节点会继承ParentNode接口 ParentNode.children children属性返回一个HTMLCollection实例,成员是当前节点的所有元素子节点,该属性只读;下面是遍历某个节点的所有元素子节点的示例 for (var i = 0; i < el.children.length; i++) { // ... } 注意,children属性只包括元素子节点,不包括其他类型的子节点(比如文本子节点);如果没有元素类型的子节点,返回值HTMLCollection实例的length属性为0。另外,HTMLCollection是动态集合,会实时反映 DOM 的任何变化 ParentNode.firstElementChild firstElementChild属性返回当前节点的第一个元素子节点;如果没有任何元素子节点,则返回null document.firstElementChild.nodeName // "HTML" 上面代码中,document节点的第一个元素子节点是 ParentNode.lastElementChild lastElementChild属性返回当前节点的最后一个元素子节点,如果不存在任何元素子节点,则返回null document.lastElementChild.nodeName // "HTML" 上面代码中,document节点的最后一个元素子节点是(因为document只包含这一个元素子节点) ParentNode.childElementCount childElementCount属性返回一个整数,表示当前节点的所有元素子节点的数目。如果不包含任何元素子节点,则返回0 document.body.childElementCount // 13 ParentNode.append(),ParentNode.prepend() append方法为当前节点追加一个或多个子节点,位置是最后一个元素子节点的后面;该方法不仅可以添加元素子节点,还可以添加文本子节点 var parent = document.body; // 添加元素子节点 var p = document.createElement('p'); parent.append(p); // 添加文本子节点 parent.append('Hello'); // 添加多个元素子节点 var p1 = document.createElement('p'); var p2 = document.createElement('p'); parent.append(p1, p2); // 添加元素子节点和文本子节点 var p = document.createElement('p'); parent.append('Hello', p); 注意,该方法没有返回值;prepend方法为当前节点追加一个或多个子节点,位置是第一个元素子节点的前面;它的用法与append方法完全一致,也是没有返回值 ChildNode 接口 如果一个节点有父节点,那么该节点就继承了ChildNode接口 ChildNode.remove() remove方法用于从父节点移除当前节点 el.remove() 上面代码在 DOM 里面移除了el节点 ChildNode.before(),ChildNode.after() before方法用于在当前节点的前面,插入一个或多个同级节点,两者拥有相同的父节点;注意,该方法不仅可以插入元素节点,还可以插入文本节点 var p = document.createElement('p'); var p1 = document.createElement('p'); el.before(p); // 插入元素节点 el.before('Hello'); // 插入文本节点 el.before(p, p1); // 插入多个元素节点 el.before(p, 'Hello'); // 插入元素节点和文本节点 after方法用于在当前节点的后面,插入一个或多个同级节点,两者拥有相同的父节点。用法与before方法完全相同 ChildNode.replaceWith() replaceWith方法使用参数节点,替换当前节点。参数可以是元素节点,也可以是文本节点 var span = document.createElement('span'); el.replaceWith(span); 上面代码中,el节点将被span节点替换 Document 节点 概述 document节点对象代表整个文档,每张网页都有自己的document对象,window.document属性就指向这个对象;只要浏览器开始载入 HTML 文档,该对象就存在了,可以直接使用。document对象有不同的办法可以获取: 1.正常的网页,直接使用document或window.document 2.iframe框架里面的网页,使用iframe节点的contentDocument属性 3.Ajax 操作返回的文档,使用XMLHttpRequest对象的responseXML属性 4.内部节点的ownerDocument属性 document对象继承了EventTarget接口、Node接口、ParentNode接口;这意味着这些接口的方法都可以在document对象上调用;除此之外,document对象还有很多自己的属性和方法 属性 快捷方式属性 以下属性是指向文档内部的某个节点的快捷方式 document.defaultView document.defaultView属性返回document对象所属的window对象。如果当前文档不属于window对象,该属性返回null document.defaultView === window // true document.doctype 对于 HTML 文档来说,document对象一般有两个子节点;第一个子节点是document.doctype,指向节点,即文档类型(Document Type Declaration,简写DTD)节点;HTML 的文档类型节点一般写成,如果网页没有声明 DTD,该属性返回null var doctype = document.doctype; doctype // "<!DOCTYPE html>" doctype.name // "html" document.firstChild通常就返回这个节点 document.documentElement document.documentElement属性返回当前文档的根元素节点(root);它通常是document节点的第二个子节点,紧跟在document.doctype节点后面;HTML网页的该属性,一般是节点 document.body,document.head document.body属性指向 节点,document.head属性指向节点;这两个属性总是存在的,如果网页源码里面省略了或,浏览器会自动创建;另外,这两个属性是可写的,如果改写它们的值,相当于移除所有子节点 document.scrollingElement document.scrollingElement属性返回文档的滚动元素。也就是说,当文档整体滚动时,到底是哪个元素在滚动;标准模式下,这个属性返回的文档的根元素document.documentElement(即)。兼容(quirk)模式下,返回的是 元素,如果该元素不存在,返回null document.scrollingElement.scrollTop = 0; // 页面滚动到浏览器顶部 document.activeElement document.activeElement属性返回获得当前焦点(focus)的 DOM 元素。通常,这个属性返回的是、、等表单元素;如果当前没有焦点元素,返回元素或null document.fullscreenElement document.fullscreenElement属性返回当前以全屏状态展示的 DOM 元素。如果不是全屏状态,该属性返回null if (document.fullscreenElement.nodeName == 'VIDEO') { console.log('全屏播放视频'); } 上面代码中,通过document.fullscreenElement可以知道 元素有没有处在全屏状态,从而判断用户行为 节点集合属性 以下属性返回一个HTMLCollection实例,表示文档内部特定元素的集合;这些集合都是动态的,原节点有任何变化,立刻会反映在集合中 document.links document.links属性返回当前文档所有设定了href属性的 // 打印文档所有的链接 var links = document.links; for(var i = 0; i < links.length; i++) { console.log(links[i]); } document.forms document.forms属性返回所有 表单节点 var selectForm = document.forms[0]; 上面代码获取文档第一个表单;除了使用位置序号,id属性和name属性也可以用来引用表单 <form name="foo" id="bar"></form> document.forms[0] === document.forms.foo // true document.forms.bar === document.forms.foo // true document.images document.images属性返回页面所有图片节点 var imglist = document.images; for(var i = 0; i < imglist.length; i++) { if (imglist[i].src === 'banner.gif') { // ... } } 上面代码在所有img标签中,寻找某张图片 document.embeds,document.plugins document.embeds属性和document.plugins属性,都返回所有节点 document.scripts document.scripts属性返回所有 var scripts = document.scripts; if (scripts.length !== 0 ) { console.log('当前网页有脚本'); } document.styleSheets document.styleSheets属性返回文档内嵌或引入的样式表集合 小结 除了document.styleSheets,以上的集合属性返回的都是HTMLCollection实例 document.links instanceof HTMLCollection // true document.images instanceof HTMLCollection // true document.forms instanceof HTMLCollection // true document.embeds instanceof HTMLCollection // true document.scripts instanceof HTMLCollection // true HTMLCollection实例是类似数组的对象,所以这些属性都有length属性,都可以使用方括号运算符引用成员。如果成员有id或name属性,还可以用这两个属性的值,在HTMLCollection实例上引用到这个成员 <form name="myForm"> document.myForm === document.forms.myForm // true 文档静态信息属性 以下属性返回文档信息 document.documentURI,document.URL document.documentURI属性和document.URL属性都返回一个字符串,表示当前文档的网址;不同之处是它们继承自不同的接口,documentURI继承自Document接口,可用于所有文档;URL继承自HTMLDocument接口,只能用于 HTML 文档 document.URL // http://www.example.com/about document.documentURI === document.URL // true 如果文档的锚点(#anchor)变化,这两个属性都会跟着变化 document.domain document.domain属性返回当前文档的域名,不包含协议和接口;比如,网页的网址是,那么domain属性就等于www.example.com。如果无法获取域名,该属性返回null;document.domain基本上是一个只读属性,只有一种情况除外;次级域名的网页,可以把document.domain设为对应的上级域名。比如,当前域名是a.sub.example.com,则document.domain属性可以设置为sub.example.com,也可以设为example.com。修改后,document.domain相同的两个网页,可以读取对方的资源,比如设置的 Cookie。 另外,设置document.domain会导致端口被改成null。因此,如果通过设置document.domain来进行通信,双方网页都必须设置这个值,才能保证端口相同 document.location Location对象是浏览器提供的原生对象,提供 URL 相关的信息和操作方法。通过window.location和document.location属性,可以拿到这个对象 document.lastModified document.lastModified属性返回一个字符串,表示当前文档最后修改的时间。不同浏览器的返回值,日期格式是不一样的 document.lastModified // "04/10/2019 12:17:01" 注意:document.lastModified属性的值是字符串,所以不能直接用来比较;Date.parse方法将其转为Date实例,才能比较两个网页 var lastVisitedDate = Date.parse('01/01/2018'); if (Date.parse(document.lastModified) > lastVisitedDate) { console.log('网页已经变更'); } document.characterSet document.characterSet属性返回当前文档的编码,比如UTF-8、ISO-8859-1等 document.referrer document.referrer属性返回一个字符串,表示当前文档的访问者来自哪里 document.referrer // "https://example.com/path" 如果无法获取来源,或者用户直接键入网址而不是从其他网页点击进入,document.referrer返回一个空字符串;document.referrer的值,总是与 HTTP 头信息的Referer字段保持一致。但是,document.referrer的拼写有两个r,而头信息的Referer字段只有一个r document.dir document.dir返回一个字符串,表示文字方向。它只有两个可能的值:rtl表示文字从右到左,阿拉伯文是这种方式;ltr表示文字从左到右,包括英语和汉语在内的大多数文字采用这种方式 document.compatMode compatMode属性返回浏览器处理文档的模式,可能的值为BackCompat(向后兼容模式)和CSS1Compat(严格模式)。一般来说,如果网页代码的第一行设置了明确的DOCTYPE(比如),document.compatMode的值都为CSS1Compat 文档状态属性 document.hidden document.hidden属性返回一个布尔值,表示当前页面是否可见;如果窗口最小化、浏览器切换了 Tab,都会导致页面不可见,使得document.hidden返回true。这个属性是 Page Visibility API 引入的,一般都是配合这个 API 使用 document.visibilityState document.visibilityState返回文档的可见状态;它的值有四种可能: 1.visible:页面可见。注意,页面可能是部分可见,即不是焦点窗口,前面被其他窗口部分挡住了 2.hidden:页面不可见,有可能窗口最小化,或者浏览器切换到了另一个 Tab 3.prerender:页面处于正在渲染状态,对于用户来说,该页面不可见 4.unloaded:页面从内存里面卸载了 这个属性可以用在页面加载时,防止加载某些资源;或者页面不可见时停掉一些页面功能 document.readyState document.readyState属性返回当前文档的状态,共有三种可能的值: 1.loading:加载 HTML 代码阶段(尚未完成解析) 2.interactive:加载外部资源阶段 3.complete:加载完成 这个属性变化的过程如下: 1.浏览器开始解析 HTML 文档,document.readyState属性等于loading 2.浏览器遇到 HTML 文档中的 3.HTML 文档解析完成,document.readyState属性变成interactive 4.浏览器等待图片、样式表、字体文件等外部资源加载完成;一旦全部加载完成,document.readyState属性变成complete 下面的代码用来检查网页是否加载成功 if (document.readyState === 'complete') { // 基本检查 // ... } var interval = setInterval(function() { // 轮询检查 if (document.readyState === 'complete') { clearInterval(interval); // ... } }, 100); 另外,每次状态变化都会触发一个readystatechange事件 document.cookie document.cookie属性用来操作浏览器 Cookie document.designMode document.designMode属性控制当前文档是否可编辑。该属性只有两个值on和off,默认值为off。一旦设为on,用户就可以编辑整个文档的内容; document.implementation document.implementation属性返回一个DOMImplementation对象。该对象有三个方法,主要用于创建独立于当前文档的新的 Document 对象; 1.DOMImplementation.createDocument():创建一个 XML 文档 2.DOMImplementation.createHTMLDocument():创建一个 HTML 文档 3.DOMImplementation.createDocumentType():创建一个 DocumentType 对象 下面是创建 HTML 文档的例子 var doc = document.implementation.createHTMLDocument('Title'); var p = doc.createElement('p'); p.innerHTML = 'hello world'; doc.body.appendChild(p); document.replaceChild( doc.documentElement, document.documentElement ); 上面代码中,第一步生成一个新的 HTML 文档doc,然后用它的根元素document.documentElement替换掉document.documentElement;这会使得当前文档的内容全部消失,变成hello world 方法 document.open(),document.close() document.open方法清除当前文档所有内容,使得文档处于可写状态,供document.write方法写入内容;document.close方法用来关闭document.open()打开的文档 document.open(); document.write('hello world'); document.close(); document.write(),document.writeln() document.write方法用于向当前文档写入内容;在网页的首次渲染阶段,只要页面没有关闭写入(即没有执行document.close()),document.write写入的内容就会追加在已有内容的后面 // 页面显示“helloworld” document.open(); document.write('hello'); document.write('world'); document.close(); 注意,document.write会当作 HTML 代码解析,不会转义 document.write('<p>hello world</p>'); 上面代码中,document.write会将 当作 HTML 标签解释;如果页面已经解析完成(DOMContentLoaded事件发生之后),再调用write方法,它会先调用open方法,擦除当前文档所有内容,然后再写入 document.addEventListener('DOMContentLoaded', function (event) { document.write('<p>Hello World!</p>'); }); // 等同于 document.addEventListener('DOMContentLoaded', function (event) { document.open(); document.write('<p>Hello World!</p>'); document.close(); }); 如果在页面渲染过程中调用write方法,并不会自动调用open方法。(可以理解成,open方法已调用,但close方法还未调用。) <html><body> hello <script type="text/javascript"> document.write("world") </script> </body></html> 在浏览器打开上面网页,将会显示hello world;document.write是 JavaScript 语言标准化之前就存在的方法,现在完全有更符合标准的方法向文档写入内容(比如对innerHTML属性赋值);所以,除了某些特殊情况,应该尽量避免使用document.write这个方法;document.writeln方法与write方法完全一致,除了会在输出内容的尾部添加换行符 document.write(1); document.write(2); // 12 document.writeln(1); document.writeln(2); // 1 // 2 注意,writeln方法添加的是 ASCII 码的换行符,渲染成 HTML 网页时不起作用,即在网页上显示不出换行。网页上的换行,必须显式写入 document.querySelector(),document.querySelectorAll() document.querySelector方法接受一个 CSS 选择器作为参数,返回匹配该选择器的元素节点;如果有多个节点满足匹配条件,则返回第一个匹配的节点;如果没有发现匹配的节点,则返回null var el1 = document.querySelector('.myclass'); var el2 = document.querySelector('#myParent > [ng-click]'); document.querySelectorAll方法与querySelector用法类似,区别是返回一个NodeList对象,包含所有匹配给定选择器的节点 elementList = document.querySelectorAll('.myclass'); 这两个方法的参数,可以是逗号分隔的多个 CSS 选择器,返回匹配其中一个选择器的元素节点,这与 CSS 选择器的规则是一致的 var matches = document.querySelectorAll('div.note, div.alert'); 上面代码返回class属性是note或alert的div元素;这两个方法都支持复杂的 CSS 选择器 document.querySelectorAll('[data-foo-bar="someval"]'); // 选中 data-foo-bar 属性等于 someval 的元素 document.querySelectorAll('#myForm :invalid'); // 选中 myForm 表单中所有不通过验证的元素 document.querySelectorAll('DIV:not(.ignore)'); // 选中div元素,那些 class 含 ignore 的除外 document.querySelectorAll('DIV, A, SCRIPT'); // 同时选中 div,a,script 三类元素 但是,它们不支持 CSS 伪元素的选择器(比如:first-line和:first-letter)和伪类的选择器(比如:link和:visited),即无法选中伪元素和伪类;如果querySelectorAll方法的参数是字符串*,则会返回文档中的所有元素节点;另外,querySelectorAll的返回结果不是动态集合,不会实时反映元素节点的变化;最后,这两个方法除了定义在document对象上,还定义在元素节点上,即在元素节点上也可以调用 document.getElementsByTagName() document.getElementsByTagName方法搜索 HTML 标签名,返回符合条件的元素。它的返回值是一个类似数组对象(HTMLCollection实例),可以实时反映 HTML 文档的变化。如果没有任何匹配的元素,就返回一个空集 var paras = document.getElementsByTagName('p'); paras instanceof HTMLCollection // true 上面代码返回当前文档的所有p元素节点;HTML 标签名是大小写不敏感的,因此getElementsByTagName方法也是大小写不敏感的。另外,返回结果中,各个成员的顺序就是它们在文档中出现的顺序;如果传入*,就可以返回文档中所有 HTML 元素 var allElements = document.getElementsByTagName('*'); 注意,元素节点本身也定义了getElementsByTagName方法,返回该元素的后代元素中符合条件的元素。也就是说,这个方法不仅可以在document对象上调用,也可以在任何元素节点上调用 var firstPara = document.getElementsByTagName('p')[0]; var spans = firstPara.getElementsByTagName('span'); 上面代码选中第一个p元素内部的所有span元素 document.getElementsByClassName() document.getElementsByClassName方法返回一个类似数组的对象(HTMLCollection实例),包括了所有class名字符合指定条件的元素,元素的变化实时反映在返回结果中 var elements = document.getElementsByClassName(names); 由于class是保留字,所以 JavaScript 一律使用className表示 CSS 的class;参数可以是多个class,它们之间使用空格分隔 var elements = document.getElementsByClassName('foo bar'); 上面代码返回同时具有foo和bar两个class的元素,foo和bar的顺序不重要。;注意,正常模式下,CSS 的class是大小写敏感的;(quirks mode下,大小写不敏感。) 与getElementsByTagName方法一样,getElementsByClassName方法不仅可以在document对象上调用,也可以在任何元素节点上调用 // 非document对象上调用 var elements = rootElement.getElementsByClassName(names); document.getElementsByName() document.getElementsByName方法用于选择拥有name属性的 HTML 元素(比如 、、、、和

优秀的个人博客,低调大师

JavaScript 教程(一)

入门篇 导论 什么是JavaScript语言 JavaScript 是一种轻量级的脚本语言。所谓“脚本语言”(script language),指的是它不具备开发操作系统的能力,而是只用来编写控制其他大型应用程序(比如浏览器)的“脚本” JavaScript 也是一种嵌入式(embedded)语言。它本身提供的核心语法不算很多,只能用来做一些数学和逻辑运算。JavaScript 本身不提供任何与 I/O(输入/输出)相关的 API,都要靠宿主环境(host)提供,所以 JavaScript 只合适嵌入更大型的应用程序环境,去调用宿主环境提供的底层 API 目前,已经嵌入 JavaScript 的宿主环境有多种,最常见的环境就是浏览器,另外还有服务器环境,也就是 Node 项目 从语法角度看,JavaScript 语言是一种“对象模型”语言。各种宿主环境通过这个模型,描述自己的功能和操作接口,从而通过 JavaScript 控制这些功能。但是,JavaScript 并不是纯粹的“面向对象语言”,还支持其他编程范式(比如函数式编程)。这导致几乎任何一个问题,JavaScript 都有多种解决方法 JavaScript 的核心语法部分相当精简,只包括两个部分:基本的语法构造(比如操作符、控制结构、语句)和标准库(就是一系列具有各种功能的对象比如Array、Date、Math等)。除此之外,各种宿主环境提供额外的 API(即只能在该环境使用的接口),以便 JavaScript 调用。以浏览器为例,它提供的额外 API 可以分成三大类。 1.浏览器控制类:操作浏览器 2.DOM 类:操作网页的各种元素 3.Web 类:实现互联网的各种功能 如果宿主环境是服务器,则会提供各种操作系统的 API,比如文件操作 API、网络通信 API等等。这些都可以在 Node 环境中找到。 本书主要介绍 JavaScript 核心语法和浏览器网页开发的基本知识,不涉及 Node。全书可以分成以下四大部分。 为什么学习 JavaScript JavaScript既适合作为学习编程的入门语言,也适合当作日常开发的工作语言。它是目前最有希望、前途最光明的计算机语言之一 操控浏览器的能力 JavaScript 的发明目的,就是作为浏览器的内置脚本语言,为网页开发者提供操控浏览器的能力。它是目前唯一一种通用的浏览器脚本语言,所有浏览器都支持。它可以让网页呈现各种特殊效果,为用户提供良好的互动体验。 目前,全世界几乎所有网页都使用 JavaScript。如果不用,网站的易用性和使用效率将大打折扣,无法成为操作便利、对用户友好的网站。 对于一个互联网开发者来说,如果你想提供漂亮的网页、令用户满意的上网体验、各种基于浏览器的便捷功能、前后端之间紧密高效的联系,JavaScript 是必不可少的工具 广泛的使用领域 近年来,JavaScript 的使用范围,慢慢超越了浏览器,正在向通用的系统语言发展 浏览器的平台化 随着 HTML5 的出现,浏览器本身的功能越来越强,不再仅仅能浏览网页,而是越来越像一个平台,JavaScript 因此得以调用许多系统功能,比如操作本地文件、操作图片、调用摄像头和麦克风等等。这使得 JavaScript 可以完成许多以前无法想象的事情 Node Node 项目使得 JavaScript 可以用于开发服务器端的大型项目,网站的前后端都用 JavaScript 开发已经成为了现实。有些嵌入式平台(Raspberry Pi)能够安装 Node,于是 JavaScript 就能为这些平台开发应用程序 数据库操作 JavaScript 甚至也可以用来操作数据库。NoSQL 数据库这个概念,本身就是在 JSON(JavaScript Object Notation)格式的基础上诞生的,大部分 NoSQL 数据库允许 JavaScript 直接操作。基于 SQL 语言的开源数据库 PostgreSQL 支持 JavaScript 作为操作语言,可以部分取代 SQL 查询语言 移动平台开发 JavaScript 也正在成为手机应用的开发语言。一般来说,安卓平台使用 Java 语言开发,iOS 平台使用 Objective-C 或 Swift 语言开发。许多人正在努力,让 JavaScript 成为各个平台的通用开发语言。 PhoneGap 项目就是将 JavaScript 和 HTML5 打包在一个容器之中,使得它能同时在 iOS 和安卓上运行。Facebook 公司的 React Native 项目则是将 JavaScript 写的组件,编译成原生组件,从而使它们具备优秀的性能。 Mozilla 基金会的手机操作系统 Firefox OS,更是直接将 JavaScript 作为操作系统的平台语言,但是很可惜这个项目没有成功 内嵌脚本语言 越来越多的应用程序,将 JavaScript 作为内嵌的脚本语言,比如 Adobe 公司的著名 PDF 阅读器 Acrobat、Linux 桌面环境 GNOME 3 跨平台的桌面应用程序 Chromium OS、Windows 8 等操作系统直接支持 JavaScript 编写应用程序。Mozilla 的 Open Web Apps 项目、Google 的 Chrome App 项目、GitHub 的 Electron 项目、以及 TideSDK 项目,都可以用来编写运行于 Windows、Mac OS 和 Android 等多个桌面平台的程序,不依赖浏览器 小结 可以预期,JavaScript 最终将能让你只用一种语言,就开发出适应不同平台(包括桌面端、服务器端、手机端)的程序。早在2013年9月的统计之中,JavaScript 就是当年 GitHub 上使用量排名第一的语言。 易学性 相比学习其他语言,学习 JavaScript 有一些有利条件 学习环境无处不在 只要有浏览器,就能运行 JavaScript 程序;只要有文本编辑器,就能编写 JavaScript 程序。这意味着,几乎所有电脑都原生提供 JavaScript 学习环境,不用另行安装复杂的 IDE(集成开发环境)和编译器 简单性 相比其他脚本语言(比如 Python 或 Ruby),JavaScript 的语法相对简单一些,本身的语法特性并不是特别多。而且,那些语法中的复杂部分,也不是必需要学会。你完全可以只用简单命令,完成大部分的操作 与主流语言的相似性 JavaScript 的语法很类似 C/C++ 和 Java,如果学过这些语言(事实上大多数学校都教),JavaScript 的入门会非常容易。必须说明的是,虽然核心语法不难,但是 JavaScript 的复杂性体现在另外两个方面。 1.它涉及大量的外部 API。JavaScript 要发挥作用,必须与其他组件配合,这些外部组件五花八门,数量极其庞大,几乎涉及网络应用的各个方面,掌握它们绝非易事。 2.JavaScript 语言有一些设计缺陷。某些地方相当不合理,另一些地方则会出现怪异的运行结果。学习 JavaScript,很大一部分时间是用来搞清楚哪些地方有陷阱 尽管如此,目前看来,JavaScript 的地位还是无法动摇。加之,语言标准的快速进化,使得 JavaScript 功能日益增强,而语法缺陷和怪异之处得到了弥补。所以,JavaScript 还是值得学习,况且它的入门真的不难 强大的性能 JavaScript 的性能优势体现在以下方面 灵活的语法,表达力强。 JavaScript 既支持类似 C 语言清晰的过程式编程,也支持灵活的函数式编程,可以用来写并发处理(concurrent)。这些语法特性已经被证明非常强大,可以用于许多场合,尤其适用异步编程。 JavaScript 的所有值都是对象,这为程序员提供了灵活性和便利性。因为你可以很方便地、按照需要随时创造数据结构,不用进行麻烦的预定义。 JavaScript 的标准还在快速进化中,并不断合理化,添加更适用的语法特性 支持编译运行。 JavaScript 语言本身虽然是一种解释型语言,但是在现代浏览器中,JavaScript 都是编译后运行。程序会被高度优化,运行效率接近二进制程序。而且,JavaScript 引擎正在快速发展,性能将越来越好。 此外,还有一种 WebAssembly 格式,它是 JavaScript 引擎的中间码格式,全部都是二进制代码。由于跳过了编译步骤,可以达到接近原生二进制代码的运行速度。各种语言(主要是 C 和 C++)通过编译成 WebAssembly,就可以在浏览器里面运行 事件驱动和非阻塞式设计。 JavaScript 程序可以采用事件驱动(event-driven)和非阻塞式(non-blocking)设计,在服务器端适合高并发环境,普通的硬件就可以承受很大的访问量 开放性 JavaScript 是一种开放的语言。它的标准 ECMA-262 是 ISO 国际标准,写得非常详尽明确;该标准的主要实现(比如 V8 和 SpiderMonkey 引擎)都是开放的,而且质量很高。这保证了这门语言不属于任何公司或个人,不存在版权和专利的问题。 语言标准由 TC39 委员会负责制定,该委员会的运作是透明的,所有讨论都是开放的,会议记录都会对外公布。 不同公司的 JavaScript 运行环境,兼容性很好,程序不做调整或只做很小的调整,就能在所有浏览器上运行 社区支持和就业机会 全世界程序员都在使用 JavaScript,它有着极大的社区、广泛的文献和图书、丰富的代码资源。绝大部分你需要用到的功能,都有多个开源函数库可供选用 实验环境 推荐安装 Chrome 浏览器,进入 Chrome 浏览器的“控制台” 1.直接进入:按下Option + Command + J(Mac)或者Ctrl + Shift + J(Windows / Linux) 2.开发者工具进入:开发者工具的快捷键是 F12,或者Option + Command + I(Mac)以及Ctrl + Shift + I(Windows / Linux),然后选择 Console 面板 JavaScript 语言的历史 诞生 JavaScript 因为互联网而生,紧跟着浏览器的出现而问世。回顾它的历史,就要从浏览器的历史讲起。 1990年底,欧洲核能研究组织(CERN)科学家 Tim Berners-Lee,在全世界最大的电脑网络——互联网的基础上,发明了万维网(World Wide Web),从此可以在网上浏览网页文件。最早的网页只能在操作系统的终端里浏览,也就是说只能使用命令行操作,网页都是在字符窗口中显示,这当然非常不方便。 1992年底,美国国家超级电脑应用中心(NCSA)开始开发一个独立的浏览器,叫做 Mosaic。这是人类历史上第一个浏览器,从此网页可以在图形界面的窗口浏览。 1994年10月,NCSA 的一个主要程序员 Marc Andreessen 联合风险投资家 Jim Clark,成立了 Mosaic 通信公司(Mosaic Communications),不久后改名为 Netscape。这家公司的方向,就是在 Mosaic 的基础上,开发面向普通用户的新一代的浏览器 Netscape Navigator。 1994年12月,Navigator 发布了1.0版,市场份额一举超过90%。 Netscape 公司很快发现,Navigator 浏览器需要一种可以嵌入网页的脚本语言,用来控制浏览器行为。当时,网速很慢而且上网费很贵,有些操作不宜在服务器端完成。比如,如果用户忘记填写“用户名”,就点了“发送”按钮,到服务器再发现这一点就有点太晚了,最好能在用户发出数据之前,就告诉用户“请填写用户名”。这就需要在网页中嵌入小程序,让浏览器检查每一栏是否都填写了。 管理层对这种浏览器脚本语言的设想是:功能不需要太强,语法较为简单,容易学习和部署。那一年,正逢 Sun 公司的 Java 语言问世,市场推广活动非常成功。Netscape 公司决定与 Sun 公司合作,浏览器支持嵌入 Java 小程序(后来称为 Java applet)。但是,浏览器脚本语言是否就选用 Java,则存在争论。后来,还是决定不使用 Java,因为网页小程序不需要 Java 这么“重”的语法。但是,同时也决定脚本语言的语法要接近 Java,并且可以支持 Java 程序。这些设想直接排除了使用现存语言,比如 Perl、Python 和 TCL。 1995年,Netscape 公司雇佣了程序员 Brendan Eich 开发这种网页脚本语言。Brendan Eich 有很强的函数式编程背景,希望以 Scheme 语言(函数式语言鼻祖 LISP 语言的一种方言)为蓝本,实现这种新语言。 1995年5月,Brendan Eich 只用了10天,就设计完成了这种语言的第一版。它是一个大杂烩,语法有多个来源。 1.基本语法:借鉴 C 语言和 Java 语言。 2.数据结构:借鉴 Java 语言,包括将值分成原始值和对象两大类。 3.函数的用法:借鉴 Scheme 语言和 Awk 语言,将函数当作第一等公民,并引入闭包。 4.原型继承模型:借鉴 Self 语言(Smalltalk 的一种变种)。 5.正则表达式:借鉴 Perl 语言。 6.字符串和数组处理:借鉴 Python 语言。 为了保持简单,这种脚本语言缺少一些关键的功能,比如块级作用域、模块、子类型(subtyping)等等,但是可以利用现有功能找出解决办法。这种功能的不足,直接导致了后来 JavaScript 的一个显著特点:对于其他语言,你需要学习语言的各种功能,而对于 JavaScript,你常常需要学习各种解决问题的模式。而且由于来源多样,从一开始就注定JavaScript 的编程风格是函数式编程和面向对象编程的一种混合体。 Netscape 公司的这种浏览器脚本语言,最初名字叫做 Mocha,1995年9月改为 LiveScript。12月,Netscape 公司与 Sun 公司(Java 语言的发明者和所有者)达成协议,后者允许将这种语言叫做 JavaScript。这样一来,Netscape 公司可以借助 Java 语言的声势,而 Sun 公司则将自己的影响力扩展到了浏览器。 之所以起这个名字,并不是因为 JavaScript 本身与 Java 语言有多么深的关系(事实上,两者关系并不深),而是因为 Netscape 公司已经决定,使用 Java 语言开发网络应用程序,JavaScript 可以像胶水一样,将各个部分连接起来。当然,后来的历史是 Java 语言的浏览器插件失败了,JavaScript 反而发扬光大。 1995年12月4日,Netscape 公司与 Sun 公司联合发布了 JavaScript 语言,对外宣传 JavaScript 是 Java 的补充,属于轻量级的 Java,专门用来操作网页。 1996年3月,Navigator 2.0 浏览器正式内置了 JavaScript 脚本语言 JavaScript 与 Java 的关系 JavaScript 和 Java是两种不一样的语言,但是彼此存在联系。 JavaScript 的基本语法和对象体系,是模仿 Java 而设计的。但是,JavaScript 没有采用 Java 的静态类型。正是因为 JavaScript 与 Java 有很大的相似性,所以这门语言才从一开始的 LiveScript 改名为 JavaScript。基本上,JavaScript 这个名字的原意是“很像Java的脚本语言”。 JavaScript 语言的函数是一种独立的数据类型,以及采用基于原型对象(prototype)的继承链。这是它与 Java 语法最大的两点区别。JavaScript 语法要比 Java 自由得多。另外,Java 语言需要编译,而 JavaScript 语言则是运行时由解释器直接执行。 总之,JavaScript 的原始设计目标是一种小型的、简单的动态语言,与 Java 有足够的相似性,使得使用者(尤其是 Java 程序员)可以快速上手 JavaScript 的基本语法 语句 JavaScript 程序的执行单位为行(line),一般情况下每一行就是一个语句。语句(statement)是为了完成某种任务而进行的操作,比如下面就是一行赋值语句 var a = 1 + 3; 1 + 3叫做表达式(expression),指一个为了得到返回值的计算式。语句和表达式的区别在于,前者主要为了进行某种操作,一般不需要返回值;后者则是为了得到返回值。凡是 JavaScript 语言中预期为值的地方,都可以使用表达式。比如,赋值语句的等号右边,预期是一个值,因此可以放置各种表达式。 语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。 var a = 1 + 3 ; var b = 'abc'; 分号前面可以没有任何内容,JavaScript 引擎将其视为空语句 ;;; 上面的代码就表示3个空语句。 表达式不需要分号结尾。一旦在表达式后面添加分号,则 JavaScript 引擎就将表达式视为语句,这样会产生一些没有任何意义的语句 1 + 3; 'abc'; 上面两行语句只是单纯地产生一个值,并没有任何实际的意义 变量 概念 变量是对“值”的具名引用。变量就是为“值”起名,然后引用这个名字,就等同于引用这个值。变量的名字就是变量名 注意,JavaScript 的变量名区分大小写,A和a是两个不同的变量。变量的声明和赋值,是分开的两个步骤,也可以将它们合在一起 如果只是声明变量而没有赋值,则该变量的值是undefined。undefined是一个特殊的值,表示“无定义” 如果变量赋值的时候,忘了写var命令,这条语句也是有效的。但是不利于表达意图,而且容易不知不觉地创建全局变量 如果一个变量没有声明就直接使用,JavaScript 会报错,告诉你变量未定义 x // ReferenceError: x is not defined 上面代码直接使用变量x,系统就报错,告诉你变量x没有声明 可以在同一条var命令中声明多个变量 JavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型 如果使用var重新声明一个已经存在的变量,是无效的 var x = 1; var x; x // 1 上面代码中,变量x声明了两次,第二次声明是无效的。但是,如果第二次声明的时候还进行了赋值,则会覆盖掉前面的值 变量提升 JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting) console.log(a); var a = 1; 上面代码首先使用console.log方法,在控制台(console)显示变量a的值。这时变量a还没有声明和赋值,所以这是一种错误的做法,但是实际上不会报错,因为存在变量提升。最后的结果是显示undefined,表示变量a已声明,但还未赋值 标识符 标识符(identifier)指的是用来识别各种值的合法名称。最常见的标识符就是变量名,以及后面要提到的函数名。JavaScript 语言的标识符对大小写敏感,所以a和A是两个不同的标识符。 标识符有一套命名规则,不符合规则的就是非法标识符。JavaScript 引擎遇到非法标识符,就会报错。 简单说,标识符命名规则如下: 第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_)。 第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字0-9 中文是合法的标识符,可以用作变量名 var 临时变量 = 1; JavaScript 有一些保留字,不能用作标识符: arguments、break、case、catch、class、const、continue、debugger、default、delete、do、else、enum、eval、export、extends、false、finally、for、function、if、implements、import、in、instanceof、interface、let、new、null、package、private、protected、public、return、static、super、switch、this、throw、true、try、typeof、var、void、while、with、yield 注释 源码中被 JavaScript 引擎忽略的部分就叫做注释,它的作用是对代码进行解释。JavaScript 提供两种注释的写法:一种是单行注释,用//起头;另一种是多行注释,放在/和/之间。此外,由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以 也被视为合法的单行注释 x = 1; <!-- x = 2; --> x = 3; 上面代码中,只有x = 1会执行,其他的部分都被注释掉了。需要注意的是,-->只有在行首,才会被当成单行注释,否则会当作正常的运算 function countdown(n) { while (n --> 0) console.log(n); } countdown(3) // 2 // 1 // 0 上面代码中,n --> 0实际上会当作n-- > 0,因此输出2、1、0 区块 JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)。对于var命令来说,JavaScript 的区块不构成单独的作用域(scope) { var a = 1; } a // 1 上面代码在区块内部,使用var命令声明并赋值了变量a,然后在区块外部,变量a依然有效,区块对于var命令不构成单独的作用域,与不使用区块的情况没有任何区别。在 JavaScript 语言中,单独使用区块并不常见,区块往往用来构成其他更复杂的语法结构,比如for、if、while、function等 条件语句 JavaScript 提供if结构和switch结构,完成条件判断,即只有满足预设的条件,才会执行相应的语句 if 结构 if结构先判断一个表达式的布尔值,然后根据布尔值的真伪,执行不同的语句。所谓布尔值,指的是 JavaScript 的两个特殊值,true表示真,false表示伪 if...else 结构 if代码块后面,还可以跟一个else代码块,表示不满足条件时,所要执行的代码。对同一个变量进行多次判断时,多个if...else语句可以连写在一起 switch 结构 多个if...else连在一起使用的时候,可以转为使用更方便的switch结构 switch (fruit) { case "banana": // ... break; case "apple": // ... break; default: // ... } 上面代码根据变量fruit的值,选择执行相应的case。如果所有case都不符合,则执行最后的default部分。需要注意的是,每个case代码块内部的break语句不能少,否则会接下去执行下一个case代码块,而不是跳出switch结构 switch语句部分和case语句部分,都可以使用表达式 switch (1 + 3) { case 2 + 2: // ... break; default: // ... } 需要注意的是,switch语句后面的表达式,与case语句后面的表示式比较运行结果时,采用的是严格相等运算符(===),而不是相等运算符(==),这意味着比较时不会发生类型转换 var x = 1; switch (x) { case true: console.log('x 发生类型转换'); break; default: console.log('x 没有发生类型转换'); } // x 没有发生类型转换 上面代码中,由于变量x没有发生类型转换,所以不会执行case true的情况。这表明,switch语句内部采用的是“严格相等运算符” 三元运算符 ?: JavaScript 还有一个三元运算符(即该运算符需要三个运算子)?:,也可以用于逻辑判断。这个三元运算符可以被视为if...else...的简写形式 循环语句 while 循环 While语句包括一个循环条件和一段代码块,只要条件为真,就不断循环执行代码块 for 循环 for语句是循环命令的另一种形式,可以指定循环的起点、终点和终止条件。所有for循环,都可以改写成while循环 do...while 循环 do...while循环与while循环类似,唯一的区别就是先运行一次循环体,然后判断循环条件 do { 语句 } while (条件); 不管条件是否为真,do...while循环至少运行一次,这是这种结构最大的特点。另外,while语句后面的分号注意不要省略 break 语句和 continue 语句 break语句和continue语句都具有跳转作用,可以让代码不按既有的顺序执行。 break语句用于跳出代码块或循环 var i = 0; while(i < 100) { console.log('i 当前为:' + i); i++; if (i === 10) break; } 上面代码只会执行10次循环,一旦i等于10,就会跳出循环。for循环也可以使用break语句跳出循环 for (var i = 0; i < 5; i++) { console.log(i); if (i === 3) break; } 上面代码执行到i等于3,就会跳出循环 continue语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环 var i = 0; while (i < 100){ i++; if (i % 2 === 0) continue; console.log('i 当前为:' + i); } 上面代码只有在i为奇数时,才会输出i的值。如果i为偶数,则直接进入下一轮循环。如果存在多重循环,不带参数的break语句和continue语句都只针对最内层循环 标签(label) JavaScript 语言允许语句的前面有标签(label),相当于定位符,用于跳转到程序的任意位置,标签的格式如下 label: 语句 标签可以是任意的标识符,但不能是保留字,语句部分可以是任意语句。标签通常与break语句和continue语句配合使用,跳出特定的循环 top: for (var i = 0; i < 3; i++){ for (var j = 0; j < 3; j++){ if (i === 1 && j === 1) break top; console.log('i=' + i + ', j=' + j); } } // i=0, j=0 // i=0, j=1 // i=0, j=2 // i=1, j=0 上面代码为一个双重循环区块,break命令后面加上了top标签(注意,top不用加引号),满足条件时,直接跳出双层循环。如果break语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。标签也可以用于跳出代码块 foo: { console.log(1); break foo; console.log('本行不会输出'); } console.log(2); // 1 // 2 上面代码执行到break foo,就会跳出区块。continue语句也可以与标签配合使用。 top: for (var i = 0; i < 3; i++){ for (var j = 0; j < 3; j++){ if (i === 1 && j === 1) continue top; console.log('i=' + i + ', j=' + j); } } // i=0, j=0 // i=0, j=1 // i=0, j=2 // i=1, j=0 // i=2, j=0 // i=2, j=1 // i=2, j=2 上面代码中,continue命令后面有一个标签名,满足条件时,会跳过当前循环,直接进入下一轮外层循环。如果continue语句后面不使用标签,则只能进入下一轮的内层循环 数据类型 数据类型概述 简介 JavaScript 语言的每一个值,都属于某一种数据类型。JavaScript 的数据类型,共有六种。(ES6 又新增了第七种 Symbol 类型的值,这里不涉及) 1.数值(number):整数和小数(比如1和3.14) 2.字符串(string):文本(比如Hello World)。 3.布尔值(boolean):表示真伪的两个特殊值,即true(真)和false(假) 4.undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值 5.null:表示空值,即此处的值为空。 6.对象(object):各种值组成的集合。 通常,数值、字符串、布尔值这三种类型,合称为原始类型(primitive type)的值,即它们是最基本的数据类型,不能再细分了。对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。至于undefined和null,一般将它们看成两个特殊值。 对象是最复杂的数据类型,又可以分成三个子类型。 1.狭义的对象(object) 2.数组(array) 3.函数(function) 狭义的对象和数组是两种不同的数据组合方式,除非特别声明,本文的“对象”都特指狭义的对象。函数其实是处理数据的方法,JavaScript 把它当成一种数据类型,可以赋值给变量,这为编程带来了很大的灵活性,也为 JavaScript 的“函数式编程”奠定了基础 typeof 运算符 JavaScript 有三种方法,可以确定一个值到底是什么类型 1.typeof运算符 2.instanceof运算符 3.Object.prototype.toString方法 typeof运算符可以返回一个值的数据类型 typeof 123 // "number" typeof '123' // "string" typeof false // "boolean" function f() {} typeof f // "function" typeof undefined // "undefined" 利用这一点,typeof可以用来检查一个没有声明的变量,而不报错 v // ReferenceError: v is not defined typeof v // "undefined" 上面代码中,变量v没有用var命令声明,直接使用就会报错。但是,放在typeof后面,就不报错了,而是返回undefined。实际编程中,这个特点通常用在判断语句 typeof window // "object" typeof {} // "object" typeof [] // "object" typeof null // "object" 上面代码中,空数组([])的类型也是object,这表示在 JavaScript 内部,数组本质上只是一种特殊的对象。这里顺便提一下,instanceof运算符可以区分数组和对象,下节 会说到 null的类型是object,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null,只把它当作object的一种特殊值。后来null独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null返回object就没法改变了 null, undefined 和布尔值 null 和 undefined 概述 null与undefined都可以表示“没有”,含义非常相似。将一个变量赋值为undefined或null,老实说,语法效果几乎没区别。在if语句中,null和undefined都会被自动转为false,相等运算符(==)甚至直接报告两者相等 既然含义与用法都差不多,为什么要同时设置两个这样的值,这不是无端增加复杂度,令初学者困扰吗?这与历史原因有关。 1995年 JavaScript 诞生时,最初像 Java 一样,只设置了null表示"无"。根据 C 语言的传统,null可以自动转为0。但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null自动转为0,很不容易发现错误。因此,他又设计了一个undefined。区别是这样的:null是一个表示“空”的对象,转为数值时为0;undefined是一个表示"此处无定义"的原始值,转为数值时为NaN Number(null) // 0 5 + null // 5 Number(undefined) // NaN 5 + undefined // NaN 用法和含义 对于null和undefined,大致可以像这样理解:null表示空值,即该处的值现在为空;调用函数时,某个参数未设置任何值,这时就可以传入null,表示该参数为空。比如,某个函数接受引擎抛出的错误作为参数,如果运行过程中未出错,那么这个参数就会传入null,表示未发生错误。undefined表示“未定义”,下面是返回undefined的典型场景 var i; // 变量声明了,但没有赋值 i // undefined function f(x) { // 调用函数时,应该提供的参数没有提供,该参数等于 undefined return x; } f() // undefined var o = new Object(); // 对象没有赋值的属性 o.p // undefined function f() {} // 函数没有返回值时,默认返回 undefined f() // undefined 布尔值 布尔值代表“真”和“假”两个状态。“真”用关键字true表示,“假”用关键字false表示。布尔值只有这两个值。 下列运算符会返回布尔值: 1.前置逻辑运算符: ! (Not) 2.相等运算符:===,!==,==,!= 3.比较运算符:>,>=,<,<= 如果JavaScript预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true undefined / null / false / 0 / NaN / "" 或 '' (空字符串) 注意,空数组([])和空对象({})对应的布尔值,都是true 数值 概述 整数和浮点数 JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。这就是说,JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)。容易造成混淆的是,某些运算只有整数才能完成,此时 JavaScript 会自动把64位浮点数,转成32位整数,然后再进行运算。由于浮点数不是精确的值,所以涉及小数的比较和运算要特别小心 数值精度 根据国际标准 IEEE 754,JavaScript 浮点数的64个二进制位,从最左边开始,是这样组成的。 第1位:符号位,0表示正数,1表示负数 第2位到第12位(共11位):指数部分 第13位到第64位(共52位):小数部分(即有效数字) 符号位决定了一个数的正负,指数部分决定了数值的大小,小数部分决定了数值的精度。 指数部分一共有11个二进制位,因此大小范围就是0到2047。IEEE 754 规定,如果指数部分的值在0到2047之间(不含两个端点),那么有效数字的第一位默认总是1,不保存在64位浮点数之中。也就是说,有效数字这时总是1.xx...xx的形式,其中xx..xx的部分保存在64位浮点数之中,最长可能为52位。因此,JavaScript 提供的有效数字最长为53个二进制位。 (-1)^符号位 * 1.xx...xx * 2^指数部分 上面公式是正常情况下(指数部分在0到2047之间),一个数在 JavaScript 内部实际的表示形式。精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-253到253,都可以精确表示 Math.pow(2, 53) // 9007199254740992 Math.pow(2, 53) + 1 // 9007199254740992 Math.pow(2, 53) + 2 // 9007199254740994 Math.pow(2, 53) + 3 // 9007199254740996 Math.pow(2, 53) + 4 // 9007199254740996 上面代码中,大于2的53次方后整数运算的结果开始出现错误。所以,大于2的53次方的数值都无法保持精度。由于2的53次方是一个16位的十进制数值,所以简单的法则就是:JavaScript 对15位的十进制数都可以精确处理 数值范围 根据标准,64位浮点数的指数部分长度是11个二进制位,意味着指数部分的最大值是2047(2的11次方减1)。也就是说,64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为21024到2-1023(开区间),超出这个范围的数无法表示。 如果一个数大于等于2的1024次方,那么就会发生“正向溢出”,即 JavaScript 无法表示这么大的数,这时就会返回Infinity Math.pow(2, 1024) // Infinity 如果一个数小于等于2的-1075次方(指数部分最小值-1023,再加上小数部分的52位),那么就会发生为“负向溢出”,即 JavaScript 无法表示这么小的数,这时会直接返回0 JavaScript 提供Number对象的MAX_VALUE和MIN_VALUE属性,返回可以表示的具体的最大值和最小值 Number.MAX_VALUE // 1.7976931348623157e+308 Number.MIN_VALUE // 5e-324 数值的表示法 JavaScript 的数值有多种表示方法,可以用字面形式直接表示,比如35(十进制)和0xFF(十六进制)。数值也可以采用科学计数法表示,下面是几个科学计数法的例子 123e3 // 123000 123e-3 // 0.123 -3.1E+12 .1e-23 科学计数法允许字母e或E的后面,跟着一个整数,表示这个数值的指数部分。以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示 1.小数点前的数字多于21位 1234567890123456789012 // 1.2345678901234568e+21 123456789012345678901 // 123456789012345680000 2.小数点后的零多于5个 // 小数点后紧跟5个以上的零,就自动转为科学计数法 0.0000003 // 3e-7 // 否则,就保持原来的字面形式 0.000003 // 0.000003 数值的进制 使用字面量(literal)直接表示一个数值时,JavaScript 对整数提供四种进制的表示方法:十进制、十六进制、八进制、二进制 十进制:没有前导0的数值 八进制:有前缀0o或0O的数值,或者有前导0、且只用到0-7的八个阿拉伯数字的数值 十六进制:有前缀0x或0X的数值 二进制:有前缀0b或0B的数值 默认情况下,JavaScript 内部会自动将八进制、十六进制、二进制转为十进制 通常来说,有前导0的数值会被视为八进制,但是如果前导0后面有数字8和9,则该数值被视为十进制,处理时很容易造成混乱。ES5 的严格模式和 ES6,已经废除了这种表示法,但是浏览器为了兼容以前的代码,目前还继续支持这种表示法 特殊数值 JavaScript 提供了几个特殊的数值 正零和负零 前面说过,JavaScript 的64位浮点数之中,有一个二进制位是符号位。这意味着,任何一个数都有一个对应的负值,就连0也不例外。JavaScript 内部实际上存在2个0:一个是+0,一个是-0,区别就是64位浮点数表示法的符号位不同。它们是等价的 几乎所有场合,正零和负零都会被当作正常的0。唯一有区别的场合是,+0或-0当作分母,返回的值是不相等的 (1 / +0) === (1 / -0) // false 上面的代码之所以出现这样结果,是因为除以正零得到+Infinity,除以负零得到-Infinity,这两者是不相等的 NaN 含义 NaN是 JavaScript 的特殊值,表示“非数字”(Not a Number),主要出现在将字符串解析成数字出错的场合 0 / 0 // NaN 需要注意的是,NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number,使用typeof运算符可以看得很清楚 运算规则 NaN不等于任何值,包括它本身 NaN === NaN // false 数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立 [NaN].indexOf(NaN) // -1 NaN在布尔运算时被当作false Boolean(NaN) // false NaN与任何数(包括它自己)的运算,得到的都是NaN Infinity 含义 Infinity表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity。由于数值正向溢出(overflow)、负向溢出(underflow)和被0除,JavaScript 都不报错,所以单纯的数学运算几乎没有可能抛出错误。Infinity大于一切数值(除了NaN),-Infinity小于一切数值(除了NaN)。Infinity与NaN比较,总是返回false Infinity > NaN // false -Infinity > NaN // false Infinity < NaN // false -Infinity < NaN // false 运算规则 Infinity的四则运算,符合无穷的数学计算规则。0乘以Infinity,返回NaN;0除以Infinity,返回0;Infinity除以0,返回Infinity 0 * Infinity // NaN 0 / Infinity // 0 Infinity / 0 // Infinity Infinity加上或乘以Infinity,返回的还是Infinity。Infinity减去或除以Infinity,得到NaN。Infinity与null计算时,null会转成0,等同于与0的计算。Infinity与undefined计算,返回的都是NaN 与数值相关的全局方法 parseInt() 基本用法 parseInt方法用于将字符串转为整数;如果字符串头部有空格,空格会被自动去除。如果parseInt的参数不是字符串,则会先转为字符串再转换。字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分 parseInt('8a') // 8 parseInt('12**') // 12 parseInt('12.34') // 12 parseInt('15e2') // 15 parseInt('15px') // 15 如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN。所以,parseInt的返回值只有两种可能,要么是一个十进制整数,要么是NaN。 如果字符串以0x或0X开头,parseInt会将其按照十六进制数解析;如果字符串以0开头,将其按照10进制解析;对于那些会自动转为科学计数法的数字,parseInt会将科学计数法的表示方法视为字符串,因此导致一些奇怪的结果 parseInt(1000000000000000000000.5) // 1 // 等同于 parseInt('1e+21') // 1 parseInt(0.0000008) // 8 // 等同于 parseInt('8e-7') // 8 进制转换 parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt的第二个参数为10,即默认是十进制转十进制 parseInt('1000') // 1000 // 等同于 parseInt('1000', 10) // 1000 parseInt('1000', 2) // 8 parseInt('1000', 6) // 216 parseInt('1000', 8) // 512 这意味着,可以用parseInt方法进行进制的转换 如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在2到36之间,才能得到有意义的结果,超出这个范围,则返回NaN。如果第二个参数是0、undefined和null,则直接忽略 parseInt('10', 37) // NaN parseInt('10', 1) // NaN parseInt('10', 0) // 10 parseInt('10', null) // 10 parseInt('10', undefined) // 10 如果字符串包含对于指定进制无意义的字符,则从最高位开始,只返回可以转换的数值。如果最高位无法转换,则直接返回NaN 前面说过,如果parseInt的第一个参数不是字符串,会被先转为字符串。这会导致一些令人意外的结果 parseInt(0x11, 36) // 43 parseInt(0x11, 2) // 1 // 等同于 parseInt(String(0x11), 36) parseInt(String(0x11), 2) // 等同于 parseInt('17', 36) parseInt('17', 2) 上面代码中,十六进制的0x11会被先转为十进制的17,再转为字符串。然后,再用36进制或二进制解读字符串17,最后返回结果43和1。这种处理方式,对于八进制的前缀0,尤其需要注意 parseInt(011, 2) // NaN // 等同于 parseInt(String(011), 2) // 等同于 parseInt(String(9), 2) 上面代码中,第一行的011会被先转为字符串9,因为9不是二进制的有效字符,所以返回NaN。如果直接计算parseInt('011', 2),011则是会被当作二进制处理,返回3。JavaScript 不再允许将带有前缀0的数字视为八进制数,而是要求忽略这个0。但是,为了保证兼容性,大部分浏览器并没有部署这一条规定 parseFloat() parseFloat方法用于将一个字符串转为浮点数。如果字符串符合科学计数法,则会进行相应的转换。如果字符串包含不能转为浮点数的字符,则不再进行往后转换,返回已经转好的部分 parseFloat('314e-2') // 3.14 parseFloat('0.0314E+2') // 3.14 parseFloat方法会自动过滤字符串前导的空格 parseFloat('\t\v\r12.34\n ') // 12.34 如果参数不是字符串,或者字符串的第一个字符不能转化为浮点数,则返回NaN parseFloat([]) // NaN parseFloat('FF2') // NaN parseFloat('') // NaN 尤其值得注意,parseFloat会将空字符串转为NaN。这些特点使得parseFloat的转换结果不同于Number函数 parseFloat(true) // NaN Number(true) // 1 parseFloat(null) // NaN Number(null) // 0 parseFloat('') // NaN Number('') // 0 parseFloat('123.45#') // 123.45 Number('123.45#') // NaN isNaN() isNaN方法可以用来判断一个值是否为NaN。但是isNaN只对数值有效,如果传入其他值,会被先转成数值。比如,传入字符串的时候,字符串会被先转成NaN,所以最后返回true,这一点要特别引起注意。也就是说,isNaN为true的值,有可能不是NaN,而是一个字符串。出于同样的原因,对于对象和数组,isNaN也返回true。但是,对于空数组和只有一个数值成员的数组,isNaN返回false isNaN([]) // false isNaN([123]) // false isNaN(['123']) // false 上面代码之所以返回false,原因是这些数组能被Number函数转成数值。因此,使用isNaN之前,最好判断一下数据类型 function myIsNaN(value) { return typeof value === 'number' && isNaN(value); } 判断NaN更可靠的方法是,利用NaN为唯一不等于自身的值的这个特点,进行判断 function myIsNaN(value) { return value !== value; } isFinite() isFinite方法返回一个布尔值,表示某个值是否为正常的数值 isFinite(Infinity) // false isFinite(-Infinity) // false isFinite(NaN) // false isFinite(undefined) // false isFinite(null) // true isFinite(-1) // true 除了Infinity、-Infinity、NaN和undefined这几个值会返回false,isFinite对于其他的数值都会返回true 字符串 概述 定义 字符串就是零个或多个排在一起的字符,放在单引号或双引号之中。单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号。如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。 由于 HTML 语言的属性值使用双引号,所以很多项目约定 JavaScript 语言的字符串只使用单引号。当然,只使用双引号也完全可以,重要的是坚持使用一种风格,不要一会使用单引号表示字符串,一会又使用双引号表示。字符串默认只能写在一行内,分成多行将会报错;如果长字符串必须分成多行,可以在每一行的尾部使用反斜杠 var longString = 'Long \ long \ long \ string'; longString // "Long long long string" 注意,反斜杠的后面必须是换行符,而不能有其他字符(比如空格),否则会报错。连接运算符(+)可以连接多个单行字符串,将长字符串拆成多行书写,输出的时候也是单行。 var longString = 'Long ' + 'long ' + 'long ' + 'string'; 如果想输出多行字符串,有一种利用多行注释的变通方法 (function () { /* line 1 line 2 line 3 */}).toString().split('\n').slice(1, -1).join('\n') // "line 1 // line 2 // line 3" 转义 反斜杠()在字符串内有特殊含义,用来表示一些特殊字符,所以又称为转义符。需要用反斜杠转义的特殊字符,主要有下面这些。 \0 :null(\u0000) \b :后退键(\u0008) \f :换页符(\u000C) \n :换行符(\u000A) \r :回车键(\u000D) \t :制表符(\u0009) \v :垂直制表符(\u000B) \' :单引号(\u0027) \" :双引号(\u0022) \\ :反斜杠(\u005C) 反斜杠还有三种特殊用法 HHH 反斜杠后面紧跟三个八进制数(000到377),代表一个字符。HHH对应该字符的 Unicode 码点,比如251表示版权符号。显然,这种方法只能输出256种字符 xHH x后面紧跟两个十六进制数(00到FF),代表一个字符。HH对应该字符的 Unicode 码点,比如xA9表示版权符号。这种方法也只能输出256种字符 uXXXX u后面紧跟四个十六进制数(0000到FFFF),代表一个字符。XXXX对应该字符的 Unicode 码点,比如u00A9表示版权符号 '\251' // "©" '\xA9' // "©" '\u00A9' // "©" '\172' === 'z' // true '\x7A' === 'z' // true '\u007A' === 'z' // true 如果在非特殊字符前面使用反斜杠,则反斜杠会被省略。如果字符串的正常内容之中,需要包含反斜杠,则反斜杠前面需要再加一个反斜杠,用来对自身转义 字符串与数组 字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined 'abc'[3] // undefined 'abc'[-1] // undefined 'abc'['x'] // undefined 但是,字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符 length 属性 length属性返回字符串的长度,该属性也是无法改变的 字符集 JavaScript 使用 Unicode 字符集。JavaScript 引擎内部,所有字符都用 Unicode 表示。JavaScript 不仅以 Unicode 储存字符,还允许直接在程序中使用 Unicode 码点表示字符,即将字符写成uxxxx的形式,其中xxxx代表该字符的 Unicode 码点。比如,u00A9代表版权符号。解析代码的时候,JavaScript 会自动识别一个字符是字面形式表示,还是 Unicode 形式表示。输出给用户的时候,所有字符都会转成字面形式 var f\u006F\u006F = 'abc'; f // "abc" 我们还需要知道,每个字符在 JavaScript 内部都是以16位(即2个字节)的 UTF-16 格式储存。也就是说,JavaScript 的单位字符长度固定为16位长度,即2个字节。但是,UTF-16 有两种长度:对于码点在U+0000到U+FFFF之间的字符,长度为16位(即2个字节);对于码点在U+10000到U+10FFFF之间的字符,长度为32位(即4个字节),而且前两个字节在0xD800到0xDBFF之间,后两个字节在0xDC00到0xDFFF之间。举例来说,码点U+1D306对应的字符为𝌆,它写成 UTF-16 就是0xD834 0xDF06。 JavaScript 对 UTF-16 的支持是不完整的,由于历史原因,只支持两字节的字符,不支持四字节的字符。这是因为 JavaScript 第一版发布的时候,Unicode 的码点只编到U+FFFF,因此两字节足够表示了。后来,Unicode 纳入的字符越来越多,出现了四字节的编码。但是,JavaScript 的标准此时已经定型了,统一将字符长度限制在两字节,导致无法识别四字节的字符。例如四字节字符𝌆,浏览器会正确识别这是一个字符,但是 JavaScript 无法识别,会认为这是两个字符 '𝌆'.length // 2 总结一下,对于码点在U+10000到U+10FFFF之间的字符,JavaScript 总是认为它们是两个字符(length属性为2)。所以处理的时候,必须把这一点考虑在内,也就是说,JavaScript 返回的字符串长度可能是不正确的 Base64 转码 有时,文本里面包含一些不可打印的符号,比如 ASCII 码0到31的符号都无法打印出来,这时可以使用 Base64 编码,将它们转成可以打印的字符。另外,有时需要以文本格式传递二进制数据,那么也可以使用 Base64 编码。所谓 Base64 就是一种编码方法,可以将任意值转成 0~9、A~Z、a-z、+和/这64个字符组成的可打印字符。使用它的主要目的,不是为了加密,而是为了不出现特殊字符,简化程序的处理。 JavaScript 原生提供两个 Base64 相关的方法: btoa():任意值转为 Base64 编码 atob():Base64 编码转为原来的值 var string = 'Hello World!'; btoa(string) // "SGVsbG8gV29ybGQh" atob('SGVsbG8gV29ybGQh') // "Hello World!" 注意,这两个方法不适合非 ASCII 码的字符,会报错。要将非 ASCII 码字符转为 Base64 编码,必须中间插入一个转码环节,再使用这两个方法 function b64Encode(str) { return btoa(encodeURIComponent(str)); } function b64Decode(str) { return decodeURIComponent(atob(str)); } b64Encode('你好') // "JUU0JUJEJUEwJUU1JUE1JUJE" b64Decode('JUU0JUJEJUEwJUU1JUE1JUJE') // "你好" 对象 概述 生成方法 对象(object)是 JavaScript 语言的核心概念,也是最重要的数据类型。什么是对象?简单说,对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合 键名 对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以 var obj = { foo: 'Hello', bar: 'World' }; 等同于 var obj = { 'foo': 'Hello', 'bar': 'World' }; 如果键名是数值,会被自动转为字符串。如果键名不符合标识名的条件(比如第一个字符为数字,或者含有空格或运算符),且也不是数字,则必须加上引号,否则会报错 var obj = { // 报错 1p: 'Hello World' }; var obj = { // 不报错 '1p': 'Hello World', 'h w': 'Hello World', 'p+q': 'Hello World' }; 上面对象的三个键名,都不符合标识名的条件,所以必须加上引号。对象的每一个键名又称为“属性”(property),它的“键值”可以是任何数据类型。如果一个属性的值为函数,通常把这个属性称为“方法”,它可以像函数那样调用 var obj = { p: function (x) { return 2 * x; } }; obj.p(1) // 2 如果属性的值还是一个对象,就形成了链式引用 var o1 = {}; var o2 = { bar: 'hello' }; o1.foo = o2; o1.foo.bar // "hello" 属性可以动态创建,不必在对象声明时就指定 对象的引用 如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量;如果取消某一个变量对于原对象的引用,不会影响到另一个变量 var x = 1; var y = x; x = 2; y // 1 上面的代码中,当x的值发生变化后,y的值并不变,这就表示y和x并不是指向同一个内存地址 表达式还是语句 对象采用大括号表示,这导致了一个问题:如果行首是一个大括号,它到底是表达式还是语句? { foo: 123 } JavaScript 引擎读到上面这行代码,会发现可能有两种含义。第一种:这是一个表达式,表示一个包含foo属性的对象;第二种:这是一个语句,表示一个代码区块,里面有一个标签foo,指向表达式123。为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块 如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象 ({ foo: 123 }) // 正确 ({ console.log(123) }) // 报错 这种差异在eval语句(作用是对字符串求值)中反映得最明显 eval('{foo: 123}') // 123 eval('({foo: 123})') // {foo: 123} 上面代码中,如果没有圆括号,eval将其理解为一个代码块;加上圆括号以后,就理解成一个对象 属性的操作 属性的读取 读取对象的属性,有两种方法,一种是使用点运算符,还有一种是使用方括号运算符 var obj = { p: 'Hello World' }; obj.p // "Hello World" obj['p'] // "Hello World" 请注意,如果使用方括号运算符,键名必须放在引号里面,否则会被当作变量处理 var foo = 'bar'; var obj = { foo: 1, bar: 2 }; obj.foo // 1 obj[foo] // 2 上面代码中,引用对象obj的foo属性时,如果使用点运算符,foo就是字符串;如果使用方括号运算符但不使用引号,foo就是一个变量,指向字符串bar 方括号运算符内部还可以使用表达式 obj['hello' + ' world'] obj[3 + 3] 数字键可以不加引号,因为会自动转成字符串 var obj = { 0.7: 'Hello World' }; obj['0.7'] // "Hello World" obj[0.7] // "Hello World" 注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符 属性的赋值 点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。JavaScript 允许属性的“后绑定”,也就是说,你可以在任意时刻新增属性,没必要在定义对象的时候,就定义好属性 var obj = { p: 1 }; // 等价于 var obj = {}; obj.p = 1; 属性的查看 查看一个对象本身的所有属性,可以使用Object.keys方法 var obj = { key1: 1, key2: 2 }; Object.keys(obj); // ['key1', 'key2'] 属性的删除:delete 命令 delete命令用于删除对象的属性,删除成功后返回true var obj = { p: 1 }; Object.keys(obj) // ["p"] delete obj.p // true obj.p // undefined Object.keys(obj) // [] 注意,删除一个不存在的属性,delete不报错,而且返回true。因此不能根据delete命令的结果,认定某个属性是存在的。只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除 var obj = Object.defineProperty({}, 'p', { //Object.defineProperty()方法会直接在一个对象上定义一个新属性,或修改一个对象的现有属性,并返回这个对象。默认使用 Object.defineProperty() 添加的属性值是不可修改的 value: 123, configurable: false //表示能否通过delete删除属性从而重新定义属性 }); obj.p // 123 delete obj.p // false 注意:delete命令只能删除对象本身的属性,无法删除继承的属性 var obj = {}; delete obj.toString // true obj.toString // function toString() { [native code] } 上面代码中,toString是对象obj继承的属性,虽然delete命令返回true,但该属性并没有被删除,依然存在。这个例子还说明,即使delete返回true,该属性依然可能读取到值 属性是否存在:in 运算符 in运算符用于检查对象是否包含某个属性(注意:检查的是键名,不是键值),如果包含就返回true,否则返回false。它的左边是一个字符串,表示属性名,右边是一个对象 var obj = { p: 1 }; 'p' in obj // true 'toString' in obj // true in运算符的一个问题是,它不能识别哪些属性是对象自身的,哪些属性是继承的。就像上面代码中,对象obj本身并没有toString属性,但是in运算符会返回true,因为这个属性是继承的。 这时,可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性 var obj = {}; if ('toString' in obj) { console.log(obj.hasOwnProperty('toString')) // false } 属性的遍历:for...in 循环 for...in循环用来遍历一个对象的全部属性 var obj = {a: 1, b: 2, c: 3}; for (var i in obj) { console.log('键名:', i); console.log('键值:', obj[i]); } // 键名: a // 键值: 1 // 键名: b // 键值: 2 // 键名: c // 键值: 3 for...in循环有两个使用注意点: 1.它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性 2.它仅遍历对象自身的属性,会忽略掉那些从原型链上继承到的属性 举例来说,对象都继承了toString属性,但是for...in循环不会遍历到这个属性 var obj = {}; // toString 属性是存在的 obj.toString // toString() { [native code] } for (var p in obj) { console.log(p); } // 没有任何输出 如果继承的属性是可遍历的,那么就会被for...in循环遍历到。但一般情况下都是只想遍历对象自身的属性,所以使用for...in的时候,应该结合使用hasOwnProperty方法,在循环内部判断一下,某个属性是否为对象自身的属性 var person = { name: '老张' }; for (var key in person) { if (person.hasOwnProperty(key)) { console.log(key); } } // name with 语句 with语句的格式如下: with (对象) { 语句; } 它的作用是操作同一个对象的多个属性时,提供一些书写的方便 // 例一 var obj = { p1: 1, p2: 2, }; with (obj) { p1 = 4; p2 = 5; } // 等同于 obj.p1 = 4; obj.p2 = 5; // 例二 with (document.links[0]){ console.log(href); console.log(title); console.log(style); } // 等同于 console.log(document.links[0].href); console.log(document.links[0].title); console.log(document.links[0].style); 注意:如果with区块内部有变量的赋值操作,必须是当前对象已经存在的属性,否则会创造一个当前作用域的全局变量 var obj = {}; with (obj) { p1 = 4; p2 = 5; } obj.p1 // undefined p1 // 4 上面代码中,对象obj并没有p1属性,对p1赋值等于创造了一个全局变量p1。正确的写法应该是,先定义对象obj的属性p1,然后在with区块内操作它 with区块没有改变作用域,它的内部依然是当前作用域。这造成了with语句的一个很大的弊病,就是绑定对象不明确。可以考虑用一个临时变量代替with with(obj1.obj2.obj3) { console.log(p1 + p2); } // 可以写成 var temp = obj1.obj2.obj3; console.log(temp.p1 + temp.p2); 函数 函数是一段可以反复调用的代码块。函数还能接受输入的参数,不同的参数会返回不同的值 概述 函数的声明 JavaScript 有三种声明函数的方法 function 命令 function命令声明的代码区块,就是一个函数。function命令后面是函数名,函数名后面是一对圆括号,里面是传入函数的参数。函数体放在大括号里面 function print(s) { console.log(s); } 上面的代码命名了一个print函数,以后使用print()这种形式,就可以调用相应的代码。这叫做函数的声明(Function Declaration) 函数表达式 除了用function命令声明函数,还可以采用变量赋值的写法 var print = function(s) { console.log(s); }; 这种写法将一个匿名函数赋值给变量。这时,这个匿名函数又称函数表达式(Function Expression),因为赋值语句的等号右侧只能放表达式。采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效 var print = function x(){ console.log(typeof x); }; x // ReferenceError: x is not defined print() // function 上面代码在函数表达式中加入了函数名x。这个x只在函数体内部可用,指代函数表达式本身,其他地方都不可用。这种写法的用处有两个,一是可以在函数体内部调用自身,二是方便除错(除错工具显示函数调用栈时,将显示函数名,而不再显示这里是一个匿名函数) 注意:函数的表达式需要在语句结尾加上分号表示语句结束。而函数的声明在结尾的大括号后面不用加分号。总的来说,这两种声明函数的方式,差别很细微,可以近似认为是等价的 Function 构造函数 第三种声明函数的方式是Function构造函数 var add = new Function( 'x', 'y', 'return x + y' ); // 等同于 function add(x, y) { return x + y; } 总的来说,这种声明函数的方式非常不直观,几乎无人使用 函数的重复声明 如果同一个函数被多次声明,后面的声明就会覆盖前面的声明 function f() { console.log(1); } f() // 2 function f() { console.log(2); } f() // 2 上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升,前一次声明在任何时候都是无效的 圆括号运算符,return 语句和递归 调用函数时,要使用圆括号运算符。圆括号之中,可以加入函数的参数 function add(x, y) { return x + y; } add(1, 1) // 2 函数体内部的return语句表示返回。JavaScript 引擎遇到return语句就直接返回return后面的那个表达式的值,不再执行后面语句。return语句不是必需的,如果没有的话,该函数就不返回任何值,或者说返回undefined。 函数可以调用自身,这就是递归(recursion)。下面就是通过递归,计算斐波那契数列的代码 function fib(num) { if (num === 0) return 0; if (num === 1) return 1; return fib(num - 2) + fib(num - 1); } fib(6) // 8 第一等公民 JavaScript 语言将函数看作一种值,与其它值(数值、字符串、布尔值等等)地位相同。凡是可以使用值的地方,就能使用函数。比如,可以把函数赋值给变量和对象的属性,也可以当作参数传入其他函数,或者作为函数的结果返回。函数只是一个可以执行的值,此外并无特殊之处。由于函数与其他数据类型地位平等,所以在 JavaScript 语言中又称函数为第一等公民 function add(x, y) { return x + y; } var operator = add; // 将函数赋值给一个变量 function a(op){ // 将函数作为参数和返回值 return op; } a(add)(1, 1) // 2 函数名的提升 JavaScript 引擎将函数名视同变量名,所以采用function命令声明函数时,整个函数会像变量声明一样,被提升到代码头部 f(); function f() {} 表面上,上面代码好像在声明之前就调用了函数f。但是实际上,由于“变量提升”,函数f被提升到了代码头部,也就是在调用之前已经声明了。但是,如果采用赋值语句定义函数,JavaScript 就会报错 f(); var f = function (){}; // TypeError: undefined is not a function 等同于 var f; f(); f = function () {}; 上面代码调用f的时候,f只是被声明了,还没有被赋值,等于undefined,所以会报错。因此,如果同时采用function命令和赋值语句声明同一个函数,最后总是采用赋值语句的定义 var f = function () { console.log('1'); } function f() { console.log('2'); } f() // 1 函数的属性和方法 name 属性 函数的name属性返回函数的名字 function f1() {} f1.name // "f1" 如果是通过变量赋值定义的函数,那么name属性返回变量名 var f2 = function () {}; f2.name // "f2" 但是,上面这种情况,只有在变量的值是一个匿名函数时才是如此。如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名 var f3 = function myName() {}; f3.name // 'myName' 上面代码中,f3.name返回函数表达式的名字。注意,真正的函数名还是f3,而myName这个名字只在函数体内部可用。name属性的一个用处,就是获取参数函数的名字 var myFunc = function () {}; function test(f) { console.log(f.name); } test(myFunc) // myFunc length 属性 函数的length属性返回函数预期传入的参数个数,即函数定义之中的参数个数 function f(a, b) {} f.length // 2 上面代码定义了空函数f,它的length属性就是定义时的参数个数。不管调用时输入了多少个参数,length属性始终等于2。length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload) toString() 函数的toString方法返回一个字符串,内容是函数的源码 function f() { /* 这是一个 多行注释 */ a(); b();; } f.toString() // function f() { // /* // 这是一个 // 多行注释 // */ // a(); // b(); // } 函数内部的注释也可以返回。利用这点可以变相实现多行字符串 var multiline = function (fn) { var arr = fn.toString().split('\n'); return arr.slice(1, arr.length - 1).join('\n'); }; function f() {/* 这是一个 多行注释 */} multiline(f); // " 这是一个 // 多行注释" 函数作用域 定义 作用域(scope)指变量存在的范围。在 ES5 的规范中,JavaScript 只有两种作用域:一种是全局作用域,变量在整个程序中一直存在,所有地方都可以读取;另一种是函数作用域,变量只在函数内部存在。ES6 又新增了块级作用域,这里不涉及。对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。函数内部定义的变量外部无法读取,称为“局部变量”(local variable)。函数内部定义的变量,会在该作用域内覆盖同名全局变量 var v = 1; function f(){ var v = 2; console.log(v); } f() // 2 v // 1 函数内部的变量提升 与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部 函数本身的作用域 函数本身也是一个值,也有自己的作用域。它的作用域与变量一样,就是其声明时所在的作用域,与其运行时所在的作用域无关 var a = 1; var x = function () { console.log(a); }; function f() { var a = 2; x(); } f() // 1 上面代码中,函数x是在函数f的外部声明的,所以它的作用域绑定外层,内部变量a不会到函数f体内取值,所以输出1,而不是2。总之,函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域。很容易犯错的一点是,如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量 同样的,函数体内部声明的函数,作用域绑定函数体内部。正是这种机制构成了“闭包”现象 function foo() { var x = 1; function bar() { console.log(x); } return bar; } var x = 2; var f = foo(); f() // 1 参数 概述 函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数 参数的省略 函数参数不是必需的,JavaScript 允许省略参数 function f(a, b) { return a; } f(1, 2, 3) // 1 f(1) // 1 f() // undefined f.length // 2 JavaScript中省略的参数值就变为undefined。但是,没办法只省略靠前的参数保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined function f(a, b) { return a; } //如果省略第一个参数,就会报错 f( , 1) // SyntaxError: Unexpected token ,(…) f(undefined, 1) // undefined 传递方式 函数参数如果是原始类型的值(数值、字符串、布尔值),传递方式是传值传递(passes by value)。这意味着,在函数体内修改参数值,不会影响到函数外部。但是,如果函数参数是复合类型的值(数组、对象、其他函数),传递方式是传址传递(pass by reference)。也就是说,传入函数的原始值的地址,因此在函数内部修改参数,将会影响到原始值 var obj = { p: 1 }; function f(o) { o.p = 2; } f(obj); obj.p // 2 上面代码中,传入函数f的是参数对象obj的地址。因此,在函数内部修改obj的属性p,会影响到原始值。但是,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值 var obj = [1, 2, 3]; function f(o) { o = [2, 3, 4]; } f(obj); obj // [1, 2, 3] 同名参数 如果有同名的参数,则取最后出现的那个值 function f(a, a) { console.log(a); } f(1) // undefined 上面代码中,函数f有两个参数,且参数名都是a。取值的时候,以后面的a为准,即使后面的a没有值或被省略,也是以其为准。这时,如果要获得第一个a的值,可以使用arguments对象 function f(a, a) { console.log(arguments[0]); } f(1) // 1 arguments 对象 定义 由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来。arguments对象包含了函数运行时的所有参数,arguments[0]就是第一个参数,arguments[1]就是第二个参数,以此类推。这个对象只有在函数体内部,才可以使用 var f = function (one) { console.log(arguments[0]); console.log(arguments[1]); console.log(arguments[2]); } f(1, 2, 3) // 1 // 2 // 3 正常模式下,arguments对象可以在运行时修改 var f = function(a, b) { arguments[0] = 3; arguments[1] = 2; return a + b; } f(1, 1) // 5 上面代码中,函数f调用时传入的参数,在函数内部被修改成3和2。严格模式下,arguments对象与函数参数不具有联动关系。也就是说:严格模式下修改arguments对象不会影响到实际的函数参数 通过arguments对象的length属性,可以判断函数调用时到底带几个参数 function f() { return arguments.length; } f(1, 2, 3) // 3 f(1) // 1 f() // 0 与数组的关系 虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用。如果要让arguments对象使用数组方法,真正的解决方法是将arguments转为真正的数组。下面是两种常用的转换方法:slice方法和逐一填入新数组 var args = Array.prototype.slice.call(arguments); // 或者 var args = []; for (var i = 0; i < arguments.length; i++) { args.push(arguments[i]); } callee 属性 arguments对象带有一个callee属性,返回它所对应的原函数 var f = function () { console.log(arguments.callee === f); } f() // true 可以通过arguments.callee,达到调用函数自身的目的。这个属性在严格模式里面是禁用的 函数的其他知识点 闭包 闭包(closure)是 JavaScript 语言的一个难点,也是它的特色,很多高级应用都要依靠闭包实现。理解闭包,首先必须理解变量作用域。前面提到,JavaScript 有两种作用域:全局作用域和函数作用域。函数内部可以直接读取全局变量。但是函数外部无法读取函数内部声明的变量。如果出于种种原因,需要得到函数内的局部变量。正常情况下,这是办不到的,只有通过变通方法才能实现。那就是在函数的内部,再定义一个函数 function f1() { var n = 999; function f2() { console.log(n); } return f2; } var result = f1(); result(); // 999 上面代码中,函数f1的返回值就是函数f2,由于f2可以读取f1的内部变量,所以就可以在外部获得f1的内部变量了。 闭包就是函数f2,即能够读取其他函数内部变量的函数。由于在 JavaScript 语言中,只有函数内部的子函数才能读取内部变量,因此可以把闭包简单理解成“定义在一个函数内部的函数”。闭包最大的特点,就是它可以“记住”诞生的环境,比如f2记住了它诞生的环境f1,所以从f2可以得到f1的内部变量。在本质上,闭包就是将函数内部和函数外部连接起来的一座桥梁。闭包的最大用处有两个,一个是可以读取函数内部的变量,另一个就是让这些变量始终保持在内存中,即闭包可以使得它诞生环境一直存在 闭包的另一个用处,是封装对象的私有属性和私有方法 function Person(name) { var _age; function setAge(n) { _age = n; } function getAge() { return _age; } return { name: name, getAge: getAge, setAge: setAge }; } var p1 = Person('张三'); p1.setAge(25); p1.getAge() // 25 上面代码中,函数Person的内部变量_age,通过闭包getAge和setAge,变成了返回对象p1的私有变量。 注意:外层函数每次运行,都会生成一个新的闭包,而这个闭包又会保留外层函数的内部变量,所以内存消耗很大。因此不能滥用闭包,否则会造成网页的性能问题 立即调用的函数表达式(IIFE) 在 JavaScript 中,圆括号()是一种运算符,跟在函数名之后,表示调用该函数。比如,print()就表示调用print函数 function这个关键字即可以当作语句,也可以当作表达式。为了避免解析上的歧义,JavaScript 引擎规定,如果function关键字出现在行首,一律解释成语句 // 语句 function f() {} // 表达式 var f = function f() {} 当我们需要在定义函数之后立即调用该函数时,不能只是在函数的定义之后加圆括号。解决方法就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面 (function(){ /* code */ }()); // 或者 (function(){ /* code */ })(); 上面两种写法都是以圆括号开头,引擎就会认为后面跟的是一个表示式,而不是函数定义语句,所以就避免了错误。这就叫做“立即调用的函数表达式”(Immediately-Invoked Function Expression),简称 IIFE。注意,上面两种写法最后的分号都是必须的。如果省略分号,遇到连着两个 IIFE,JavaScript 会将它们连在一起解释,将第二行解释为第一行的参数,从而报错 推而广之,任何让解释器以表达式来处理函数定义的方法,都能产生同样的效果,比如下面写法 var i = function(){ return 10; }(); true && function(){ /* code */ }(); 0, function(){ /* code */ }(); !function () { /* code */ }(); ~function () { /* code */ }(); -function () { /* code */ }(); +function () { /* code */ }(); 通常情况下,只对匿名函数使用这种“立即执行的函数表达式”。它的目的有两个:一是不必为函数命名,避免了污染全局变量;二是 IIFE 内部形成了一个单独的作用域,可以封装一些外部无法读取的私有变量 // 写法一 var tmp = newData; processData(tmp); storeData(tmp); // 写法二 (function () { var tmp = newData; processData(tmp); storeData(tmp); }()); 上面代码中,写法二比写法一更好,因为完全避免了污染全局变量 eval 命令 基本用法 eval命令接受一个字符串作为参数,并将这个字符串当作语句执行 eval('var a = 1;'); a // 1 上面代码将字符串当作语句运行,生成了变量a。如果参数字符串无法当作语句运行,那么就会报错 eval('3x') // Uncaught SyntaxError: Invalid or unexpected token 放在eval中的字符串,应该有独自存在的意义,不能用来与eval以外的命令配合使用。举例来说,下面的代码将会报错 eval('return;'); // Uncaught SyntaxError: Illegal return statement 上面代码会报错,因为return不能单独使用,必须在函数中使用。如果eval的参数不是字符串,那么会原样返回 eval(123) // 123 eval没有自己的作用域,都在当前作用域内执行,因此可能会修改当前作用域的变量的值,造成安全问题 var a = 1; eval('a = 2'); a // 2 上面代码中,eval命令修改了外部变量a的值,因此eval有安全风险。为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域 (function f() { 'use strict'; eval('var foo = 123'); console.log(foo); // ReferenceError: foo is not defined })() 上面代码中,函数f内部是严格模式,这时eval内部声明的foo变量,就不会影响到外部。不过,即使在严格模式下,eval依然可以读写当前作用域的变量 (function f() { 'use strict'; var foo = 1; eval('foo = 2'); console.log(foo); // 2 })() 上面代码中,严格模式下,eval内部还是改写了外部变量,可见安全风险依然存在。总之,eval的本质是在当前作用域中注入代码。由于安全风险和不利于 JavaScript 引擎优化执行速度,所以一般不推荐使用。通常情况下,eval最常见的场合是解析 JSON 数据的字符串,不过正确的做法应该是使用原生的JSON.parse方法 eval 的别名调用 前面说过eval不利于引擎优化执行速度。更麻烦的是,还有下面这种情况,引擎在静态代码分析的阶段,根本无法分辨执行的是eval var m = eval; m('var x = 1'); x // 1 上面代码中,变量m是eval的别名。静态代码分析阶段,引擎分辨不出m('var x = 1')执行的是eval命令。为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一律是全局作用域 var a = 1; function f() { var a = 2; var e = eval; e('console.log(a)'); } f() // 1 上面代码中,eval是别名调用,所以即使它是在函数中,它的作用域还是全局作用域,因此输出的a为全局变量。这样的话,引擎就能确认e()不会对当前的函数作用域产生影响,优化的时候就可以把这一行排除掉。eval的别名调用的形式五花八门,只要不是直接调用,都属于别名调用,因为引擎只能分辨eval()这一种形式是直接调用 eval.call(null, '...') window.eval('...') (1, eval)('...') (eval, eval)('...') 上面这些形式都是eval的别名调用,作用域都是全局作用域 数组 定义 数组(array)是按次序排列的一组值。每个值的位置都有编号(从0开始),整个数组用方括号表示 var arr = ['a', 'b', 'c']; 除了在定义时赋值,数组也可以先定义后赋值;任何类型的数据都可以放入数组;如果数组的元素还是数组,就形成了多维数组 数组的本质 本质上,数组属于一种特殊的对象。typeof运算符会返回数组的类型是object;数组的特殊性体现在,它的键名是按次序排列的一组整数(0,1,2...) var arr = ['a', 'b', 'c']; Object.keys(arr) // ["0", "1", "2"] 上面代码中,Object.keys方法返回数组的所有键名。由于数组成员的键名是固定的(默认总是0、1、2...),因此数组不用为每个元素指定键名,而对象的每个成员都必须指定键名。JavaScript 语言规定,对象的键名一律为字符串,所以,数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串。这点在赋值时也成立,一个值总是先转成字符串再进行赋值 var arr = ['a', 'b', 'c']; arr['0'] // 'a' arr[0] // 'a' 对象有两种读取成员的方法:点结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构 length 属性 数组的length属性,返回数组的成员数量。JavaScript 使用一个32位整数保存数组的元素个数,这意味着数组成员最多只有4294967295个(232 - 1)个,也就是说length属性的最大值就是4294967295。只要是数组,就一定有length属性。该属性是一个动态值,等于键名中的最大整数加上1 var arr = ['a', 'b']; arr.length // 2 arr[2] = 'c'; arr.length // 3 arr[9] = 'd'; arr.length // 10 arr[1000] = 'e'; arr.length // 1001 上面代码表示:数组的数字键不需要连续,length属性的值总是比最大的那个整数键大1。另外,这也表明数组是一种动态的数据结构,可以随时增减数组的成员,length属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员会自动减少到length设置的值 var arr = [ 'a', 'b', 'c' ]; arr.length // 3 arr.length = 2; arr // ["a", "b"] 清空数组的一个有效方法,就是将length属性设为0。如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位,,读取新增的位置都会返回undefined。如果人为设置length为不合法的值JavaScript 会报错 // 设置负值 [].length = -1 // RangeError: Invalid array length // 数组元素个数大于等于2的32次方 [].length = Math.pow(2, 32) // RangeError: Invalid array length // 设置字符串 [].length = 'abc' // RangeError: Invalid array length 值得注意的是,由于数组本质上是一种对象,所以可以为数组添加属性,但是这不影响length属性的值 var a = []; a['p'] = 'abc'; a.length // 0 a[2.1] = 'abc'; a.length // 0 上面代码将数组的键分别设为字符串和小数,结果都不影响length属性。因为,length属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。如果数组的键名是添加超出范围的数值,该键名会自动转为字符串 var arr = []; arr[-1] = 'a'; arr[Math.pow(2, 32)] = 'b'; arr.length // 0 arr[-1] // "a" arr[4294967296] // "b" 上面代码中为数组arr添加了两个不合法的数字键,结果length属性没有发生变化,这些数字键都变成了字符串键名。最后两行之所以会取到值,是因为取键值时,数字键名会默认转为字符串 in 运算符 检查某个键名是否存在的运算符in,适用于对象,也适用于数组 var arr = [ 'a', 'b', 'c' ]; 2 in arr // true '2' in arr // true 4 in arr // false 上面代码表明,数组存在键名为2的键;由于键名都是字符串,所以数值2会自动转成字符串。注意:如果数组的某个位置是空位,in运算符返回false var arr = []; arr[100] = 'a'; 100 in arr // true 1 in arr // false for...in 循环和数组的遍历 for...in循环不仅可以遍历对象,也可以遍历数组,毕竟数组只是一种特殊对象;for...in不仅会遍历数组所有的数字键,还会遍历非数字键;所以不推荐使用for...in遍历数组 var a = [1, 2, 3]; a.foo = true; for (var key in a) { console.log(key); } // 0 // 1 // 2 // foo 数组的遍历可以考虑使用for循环或while循环 var a = [1, 2, 3]; // for循环 for(var i = 0; i < a.length; i++) { console.log(a[i]); } // while循环 var i = 0; while (i < a.length) { console.log(a[i]); i++; } var l = a.length; while (l--) { //逆向遍历 console.log(a[l]); } 数组的forEach方法,也可以用来遍历数组 var colors = ['red', 'green', 'blue']; colors.forEach(function (color) { console.log(color); }); // red // green // blue 数组的空位 当数组的某个位置是空元素,即两个逗号之间没有任何值,我们称该数组存在空位(hole)。数组的空位不影响length属性。需要注意的是,如果最后一个元素后面有逗号,并不会产生空位。也就是说,有没有这个逗号,结果都是一样的 var a = [1, 2, 3,]; a.length // 3 a // [1, 2, 3] 数组的空位是可以读取的,返回undefined;使用delete命令删除一个数组成员会形成空位,且不会影响length属性,也就是说length属性不过滤空位 数组的某个位置是空位,与某个位置是undefined,是不一样的。如果是空位,使用数组的forEach方法、for...in结构、以及Object.keys方法进行遍历,空位都会被跳过。如果某个位置是undefined,遍历的时候就不会被跳过 var a = [, , ,]; a.forEach(function (x, i) { console.log(i + '. ' + x); }) // 0. undefined for (var i in a) { console.log(i); } // 0 Object.keys(a) // ["0"] 这就是说,空位就是数组没有这个元素,所以不会被遍历到,而undefined则表示数组有这个元素,值是undefined,所以遍历不会跳过 类似数组的对象 如果一个对象的所有键名都是正整数或零,并且有length属性,那么这个对象就很像数组,语法上称为“类似数组的对象”(array-like object) var obj = { 0: 'a', 1: 'b', 2: 'c', length: 3 }; obj[0] // 'a' obj[1] // 'b' obj.length // 3 obj.push('d') // TypeError: obj.push is not a function 对象obj就是一个类似数组的对象。但是,“类似数组的对象”并不是数组,因为它们不具备数组特有的方法。对象obj没有数组的push方法,使用该方法就会报错。“类似数组的对象”的根本特征,就是具有length属性。只要有length属性,就可以认为这个对象类似于数组。但是有一个问题,这种length属性不是动态值,不会随着成员的变化而变化 典型的“类似数组的对象”是函数的arguments对象,以及大多数 DOM 元素集,还有字符串 // arguments对象 function args() { return arguments } var arrayLike = args('a', 'b'); arrayLike[0] // 'a' arrayLike.length // 2 arrayLike instanceof Array // false // DOM元素集 var elts = document.getElementsByTagName('h3'); elts.length // 3 elts instanceof Array // false // 字符串 'abc'[1] // 'b' 'abc'.length // 3 'abc' instanceof Array // false 数组的slice方法可以将“类似数组的对象”变成真正的数组 var arr = Array.prototype.slice.call(arrayLike); 除了转为真正的数组,“类似数组的对象”还有一个办法可以使用数组的方法,就是通过call()把数组的方法放到对象上面 function print(value, index) { console.log(index + ' : ' + value); } Array.prototype.forEach.call(arrayLike, print); 上面代码中,arrayLike代表一个类似数组的对象,本来是不可以使用数组的forEach()方法的,但是通过call(),可以把forEach()嫁接到arrayLike上面调用。下面的例子就是通过这种方法,在arguments对象上面调用forEach方法 // forEach 方法 function logArgs() { Array.prototype.forEach.call(arguments, function (elem, i) { console.log(i + '. ' + elem); }); } // 等同于 for 循环 function logArgs() { for (var i = 0; i < arguments.length; i++) { console.log(i + '. ' + arguments[i]); } } 字符串也是类似数组的对象,所以也可以用Array.prototype.forEach.call遍历 注意,这种方法比直接使用数组原生的forEach要慢,所以最好还是先将“类似数组的对象”转为真正的数组,然后再直接调用数组的forEach方法 运算符 算术运算符 运算符是处理数据的基本方法,用来从现有的值得到新的值。JavaScript 提供了多种运算符,覆盖了所有主要的运算 概述 JavaScript 共提供10个算术运算符,用来完成基本的算术运算。 加法运算符:x + y 减法运算符: x - y 乘法运算符: x * y 除法运算符:x / y 指数运算符:x ** y 余数运算符:x % y 自增运算符:++x 或者 x++ 自减运算符:--x 或者 x-- 数值运算符: +x 负数值运算符:-x 减法、乘法、除法运算法比较单纯,就是执行相应的数学运算。下面介绍其他几个算术运算符,重点是加法运算符 加法运算符 基本规则 加法运算符(+)是最常见的运算符,用来求两个数值的和。JavaScript 允许非数值的相加 true + true // 2 1 + true // 2 上面代码中,第一行是两个布尔值相加,第二行是数值与布尔值相加。这两种情况,布尔值都会自动转成数值,然后再相加。比较特殊的是,如果是两个字符串相加,这时加法运算符会变成连接运算符,返回一个新的字符串,将两个原字符串连接在一起。如果一个运算子是字符串,另一个运算子是非字符串,这时非字符串会转成字符串,再连接在一起 加法运算符是在运行时决定,到底是执行相加还是执行连接。也就是说,运算值的不同导致了不同的语法行为,这种现象称为“重载”(overload) '3' + 4 + 5 // "345" 3 + 4 + '5' // "75" 上面代码中,由于从左到右的运算次序,字符串的位置不同会导致不同的结果 除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算值一律转为数值,再进行相应的数学运算 对象的相加 如果运算值是对象,必须先转成原始类型的值,然后再相加 var obj = { p: 1 }; obj + 2 // "[object Object]2" 上面代码中,对象obj转成原始类型的值是[object Object],再加2就得到了上面的结果 对象转成原始类型的值,规则如下 首先,自动调用对象的valueOf方法 var obj = { p: 1 }; obj.valueOf() // { p: 1 } 一般来说,对象的valueOf方法总是返回对象自身,这时再自动调用对象的toString方法,将其转为字符串 var obj = { p: 1 }; obj.valueOf().toString() // "[object Object]" 对象的toString方法默认返回[object Object],所以就得到了最前面那个例子的结果。知道了这个规则以后,就可以自己定义valueOf方法或toString方法,得到想要的结果 var obj = { valueOf: function () { return 1; } }; obj + 2 // 3 由于valueOf方法直接返回一个原始类型的值,所以不再调用toString方法。下面是自定义toString方法的例子 var obj = { toString: function () { return 'hello'; } }; obj + 2 // "hello2" 如果运算值是一个Date对象的实例,那么会优先执行toString方法 var obj = new Date(); obj.valueOf = function () { return 1 }; obj.toString = function () { return 'hello' }; obj + 2 // "hello2" 上面代码中,对象obj是一个Date对象的实例,并且自定义了valueOf方法和toString方法,结果toString方法优先执行 余数运算符 余数运算符(%)返回前一个运算值被后一个运算值除,所得的余数。需要注意的是,运算结果的正负号由第一个运算子的正负号决定 -1 % 2 // -1 12 % 5 // 2 所以,为了得到负数的正确余数值,可以先使用绝对值函数 // 错误的写法 function isOdd(n) { return n % 2 === 1; } isOdd(-5) // false isOdd(-4) // false // 正确的写法 function isOdd(n) { return Math.abs(n % 2) === 1; } isOdd(-5) // true isOdd(-4) // false 余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果 自增和自减运算符 自增和自减运算符,是一元运算符,只需要一个运算值。它们的作用是将运算值首先转为数值,然后加上1或者减去1。它们会修改原始变量。运算之后,变量的值发生变化,这种效应叫做运算的副作用(side effect)。自增和自减运算符是仅有的两个具有副作用的运算符,其他运算符都不会改变变量的值 自增和自减运算符有一个需要注意的地方,就是放在变量之后,会先返回变量操作前的值,再进行自增/自减操作;放在变量之前,会先进行自增/自减操作,再返回变量操作后的值 var x = 1; var y = 1; x++ // 1 ++y // 2 数值运算符,负数值运算符 数值运算符(+)同样使用加号,但它是一元运算符(只需要一个操作数),而加法运算符是二元运算符(需要两个操作数)。数值运算符的作用在于可以将任何值转为数值(与Number函数的作用相同) +true // 1 +[] // 0 +{} // NaN 负数值运算符(-),也同样具有将一个值转为数值的功能,只不过得到的值正负相反。连用两个负数值运算符,等同于数值运算符 var x = 1; -x // -1 -(-x) // 1 上面代码最后一行的圆括号不可少,否则会变成自减运算符。数值运算符号和负数值运算符,都会返回一个新的值,而不会改变原始变量的值 指数运算符 指数运算符(**)完成指数运算,前一个运算子是底数,后一个运算子是指数 注意,指数运算符是右结合,而不是左结合。即多个指数运算符连用时,先进行最右边的计算 // 相当于 2 ** (3 ** 2) 2 ** 3 ** 2 // 512 赋值运算符 赋值运算符(Assignment Operators)用于给变量赋值。最常见的赋值运算符,当然就是等号(=)。赋值运算符还可以与其他运算符结合,形成变体。下面是与算术运算符的结合 x += y // 等同于 x = x + y x -= y // 等同于 x = x - y x *= y // 等同于 x = x * y x /= y // 等同于 x = x / y x %= y // 等同于 x = x % y x **= y // 等同于 x = x ** y 下面是与位运算符的结合 x >>= y // 等同于 x = x >> y x <<= y // 等同于 x = x << y x >>>= y // 等同于 x = x >>> y x &= y // 等同于 x = x & y x |= y // 等同于 x = x | y x ^= y // 等同于 x = x ^ y 这些复合的赋值运算符,都是先进行指定运算,然后将得到值返回给左边的变量 比较运算符 概述 比较运算符用于比较两个值的大小,然后返回一个布尔值,表示是否满足指定的条件。 注意:比较运算符可以比较各种类型的值,不仅仅是数值 JavaScript 一共提供了8个比较运算符 > 大于运算符 < 小于运算符 <= 小于或等于运算符 >= 大于或等于运算符 == 相等运算符 === 严格相等运算符 != 不相等运算符 !== 严格不相等运算符 这八个比较运算符分成两类:相等比较和非相等比较。两者的规则是不一样的,对于非相等的比较,算法是先看两个运算值是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算值都转成数值,再比较数值的大小 非相等运算符:字符串的比较 字符串按照字典顺序进行比较 'cat' > 'dog' // false 'cat' > 'catalog' // false 'cat' > 'Cat' // true' '大' > '小' // false JavaScript 引擎内部首先比较首字符的 Unicode 码点。如果相等,再比较第二个字符的 Unicode 码点,以此类推。上面代码中,小写的c的 Unicode 码点(99)大于大写的C的 Unicode 码点(67),所以返回true。由于所有字符都有 Unicode 码点,因此汉字也可以比较 非相等运算符:非字符串的比较 如果两个运算值之中,至少有一个不是字符串,需要分成以下两种情况 原始类型值 如果两个运算值都是原始类型的值,则是先转成数值再比较 5 > '4' // true // 等同于 5 > Number('4') // 即 5 > 4 true > false // true // 等同于 Number(true) > Number(false) // 即 1 > 0 2 > true // true // 等同于 2 > Number(true) // 即 2 > 1 这里注意,任何值(包括NaN本身)与NaN比较,返回的都是false 对象 如果运算值是对象,会转为原始类型的值,再进行比较。对象转换成原始类型的值,算法是先调用valueOf方法;如果返回的还是对象,再接着调用toString方法(前面有讲到) var x = [2]; x > '11' // true // 等同于 [2].valueOf().toString() > '11' // 即 '2' > '11' x.valueOf = function () { return '1' }; x > '11' // false // 等同于 [2].valueOf() > '11' // 即 '1' > '11' [2] > [1] // true // 等同于 [2].valueOf().toString() > [1].valueOf().toString() // 即 '2' > '1' [2] > [11] // true // 等同于 [2].valueOf().toString() > [11].valueOf().toString() // 即 '2' > '11' { x: 2 } >= { x: 1 } // true // 等同于 { x: 2 }.valueOf().toString() >= { x: 1 }.valueOf().toString() // 即 '[object Object]' >= '[object Object]' 严格相等运算符 JavaScript 提供两种相等运算符:==和===。简单说,它们的区别是相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。如果两个值不是同一类型,严格相等运算符(===)直接返回false,而相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。 不同类型的值 如果两个值的类型不同,直接返回false 同一类的原始类型值 同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true,值不同就返回false 注意:NaN与任何值都不相等(包括自身);另外,正0等于负0 复合类型值 两个复合类型(对象、数组、函数)的数据比较时,不是比较它们的值是否相等,而是比较它们是否指向同一个地址 {} === {} // false [] === [] // false (function () {} === function () {}) // false 上面代码分别比较两个空对象、两个空数组、两个空函数,结果都是不相等。原因是对于复合类型的值,严格相等运算比较的是,它们是否引用同一个内存地址,而运算符两边的空对象、空数组、空函数的值,都存放在不同的内存地址,结果当然是false。如果两个变量引用同一个对象,则它们相等 var v1 = {}; var v2 = v1; v1 === v2 // true 注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值 var obj1 = {}; var obj2 = {}; obj1 > obj2 // false obj1 < obj2 // false obj1 === obj2 // false undefined 和 null undefined和null与自身严格相等。由于变量声明后默认值是undefined,因此两个只声明未赋值的变量是相等的 undefined === undefined // true null === null // true var v1; var v2; v1 === v2 // true 严格不相等运算符 严格相等运算符有一个对应的“严格不相等运算符”(!==),它的算法就是先求严格相等运算符的结果,然后返回相反值 1 !== '1' // true // 等同于 !(1 === '1') 相等运算符 相等运算符用来比较相同类型的数据时,与严格相等运算符完全一样 1 == 1.0 // 等同于 1 === 1.0 比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。下面分成四种情况,讨论不同类型的值互相比较的规则 原始类型值 原始类型的值会转换成数值再进行比较 1 == true // true // 等同于 1 === Number(true) 0 == false // true // 等同于 0 === Number(false) 2 == true // false // 等同于 2 === Number(true) 2 == false // false // 等同于 2 === Number(false) 'true' == true // false // 等同于 Number('true') === Number(true) // 等同于 NaN === 1 '' == 0 // true // 等同于 Number('') === 0 // 等同于 0 === 0 '' == false // true // 等同于 Number('') === Number(false) // 等同于 0 === 0 '1' == true // true // 等同于 Number('1') === Number(true) // 等同于 1 === 1 '\n 123 \t' == 123 // true // 因为字符串转为数字时,省略前置和后置的空格 对象与原始类型值比较 对象(这里指广义的对象,包括数组和函数)与原始类型的值比较时,对象转换成原始类型的值,再进行比较 // 对象与数值比较时,对象转为数值 [1] == 1 // true // 等同于 Number([1]) == 1 // 对象与字符串比较时,对象转为字符串 [1] == '1' // true // 等同于 String([1]) == '1' [1, 2] == '1,2' // true // 等同于 String([1, 2]) == '1,2' // 对象与布尔值比较时,两边都转为数值 [1] == true // true // 等同于 Number([1]) == Number(true) [2] == true // false // 等同于 Number([2]) == Number(true) undefined 和 null undefined和null与其他类型的值比较时,结果都为false,它们互相比较时结果为true false == null // false false == undefined // false 0 == null // false 0 == undefined // false undefined == null // true 相等运算符的缺点 相等运算符隐藏的类型转换,会带来一些违反直觉的结果 0 == '' // true 0 == '0' // true 2 == true // false 2 == false // false false == 'false' // false false == '0' // true false == undefined // false false == null // false null == undefined // true ' \t\r\n ' == 0 // true 不相等运算符 相等运算符有一个对应的“不相等运算符”(!=),它的算法就是先求相等运算符的结果,然后返回相反值 布尔运算符 概述 布尔运算符用于将表达式转为布尔值,一共包含四个运算符。 取反运算符:! 且运算符:&& 或运算符:|| 三元运算符:?: 取反运算符(!) 取反运算符是一个感叹号,用于将布尔值变为相反值,即true变成false,false变成true 对于非布尔值,取反运算符会将其转为布尔值。可以这样记忆,以下六个值取反后为true,其他值都为false。 undefined null false 0 NaN 空字符串('') 如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同。这是一种常用的类型转换的写法 !!x // 等同于 Boolean(x) 两次取反就是将一个值转为布尔值的简便写法 且运算符(&&) 且运算符(&&)往往用于多个表达式的求值。它的运算规则是:如果第一个运算值为true,则返回第二个运算值(注意是值,不是布尔值);如果第一个运算值为false,则直接返回第一个运算值,且不再对第二个运算值求值 't' && '' // "" '' && 'f' // "" '' && '' // "" var x = 1; (1 - 1) && ( x += 1) // 0 x // 1 上面代码的最后一个例子,由于且运算符的第一个运算布尔值为false,则直接返回它的值0,而不再对第二个运算值求值,所以变量x的值没变。这种跳过第二个运算值的机制,被称为“短路”。有些程序员喜欢用它取代if结构,比如下面是一段if结构的代码,就可以用且运算符改写 if (i) { doSomething(); } // 等价于 i && doSomething(); 上面代码的两种写法是等价的,但是后一种不容易看出目的,也不容易除错,建议谨慎使用。且运算符可以多个连用,这时返回第一个布尔值为false的表达式的值。如果所有表达式的布尔值都为true,则返回最后一个表达式的值 或运算符(||) 或运算符(||)也用于多个表达式的求值。它的运算规则是:如果第一个运算值的布尔值为true,则返回第一个运算值,且不再对第二个运算值求值;如果第一个运算值的布尔值为false,则返回第二个运算值 't' || '' // "t" 't' || 'f' // "t" '' || 'f' // "f" '' || '' // "" 短路规则对这个运算符也适用 var x = 1; true || (x = 2) // true x // 1 上面代码中,且运算符的第一个运算值为true,所以直接返回true,不再运行第二个运算值。所以,x的值没有改变。这种只通过第一个表达式的值,控制是否运行第二个表达式的机制,就称为“短路”(short-cut)。或运算符可以多个连用,这时返回第一个布尔值为true的表达式的值。如果所有表达式都为false,则返回最后一个表达式的值 三元条件运算符(?:) 三元条件运算符由问号(?)和冒号(:)组成,分隔三个表达式。它是 JavaScript 语言唯一一个需要三个运算值的运算符。如果第一个表达式的布尔值为true,则返回第二个表达式的值,否则返回第三个表达式的值 通常来说,三元条件表达式与if...else语句具有同样表达效果,前者可以表达的,后者也能表达。但是两者具有一个重大差别,if...else是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用if..else console.log(true ? 'T' : 'F'); 上面代码中,console.log方法的参数必须是一个表达式,这时就只能使用三元条件表达式。如果要用if...else语句,就必须改变整个代码写法了 二进制位运算符 概述 二进制位运算符用于直接对二进制位进行计算,一共有7个 二进制或运算符(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1 二进制与运算符(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0 二进制否运算符(not):符号为~,表示对一个二进制位取反 异或运算符(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0 左移运算符(left shift):符号为<< 右移运算符(right shift):符号为>> 带符号位的右移运算符(zero filled right shift):符号为>>> 这些位运算符直接处理每一个比特位(bit),所以是非常底层的运算,好处是速度极快,缺点是很不直观,许多场合不能使用它们,否则会使代码难以理解和查错。 有一点需要特别注意,位运算符只对整数起作用,如果一个运算值不是整数,会自动转为整数后再执行。另外,虽然在 JavaScript 内部,数值都是以64位浮点数的形式储存,但是做位运算的时候,是以32位带符号的整数进行运算的,并且返回值也是一个32位带符号的整数 i = i | 0; 上面这行代码的意思,就是将i(不管是整数或小数)转为32位整数。利用这个特性,可以写出一个函数,将任意数值转为32位整数 function toInt32(x) { return x | 0; } 上面代码中,toInt32可以将小数转为整数。对于一般的整数,返回值不会有任何变化。对于大于或等于2的32次方的整数,大于32位的数位都会被舍去 二进制或运算符 二进制或运算符(|)逐位比较两个运算子,两个二进制位之中只要有一个为1,就返回1,否则返回0 0 | 3 // 3 上面代码中,0和3的二进制形式分别是00和11,所以进行二进制或运算会得到11(即3)。位运算只对整数有效,遇到小数时,会将小数部分舍去,只保留整数部分。所以,将一个小数与0进行二进制或运算,等同于对该数去除小数部分,即取整数位 2.9 | 0 // 2 -2.9 | 0 // -2 需要注意的是,这种取整方法不适用超过32位整数最大值2147483647的数 2147483649.4 | 0; // -2147483647 二进制与运算符 二进制与运算符(&)的规则是逐位比较两个运算子,两个二进制位之中只要有一个位为0,就返回0,否则返回1 二进制否运算符 二进制否运算符(~)将每个二进制位都变为相反值(0变为1,1变为0)。它的返回结果有时比较难理解,因为涉及到计算机内部的数值表示机制 ~ 3 // -4 上面表达式对3进行二进制否运算,得到-4。之所以会有这样的结果,是因为位运算时JavaScript 内部将所有的运算值都转为32位的二进制整数再进行运算。3的32位整数形式是00000000000000000000000000000011,二进制否运算以后得到11111111111111111111111111111100。由于第一位(符号位)是1,所以这个数是一个负数。JavaScript 内部采用补码形式表示负数,即需要将这个数减去1,再取一次反,然后加上负号,才能得到这个负数对应的10进制值。这个数减去1等于11111111111111111111111111111011,再取一次反得到00000000000000000000000000000100,再加上负号就是-4。考虑到这样的过程比较麻烦,可以简单记忆成,一个数与自身的取反值相加,等于-1 对一个整数连续两次二进制否运算,得到它自身。所有的位运算都只对整数有效。二进制否运算遇到小数时,也会将小数部分舍去,只保留整数部分。所以,对一个小数连续进行两次二进制否运算,能达到取整效果 ~~2.9 // 2 ~~47.11 // 47 ~~3 // 3 使用二进制否运算取整,是所有取整方法中最快的一种。对字符串进行二进制否运算,JavaScript 引擎会先调用Number函数,将字符串转为数值 对于其他类型的值,二进制否运算也是先用Number转为数值,然后再进行处理 // 相当于~Number('011') ~'011' // -12 // 相当于~Number('42 cats') ~'42 cats' // -1 // 相当于~Number('0xcafebabe') ~'0xcafebabe' // 889275713 // 相当于~Number('deadbeef') ~'deadbeef' // -1 // 相当于 ~Number([]) ~[] // -1 // 相当于 ~Number(NaN) ~NaN // -1 // 相当于 ~Number(null) ~null // -1 异或运算符 异或运算(^)在两个二进制位不同时返回1,相同时返回0 0 ^ 3 // 3 上面表达式中,0(二进制00)与3(二进制11)进行异或运算,它们每一个二进制位都不同,所以得到11(即3)。“异或运算”有一个特殊运用,连续对两个数a和b进行三次异或运算,a^=b; b^=a; a^=b;,可以互换它们的值。这意味着,使用“异或运算”可以在不引入临时变量的前提下,互换两个变量的值 var a = 10; var b = 99; a ^= b, b ^= a, a ^= b; a // 99 b // 10 这是互换两个变量的值的最快方法。异或运算也可以用来取整 12.9 ^ 0 // 12 左移运算符 左移运算符(<<)表示将一个数的二进制值向左移动指定的位数,尾部补0,即乘以2的指定次方 // 4 的二进制形式为100,左移一位为1000(即十进制的8),相当于乘以2的1次方 4 << 1 // 8 -4 << 1 // -8 如果左移0位,就相当于将该数值转为32位整数,等同于取整,对于正数和负数都有效 13.5 << 0 // 13 -13.5 << 0 // -13 左移运算符用于二进制数值非常方便 var color = {r: 186, g: 218, b: 85}; // RGB to HEX (1 << 24)的作用为保证结果是6位数 var rgb2hex = function(r, g, b) { return '#' + ((1 << 24) + (r << 16) + (g << 8) + b) .toString(16) // 先转成十六进制,然后返回字符串 .substr(1); // 去除字符串的最高位,返回后面六个字符串 } rgb2hex(color.r, color.g, color.b) // "#bada55" 上面代码使用左移运算符,将颜色的 RGB 值转为 HEX 值 右移运算符 右移运算符(>>)表示将一个数的二进制值向右移动指定的位数,头部补0,即除以2的指定次方(最高位即符号位不参与移动) 4 >> 1 // 2 // 因为4的二进制形式为 00000000000000000000000000000100,右移一位得到 00000000000000000000000000000010,即为十进制的2 -4 >> 1 // -2 // 因为-4的二进制形式为 11111111111111111111111111111100,右移一位,头部补1,得到 11111111111111111111111111111110,即为十进制的-2 右移运算可以模拟 2 的整除运算 5 >> 1 // 2 // 相当于 5 / 2 = 2 21 >> 2 // 5 // 相当于 21 / 4 = 5 21 >> 3 // 2 // 相当于 21 / 8 = 2 21 >> 4 // 1 // 相当于 21 / 16 = 1 带符号位的右移运算符 带符号位的右移运算符(>>>)表示将一个数的二进制形式向右移动,包括符号位也参与移动,头部补0。所以,该运算总是得到正值。对于正数,该运算的结果与右移运算符(>>)完全一致,区别主要在于负数 4 >>> 1 // 2 -4 >>> 1 // 2147483646 // 因为-4的二进制形式为11111111111111111111111111111100,带符号位的右移一位,得到01111111111111111111111111111110, 即为十进制的2147483646。 这个运算实际上将一个值转为32位无符号整数。查看一个负整数在计算机内部的储存形式,最快的方法就是使用这个运算符 -1 >>> 0 // 4294967295 上面代码表示,-1作为32位整数时,内部的储存形式使用无符号整数格式解读,值为 4294967295(即(2^32)-1,等于11111111111111111111111111111111) 开关作用 位运算符可以用作设置对象属性的开关。假定某个对象有四个开关,每个开关都是一个变量。那么,可以设置一个四位的二进制数,它的每个位对应一个开关 var FLAG_A = 1; // 0001 var FLAG_B = 2; // 0010 var FLAG_C = 4; // 0100 var FLAG_D = 8; // 1000 上面代码设置 A、B、C、D 四个开关,每个开关分别占有一个二进制位。然后,就可以用二进制与运算检验,当前设置是否打开了指定开关 var flags = 5; // 二进制的0101 if (flags & FLAG_C) { // ... } // 0101 & 0100 => 0100 => true 上面代码检验是否打开了开关C,如果打开会返回true,否则返回false。现在假设需要打开A、B、D三个开关,我们可以构造一个掩码变量 var mask = FLAG_A | FLAG_B | FLAG_D; // 0001 | 0010 | 1000 => 1011 上面代码对A、B、D三个变量进行二进制或运算,得到掩码值为二进制的1011。有了掩码,二进制或运算可以确保打开指定的开关 flags = flags | mask; 二进制与运算可以将当前设置中凡是与开关设置不一样的项,全部关闭 flags = flags & mask; 异或运算可以切换(toggle)当前设置,即第一次执行可以得到当前设置的相反值,再执行一次又得到原来的值 flags = flags ^ mask; 二进制否运算可以翻转当前设置,即原设置为0,运算后变为1;原设置为1,运算后变为0 flags = ~flags; 其他运算符,运算顺序 void 运算符 void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined void 0 // undefined void(0) // undefined 上面是void运算符的两种写法,都正确。建议采用后一种形式,即总是使用圆括号。因为void运算符的优先性很高,如果不使用括号,容易造成错误的结果。比如,void 4 + 7实际上等同于(void 4) + 7。 这个运算符的主要用途是浏览器的书签工具(Bookmarklet),以及在超级链接中插入代码防止网页跳转 <script> function f() { console.log('Hello World'); } </script> <a href="http://example.com" onclick="f(); return false;">点击</a> 上面代码中,点击链接后,会先执行onclick的代码,由于onclick返回false,所以浏览器不会跳转到 example.com。void运算符可以取代上面的写法 <a href="javascript: void(f())">文字</a> 下面是一个更实际的例子,用户点击链接提交表单,但是不产生页面跳转 <a href="javascript: void(document.form.submit())"> 提交 </a> 逗号运算符 逗号运算符用于对两个表达式求值,并返回后一个表达式的值 'a', 'b' // "b" var x = 0; var y = (x++, 10); x // 1 y // 10 逗号运算符的一个用途是,在返回一个值之前,进行一些辅助操作 var value = (console.log('Hi!'), true); // Hi! value // true 上面代码中,先执行逗号之前的操作,然后返回逗号后面的值 运算顺序 优先级 JavaScript 各种运算符的优先级别(Operator Precedence)是不一样的。优先级高的运算符先执行,优先级低的运算符后执行 var x = 1; var arr = []; var y = arr.length <= 0 || arr[0] === undefined ? x : arr[0]; 上面代码中,变量y的值就很难看出来,因为这个表达式涉及5个运算符,到底谁的优先级最高,实在不容易记住。根据语言规格,这五个运算符的优先级从高到低依次为:小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)。因此上面的表达式,实际的运算顺序如下 var y = ((arr.length <= 0) || (arr[0] === undefined)) ? x : arr[0]; 圆括号的作用 圆括号(())可以用来提高运算的优先级,因为它的优先级是最高的,即圆括号中的表达式会第一个运算 注意,因为圆括号不是运算符,而是一种语法结构,所以不具有求值作用,只改变运算的优先级 函数放在圆括号中,会返回函数本身。如果圆括号紧跟在函数的后面,就表示调用函数 function f() { return 1; } (f) // function f(){return 1;} f() // 1 圆括号之中,只能放置表达式,如果将语句放在圆括号之中,就会报错 (var a = 1) // SyntaxError: Unexpected token var 左结合与右结合 对于优先级别相同的运算符,大多数情况,计算顺序总是从左到右,这叫做运算符的“左结合”(left-to-right associativity),即从左边开始计算;但是少数运算符的计算顺序是从右到左,即从右边开始计算,这叫做运算符的“右结合”(right-to-left associativity)。其中,最主要的是赋值运算符(=)和三元条件运算符(?:);指数运算符(**)也是右结合的 语法专题 数据类型的转换 概述 JavaScript 是一种动态类型语言,变量没有类型限制,可以随时赋予任意值 var x = y ? 1 : 'a'; 上面代码中,变量x到底是数值还是字符串,取决于另一个变量y的值。y为true时,x是一个数值;y为false时,x是一个字符串。这意味着,x的类型没法在编译阶段就知道,必须等到运行时才能知道。虽然变量的数据类型是不确定的,但是各种运算符对数据类型是有要求的。如果运算符发现,运算值的类型与预期不符,就会自动转换类型。比如,减法运算符预期左右两侧的运算值应该是数值,如果不是,就会自动将它们转为数值 '4' - '3' // 1 强制转换 强制转换主要指使用Number()、String()和Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值 Number() 使用Number函数,可以将任意类型的值转化成数值。下面分成两种情况讨论,一种是参数是原始类型的值,另一种是参数是对象 原始类型值 // 数值:转换后还是原来的值 Number(324) // 324 // 字符串:如果可以被解析为数值,则转换为相应的数值 Number('324') // 324 // 字符串:如果不可以被解析为数值,返回 NaN Number('324abc') // NaN // 空字符串转为0 Number('') // 0 // 布尔值:true 转成 1,false 转成 0 Number(true) // 1 Number(false) // 0 // undefined:转成 NaN Number(undefined) // NaN // null:转成0 Number(null) // 0 Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN。另外,parseInt和Number函数都会自动过滤一个字符串前导和后缀的空格 parseInt('\t\v\r12.34\n') // 12 Number('\t\v\r12.34\n') // 12.34 对象 简单的规则是,Number方法的参数是对象时,将返回NaN,除非是包含单个数值的数组 Number({a: 1}) // NaN Number([1, 2, 3]) // NaN Number([5]) // 5 之所以会这样,是因为Number背后的转换规则比较复杂。 第一步,调用对象自身的valueOf方法。如果返回原始类型的值,则直接对该值使用Number函数,不再进行后续步骤 第二步,如果valueOf方法返回的还是对象,则改为调用对象自身的toString方法。如果toString方法返回原始类型的值,则对该值使用Number函数,不再进行后续步骤 第三步,如果toString方法返回的是对象,就报错 var obj = {x: 1}; Number(obj) // NaN // 等同于 if (typeof obj.valueOf() === 'object') { Number(obj.toString()); } else { Number(obj.valueOf()); } 上面代码中,Number函数将obj对象转为数值。背后发生了一连串的操作,首先调用obj.valueOf方法, 结果返回对象本身;于是,继续调用obj.toString方法,这时返回字符串[object Object],对这个字符串使用Number函数,得到NaN。默认情况下,对象的valueOf方法返回对象本身,所以一般总是会调用toString方法,而toString方法返回对象的类型字符串(比如[object Object])。所以,会有下面的结果 Number({}) // NaN 如果toString方法返回的不是原始类型的值,结果就会报错。valueOf和toString方法,都是可以自定义的 Number({ valueOf: function () { return 2; } }) // 2 Number({ toString: function () { return 3; } }) // 3 Number({ valueOf: function () { return 2; }, toString: function () { return 3; } }) // 2 valueOf方法先于toString方法执行 String() String函数可以将任意类型的值转化成字符串,转换规则如下 原始类型值 数值:转为相应的字符串 字符串:转换后还是原来的值 布尔值:true转为字符串"true",false转为字符串"false" undefined:转为字符串"undefined" null:转为字符串"null" 对象 String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式 String({a: 1}) // "[object Object]" String([1, 2, 3]) // "1,2,3" String方法背后的转换规则,与Number方法基本相同,只是互换了valueOf方法和toString方法的执行顺序 1.先调用对象自身的toString方法。如果返回原始类型的值,则对该值使用String函数,不再进行以下步骤。 2.如果toString方法返回的是对象,再调用原对象的valueOf方法。如果valueOf方法返回原始类型的值,则对该值使用String函数,不再进行以下步骤。 3.如果valueOf方法返回的是对象,就报错 String({a: 1}) // "[object Object]" // 等同于 String({a: 1}.toString()) // "[object Object]" 如果toString法和valueOf方法,返回的都是对象,就会报错 var obj = { valueOf: function () { return {}; }, toString: function () { return {}; } }; String(obj) // TypeError: Cannot convert object to primitive value toString方法先于valueOf方法执行 String({ toString: function () { return 3; } }) // "3" String({ valueOf: function () { return 2; } }) // "[object Object]" String({ valueOf: function () { return 2; }, toString: function () { return 3; } }) // "3" Boolean() Boolean函数可以将任意类型的值转为布尔值。它的转换规则相对简单:除了以下五个值的转换结果为false,其他的值全部为true Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean(NaN) // false Boolean('') // false 注意:所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true Boolean({}) // true Boolean([]) // true Boolean(new Boolean(false)) // true 自动转换 自动转换是以强制转换为基础的。遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见 1.不同类型的数据互相运算 123 + 'abc' // "123abc" 2.对非布尔值类型的数据求布尔值 if ('abc') { console.log('hello') } // "hello" 3.对非数值类型的值使用一元运算符(即+和-) + {foo: 'bar'} // NaN - [1, 2, 3] // NaN 自动转换的规则是这样的:预期什么类型的值,就调用该类型的转换函数。比如,某个位置预期为字符串,就调用String函数进行转换。如果该位置即可以是字符串,也可能是数值,那么默认转为数值。由于自动转换具有不确定性,而且不易除错,建议在预期为布尔值、数值、字符串的地方,全部使用Boolean、Number和String函数进行显式转换 自动转换为布尔值 JavaScript 遇到预期为布尔值的地方(比如if语句的条件部分),就会将非布尔值的参数自动转换为布尔值。系统内部会自动调用Boolean函数。因此除了以下五个值,其他都是自动转为true undefined null +0或-0 NaN ''(空字符串) 下面两种写法,有时也用于将一个表达式转为布尔值。它们内部调用的也是Boolean函数 // 写法一 expression ? true : false // 写法二 !! expression 自动转换为字符串 JavaScript 遇到预期为字符串的地方,就会将非字符串的值自动转为字符串。具体规则是,先将复合类型的值转为原始类型的值,再将原始类型的值转为字符串。字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串 '5' + 1 // '51' '5' + true // "5true" '5' + false // "5false" '5' + {} // "5[object Object]" '5' + [] // "5" '5' + function (){} // "5function (){}" '5' + undefined // "5undefined" '5' + null // "5null" 这种自动转换很容易出错 自动转换为数值 JavaScript 遇到预期为数值的地方,就会将参数值自动转换为数值。系统内部会自动调用Number函数。除了加法运算符(+)有可能把运算值转为字符串,其他运算符都会把运算值自动转成数值 '5' - '2' // 3 '5' * '2' // 10 true - 1 // 0 false - 1 // -1 '1' - 1 // 0 '5' * [] // 0 false / '5' // 0 'abc' - 1 // NaN null + 1 // 1 undefined + 1 // NaN 注意:null转为数值时为0,而undefined转为数值时为NaN 一元运算符也会把运算子转成数值 +'abc' // NaN -'abc' // NaN +true // 1 -false // 0 错误处理机制 Error 实例对象 JavaScript 解析或运行时,一旦发生错误,引擎就会抛出一个错误对象。JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例 var err = new Error('出错了'); err.message // "出错了" 上面代码中,我们调用Error构造函数,生成一个实例对象err。Error构造函数接受一个参数,表示错误提示,可以从实例的message属性读到这个参数。抛出Error实例对象以后,整个程序就中断在发生错误的地方,不再往下执行。JavaScript 语言标准只提到,Error实例对象必须有message属性,表示出错时的提示信息,没有提到其他属性。大多数 JavaScript 引擎,对Error实例还提供name和stack属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有 message:错误提示信息 name:错误名称(非标准属性) stack:错误的堆栈(非标准属性) 使用name和message这两个属性,可以对发生什么错误有一个大概的了解 if (error.name) { console.log(error.name + ': ' + error.message); } stack属性用来查看错误发生时的堆栈 function throwit() { throw new Error(''); } function catchit() { try { throwit(); } catch(e) { console.log(e.stack); // print stack trace } } catchit() // Error // at throwit (~/examples/throwcatch.js:9:11) // at catchit (~/examples/throwcatch.js:3:9) // at repl:1:5 上面代码中,错误堆栈的最内层是throwit函数,然后是catchit函数,最后是函数的运行环境 原生错误类型 Error实例对象是最一般的错误类型,在它的基础上,JavaScript 还定义了其他6种错误对象。也就是说,存在Error的6个派生对象 SyntaxError 对象 SyntaxError对象是解析代码时发生的语法错误 // 变量名错误 var 1a; // Uncaught SyntaxError: Invalid or unexpected token // 缺少括号 console.log 'hello'); // Uncaught SyntaxError: Unexpected string 上面代码的错误,都是在语法解析阶段就可以发现,所以会抛出SyntaxError。第一个错误提示是“token 非法”,第二个错误提示是“字符串不符合要求” ReferenceError 对象 ReferenceError对象是引用一个不存在的变量时发生的错误 // 使用一个不存在的变量 unknownVariable // Uncaught ReferenceError: unknownVariable is not defined 另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果或者this赋值 // 等号左侧不是变量 console.log() = 1 // Uncaught ReferenceError: Invalid left-hand side in assignment // this 对象不能手动赋值 this = 1 // ReferenceError: Invalid left-hand side in assignment RangeError 对象 RangeError对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值 // 数组长度不得为负数 new Array(-1) // Uncaught RangeError: Invalid array length TypeError 对象 TypeError对象是变量或参数不是预期类型时发生的错误。比如,对字符串、布尔值、数值等原始类型的值使用new命令,就会抛出这种错误,因为new命令的参数应该是一个构造函数 new 123 // Uncaught TypeError: number is not a func var obj = {}; obj.unknownMethod() // Uncaught TypeError: obj.unknownMethod is not a function 上面代码的第二种情况,调用对象不存在的方法,也会抛出TypeError错误,因为obj.unknownMethod的值是undefined,而不是一个函数 URIError 对象 URIError对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数 decodeURI('%2') // URIError: URI malformed EvalError 对象 eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留 总结 以上这6种派生错误,连同原始的Error对象,都是构造函数。开发者可以使用它们,手动生成错误对象的实例。这些构造函数都接受一个参数,代表错误提示信息(message) var err1 = new Error('出错了!'); var err2 = new RangeError('出错了,变量超出有效范围!'); var err3 = new TypeError('出错了,变量类型无效!'); err1.message // "出错了!" err2.message // "出错了,变量超出有效范围!" err3.message // "出错了,变量类型无效!" 自定义错误 除了 JavaScript 原生提供的七种错误对象,还可以定义自己的错误对象 function UserError(message) { this.message = message || '默认信息'; this.name = 'UserError'; } UserError.prototype = new Error(); UserError.prototype.constructor = UserError; 上面代码自定义一个错误对象UserError,让它继承Error对象。然后,就可以生成这种自定义类型的错误了 new UserError('这是自定义的错误!'); throw 语句 throw语句的作用是手动中断程序执行,抛出一个错误 if (x <= 0) { throw new Error('x 必须为正数'); } // Uncaught ReferenceError: x is not defined 上面代码中,如果变量x小于等于0,就手动抛出一个错误,告诉用户x的值不正确,整个程序就会在这里中断执行。可以看到,throw抛出的错误就是它的参数,这里是一个Error实例。throw也可以抛出自定义错误 function UserError(message) { this.message = message || '默认信息'; this.name = 'UserError'; } throw new UserError('出错了!'); // Uncaught UserError {message: "出错了!", name: "UserError"} 实际上,throw可以抛出任何类型的值。也就是说,它的参数可以是任何值 // 抛出一个字符串 throw 'Error!'; // Uncaught Error! // 抛出一个数值 throw 42; // Uncaught 42 // 抛出一个布尔值 throw true; // Uncaught true // 抛出一个对象 throw { toString: function () { return 'Error!'; } }; // Uncaught {toString: ƒ} 对于 JavaScript 引擎来说,遇到throw语句,程序就中止了。引擎会接收到throw抛出的信息,可能是一个错误实例,也可能是其他类型的值 try...catch 结构 一旦发生错误,程序就中止执行了。JavaScript 提供了try...catch结构,允许对错误进行处理,选择是否往下执行 try { throw new Error('出错了!'); } catch (e) { console.log(e.name + ": " + e.message); console.log(e.stack); } // Error: 出错了! // at <anonymous>:3:9 上面代码中,try代码块抛出错误(上例用的是throw语句),JavaScript 引擎就立即把代码的执行,转到catch代码块,或者说错误被catch代码块捕获了。catch接受一个参数,表示try代码块抛出的值。如果你不确定某些代码是否会报错,就可以把它们放在try...catch代码块之中,便于进一步对错误进行处理 try { f(); } catch(e) { // 处理错误 } 上面代码中,如果函数f执行报错,就会进行catch代码块,接着对错误进行处理。catch代码块捕获错误之后,程序不会中断,会按照正常流程继续执行下去 try { throw "出错了"; } catch (e) { console.log(111); } console.log(222); // 111 // 222 上面代码中,try代码块抛出的错误,被catch代码块捕获后,程序会继续向下执行。catch代码块之中,还可以再抛出错误,甚至使用嵌套的try...catch结构 try { foo.bar(); } catch (e) { if (e instanceof EvalError) { console.log(e.name + ": " + e.message); } else if (e instanceof RangeError) { console.log(e.name + ": " + e.message); } // ... } finally 代码块 try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句 function cleansUp() { try { throw new Error('出错了……'); console.log('此行不会执行'); } finally { console.log('完成清理工作'); } } cleansUp() // 完成清理工作 // Uncaught Error: 出错了…… // at cleansUp (<anonymous>:3:11) // at <anonymous>:10:1 上面代码中,由于没有catch语句块,一旦发生错误,代码就会中断执行。中断执行前,会先执行finally代码块,然后再向用户提示报错信息 function idle(x) { try { console.log(x); return 'result'; } finally { console.log('FINALLY'); } } idle('hello') // hello // FINALLY 上面代码中,try代码块没有发生错误,而且里面还包括return语句,但是finally代码块依然会执行。而且,这个函数的返回值还是result var count = 0; function countUp() { try { return count; } finally { count++; } } countUp() // 0 count // 1 上面代码说明,return语句里面的count的值,是在finally代码块运行之前就获取了 下面是finally代码块用法的典型场景 openFile(); try { writeFile(Data); } catch(e) { handleError(e); } finally { closeFile(); } 上面代码首先打开一个文件,然后在try代码块中写入文件,如果没有发生错误,则运行finally代码块关闭文件;一旦发生错误,则先使用catch代码块处理错误,再使用finally代码块关闭文件。下面的例子充分反映了try...catch...finally这三者之间的执行顺序 function f() { try { console.log(0); throw 'bug'; } catch(e) { console.log(1); return true; // 这句原本会延迟到 finally 代码块结束再执行 console.log(2); // 不会运行 } finally { console.log(3); return false; // 这句会覆盖掉前面那句 return console.log(4); // 不会运行 } console.log(5); // 不会运行 } var result = f(); // 0 // 1 // 3 result // false 上面代码中,catch代码块结束执行之前,会先执行finally代码块。catch代码块之中,触发转入finally代码快的标志,不仅有return语句,还有throw语句 function f() { try { throw '出错了!'; } catch(e) { console.log('捕捉到内部错误'); throw e; // 这句原本会等到finally结束再执行 } finally { return false; // 直接返回 } } try { f(); } catch(e) { // 此处不会执行 console.log('caught outer "bogus"'); } // 捕捉到内部错误 上面代码中,进入catch代码块之后,一遇到throw语句,就会去执行finally代码块,其中有return false语句,因此就直接返回了,不再会回去执行catch代码块剩下的部分了 try { try { console.log('Hello world!'); // 报错 } finally { console.log('Finally'); } console.log('Will I run?'); } catch(error) { console.error(error.message); } // Finally // console is not defined 上面代码中,try里面还有一个try。内层的try报错,这时会执行内层的finally代码块,然后抛出错误,被外层的catch捕获 编程风格 概述 “编程风格”(programming style)指的是编写代码的样式规则。不同的程序员,往往有不同的编程风格。 有人说,编译器的规范叫做“语法规则”(grammar),这是程序员必须遵守的;而编译器忽略的部分,就叫“编程风格”(programming style),这是程序员可以自由选择的。这种说法不完全正确,程序员固然可以自由选择编程风格,但是好的编程风格有助于写出质量更高、错误更少、更易于维护的程序。所以,编程风格的选择不应该基于个人爱好、熟悉程度、打字量等因素,而要考虑如何尽量使代码清晰易读、减少出错。你选择的,不是你喜欢的风格,而是一种能够清晰表达你的意图的风格。这一点,对于 JavaScript 这种语法自由度很高的语言尤其重要。必须牢记的一点是,如果你选定了一种“编程风格”,就应该坚持遵守,切忌多种风格混用。如果你加入他人的项目,就应该遵守现有的风格 缩进 行首的空格和 Tab 键,都可以产生代码缩进效果(indent)。Tab 键可以节省击键次数,但不同的文本编辑器对 Tab 的显示不尽相同,有的显示四个空格,有的显示两个空格,所以有人觉得,空格键可以使得显示效果更统一。无论你选择哪一种方法,都是可以接受的,要做的就是始终坚持这一种选择。不要一会使用 Tab 键,一会使用空格键 区块 如果循环和判断的代码体只有一行,JavaScript 允许该区块(block)省略大括号;但是,建议总是使用大括号表示区块 JavaScript 要起首的大括号要跟在关键字的后面,因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误 return { key: value }; // 相当于 return; { key: value }; 圆括号 圆括号(parentheses)在 JavaScript 中有两种作用,一种表示函数的调用,另一种表示表达式的组合(grouping) console.log('abc'); // 圆括号表示函数的调用 (1 + 2) * 3 // 圆括号表示表达式的组合 建议可以用空格,区分这两种不同的括号 1.表示函数调用时,函数名与左括号之间没有空格 2.表示函数定义时,函数名与左括号之间没有空格 3.其他情况时,前面位置的语法元素与左括号之间,都有一个空格 行尾的分号 分号表示一条语句的结束,JavaScript 允许省略行尾的分号。事实上,确实有一些开发者行尾从来不写分号。但是,由于下面要讨论的原因,建议还是不要省略这个分号 不使用分号的情况 首先,以下三种情况,语法规定本来就不需要在结尾添加分号 1.for 和 while 循环 for ( ; ; ) { } // 没有分号 while (true) { } // 没有分号 注意,do...while循环是有分号的 2.分支语句:if,switch,try if (true) { } // 没有分号 switch () { } // 没有分号 try { } catch { } // 没有分号 3.函数的声明语句 function f() { } // 没有分号 注意,函数表达式仍然要使用分号 var f = function f() { }; 以上三种情况,如果使用了分号,并不会出错。因为,解释引擎会把这个分号解释为空语句 分号的自动添加 除了上面三种情况,所有语句都应该使用分号。但是,如果没有使用分号,大多数情况下,JavaScript 会自动添加 var a = 1 // 等同于 var a = 1; 这种语法特性被称为“分号的自动添加”(Automatic Semicolon Insertion,简称 ASI)。因此,有人提倡省略句尾的分号。麻烦的是,如果下一行的开始可以与本行的结尾连在一起解释,JavaScript 就不会自动添加分号;另外,如果一行的起首是“自增”(++)或“自减”(--)运算符,则它们的前面会自动添加分号。如果continue、break、return和throw这四个语句后面直接跟换行符,则会自动添加分号。这意味着,如果return语句返回的是一个对象的字面量,起首的大括号一定要写在同一行,否则得不到预期结果 a = b = c = 1 a ++ b -- c // 等同于 a = b = c = 1; a; ++b; --c; return { first: 'Jane' }; // 等同于 return; { first: 'Jane' }; 由于解释引擎自动添加分号的行为难以预测,因此编写代码的时候不应该省略行尾的分号。有些 JavaScript 代码压缩器(uglifier)不会自动添加分号,因此遇到没有分号的结尾,就会让代码保持原状,而不是压缩成一行,使得压缩无法得到最优的结果。另外,不写结尾的分号,可能会导致脚本合并出错。所以,有的代码库在第一行语句开始前,会加上一个分号 全局变量 JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用非常不利,因此建议避免使用全局变量。如果不得不使用,可以考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如UPPER_CASE 变量声明 JavaScript 会自动将变量声明“提升”(hoist)到代码块(block)的头部 if (!x) { var x = {}; } // 等同于 var x; if (!x) { x = {}; } 另外,所有函数都应该在使用之前定义。函数内部的变量声明,都应该放在函数的头部 with 语句 with可以减少代码的书写,但是会造成混淆 with (o) { foo = bar; } 上面的代码,可以有四种运行结果 o.foo = bar; o.foo = o.bar; foo = bar; foo = o.bar; 这四种结果都可能发生,取决于不同的变量是否有定义。因此,不要使用with语句 相等和严格相等 JavaScript 有两个表示相等的运算符:“相等”(==)和“严格相等”(===)。相等运算符会自动转换变量类型,造成很多意想不到的情况;因此,建议不要使用相等运算符(==),只使用严格相等运算符(===) 语句的合并 有些程序员追求简洁,喜欢合并不同目的的语句。虽然语句少了,但是可读性大打折扣,而且会造成误读,让别人误解代码的意思。建议不要将不同目的的语句,合并成一行 自增和自减运算符 自增(++)和自减(--)运算符,放在变量的前面或后面,返回的值不一样,很容易发生错误。事实上,所有的++运算符都可以用+= 1代替;改用+= 1,代码变得更清晰了 switch...case 结构 switch...case结构要求,在每一个case的最后一行必须是break语句,否则会接着运行下一个case。这样不仅容易忘记,还会造成代码的冗长。而且,switch...case不使用大括号,不利于代码形式的统一。此外,这种结构类似于goto语句,容易造成程序流程的混乱,使得代码结构混乱不堪,不符合面向对象编程的原则 function doAction(action) { switch (action) { case 'hack': return 'hack'; case 'slash': return 'slash'; case 'run': return 'run'; default: throw new Error('Invalid action.'); } } 上面的代码建议改写成对象结构 function doAction(action) { var actions = { 'hack': function () { return 'hack'; }, 'slash': function () { return 'slash'; }, 'run': function () { return 'run'; } }; if (typeof actions[action] !== 'function') { throw new Error('Invalid action.'); } return actions[action](); } 因此,建议switch...case结构可以用对象结构代替 console 对象与控制台 console 对象 console对象是 JavaScript 的原生对象,它有点像 Unix 系统的标准输出stdout和标准错误stderr,可以输出各种信息到控制台,并且还提供了很多有用的辅助方法 console的常见用途有两个: 1.调试程序,显示网页代码运行时的错误信息 2.提供了一个命令行接口,用来与网页代码互动 console对象的浏览器实现,包含在浏览器自带的开发工具之中。以 Chrome 浏览器的“开发者工具”(Developer Tools)为例,可以使用下面三种方法的打开它 1.按 F12 或者Control + Shift + i(PC)/ Command + Option + i(Mac)。 2.浏览器菜单选择“工具/开发者工具”。 3.在一个页面元素上,打开右键菜单,选择其中的“Inspect Element” 打开开发者工具以后,顶端有多个面板,包括: Elements:查看网页的 HTML 源码和 CSS 代码 Resources:查看网页加载的各种资源文件(比如代码文件、字体文件 CSS 文件等),以及在硬盘上创建的各种内容(比如本地缓存、Cookie、Local Storage等) Network:查看网页的 HTTP 通信情况 Sources:查看网页加载的脚本源码 Timeline:查看各种网页行为随时间变化的情况 Performance:查看网页的性能情况,比如 CPU 和内存消耗 Console:用来运行 JavaScript 命令 console 对象的静态方法 console对象提供的各种静态方法,用来与控制台窗口互动 console.log(),console.info(),console.debug() console.log方法用于在控制台输出信息。它可以接受一个或多个参数,将它们连接起来输出;console.log方法会自动在每次输出的结尾,添加换行符 console.log('Hello World') // Hello World console.log('a', 'b', 'c') // a b c 如果第一个参数是格式字符串(使用了格式占位符),console.log方法将依次用后面的参数替换占位符,然后再进行输出 console.log(' %s + %s = %s', 1, 1, 2) // 1 + 1 = 2 上面代码中,console.log方法的第一个参数有三个占位符(%s),第二、三、四个参数会在显示时,依次替换掉这个三个占位符 console.log方法支持以下占位符,不同类型的数据必须使用对应的占位符。 %s 字符串 %d 整数 %i 整数 %f 浮点数 %o 对象的链接 %c CSS格式字符串 使用%c占位符时,对应的参数必须是 CSS 代码,用来对输出内容进行 CSS 渲染 var number = 11 * 9; var color = 'red'; console.log('%d %s balloons', number, color); // 99 red balloons console.log( '%cThis text is styled!', 'color: red; background: yellow; font-size: 24px;' ) 上面代码运行后,输出的内容将显示为黄底红字;如果参数是一个对象,console.log会显示该对象的值 console.log({foo: 'bar'}) // Object {foo: "bar"} console.log(Date) // function Date() { [native code] } 上面代码输出Date对象的值,结果为一个构造函数 console.info是console.log方法的别名,用法完全一样。只不过console.info方法会在输出信息的前面,加上一个蓝色图标。console.debug方法与console.log方法类似,会在控制台输出调试信息。但是,默认情况下,console.debug输出的信息不会显示,只有在打开显示级别在verbose的情况下才会显示 console对象的所有方法,都可以被覆盖。因此,可以按照自己的需要,定义console.log方法 ['log', 'info', 'warn', 'error'].forEach(function(method) { console[method] = console[method].bind( console, new Date().toISOString() ); }); console.log("出错了!"); // 2014-05-18T09:00.000Z 出错了! 上面代码表示,使用自定义的console.log方法,可以在显示结果添加当前时间 console.warn(),console.error() warn方法和error方法也是在控制台输出信息,它们与log方法的不同之处在于,warn方法输出信息时,在最前面加一个黄色三角,表示警告;error方法输出信息时,在最前面加一个红色的叉,表示出错。同时,还会高亮显示输出文字和错误发生的堆栈。其他方面都一样 console.error('Error: %s (%i)', 'Server is not responding', 500) // Error: Server is not responding (500) console.warn('Warning! Too few nodes (%d)', document.childNodes.length) // Warning! Too few nodes (1) console.table() 对于某些复合类型的数据,console.table方法可以将其转为表格显示 var languages = [ { name: "JavaScript", fileExtension: ".js" }, { name: "TypeScript", fileExtension: ".ts" }, { name: "CoffeeScript", fileExtension: ".coffee" } ]; console.table(languages); 上面代码的language变量,转为表格显示如下 (index) name fileExtension 0 "JavaScript" ".js" 1 "TypeScript" ".ts" 2 "CoffeeScript" ".coffee" console.count() count方法用于计数,输出它被调用了多少次 function greet(user) { console.count(user); return "hi " + user; } greet('bob') // bob: 1 // "hi bob" greet('alice') // alice: 1 // "hi alice" greet('bob') // bob: 2 // "hi bob" 上面代码根据参数的不同,显示bob执行了两次,alice执行了一次 console.dir(),console.dirxml() dir方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示 console.log({f1: 'foo', f2: 'bar'}) // Object {f1: "foo", f2: "bar"} console.dir({f1: 'foo', f2: 'bar'}) // Object // f1: "foo" // f2: "bar" // __proto__: Object 上面代码显示dir方法的输出结果,比log方法更易读,信息也更丰富。该方法对于输出 DOM 对象非常有用,因为会显示 DOM 对象的所有属性 Node 环境之中,还可以指定以代码高亮的形式输出 console.dir(obj, {colors: true}) dirxml方法主要用于以目录树的形式,显示 DOM 节点 console.dirxml(document.body) 如果参数不是 DOM 节点,而是普通的 JavaScript 对象,console.dirxml等同于console.dir console.assert() console.assert方法主要用于程序运行过程中进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。它接受两个参数,第一个参数是表达式,第二个参数是字符串。只有当第一个参数为false,才会提示有错误,在控制台输出第二个参数,否则不会有任何结果 console.assert(false, '判断条件不成立') // Assertion failed: 判断条件不成立 // 相当于 try { if (!false) { throw new Error('判断条件不成立'); } } catch(e) { console.error(e); } console.time(),console.timeEnd() 这两个方法用于计时,可以算出一个操作所花费的准确时间 console.time('Array initialize'); var array= new Array(1000000); for (var i = array.length - 1; i >= 0; i--) { array[i] = new Object(); }; console.timeEnd('Array initialize'); // Array initialize: 1914.481ms time方法表示计时开始,timeEnd方法表示计时结束。它们的参数是计时器的名称。调用timeEnd方法之后,控制台会显示“计时器名称: 所耗费的时间” console.group(),console.groupEnd(),console.groupCollapsed() console.group和console.groupEnd这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开 console.group('一级分组'); console.log('一级分组的内容'); console.group('二级分组'); console.log('二级分组的内容'); console.groupEnd(); // 二级分组结束 console.groupEnd(); // 一级分组结束 上面代码会将“二级分组”显示在“一级分组”内部,并且“一级分组”和“二级分组”前面都有一个折叠符号,可以用来折叠本级的内容。console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的 console.groupCollapsed('Fetching Data'); console.log('Request Sent'); console.error('Error: Server not responding (500)'); console.groupEnd(); 上面代码只显示一行”Fetching Data“,点击后才会展开,显示其中包含的两行 console.trace(),console.clear() console.trace方法显示当前执行的代码在堆栈中的调用路径 console.trace() // console.trace() // (anonymous function) // InjectedScript._evaluateOn // InjectedScript._evaluateAndWrap // InjectedScript.evaluate console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。如果用户选中了控制台的“Preserve log”选项,console.clear方法将不起作用 控制台命令行 API 浏览器控制台中,除了使用console对象,还可以使用一些控制台自带的命令行方法 $_ $_属性返回上一个表达式的值 2 + 2 // 4 $_ // 4 $0 - $4 控制台保存了最近5个在 Elements 面板选中的 DOM 元素,$0代表倒数第一个(最近一个),$1代表倒数第二个,以此类推直到$4 $(selector) $(selector)返回第一个匹配的元素,等同于document.querySelector()。注意,如果页面脚本对$有定义,则会覆盖原始的定义。比如,页面里面有 jQuery,控制台执行$(selector)就会采用 jQuery 的实现,返回一个数组 $$(selector) $$(selector)返回选中的 DOM 对象,等同于document.querySelectorAll #### $x(path) $x(path)方法返回一个数组,包含匹配特定 XPath 表达式的所有 DOM 元素 $x("//p[a]") 上面代码返回所有包含a元素的p元素 inspect(object) inspect(object)方法打开相关面板,并选中相应的元素,显示它的细节。DOM 元素在Elements面板中显示,比如inspect(document)会在 Elements 面板显示document元素。JavaScript 对象在控制台面板Profiles面板中显示,比如inspect(window) getEventListeners(object) getEventListeners(object)方法返回一个对象,该对象的成员为object登记了回调函数的各种事件(比如click或keydown),每个事件对应一个数组,数组的成员为该事件的回调函数 keys(object),values(object) keys(object)方法返回一个数组,包含object的所有键名。values(object)方法返回一个数组,包含object的所有键值 var o = {'p1': 'a', 'p2': 'b'}; keys(o) // ["p1", "p2"] values(o) // ["a", "b"] monitorEvents(object[, events]) ,unmonitorEvents(object[, events]) monitorEvents(object[, events])方法监听特定对象上发生的特定事件。事件发生时,会返回一个Event对象,包含该事件的相关信息。unmonitorEvents方法用于停止监听 monitorEvents(window, "resize"); monitorEvents(window, ["resize", "scroll"]) 上面代码分别表示单个事件和多个事件的监听方法 monitorEvents($0, 'mouse'); unmonitorEvents($0, 'mousemove'); 上面代码表示如何停止监听。monitorEvents允许监听同一大类的事件。所有事件可以分成四个大类 1.mouse:"mousedown", "mouseup", "click", "dblclick", "mousemove", "mouseover", "mouseout", "mousewheel" 2.key:"keydown", "keyup", "keypress", "textInput" 3.touch:"touchstart", "touchmove", "touchend", "touchcancel" 4.control:"resize", "scroll", "zoom", "focus", "blur", "select", "change", "submit", "reset" monitorEvents($("#msg"), "key"); 上面代码表示监听所有key大类的事件 其他方法 命令行 API 还提供以下方法。 1.clear():清除控制台的历史 2.copy(object):复制特定 DOM 元素到剪贴板 3.dir(object):显示特定对象的所有属性,是console.dir方法的别名 4.dirxml(object):显示特定对象的 XML 形式,是console.dirxml方法的别名 debugger 语句 debugger语句主要用于除错,作用是设置断点。如果有正在运行的除错工具,程序运行到debugger语句时会自动停下。如果没有除错工具,debugger语句不会产生任何结果,JavaScript 引擎自动跳过这一句。Chrome 浏览器中,当代码运行到debugger语句时,就会暂停运行,自动打开脚本源码界面 for(var i = 0; i < 5; i++){ console.log(i); if (i === 2) debugger; } 上面代码打印出0,1,2以后,就会暂停,自动打开源码界面,等待进一步处理 标准库 Object 对象 概述 JavaScript 原生提供Object对象(注意起首的O是大写),现在介绍该对象原生的各种方法。JavaScript 的所有其他对象都继承自Object对象,即那些对象都是Object的实例。 Object对象的原生方法分成两类:Object本身的方法与Object的实例方法 Object对象本身的方法 所谓“本身的方法”就是直接定义在Object对象的方法 Object.print = function (o) { console.log(o) }; 上面代码中,print方法就是直接定义在Object对象上 Object的实例方法 所谓实例方法就是定义在Object原型对象Object.prototype上的方法。它可以被Object实例直接使用 Object.prototype.print = function () { console.log(this); }; var obj = new Object(); obj.print() // Object 上面代码中,Object.prototype定义了一个print方法,然后生成一个Object的实例obj。obj直接继承了Object.prototype的属性和方法,可以直接使用obj.print调用print方法。也就是说,obj对象的print方法实质上就是调用Object.prototype.print方法。凡是定义在Object.prototype对象上面的属性和方法,将被所有实例对象共享 以下先介绍Object作为函数的用法,然后再介绍Object对象的原生方法,分成对象自身的方法(又称为“静态方法”)和实例方法两部分 Object() Object本身是一个函数,可以当作工具方法使用,将任意值转为对象。这个方法常用于保证某个值一定是对象。如果参数为空(或者为undefined和null),Object()返回一个空对象 var obj = Object(); // 等同于 var obj = Object(undefined); var obj = Object(null); obj instanceof Object // true 上面代码的含义,是将undefined和null转为对象,结果得到了一个空对象obj。instanceof运算符用来验证,一个对象是否为指定的构造函数的实例。obj instanceof Object返回true,就表示obj对象是Object的实例。如果参数是原始类型的值,Object方法将其转为对应的包装对象的实例 var obj = Object(1); obj instanceof Object // true obj instanceof Number // true var obj = Object('foo'); obj instanceof Object // true obj instanceof String // true var obj = Object(true); obj instanceof Object // true obj instanceof Boolean // true 上面代码中,Object函数的参数是各种原始类型的值,转换成对象就是原始类型值对应的包装对象。如果Object方法的参数是一个对象,它总是返回该对象,即不用转换 var arr = []; var obj = Object(arr); // 返回原数组 obj === arr // true var value = {}; var obj = Object(value) // 返回原对象 obj === value // true var fn = function () {}; var obj = Object(fn); // 返回原函数 obj === fn // true 利用这一点,可以写一个判断变量是否为对象的函数 function isObject(value) { return value === Object(value); } isObject([]) // true isObject(true) // false Object 构造函数 Object不仅可以当作工具函数使用,还可以当作构造函数使用,即前面可以使用new命令。Object构造函数的首要用途,是直接通过它来生成新对象 var obj = new Object(); 注意:通过var obj = new Object()的写法生成新对象,与字面量的写法var obj = {}是等价的。或者说,后者只是前者的一种简便写法 Object构造函数的用法与工具方法很相似,使用时可以接受一个参数;如果该参数是一个对象,则直接返回这个对象;如果是一个原始类型的值,则返回该值对应的包装对象 var o1 = {a: 1}; var o2 = new Object(o1); o1 === o2 // true var obj = new Object(123); obj instanceof Number // true 虽然用法相似,但是Object(value)与new Object(value)两者的语义是不同的,Object(value)表示将value转成一个对象,new Object(value)则表示新生成一个对象,它的值是value Object 的静态方法 所谓“静态方法”,是指部署在Object对象自身的方法 Object.keys(),Object.getOwnPropertyNames() Object.keys方法和Object.getOwnPropertyNames方法都用来遍历对象的属性。Object.keys方法的参数是一个对象,返回一个数组。该数组的成员都是该对象自身的(而不是继承的)所有属性名 Object.getOwnPropertyNames方法与Object.keys类似,也是接受一个对象作为参数,返回一个数组,包含了该对象自身的所有属性名 var obj = { p1: 123, p2: 456 }; Object.keys(obj) // ["p1", "p2"] Object.getOwnPropertyNames(obj) // ["p1", "p2"] 对于一般的对象来说,Object.keys()和Object.getOwnPropertyNames()返回的结果是一样的。只有涉及不可枚举属性时,才会有不一样的结果。Object.keys方法只返回可枚举的属性,Object.getOwnPropertyNames方法还返回不可枚举的属性名 var a = ['Hello', 'World']; Object.keys(a) // ["0", "1"] Object.getOwnPropertyNames(a) // ["0", "1", "length"] 上面代码中,数组的length属性是不可枚举的属性,所以只出现在Object.getOwnPropertyNames方法的返回结果中。由于 JavaScript 没有提供计算对象属性个数的方法,所以可以用这两个方法代替 var obj = { p1: 123, p2: 456 }; Object.keys(obj).length // 2 Object.getOwnPropertyNames(obj).length // 2 一般情况下,几乎总是使用Object.keys方法,遍历对象的属性 其他方法 除了上面提到的两个方法,Object还有不少其他静态方法 对象属性模型的相关方法 Object.getOwnPropertyDescriptor():获取某个属性的描述对象 Object.defineProperty():通过描述对象,定义某个属性 Object.defineProperties():通过描述对象,定义多个属性 控制对象状态的方法 Object.preventExtensions():防止对象扩展 Object.isExtensible():判断对象是否可扩展 Object.seal():禁止对象配置 Object.isSealed():判断一个对象是否可配置 Object.freeze():冻结一个对象 Object.isFrozen():判断一个对象是否被冻结 原型链相关方法 Object.create():该方法可以指定原型对象和属性,返回一个新的对象 Object.getPrototypeOf():获取对象的Prototype对象 Object 的实例方法 除了静态方法,还有不少方法定义在Object.prototype对象。它们称为实例方法,所有Object的实例对象都继承了这些方法。Object实例对象的方法,主要有以下六个: Object.prototype.valueOf():返回当前对象对应的值 Object.prototype.toString():返回当前对象对应的字符串形式 Object.prototype.toLocaleString():返回当前对象对应的本地字符串形式 Object.prototype.hasOwnProperty():判断某个属性是否为当前对象自身的属性,还是继承自原型对象的属性 Object.prototype.isPrototypeOf():判断当前对象是否为另一个对象的原型 Object.prototype.propertyIsEnumerable():判断某个属性是否可枚举 Object.prototype.valueOf() valueOf方法的作用是返回一个对象的“值”,默认情况下返回对象本身 var obj = new Object(); obj.valueOf() === obj // true valueOf方法的主要用途是,JavaScript 自动类型转换时会默认调用这个方法 var obj = new Object(); 1 + obj // "1[object Object]" 上面代码将对象obj与数字1相加,这时 JavaScript 就会默认调用valueOf()方法,求出obj的值再与1相加。所以,如果自定义valueOf方法,就可以得到想要的结果 var obj = new Object(); obj.valueOf = function () { return 2; }; 1 + obj // 3 上面代码自定义了obj对象的valueOf方法,于是1 + obj就得到3。这种方法就相当于用自定义的obj.valueOf,覆盖Object.prototype.valueOf Object.prototype.toString() toString方法的作用是返回一个对象的字符串形式,默认情况下返回类型字符串 var o1 = new Object(); o1.toString() // "[object Object]" var o2 = {a:1}; o2.toString() // "[object Object]" 上面代码表示,对于一个对象调用toString方法,会返回字符串[object Object],该字符串说明对象的类型。字符串[object Object]本身没有太大的用处,但是通过自定义toString方法,可以让对象在自动类型转换时,得到想要的字符串形式 var obj = new Object(); obj.toString = function () { return 'hello'; }; obj + ' ' + 'world' // "hello world" 上面代码表示,当对象用于字符串加法时,会自动调用toString方法。由于自定义了toString方法,所以返回字符串hello world。数组、字符串、函数、Date 对象都分别部署了自定义的toString方法,覆盖了Object.prototype.toString方法 [1, 2, 3].toString() // "1,2,3" '123'.toString() // "123" (function () { return 123; }).toString() // "function () { // return 123; // }" (new Date()).toString() // "Tue May 10 2016 09:11:31 GMT+0800 (CST)" 上面代码中,数组、字符串、函数、Date 对象调用toString方法,并不会返回[object Object],因为它们都自定义了toString方法,覆盖原始方法 toString() 的应用:判断数据类型 Object.prototype.toString方法返回对象的类型字符串,因此可以用来判断一个值的类型 var obj = {}; obj.toString() // "[object Object]" 上面代码调用空对象的toString方法,结果返回一个字符串object Object,其中第二个Object表示该值的构造函数。这是一个十分有用的判断数据类型的方法。由于实例对象可能会自定义toString方法,覆盖掉Object.prototype.toString方法,所以为了得到类型字符串,最好直接使用Object.prototype.toString方法。通过函数的call方法,可以在任意值上调用这个方法,帮助我们判断这个值的类型 Object.prototype.toString.call(value) 不同数据类型的Object.prototype.toString方法返回值如下;这就是说,Object.prototype.toString可以看出一个值到底是什么类型 Object.prototype.toString.call(2) // "[object Number]" Object.prototype.toString.call('') // "[object String]" Object.prototype.toString.call(true) // "[object Boolean]" Object.prototype.toString.call(undefined) // "[object Undefined]" Object.prototype.toString.call(null) // "[object Null]" Object.prototype.toString.call(Math) // "[object Math]" Object.prototype.toString.call({}) // "[object Object]" Object.prototype.toString.call([]) // "[object Array]" 利用这个特性,可以写出一个比typeof运算符更准确的类型判断函数 var type = function (o){ var s = Object.prototype.toString.call(o); return s.match(/\[object (.*?)\]/)[1].toLowerCase(); }; type({}); // "object" type([]); // "array" type(5); // "number" type(null); // "null" type(); // "undefined" type(/abcd/); // "regex" type(new Date()); // "date" 在上面这个type函数的基础上,还可以加上专门判断某种类型数据的方法 var type = function (o){ var s = Object.prototype.toString.call(o); return s.match(/\[object (.*?)\]/)[1].toLowerCase(); }; ['Null', 'Undefined', 'Object', 'Array', 'String', 'Number', 'Boolean', 'Function', 'RegExp'].forEach(function (t) { type['is' + t] = function (o) { return type(o) === t.toLowerCase(); }; }); type.isObject({}) // true type.isNumber(NaN) // true type.isRegExp(/abc/) // true Object.prototype.toLocaleString() Object.prototype.toLocaleString方法与toString的返回结果相同,也是返回一个值的字符串形式。这个方法的主要作用是留出一个接口,让各种不同的对象实现自己版本的toLocaleString,用来返回针对某些地域的特定的值 var person = { toString: function () { return 'Henry Norman Bethune'; }, toLocaleString: function () { return '白求恩'; } }; person.toString() // Henry Norman Bethune person.toLocaleString() // 白求恩 目前,主要有三个对象自定义了toLocaleString方法。 1.Array.prototype.toLocaleString() 2.Number.prototype.toLocaleString() 3.Date.prototype.toLocaleString() 举例来说,日期的实例对象的toString和toLocaleString返回值就不一样,而且toLocaleString的返回值跟用户设定的所在地域相关 var date = new Date(); date.toString() // "Tue Jan 01 2018 12:01:33 GMT+0800 (CST)" date.toLocaleString() // "1/01/2018, 12:01:33 PM" Object.prototype.hasOwnProperty() Object.prototype.hasOwnProperty方法接受一个字符串作为参数,返回一个布尔值,表示该实例对象自身是否具有该属性 var obj = { p: 123 }; obj.hasOwnProperty('p') // true obj.hasOwnProperty('toString') // false 上面代码中,对象obj自身具有p属性,所以返回true。toString属性是继承的,所以返回false 属性描述对象 概述 JavaScript 提供了一个内部数据结构,用来描述对象的属性,控制它的行为,比如该属性是否可写、可遍历等等。这个内部数据结构称为“属性描述对象”(attributes object)。每个属性都有自己对应的属性描述对象,保存该属性的一些元信息。 { value: 123, writable: false, enumerable: true, configurable: false, get: undefined, set: undefined } 属性描述对象提供6个元属性 1.value:value是该属性的属性值,默认为undefined 2.writable:writable是一个布尔值,表示属性值(value)是否可改变(即是否可写),默认为true 3.enumerable:enumerable是一个布尔值,表示该属性是否可遍历,默认为true。如果设为false,会使得某些操作(比如for...in循环、Object.keys())跳过该属性 4.configurable:configurable是一个布尔值,表示可配置性,默认为true。如果设为false,将阻止某些操作改写该属性,比如无法删除该属性,也不得改变该属性的属性描述对象(value属性除外)。也就是说,configurable属性控制了属性描述对象的可写性 5.get:get是一个函数,表示该属性的取值函数(getter),默认为undefined 6.set:set是一个函数,表示该属性的存值函数(setter),默认为undefined Object.getOwnPropertyDescriptor() Object.getOwnPropertyDescriptor()方法可以获取属性描述对象。它的第一个参数是目标对象,第二个参数是一个字符串,对应目标对象的某个属性名 var obj = { p: 'a' }; Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: true } 上面代码中,Object.getOwnPropertyDescriptor()方法获取obj.p的属性描述对象。 注意,Object.getOwnPropertyDescriptor()方法只能用于对象自身的属性,不能用于继承的属性 var obj = { p: 'a' }; Object.getOwnPropertyDescriptor(obj, 'toString') // undefined 上面代码中,toString是obj对象继承的属性,Object.getOwnPropertyDescriptor()无法获取 Object.getOwnPropertyNames() Object.getOwnPropertyNames方法返回一个数组,成员是参数对象自身的全部属性的属性名,不管该属性是否可遍历 var obj = Object.defineProperties({}, { p1: { value: 1, enumerable: true }, p2: { value: 2, enumerable: false } }); Object.getOwnPropertyNames(obj) // ["p1", "p2"] 上面代码中,obj.p1是可遍历的,obj.p2是不可遍历的。Object.getOwnPropertyNames会将它们都返回。这跟Object.keys的行为不同,Object.keys只返回对象自身的可遍历属性的全部属性名 Object.keys([]) // [] Object.getOwnPropertyNames([]) // [ 'length' ] Object.keys(Object.prototype) // [] Object.getOwnPropertyNames(Object.prototype) // ['hasOwnProperty', 'valueOf', 'constructor', 'toLocaleString', 'isPrototypeOf', 'propertyIsEnumerable', 'toString'] 上面代码中,数组自身的length属性是不可遍历的,Object.keys不会返回该属性。第二个例子的Object.prototype也是一个对象,所有实例对象都会继承它,它自身的属性都是不可遍历的 Object.defineProperty(),Object.defineProperties() Object.defineProperty()方法允许通过属性描述对象,定义或修改一个属性,然后返回修改后的对象,它的用法如下 Object.defineProperty(object, propertyName, attributesObject) Object.defineProperty方法接受三个参数: 1.object:属性所在的对象 2.propertyName:字符串,表示属性名 3.attributesObject:属性描述对象 举例来说,定义obj.p可以写成下面这样 var obj = Object.defineProperty({}, 'p', { value: 123, writable: false, enumerable: true, configurable: false }); obj.p // 123 obj.p = 246; obj.p // 123 上面代码中,Object.defineProperty()方法定义了obj.p属性。由于属性描述对象的writable属性为false,所以obj.p属性不可写。注意,这里的Object.defineProperty方法的第一个参数是{}(一个新建的空对象),p属性直接定义在这个空对象上面,然后返回这个对象,这是Object.defineProperty()的常见用法。如果属性已经存在,Object.defineProperty()方法相当于更新该属性的属性描述对象。如果一次性定义或修改多个属性,可以使用Object.defineProperties()方法 var obj = Object.defineProperties({}, { p1: { value: 123, enumerable: true }, p2: { value: 'abc', enumerable: true }, p3: { get: function () { return this.p1 + this.p2 }, enumerable:true, configurable:true } }); obj.p1 // 123 obj.p2 // "abc" obj.p3 // "123abc" 上面代码中,Object.defineProperties()同时定义了obj对象的三个属性。其中,p3属性定义了取值函数get,即每次读取该属性,都会调用这个取值函数。注意,一旦定义了取值函数get(或存值函数set),就不能将writable属性设为true,或者同时定义value属性,否则会报错 Object.defineProperty()和Object.defineProperties()参数里面的属性描述对象,writable、configurable、enumerable这三个属性的默认值都为false var obj = {}; Object.defineProperty(obj, 'foo', {}); Object.getOwnPropertyDescriptor(obj, 'foo') // { value: undefined, writable: false, enumerable: false, configurable: false } 上面代码中,定义obj.foo时用了一个空的属性描述对象,就可以看到各个元属性的默认值 Object.prototype.propertyIsEnumerable() 实例对象的propertyIsEnumerable()方法返回一个布尔值,用来判断某个属性是否可遍历。注意,这个方法只能用于判断对象自身的属性,对于继承的属性一律返回false var obj = {}; obj.p = 123; obj.propertyIsEnumerable('p') // true obj.propertyIsEnumerable('toString') // false 元属性 属性描述对象的各个属性称为“元属性”,因为它们可以看作是控制属性的属性 value value属性是目标属性的值 var obj = {}; obj.p = 123; Object.getOwnPropertyDescriptor(obj, 'p').value // 123 Object.defineProperty(obj, 'p', { value: 246 }); obj.p // 246 上面代码是通过value属性,读取或改写obj.p的例子 writable writable属性是一个布尔值,决定了目标属性的值(value)是否可以被改变 var obj = {}; Object.defineProperty(obj, 'a', { value: 37, writable: false}); obj.a // 37 obj.a = 25; obj.a // 37 上面代码中,obj.a的writable属性是false。然后,改变obj.a的值,不会有任何效果。 注意,正常模式下,对writable为false的属性赋值不会报错,只会默默失败。但是,严格模式下会报错,即使对a属性重新赋予一个同样的值 如果原型对象的某个属性的writable为false,那么子对象将无法自定义这个属性 var proto = Object.defineProperty({}, 'foo', { value: 'a', writable: false }); var obj = Object.create(proto); obj.foo = 'b'; obj.foo // 'a' 上面代码中,proto是原型对象,它的foo属性不可写。obj对象继承proto,也不可以再自定义这个属性了。如果是严格模式,这样做还会抛出一个错误。 但是,有一个规避方法,就是通过覆盖属性描述对象,绕过这个限制。原因是这种情况下,原型链会被完全忽视 var proto = Object.defineProperty({}, 'foo', { value: 'a', writable: false}); var obj = Object.create(proto); Object.defineProperty(obj, 'foo', { value: 'b'}); obj.foo // "b" enumerable enumerable(可遍历性)返回一个布尔值,表示目标属性是否可遍历。 JavaScript 的早期版本,for...in循环是基于in运算符的。我们知道,in运算符不管某个属性是对象自身的还是继承的,都会返回true var obj = {}; 'toString' in obj // true toString不是obj对象自身的属性,但是in运算符也返回true,这导致了toString属性也会被for...in循环遍历,这显然不太合理,后来就引入了“可遍历性”这个概念。只有可遍历的属性,才会被for...in循环遍历,同时还规定toString这一类实例对象继承的原生属性,都是不可遍历的,这样就保证了for...in循环的可用性。具体来说,如果一个属性的enumerable为false,下面三个操作不会取到该属性 1.for..in循环 2.Object.keys方法 3.JSON.stringify方法 因此,enumerable可以用来设置“秘密”属性 var obj = {}; Object.defineProperty(obj, 'x', { value: 123, enumerable: false}); obj.x // 123 for (var key in obj) { console.log(key); } // undefined Object.keys(obj) // [] JSON.stringify(obj) // "{}" 上面代码中,obj.x属性的enumerable为false,所以一般的遍历操作都无法获取该属性,使得它有点像“秘密”属性,但不是真正的私有属性,还是可以直接获取它的值。 注意,for...in循环包括继承的属性,Object.keys方法不包括继承的属性。如果需要获取对象自身的所有属性,不管是否可遍历,可以使用Object.getOwnPropertyNames方法。另外,JSON.stringify方法会排除enumerable为false的属性,有时可以利用这一点。如果对象的 JSON 格式输出要排除某些属性,就可以把这些属性的enumerable设为false configurable configurable(可配置性)返回一个布尔值,决定了是否可以修改属性描述对象。也就是说,configurable为false时,value、writable、enumerable和configurable都不能被修改了 var obj = Object.defineProperty({}, 'p', { writable: true, configurable: false }); Object.defineProperty(obj, 'p', {writable: false}) // 修改成功 注意,writable只有在false改为true会报错,true改为false是允许的。至于value,只要writable和configurable有一个为true,就允许改动。另外,configurable为false时,直接目标属性赋值,不报错,但不会成功;如果是严格模式,还会报错 var obj = Object.defineProperty({}, 'p', { value: 1, configurable: false }); obj.p = 2; obj.p // 1 可配置性决定了目标属性是否可以被删除(delete) var obj = Object.defineProperties({}, { p1: { value: 1, configurable: true }, p2: { value: 2, configurable: false } }); delete obj.p1 // true delete obj.p2 // false obj.p1 // undefined obj.p2 // 2 上面代码中,obj.p1的configurable是true,所以可以被删除,obj.p2就无法删除 存取器 除了直接定义以外,属性还可以用存取器(accessor)定义。其中,存值函数称为setter,使用属性描述对象的set属性;取值函数称为getter,使用属性描述对象的get属性。一旦对目标属性定义了存取器,那么存取的时候,都将执行对应的函数。利用这个功能,可以实现许多高级特性,比如某个属性禁止赋值 var obj = Object.defineProperty({}, 'p', { get: function () { return 'getter'; }, set: function (value) { console.log('setter: ' + value); } }); obj.p // "getter" obj.p = 123 // "setter: 123" 上面代码中,obj.p定义了get和set属性。obj.p取值时,就会调用get;赋值时,就会调用set。JavaScript 还提供了存取器的另一种写法 var obj = { get p() { return 'getter'; }, set p(value) { console.log('setter: ' + value); } } 上面的写法与定义属性描述对象是等价的,而且使用更广泛。 注意,取值函数get不能接受参数,存值函数set只能接受一个参数(即属性的值)。存取器往往用于属性的值依赖对象内部数据的场合 var obj ={ $n : 5, get next() { return this.$n++ }, set next(n) { if (n >= this.$n) this.$n = n; else throw new Error('新的值必须大于当前值'); } }; obj.next // 5 obj.next = 10; obj.next // 10 obj.next = 5; // Uncaught Error: 新的值必须大于当前值 上面代码中,next属性的存值函数和取值函数,都依赖于内部属性$n 对象的拷贝 有时,我们需要将一个对象的所有属性,拷贝到另一个对象,可以用下面的方法实现 var extend = function (to, from) { for (var property in from) { to[property] = from[property]; } return to; } extend({}, { a: 1 }) // {a: 1} 上面这个方法的问题在于,如果遇到存取器定义的属性,会只拷贝值 extend({}, { get a() { return 1 } }) // {a: 1} 为了解决这个问题,我们可以通过Object.defineProperty方法来拷贝属性 var extend = function (to, from) { for (var property in from) { if (!from.hasOwnProperty(property)) continue; Object.defineProperty( to, property, Object.getOwnPropertyDescriptor(from, property) ); } return to; } extend({}, { get a(){ return 1 } }) // { get a(){ return 1 } }) 上面代码中,hasOwnProperty那一行用来过滤掉继承的属性,否则可能会报错,因为Object.getOwnPropertyDescriptor读不到继承属性的属性描述对象 控制对象状态 有时需要冻结对象的读写状态,防止对象被改变。JavaScript 提供了三种冻结方法,最弱的一种是Object.preventExtensions,其次是Object.seal,最强的是Object.freeze Object.preventExtensions() Object.preventExtensions方法可以使得一个对象无法再添加新的属性 var obj = new Object(); Object.preventExtensions(obj); Object.defineProperty(obj, 'p', { value: 'hello' }); // TypeError: Cannot define property:p, object is not extensible. obj.p = 1; obj.p // undefined Object.isExtensible() Object.isExtensible方法用于检查一个对象是否使用了Object.preventExtensions方法。也就是说,检查是否可以为一个对象添加属性 var obj = new Object(); Object.isExtensible(obj) // true Object.preventExtensions(obj); Object.isExtensible(obj) // false 上面代码中,对obj对象使用Object.preventExtensions方法以后,再使用Object.isExtensible方法,返回false,表示已经不能添加新属性了 Object.seal() Object.seal方法使得一个对象既无法添加新属性,也无法删除旧属性 var obj = { p: 'hello' }; Object.seal(obj); delete obj.p; obj.p // "hello" obj.x = 'world'; obj.x // undefined Object.seal实质是把属性描述对象的configurable属性设为false,因此属性描述对象不再能改变了 var obj = { p: 'a' }; // seal方法之前 Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: true } Object.seal(obj); // seal方法之后 Object.getOwnPropertyDescriptor(obj, 'p') // Object { value: "a", writable: true, enumerable: true, configurable: false } Object.defineProperty(o, 'p', { enumerable: false }) // TypeError: Cannot redefine property: p Object.seal只是禁止新增或删除属性,并不影响修改某个属性的值 var obj = { p: 'a' }; Object.seal(obj); obj.p = 'b'; obj.p // 'b' Object.isSealed() Object.isSealed方法用于检查一个对象是否使用了Object.seal方法 var obj = { p: 'a' }; Object.seal(obj); Object.isSealed(obj) // true 这时,Object.isExtensible方法也返回false var obj = { p: 'a' }; Object.seal(obj); Object.isExtensible(obj) // false Object.freeze() Object.freeze方法可以使得一个对象无法添加新属性、无法删除旧属性、也无法改变属性的值,使得这个对象实际上变成了常量 var obj = { p: 'hello' }; Object.freeze(obj); obj.p = 'world'; obj.p // "hello" obj.t = 'hello'; obj.t // undefined delete obj.p // false obj.p // "hello" 上面代码中,对obj对象进行Object.freeze()以后,修改属性、新增属性、删除属性都无效了。这些操作并不报错,只是默默地失败。如果在严格模式下,则会报错 Object.isFrozen() Object.isFrozen方法用于检查一个对象是否使用了Object.freeze方法 var obj = { p: 'hello'}; Object.freeze(obj); Object.isFrozen(obj) // true 使用Object.freeze方法以后,Object.isSealed将会返回true,Object.isExtensible返回false var obj = { p: 'hello' }; Object.freeze(obj); Object.isSealed(obj) // true Object.isExtensible(obj) // false Object.isFrozen的一个用途是,确认某个对象没有被冻结后,再对它的属性赋值 var obj = { p: 'hello' }; Object.freeze(obj); if (!Object.isFrozen(obj)) { obj.p = 'world'; } 上面代码中,确认obj没有被冻结后,再对它的属性赋值,就不会报错了 局限性 上面的三个方法锁定对象的可写性有一个漏洞:可以通过改变原型对象,来为对象增加属性 var obj = new Object(); Object.preventExtensions(obj); var proto = Object.getPrototypeOf(obj); proto.t = 'hello'; obj.t // hello 上面代码中,对象obj本身不能新增属性,但是可以在它的原型对象上新增属性,就依然能够在obj上读到。一种解决方案是,把obj的原型也冻结住 var obj = new Object(); Object.preventExtensions(obj); var proto = Object.getPrototypeOf(obj); Object.preventExtensions(proto); proto.t = 'hello'; obj.t // undefined 另外一个局限是,如果属性值是对象,上面这些方法只能冻结属性指向的对象,而不能冻结对象本身的内容 var obj = { foo: 1, bar: ['a', 'b'] }; Object.freeze(obj); obj.bar.push('c'); obj.bar // ["a", "b", "c"] 上面代码中,obj.bar属性指向一个数组,obj对象被冻结以后,这个指向无法改变,即无法指向其他值,但是所指向的数组是可以改变的 Array 对象 构造函数 Array是 JavaScript 的原生对象,同时也是一个构造函数,可以用它生成新的数组 var arr = new Array(2); arr.length // 2 arr // [ empty x 2 ] 上面代码中,Array构造函数的参数2,表示生成一个两个成员的数组,每个位置都是空值。如果没有使用new,运行结果也是一样的 var arr = new Array(2); // 等同于 var arr = Array(2); Array构造函数有一个很大的缺陷,就是不同的参数,会导致它的行为不一致 // 无参数时,返回一个空数组 new Array() // [] // 单个正整数参数,表示返回的新数组的长度 new Array(1) // [ empty ] new Array(2) // [ empty x 2 ] // 非正整数的数值作为参数,会报错 new Array(3.2) // RangeError: Invalid array length new Array(-3) // RangeError: Invalid array length // 单个非数值(比如字符串、布尔值、对象等)作为参数,则该参数是返回的新数组的成员 new Array('abc') // ['abc'] new Array([1]) // [Array[1]] // 多参数时,所有参数都是返回的新数组的成员 new Array(1, 2) // [1, 2] new Array('a', 'b', 'c') // ['a', 'b', 'c'] 可以看到,Array作为构造函数,行为很不一致。因此,不建议使用它生成新数组,直接使用数组字面量是更好的做法 // bad var arr = new Array(1, 2); // good var arr = [1, 2]; 注意,如果参数是一个正整数,返回数组的成员都是空位。虽然读取的时候返回undefined,但实际上该位置没有任何值。虽然可以取到length属性,但是取不到键名 var a = new Array(3); var b = [undefined, undefined, undefined]; a.length // 3 b.length // 3 a[0] // undefined b[0] // undefined 0 in a // false 0 in b // true 上面代码中,a是一个长度为3的空数组,b是一个三个成员都是undefined的数组。读取键值的时候,a和b都返回undefined,但是a的键位都是空的,b的键位是有值的 静态方法 Array.isArray() Array.isArray方法返回一个布尔值,表示参数是否为数组。它可以弥补typeof运算符的不足 var arr = [1, 2, 3]; typeof arr // "object" Array.isArray(arr) // true 实例方法 valueOf(),toString() valueOf方法是一个所有对象都拥有的方法,表示对该对象求值。不同对象的valueOf方法不尽一致,数组的valueOf方法返回数组本身。toString方法也是对象的通用方法,数组的toString方法返回数组的字符串形式 var arr = [1, 2, 3, [4, 5, 6]]; arr.valueOf() // [1, 2, 3, [4, 5, 6]] arr.toString() // "1,2,3,4,5,6" push(),pop() push方法用于在数组的末端添加一个或多个元素,并返回添加新元素后的数组长度。pop方法用于删除数组的最后一个元素,并返回该元素。注意,这两个方法都会改变原数组 var arr = []; arr.push(1) // 1 arr.push('a') // 2 arr.push(true, {},'c') // 4 arr // [1, 'a', true, {}] arr.pop() // 'c' arr // [1, 'a', true, {}] 对空数组使用pop方法,不会报错,而是返回undefined [].pop() // undefined push和pop结合使用,就构成了“后进先出”的栈结构(stack) shift(),unshift() shift()方法用于删除数组的第一个元素,并返回该元素。unshift()方法用于在数组的第一个位置添加元素,并返回添加新元素后的数组长度。注意,这两个方法都会改变原数组 var a = ['a', 'b', 'c']; a.shift() // 'a' a // ['b', 'c'] a.unshift('x','y'); // 3 a // ['x','y', 'b', 'c'] unshift()方法可以接受多个参数,这些参数都会添加到目标数组头部。shift()方法可以遍历并清空一个数组 var list = [1, 2, 3, 4]; var item; while (item = list.shift()) { console.log(list); } list // [] 上面代码通过list.shift()方法每次取出一个元素,从而遍历数组。它的前提是数组元素不能是0或任何布尔值等于false的元素,因此这样的遍历不是很可靠。push()和shift()结合使用,就构成了“先进先出”的队列结构(queue) join() join()方法以指定参数作为分隔符,将所有数组成员连接为一个字符串返回。如果不提供参数,默认用逗号分隔;如果数组成员是undefined或null或空位,会被转成空字符串 var a = [1,,2, 3, undefined, null, 4]; a.join(' ') // '1 2 3 4' a.join(' | ') // '1 | | 2 | 3 | | | 4' a.join() // '1,,2,3,,,4' 通过call方法,这个方法也可以用于字符串或类似数组的对象 Array.prototype.join.call('hello', '-') // "h-e-l-l-o" var obj = { 0: 'a', 1: 'b', length: 2 }; Array.prototype.join.call(obj, '-') // 'a-b' concat() concat方法用于多个数组的合并。它将新数组的成员,添加到原数组成员的后部,然后返回一个新数组,原数组不变 ['hello'].concat(['world'], ['!']) // ["hello", "world", "!"] [].concat({a: 1}, {b: 2}) // [{ a: 1 }, { b: 2 }] [2].concat({a: 1}) // [2, {a: 1}] 除了数组作为参数,concat也接受其他类型的值作为参数,添加到目标数组尾部。如果数组成员包括对象,concat方法返回当前数组的一个浅拷贝。所谓“浅拷贝”,指的是新数组拷贝的是对象的引用 var obj = { a: 1 }; var oldArray = [obj]; var newArray = oldArray.concat(); obj.a = 2; newArray[0].a // 2 上面代码中,原数组包含一个对象,concat方法生成的新数组包含这个对象的引用。所以,改变原对象以后,新数组跟着改变 reverse() reverse方法用于颠倒排列数组元素,返回改变后的数组。注意,该方法将改变原数组 var a = ['a', 'b', 'c']; a.reverse() // ["c", "b", "a"] a // ["c", "b", "a"] slice() slice方法用于提取目标数组的一部分,返回一个新数组,原数组不变 arr.slice(start, end); 它的第一个参数为起始位置(从0开始),第二个参数为终止位置(但该位置的元素本身不包括在内)。如果省略第二个参数,则一直返回到原数组的最后一个成员 var a = ['a', 'b', 'c']; a.slice(1) // ["b", "c"] a.slice(2, 6) // ["c"] a.slice() // ["a", "b", "c"] a.slice(-2, -1) // ["b"] slice没有参数则等于返回一个原数组的拷贝;如果slice方法的参数是负数,则表示倒数计算的位置;如果第一个参数大于等于数组长度或者第二个参数小于第一个参数,则返回空数组;slice方法的一个重要应用,是将类似数组的对象转为真正的数组 Array.prototype.slice.call({ 0: 'a', 1: 'b', length: 2 }) // ['a', 'b'] Array.prototype.slice.call(document.querySelectorAll("div")); Array.prototype.slice.call(arguments); 上面代码的参数都不是数组,但是通过call方法,在它们上面调用slice方法,就可以把它们转为真正的数组 splice() splice方法用于删除原数组的一部分成员,并可以在删除的位置添加新的数组成员,返回值是被删除的元素。注意,该方法会改变原数组 arr.splice(start, count, addElement1, addElement2, ...); splice的第一个参数是删除的起始位置(从0开始),第二个参数是被删除的元素个数。如果后面还有更多参数,则表示这些就是要被插入数组的新元素;起始位置如果是负数,就表示从倒数位置开始删除 var a = ['a', 'b', 'c', 'd', 'e', 'f']; a.splice(4, 2, 1, 2) // ["e", "f"] a // ["a", "b", "c", "d", 1, 2] a.splice(-4, 2) // ["c", "d"] a // ["a", "b", 1, 2] 如果只是单纯地插入元素,splice方法的第二个参数可以设为0;如果只提供第一个参数,等同于将原数组在指定位置拆分成两个数组 var a = [1, 2, 3, 4]; a.splice(2) // [3, 4] a // [1, 2] sort() sort方法对数组成员进行排序,默认是按照字典顺序排序。排序后,原数组将被改变 ['d', 'c', 'b', 'a'].sort() // ['a', 'b', 'c', 'd'] [4, 3, 2, 1].sort() // [1, 2, 3, 4] [11, 101].sort() // [101, 11] [10111, 1101, 111].sort() // [10111, 1101, 111] 需要特殊注意:sort方法不是按照大小排序,而是按照字典顺序;也就是说,数值会被先转成字符串,再按照字典顺序进行比较,所以101排在11的前面。如果想让sort方法按照自定义方式排序,可以传入一个函数作为参数。sort的参数函数本身接受两个参数,表示进行比较的两个数组成员。如果该函数的返回值大于0,表示第一个成员排在第二个成员后面;其他情况下,都是第一个元素排在第二个元素前面 [ { name: "张三", age: 30 }, { name: "李四", age: 24 }, { name: "王五", age: 28 } ].sort(function (o1, o2) { return o1.age - o2.age; }) // [ // { name: "李四", age: 24 }, // { name: "王五", age: 28 }, // { name: "张三", age: 30 } // ] map() map方法将数组的所有成员依次传入参数函数,然后把每一次的执行结果组成一个新数组返回,原数组没有变化 var numbers = [1, 2, 3]; numbers.map(function (n) { return n + 1; }); // [2, 3, 4] numbers // [1, 2, 3] map方法接受一个函数作为参数。该函数调用时,map方法向它传入三个参数:当前成员、当前位置和数组本身 [1, 2, 3].map(function(elem, index, arr) { return elem * index; }); // [0, 2, 6] map方法还可以接受第二个参数,用来绑定回调函数内部的this变量 var arr = ['a', 'b', 'c']; [1, 2].map(function (e) { return this[e]; }, arr) // ['b', 'c'] 上面代码通过map方法的第二个参数,将回调函数内部的this对象,指向arr数组。如果数组有空位,map方法的回调函数在这个位置不会执行,会跳过数组的空位 var f = function (n) { return 'a' }; [1, undefined, 2].map(f) // ["a", "a", "a"] [1, null, 2].map(f) // ["a", "a", "a"] [1, , 2].map(f) // ["a", , "a"] 上面代码中,map方法不会跳过undefined和null,但是会跳过空位 forEach() forEach方法与map方法很相似,也是对数组的所有成员依次执行参数函数。但是,forEach方法不返回值,只用来操作数据。这就是说,如果数组遍历的目的是为了得到返回值,那么使用map方法,否则使用forEach方法。forEach的用法与map方法一致,参数是一个函数,该函数同样接受三个参数:当前值、当前位置、整个数组 function log(element, index, array) { console.log('[' + index + '] = ' + element); } [2, 5, 9].forEach(log); // [0] = 2 // [1] = 5 // [2] = 9 上面代码中,forEach遍历数组不是为了得到返回值,而是为了在屏幕输出内容,所以不必使用map方法。forEach方法也可以接受第二个参数,绑定参数函数的this变量 var out = []; [1, 2, 3].forEach(function(elem) { this.push(elem * elem); }, out); out // [1, 4, 9] 上面代码中,空数组out是forEach方法的第二个参数,结果,回调函数内部的this关键字就指向out。注意,forEach方法无法中断执行,总是会将所有成员遍历完。如果希望符合某种条件时就中断遍历要使用for循环 var arr = [1, 2, 3]; for (var i = 0; i < arr.length; i++) { if (arr[i] === 2) break; console.log(arr[i]); } // 1 上面代码中,执行到数组的第二个成员时,就会中断执行,forEach方法做不到这一点。forEach方法不会跳过undefined和null,但会跳过空位 var log = function (n) { console.log(n + 1); }; [1, undefined, 2].forEach(log) // 2 NaN 3 [1, null, 2].forEach(log) // 2 1 3 [1, , 2].forEach(log)// 2 3 filter() filter方法用于过滤数组成员,满足条件的成员组成一个新数组返回。它的参数是一个函数,所有数组成员依次执行该函数,返回结果为true的成员组成一个新数组返回。该方法不会改变原数组 [1, 2, 3, 4, 5].filter(function (elem) { return (elem > 3); }) // [4, 5] filter方法的参数函数可以接受三个参数:当前成员,当前位置和整个数组 [1, 2, 3, 4, 5].filter(function (elem, index, arr) { return index % 2 === 0; }); // [1, 3, 5] filter方法还可以接受第二个参数,用来绑定参数函数内部的this变量 var obj = { MAX: 3 }; var myFilter = function (item) { if (item > this.MAX) return true; }; var arr = [2, 8, 3, 4, 1, 3, 2, 9]; arr.filter(myFilter, obj) // [8, 4, 9] 上面代码中,过滤器myFilter内部有this变量,它可以被filter方法的第二个参数obj绑定,返回大于3的成员 some(),every() 这两个方法类似“断言”(assert),返回一个布尔值,表示判断数组成员是否符合某种条件。它们接受一个函数作为参数,所有数组成员依次执行该函数。该函数接受三个参数:当前成员、当前位置和整个数组,然后返回一个布尔值。 some方法是只要一个成员的返回值是true,则整个some方法的返回值就是true,否则返回false。every方法是所有成员的返回值都是true,整个every方法才返回true,否则返回false var arr = [1, 2, 3, 4, 5]; arr.some(function (elem, index, arr) { return elem >= 3; }); // true arr.every(function (elem, index, arr) { return elem >= 3; }); // false some和every方法还可以接受第二个参数,用来绑定参数函数内部的this变量 function isEven(x) { return x % 2 === 0 } [].some(isEven) // false [].every(isEven) // true reduce(),reduceRight() reduce方法和reduceRight方法依次处理数组的每个成员,最终累计为一个值。它们的差别是,reduce是从左到右处理(从第一个成员到最后一个成员),reduceRight则是从右到左(从最后一个成员到第一个成员),其他完全一样 [1, 2, 3, 4, 5].reduce(function (a, b) { console.log(a, b); return a + b; }) // 1 2 // 3 3 // 6 4 // 10 5 //最后结果:15 reduce方法和reduceRight方法的第一个参数都是一个函数。该函数接受以下四个参数。 1.累积变量,默认为数组的第一个成员 2.当前变量,默认为数组的第二个成员 3.当前位置(从0开始) 4.原数组 这四个参数之中,只有前两个是必须的,后两个则是可选的。如果要对累积变量指定初值,可以把它放在reduce方法和reduceRight方法的第二个参数 [1, 2, 3, 4, 5].reduce(function (a, b) { return a + b; }, 10); // 25 上面的第二个参数相当于设定了默认值,处理空数组时尤其有用 function add(prev, cur) { return prev + cur; } [].reduce(add) // TypeError: Reduce of empty array with no initial value [].reduce(add, 1) // 1 上面代码中,由于空数组取不到初始值,reduce方法会报错。这时,加上第二个参数,就能保证总是会返回一个值 function subtract(prev, cur) { return prev - cur; } [3, 2, 1].reduce(subtract) // 0 [3, 2, 1].reduceRight(subtract) // -4 由于这两个方法会遍历数组,所以实际上还可以用来做一些遍历相关的操作。比如,找出字符长度最长的数组成员 function findLongest(entries) { return entries.reduce(function (longest, entry) { return entry.length > longest.length ? entry : longest; }, ''); } findLongest(['aaa', 'bb', 'c']) // "aaa" 上面代码中,reduce的参数函数会将字符长度较长的那个数组成员,作为累积值。这导致遍历所有成员之后,累积值就是字符长度最长的那个成员 indexOf(),lastIndexOf() indexOf方法返回给定元素在数组中第一次出现的位置,如果没有出现则返回-1 var a = ['a', 'b', 'c']; a.indexOf('b') // 1 a.indexOf('y') // -1 indexOf方法还可以接受第二个参数,表示搜索的开始位置 ['a', 'b', 'c'].indexOf('a', 1) // -1 上面代码从1号位置开始搜索字符a,结果为-1,表示没有搜索到 lastIndexOf方法返回给定元素在数组中最后一次出现的位置,如果没有出现则返回-1 var a = [2, 5, 9, 2]; a.lastIndexOf(2) // 3 a.lastIndexOf(7) // -1 注意,这两个方法不能用来搜索NaN的位置,即它们无法确定数组成员是否包含NaN [NaN].indexOf(NaN) // -1 [NaN].lastIndexOf(NaN) // -1 这是因为这两个方法内部,使用严格相等运算符(===)进行比较,而NaN是唯一一个不等于自身的值 链式使用 上面这些数组方法之中,有不少返回的还是数组,所以可以链式使用 var users = [ {name: 'tom', email: 'tom@example.com'}, {name: 'peter', email: 'peter@example.com'} ]; users.map(function (user) { return user.email; }).filter(function (email) { return /^t/.test(email); }) .forEach(function (email) { console.log(email); }); // "tom@example.com" 上面代码中,先产生一个所有 Email 地址组成的数组,然后再过滤出以t开头的 Email 地址,最后将它打印出来 包装对象 定义 对象是 JavaScript 语言最主要的数据类型,三种原始类型的值——数值、字符串、布尔值——在一定条件下,也会自动转为对象,也就是原始类型的“包装对象”。所谓“包装对象”,就是分别与数值、字符串、布尔值相对应的Number、String、Boolean三个原生对象。这三个原生对象可以把原始类型的值变成(包装成)对象 var v1 = new Number(123); var v2 = new String('abc'); var v3 = new Boolean(true); typeof v1 // "object" typeof v2 // "object" typeof v3 // "object" v1 === 123 // false v2 === 'abc' // false v3 === true // false 包装对象的最大目的,首先是使得 JavaScript 的对象涵盖所有的值,其次使得原始类型的值可以方便地调用某些方法。 Number、String和Boolean如果不作为构造函数调用(即调用时不加new),常常用于将任意类型的值转为数值、字符串和布尔值。即这三个对象作为构造函数使用(带有new)时,可以将原始类型的值转为对象;作为普通函数使用时(不带有new),可以将任意类型的值,转为原始类型的值 实例方法 三种包装对象各自提供了许多实例方法,这里介绍两种它们共同具有、从Object对象继承的方法:valueOf和toString valueOf() valueOf方法返回包装对象实例对应的原始类型的值 new Number(123).valueOf() // 123 new String('abc').valueOf() // "abc" new Boolean(true).valueOf() // true toString() toString方法返回对应的字符串形式 new Number(123).toString() // "123" new String('abc').toString() // "abc" new Boolean(true).toString() // "true" 原始类型与实例对象的自动转换 原始类型的值,可以自动当作包装对象调用,即调用包装对象的属性和方法。这时,JavaScript 引擎会自动将原始类型的值转为包装对象实例,在使用后立刻销毁实例。比如,字符串可以调用length属性,返回字符串的长度 var str = 'abc'; str.length // 3 // 等同于 var strObj = new String(str) // String { 0: "a", 1: "b", 2: "c", length: 3, [[PrimitiveValue]]: "abc" } strObj.length // 3 上面代码中,abc是一个字符串,本身不是对象,不能调用length属性。JavaScript 引擎自动将其转为包装对象,在这个对象上调用length属性。调用结束后,这个临时对象就会被销毁。这就叫原始类型与实例对象的自动转换 自动转换生成的包装对象是只读的,无法修改。所以,字符串无法添加新属性 var s = 'Hello World'; s.x = 123; s.x // undefined 另一方面,调用结束后,包装对象实例会自动销毁。这意味着,下一次调用字符串的属性时,实际是调用一个新生成的对象,而不是上一次调用时生成的那个对象,所以取不到赋值在上一个对象的属性。如果要为字符串添加属性,只有在它的原型对象String.prototype上定义 自定义方法 除了原生的实例方法,包装对象还可以自定义方法和属性供原始类型的值直接调用。比如,我们可以新增一个double方法,使得字符串和数字翻倍 String.prototype.double = function () { return this.valueOf() + this.valueOf(); }; 'abc'.double() // abcabc Number.prototype.double = function () { return this.valueOf() + this.valueOf(); }; (123).double() // 246 上面代码在123外面必须要加上圆括号,否则后面的点运算符(.)会被解释成小数点。但是,这种自定义方法和属性的机制,只能定义在包装对象的原型上,如果直接对原始类型的变量添加属性,则无效 Boolean 对象 概述 Boolean对象是 JavaScript 的三个包装对象之一。作为构造函数,它主要用于生成布尔值的包装对象实例 注意,false对应的包装对象实例,布尔运算结果也是true if (new Boolean(false)) { console.log('true'); } // true if (new Boolean(false).valueOf()) { console.log('true'); } // 无输出 上面代码的第一个例子之所以得到true,是因为false对应的包装对象实例是一个对象,进行逻辑运算时,被自动转化成布尔值true(因为所有对象对应的布尔值都是true)。而实例的valueOf方法,则返回实例对应的原始值,本例为false Boolean 函数的类型转换作用 Boolean对象除了可以作为构造函数,还可以单独使用,将任意值转为布尔值。这时Boolean就是一个单纯的工具方法 Boolean(undefined) // false Boolean(null) // false Boolean(0) // false Boolean('') // false Boolean(NaN) // false Boolean(1) // true Boolean('false') // true Boolean([]) // true Boolean({}) // true Boolean(function () {}) // true Boolean(/foo/) // true 使用双重的否运算符(!)也可以将任意值转为对应的布尔值 !!undefined // false !!null // false !!0 // false !!'' // false !!NaN // false !!1 // true !!'false' // true !![] // true !!{} // true !!function(){} // true !!/foo/ // true 对于一些特殊值,Boolean对象前面加不加new,会得到完全相反的结果,必须小心 if (Boolean(false)) { console.log('true'); } // 无输出 if (new Boolean(false)) { console.log('true'); } // true if (Boolean(null)) { console.log('true'); } // 无输出 if (new Boolean(null)) { console.log('true'); } // true Number 对象 概述 Number对象是数值对应的包装对象,可以作为构造函数使用,也可以作为工具函数使用。作为构造函数时,它用于生成值为数值的对象 var n = new Number(1); typeof n // "object" 作为工具函数时,它可以将任何类型的值转为数值 Number(true) // 1 静态属性 Number对象拥有以下一些静态属性(即直接定义在Number对象上的属性,而不是定义在实例上的属性)。 //Number.POSITIVE_INFINITY:正的无限,指向Infinity Number.POSITIVE_INFINITY // Infinity //Number.NEGATIVE_INFINITY:负的无限,指向-Infinity Number.NEGATIVE_INFINITY // -Infinity //Number.NaN:表示非数值,指向NaN Number.NaN // NaN Number.MAX_VALUE // 1.7976931348623157e+308 Number.MAX_VALUE < Infinity // true //Number.MIN_VALUE:表示最小的正数(即最接近0的正数,在64位浮点数体系中为5e-324),相应的,最接近0的负数为-Number.MIN_VALUE Number.MIN_VALUE // 5e-324 Number.MIN_VALUE > 0 // true //Number.MAX_SAFE_INTEGER:表示能够精确表示的最大整数,即9007199254740991 Number.MAX_SAFE_INTEGER // 9007199254740991 //Number.MIN_SAFE_INTEGER:表示能够精确表示的最小整数,即-9007199254740991 Number.MIN_SAFE_INTEGER // -9007199254740991 实例方法 Number对象有4个实例方法,都跟将数值转换成指定格式有关 Number.prototype.toString() Number对象部署了自己的toString方法,用来将一个数值转为字符串形式。toString方法可以接受一个参数,表示输出的进制。如果省略这个参数,默认将数值先转为十进制,再输出字符串;否则,就根据参数指定的进制,将一个数字转化成某个进制的字符串 (10).toString() // "10" (10).toString(2) // "1010" (10).toString(8) // "12" (10).toString(16) // "a" 只要能够让 JavaScript 引擎不混淆小数点和对象的点运算符,各种写法都能用。除了为10加上括号,还可以在10后面加两个点,JavaScript 会把第一个点理解成小数点(即10.0),把第二个点理解成调用对象属性,从而得到正确结果 10..toString(2) // "1010" // 其他方法还包括 10 .toString(2) // "1010" 10.0.toString(2) // "1010" 这实际上意味着,可以直接对一个小数使用toString方法。通过方括号运算符也可以调用toString方法 10['toString'](2) // "1010" toString方法只能将十进制的数,转为其他进制的字符串。如果要将其他进制的数,转回十进制,需要使用parseInt方法 Number.prototype.toFixed() toFixed方法先将一个数转为指定位数的小数,然后返回这个小数对应的字符串 (10).toFixed(2) // "10.00" 10.005.toFixed(2) // "10.01" toFixed方法的参数为小数位数,有效范围为0到20,超出这个范围将抛出 RangeError 错误 Number.prototype.toExponential() toExponential方法用于将一个数转为科学计数法形式 (10).toExponential() // "1e+1" (10).toExponential(1) // "1.0e+1" (10).toExponential(2) // "1.00e+1" (1234).toExponential() // "1.234e+3" (1234).toExponential(1) // "1.2e+3" (1234).toExponential(2) // "1.23e+3" toExponential方法的参数是小数点后有效数字的位数,范围为0到20,超出这个范围,会抛出一个 RangeError 错误 Number.prototype.toPrecision() toPrecision方法用于将一个数转为指定位数的有效数字 (12.35).toPrecision(1) // "1e+1" (12.35).toPrecision(2) // "12" (12.35).toPrecision(3) // "12.3" (12.25).toPrecision(3) // "12.3" (12.35).toPrecision(4) // "12.35" (12.35).toPrecision(5) // "12.350" toPrecision方法的参数为有效数字的位数,范围是1到21,超出这个范围会抛出 RangeError 错误。toPrecision方法用于四舍五入时不太可靠,跟浮点数不是精确储存有关 自定义方法 与其他对象一样,Number.prototype对象上面可以自定义方法,被Number的实例继承。由于add方法返回的还是数值,所以可以链式运算 Number.prototype.add = function (x) { return this + x; }; Number.prototype.subtract = function (x) { return this - x; }; (8).add(2).subtract(4) // 6 我们还可以部署更复杂的方法 Number.prototype.iterate = function () { var result = []; for (var i = 0; i <= this; i++) { result.push(i); } return result; }; (8).iterate() // [0, 1, 2, 3, 4, 5, 6, 7, 8] 注意,数值的自定义方法,只能定义在它的原型对象Number.prototype上面,数值本身是无法自定义属性的 var n = 1; n.x = 1; n.x // undefined 上面代码中,n是一个原始类型的数值。直接在它上面新增一个属性x,不会报错,但毫无作用,总是返回undefined。这是因为一旦被调用属性,n就自动转为Number的实例对象,调用结束后,该对象自动销毁。所以,下一次调用n的属性时,实际取到的是另一个对象,属性x当然就读不出来 String 对象 概述 String对象是 JavaScript 原生提供的三个包装对象之一,用来生成字符串对象 var s1 = 'abc'; var s2 = new String('abc'); typeof s1 // "string" typeof s2 // "object" s2.valueOf() // "abc" 字符串对象是一个类似数组的对象(很像数组,但不是数组) new String('abc') // String {0: "a", 1: "b", 2: "c", length: 3} (new String('abc'))[1] // "b" 除了用作构造函数,String对象还可以当作工具方法使用,将任意类型的值转为字符串 静态方法 String.fromCharCode() String对象提供的静态方法(即定义在对象本身,而不是定义在对象实例的方法),主要是String.fromCharCode()。该方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串 String.fromCharCode() // "" String.fromCharCode(97) // "a" String.fromCharCode(104, 101, 108, 108, 111) // "hello" String.fromCharCode方法的参数为空,就返回空字符串;否则,返回参数对应的 Unicode 字符串。 注意,该方法不支持 Unicode 码点大于0xFFFF的字符,即传入的参数不能大于0xFFFF(即十进制的 65535) String.fromCharCode(0x20BB7) // "ஷ" String.fromCharCode(0x20BB7) === String.fromCharCode(0x0BB7) // true 上面代码中,String.fromCharCode参数0x20BB7大于0xFFFF,导致返回结果出错。0x20BB7对应的字符是汉字𠮷,但是返回结果却是另一个字符(码点0x0BB7)。这是因为String.fromCharCode发现参数值大于0xFFFF,就会忽略多出的位(即忽略0x20BB7里面的2)。这种现象的根本原因在于,码点大于0xFFFF的字符占用四个字节,而 JavaScript 默认支持两个字节的字符。这种情况下,必须把0x20BB7拆成两个字符表示 String.fromCharCode(0xD842, 0xDFB7) // "𠮷" 实例属性 String.prototype.length 字符串实例的length属性返回字符串的长度 'abc'.length // 3 实例方法 String.prototype.charAt() charAt方法返回指定位置的字符,参数是从0开始编号的位置;这个方法也完全可以用数组下标替代;如果参数为负数,或大于等于字符串的长度,charAt返回空字符串 var s = new String('abc'); s.charAt(1) // "b" s.charAt(s.length - 1) // "c" 'abc'[1] // "b" 'abc'.charAt(-1) // "" String.prototype.charCodeAt() charCodeAt方法返回字符串指定位置的 Unicode 码点(十进制表示),相当于String.fromCharCode()的逆操作;如果没有任何参数,charCodeAt返回首字符的 Unicode 码点;如果参数为负数,或大于等于字符串的长度,charCodeAt返回NaN 'abc'.charCodeAt(1) // 98 'abc'.charCodeAt() // 97 'abc'.charCodeAt(-1) // NaN 注意,charCodeAt方法返回的 Unicode 码点不会大于65536(0xFFFF),也就是说,只返回两个字节的字符的码点。如果遇到码点大于 65536 的字符(四个字节的字符),必需连续使用两次charCodeAt,不仅读入charCodeAt(i),还要读入charCodeAt(i+1),将两个值放在一起,才能得到准确的字符 String.prototype.concat() concat方法用于连接两个字符串,返回一个新字符串,不改变原字符串;该方法可以接受多个参数 var s1 = 'abc'; var s2 = 'def'; s1.concat(s2) // "abcdef" s1 // "abc" 'a'.concat('b', 'c') // "abc" 如果参数不是字符串,concat方法会将其先转为字符串,然后再连接 var one = 1; var two = 2; var three = '3'; ''.concat(one, two, three) // "123" one + two + three // "33" String.prototype.slice() slice方法用于从原字符串取出子字符串并返回,不改变原字符串。它的第一个参数是子字符串的开始位置,第二个参数是子字符串的结束位置(不含该位置);如果省略第二个参数,则表示子字符串一直到原字符串结束;如果参数是负值,表示从结尾开始倒数计算的位置,即该负值加上字符串长度;如果第一个参数大于第二个参数,slice方法返回一个空字符串 'JavaScript'.slice(0, 4) // "Java" 'JavaScript'.slice(4) // "Script" 'JavaScript'.slice(-6) // "Script" 'JavaScript'.slice(-2, -1) // "p" 'JavaScript'.slice(2, 1) // "" String.prototype.substring() substring方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice方法很相像。它的第一个参数表示子字符串的开始位置,第二个位置表示结束位置(返回结果不含该位置);如果省略第二个参数,则表示子字符串一直到原字符串的结束;如果第一个参数大于第二个参数,substring方法会自动更换两个参数的位置;如果参数是负数,substring方法会自动将负数转为0 'JavaScript'.substring(0, 4) // "Java" 'JavaScript'.substring(4) // "Script" 'JavaScript'.substring(10, 4) // "Script" // 等同于 'JavaScript'.substring(4, 10) // "Script" 'JavaScript'.substring(4, -3) // "Java" 由于substring某些规则违反直觉,因此不建议使用substring方法,应该优先使用slice String.prototype.substr() substr方法用于从原字符串取出子字符串并返回,不改变原字符串,跟slice和substring方法的作用相同。substr方法的第一个参数是子字符串的开始位置(从0开始计算),第二个参数是子字符串的长度;如果省略第二个参数,则表示子字符串一直到原字符串的结束;如果第一个参数是负数,表示倒数计算的字符位置。如果第二个参数是负数,将被自动转为0,因此会返回空字符串 'JavaScript'.substr(4, 6) // "Script" 'JavaScript'.substr(4) // "Script" 'JavaScript'.substr(-6) // "Script" 'JavaScript'.substr(4, -1) // "" String.prototype.indexOf(),String.prototype.lastIndexOf() indexOf方法用于确定一个字符串在另一个字符串中第一次出现的位置,返回结果是匹配开始的位置。如果返回-1,就表示不匹配;indexOf方法还可以接受第二个参数,表示从该位置开始向后匹配 'hello world'.indexOf('o') // 4 'JavaScript'.indexOf('script') // -1 'hello world'.indexOf('o', 6) // 7 lastIndexOf方法的用法跟indexOf方法一致,主要的区别是lastIndexOf从尾部开始匹配,indexOf则是从头部开始匹配;另外,lastIndexOf的第二个参数表示从该位置起向前匹配 'hello world'.lastIndexOf('o') // 7 'hello world'.lastIndexOf('o', 6) // 4 String.prototype.trim() trim方法用于去除字符串两端的空格,返回一个新字符串,不改变原字符串;该方法去除的不仅是空格,还包括制表符(t、v)、换行符(n)和回车符(r) ' hello world '.trim() // "hello world" '\r\nabc \t'.trim() // 'abc' String.prototype.toLowerCase(),String.prototype.toUpperCase() toLowerCase方法用于将一个字符串全部转为小写,toUpperCase则是全部转为大写。它们都返回一个新字符串,不改变原字符串 'Hello World'.toLowerCase() // "hello world" 'Hello World'.toUpperCase() // "HELLO WORLD" String.prototype.match() match方法用于确定原字符串是否匹配某个子字符串,返回一个数组,成员为匹配的第一个字符串。如果没有找到匹配,则返回null;返回的数组还有index属性和input属性,分别表示匹配字符串开始的位置和原始字符串 var matches = 'cat, bat, sat, fat'.match('at'); 'cat, bat, sat, fat'.match('at') // ["at"] 'cat, bat, sat, fat'.match('xt') // null matches.index // 1 matches.input // "cat, bat, sat, fat" String.prototype.search(),String.prototype.replace() search方法的用法基本等同于match,但是返回值为匹配的第一个位置。如果没有找到匹配,则返回-1;search方法还可以使用正则表达式作为参数 'cat, bat, sat, fat'.search('at') // 1 replace方法用于替换匹配的子字符串,一般情况下只替换第一个匹配(除非使用带有g修饰符的正则表达式);replace方法还可以使用正则表达式作为参数 'aaa'.replace('a', 'b') // "baa" String.prototype.split() split方法按照给定规则分割字符串,返回一个由分割出来的子字符串组成的数组;如果分割规则为空字符串,则返回数组的成员是原字符串的每一个字符;如果省略参数,则返回数组的唯一成员就是原字符串;如果满足分割规则的两个部分紧邻着(即两个分割符中间没有其他字符),则返回数组之中会有一个空字符串;如果满足分割规则的部分处于字符串的开头或结尾(即它的前面或后面没有其他字符),则返回数组的第一个或最后一个成员是一个空字符串;split方法还可以接受第二个参数,限定返回数组的最大成员数;split方法还可以使用正则表达式作为参数 'a|b|c'.split('|') // ["a", "b", "c"] 'a|b|c'.split('') // ["a", "|", "b", "|", "c"] 'a|b|c'.split() // ["a|b|c"] 'a||c'.split('|') // ['a', '', 'c'] '|b|c'.split('|') // ["", "b", "c"] 'a|b|'.split('|') // ["a", "b", ""] 'a|b|c'.split('|', 2) // ["a", "b"] 'a|b|c'.split('|', 3) // ["a", "b", "c"] 'a|b|c'.split('|', 4) // ["a", "b", "c"] String.prototype.localeCompare() localeCompare方法用于比较两个字符串。它返回一个整数,如果小于0,表示第一个字符串小于第二个字符串;如果等于0,表示两者相等;如果大于0,表示第一个字符串大于第二个字符串;该方法的最大特点,就是会考虑自然语言的顺序。举例来说,正常情况下,大写的英文字母小于小写字母 'apple'.localeCompare('banana') // -1 'apple'.localeCompare('apple') // 0 'B' > 'a' // false JavaScript 采用的是 Unicode 码点比较,B的码点是66,而a的码点是97。但是,localeCompare方法会考虑自然语言的排序情况,将B排在a的前面。 'B'.localeCompare('a') // 1 上面代码中,localeCompare方法返回整数1,表示B较大。localeCompare还可以有第二个参数,指定所使用的语言(默认是英语),然后根据该语言的规则进行比较 'ä'.localeCompare('z', 'de') // -1 'ä'.localeCompare('z', 'sv') // 1 上面代码中,de表示德语,sv表示瑞典语。德语中,ä小于z,所以返回-1;瑞典语中,ä大于z,所以返回1 Math 对象 Math是 JavaScript 的原生对象,提供各种数学功能。该对象不是构造函数,不能生成实例,所有的属性和方法都必须在Math对象上调用 静态属性 Math对象的静态属性,提供以下一些数学常数。 //Math.E:常数e Math.E // 2.718281828459045 //Math.LN2:2 的自然对数 Math.LN2 // 0.6931471805599453 //Math.LN10:10 的自然对数 Math.LN10 // 2.302585092994046 //Math.LOG2E:以 2 为底的e的对数 Math.LOG2E // 1.4426950408889634 //Math.LOG10E:以 10 为底的e的对数 Math.LOG10E // 0.4342944819032518 //Math.PI:常数π Math.PI // 3.141592653589793 //Math.SQRT1_2:0.5 的平方根 Math.SQRT1_2 // 0.7071067811865476 //Math.SQRT2:2 的平方根 Math.SQRT2 // 1.4142135623730951 这些属性都是只读的,不能修改 静态方法 Math对象提供以下一些静态方法。 Math.abs():绝对值 Math.ceil():向上取整 Math.floor():向下取整 Math.max():最大值 Math.min():最小值 Math.pow():指数运算 Math.sqrt():平方根 Math.log():自然对数 Math.exp():e的指数 Math.round():四舍五入 Math.random():随机数 Math.abs() Math.abs方法返回参数值的绝对值 Math.max(),Math.min() Math.max方法返回参数之中最大的那个值,Math.min返回最小的那个值。如果参数为空, Math.min返回Infinity, Math.max返回-Infinity Math.max(2, -1, 5) // 5 Math.min(2, -1, 5) // -1 Math.min() // Infinity Math.max() // -Infinity Math.floor(),Math.ceil() Math.floor方法返回小于参数值的最大整数(地板值);Math.ceil方法返回大于参数值的最小整数(天花板值) Math.floor(3.2) // 3 Math.floor(-3.2) // -4 Math.ceil(3.2) // 4 Math.ceil(-3.2) // -3 这两个方法可以结合起来,实现一个总是返回数值的整数部分的函数 function ToInteger(x) { x = Number(x); return x < 0 ? Math.ceil(x) : Math.floor(x); } ToInteger(3.2) // 3 ToInteger(3.5) // 3 ToInteger(3.8) // 3 ToInteger(-3.2) // -3 ToInteger(-3.5) // -3 ToInteger(-3.8) // -3 Math.round() Math.round方法用于四舍五入;要注意它对负数的处理(主要是对0.5的处理) Math.round(0.1) // 0 Math.round(0.5) // 1 Math.round(0.6) // 1 Math.round(-1.1) // -1 Math.round(-1.5) // -1 Math.round(-1.6) // -2 Math.pow() Math.pow方法返回以第一个参数为底数、第二个参数为幂的指数值 Math.pow(2, 3) // 8 var r = 20; var area = Math.PI * Math.pow(r, 2) //计算圆面积 Math.sqrt() Math.sqrt方法返回参数值的平方根。如果参数是一个负值,则返回NaN Math.sqrt(4) // 2 Math.sqrt(-4) // NaN Math.log() Math.log方法返回以e为底的自然对数值。如果要计算以10为底的对数,可以先用Math.log求出自然对数,然后除以Math.LN10;求以2为底的对数,可以除以Math.LN2 Math.log(Math.E) // 1 Math.log(10) // 2.302585092994046 Math.log(100)/Math.LN10 // 2 Math.log(8)/Math.LN2 // 3 Math.exp() Math.exp方法返回常数e的参数次方 Math.exp(1) // 2.718281828459045 Math.exp(3) // 20.085536923187668 Math.random() Math.random()返回0到1之间的一个伪随机数,可能等于0,但是一定小于1 //任意范围的随机数生成函数 function getRandomArbitrary(min, max) { return Math.random() * (max - min) + min; } getRandomArbitrary(1.5, 6.5) // 2.4942810038223864 //任意范围的随机整数生成函数 function getRandomInt(min, max) { return Math.floor(Math.random() * (max - min + 1)) + min; } getRandomInt(1, 6) // 5 三角函数方法 Math对象还提供一系列三角函数方法 //Math.sin():返回参数的正弦(参数为弧度值) Math.sin(0) // 0 Math.sin(Math.PI / 2) // 1 //Math.cos():返回参数的余弦(参数为弧度值) Math.cos(0) // 1 //Math.tan():返回参数的正切(参数为弧度值) Math.tan(0) // 0 //Math.asin():返回参数的反正弦(返回值为弧度值) Math.asin(1) // 1.5707963267948966 //Math.acos():返回参数的反余弦(返回值为弧度值) Math.acos(1) // 0 //Math.atan():返回参数的反正切(返回值为弧度值) Math.atan(1) // 0.7853981633974483 Date 对象 Date对象是 JavaScript 原生的时间库。它以国际标准时间(UTC)1970年1月1日00:00:00作为时间的零点,可以表示的时间范围是前后各1亿天(单位为毫秒) 普通函数的用法 Date对象可以作为普通函数直接调用,返回一个代表当前时间的字符串 注意,即使带有参数,Date作为普通函数使用时,返回的还是当前时间 Date() // "Tue Mar 12 2019 13:53:36 GMT+0800 (中国标准时间)" Date(2000, 1, 1) // "Tue Mar 12 2019 13:53:36 GMT+0800 (中国标准时间)" 构造函数的用法 Date还可以当作构造函数使用。对它使用new命令,会返回一个Date对象的实例。如果不加参数,实例代表的就是当前时间 var today = new Date(); Date实例有一个独特的地方。其他对象求值的时候,都是默认调用.valueOf()方法,但是Date实例求值的时候,默认调用的是toString()方法。这导致对Date实例求值,返回的是一个字符串,代表该实例对应的时间 var today = new Date(); today // "Tue Mar 12 2019 13:56:06 GMT+0800 (中国标准时间)" // 等同于 today.toString() // "Tue Mar 12 2019 13:56:06 GMT+0800 (中国标准时间)" 上面代码中,today是Date的实例,直接求值等同于调用toString方法。作为构造函数时,Date对象可以接受多种格式的参数,返回一个该参数对应的时间实例 // 参数为时间零点开始计算的毫秒数 new Date(1546704000000) // Sun Jan 06 2019 00:00:00 GMT+0800 (中国标准时间) // 参数为日期字符串 new Date('January 6, 2019'); // Sun Jan 06 2019 00:00:00 GMT+0800 (中国标准时间) // 参数为多个整数,代表年、月、日、小时、分钟、秒、毫秒 new Date(2019, 0, 1, 0, 0, 0, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间) 关于Date构造函数的参数,有几点说明: 1.参数可以是负整数,代表1970年元旦之前的时间 new Date(-1378218728000) // Fri Apr 30 1926 17:27:52 GMT+0800 (中国标准时间) 2.只要是能被Date.parse()方法解析的字符串,都可以当作参数 new Date('2019-2-15') new Date('2019/2/15') new Date('02/15/2019') new Date('2019-FEB-15') new Date('FEB, 15, 2019') new Date('FEB 15, 2019') new Date('February, 15, 2019') new Date('February 15, 2019') new Date('15 Feb 2019') new Date('15, February, 2019') // Fri Feb 15 2019 00:00:00 GMT+0800 (中国标准时间) 上面多种日期字符串的写法,返回的都是同一个时间 3.参数为年、月、日等多个整数时,年和月是不能省略的,其他参数都可以省略的。也就是说,这时至少需要两个参数,因为如果只使用“年”这一个参数,Date会将其解释为毫秒数2.只要是能被Date.parse()方法解析的字符串,都可以当作参数 new Date(2019) //Thu Jan 01 1970 08:00:02 GMT+0800 (中国标准时间) new Date(2019, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间) new Date(2019, 0, 1) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间) new Date(2019, 0, 1, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间) new Date(2019, 0, 1, 0, 0, 0, 0) // Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间) 上面代码中,不管有几个参数,返回的都是2019年1月1日零点 各个参数的取值范围如下: 1.年:使用四位数年份,比如2000。如果写成两位数或个位数,则加上1900,即10代表1910年。如果是负数,表示公元前。 2.月:0表示一月,依次类推,11表示12月。 3.日:1到31。 4.小时:0到23。 5.分钟:0到59。 6.秒:0到59 7.毫秒:0到999 注意:月份从0开始计算,天数从1开始计算。另外,除了日期的默认值为1,小时、分钟、秒钟和毫秒的默认值都是0。这些参数如果超出了正常范围,会被自动折算。比如,如果月设为15,就折算为下一年的4月;日期设为0,就代表上个月的最后一天;参数还可以使用负数,表示扣去的时间 new Date(2019, -1) // Sat Dec 01 2018 00:00:00 GMT+0800 (中国标准时间) new Date(2019, 0, -1) // Sun Dec 30 2018 00:00:00 GMT+0800 (中国标准时间) 日期的运算 类型自动转换时,Date实例如果转为数值,则等于对应的毫秒数;如果转为字符串,则等于对应的日期字符串。所以,两个日期实例对象进行减法运算时,返回的是它们间隔的毫秒数;进行加法运算时,返回的是两个字符串连接而成的新字符串 var d1 = new Date(2000, 2, 1); var d2 = new Date(2000, 3, 1); d2 - d1 // 2678400000 d2 + d1 // "Sat Apr 01 2000 00:00:00 GMT+0800 (CST)Wed Mar 01 2000 00:00:00 GMT+0800 (CST)" 静态方法 Date.now() Date.now方法返回当前时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数,相当于 Unix 时间戳乘以1000 Date.now() // 1364026285194 Date.parse() Date.parse方法用来解析日期字符串,返回该时间距离时间零点(1970年1月1日 00:00:00)的毫秒数。日期字符串应该符合 RFC 2822 和 ISO 8061 这两个标准,即YYYY-MM-DDTHH:mm:ss.sssZ格式,其中最后的Z表示时区。但是,其他格式也可以被解析 Date.parse('Aug 9, 1995') Date.parse('January 26, 2011 13:51:50') Date.parse('Mon, 25 Dec 1995 13:30:00 GMT') Date.parse('Mon, 25 Dec 1995 13:30:00 +0430') Date.parse('2011-10-10') Date.parse('2011-10-10T14:48:00') 上面的日期字符串都可以解析。如果解析失败,返回NaN Date.UTC() Date.UTC方法接受年、月、日等变量作为参数,返回该时间距离时间零点(1970年1月1日 00:00:00 UTC)的毫秒数 // 格式 Date.UTC(year, month[, date[, hrs[, min[, sec[, ms]]]]]) // 用法 Date.UTC(2011, 0, 1, 2, 3, 4, 567) // 1293847384567 该方法的参数用法与Date构造函数完全一致,比如月从0开始计算,日期从1开始计算。区别在于Date.UTC方法的参数,会被解释为 UTC 时间(世界标准时间),Date构造函数的参数会被解释为当前时区的时间 实例方法 Date的实例对象,有几十个自己的方法,除了valueOf和toString,可以分为以下三类 to类:从Date对象返回一个字符串,表示指定的时间。 get类:获取Date对象的日期和时间。 set类:设置Date对象的日期和时间 Date.prototype.valueOf() valueOf方法返回实例对象距离时间零点(1970年1月1日00:00:00 UTC)对应的毫秒数,该方法等同于getTime方法 var d = new Date(); d.valueOf() // 1552374623595 d.getTime() // 1552374623595 预期为数值的场合,Date实例会自动调用该方法,所以可以用该方法计算时间的间隔 to 类方法 Date.prototype.toString() toString方法返回一个完整的日期字符串。toString是默认的调用方法,所以如果直接读取Date实例,就相当于调用这个方法 var d = new Date(2019, 0, 1); d.toString() // "Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)" d // "Tue Jan 01 2019 00:00:00 GMT+0800 (中国标准时间)" Date.prototype.toUTCString() toUTCString方法返回对应的 UTC 时间,也就是比北京时间晚8个小时 var d = new Date(2019, 0, 1); d.toUTCString() // "Mon, 31 Dec 2018 16:00:00 GMT" Date.prototype.toISOString() toISOString方法返回对应时间的 ISO8601 写法 var d = new Date(2019, 0, 1); d.toISOString() // "2018-12-31T16:00:00.000Z" 注意,toISOString方法返回的总是 UTC 时区的时间 Date.prototype.toJSON() toJSON方法返回一个符合 JSON 格式的 ISO 日期字符串,与toISOString方法的返回结果完全相同 var d = new Date(2019, 0, 1); d.toJSON() // "2018-12-31T16:00:00.000Z" Date.prototype.toDateString() toDateString方法返回日期字符串(不含小时、分和秒) var d = new Date(2019, 0, 1); d.toDateString() // "Tue Jan 01 2019" Date.prototype.toTimeString() toTimeString方法返回时间字符串(不含年月日) var d = new Date(2019, 0, 1); d.toTimeString() // "00:00:00 GMT+0800 (中国标准时间)" 本地时间 以下三种方法,可以将 Date 实例转为表示本地时间的字符串。 Date.prototype.toLocaleString():完整的本地时间。 Date.prototype.toLocaleDateString():本地日期(不含小时、分和秒)。 Date.prototype.toLocaleTimeString():本地时间(不含年月日)。 这三个方法都有两个可选的参数 dateObj.toLocaleString([locales[, options]]) dateObj.toLocaleDateString([locales[, options]]) dateObj.toLocaleTimeString([locales[, options]]) 这两个参数中,locales是一个指定所用语言的字符串,options是一个配置对象 var d = new Date(2019, 0, 1); d.toLocaleString('en-US') // "1/1/2019, 12:00:00 AM" d.toLocaleString('zh-CN') // "2019/1/1 上午12:00:00" d.toLocaleDateString('en-US') // "1/1/2019" d.toLocaleDateString('zh-CN') // "2019/1/1" d.toLocaleTimeString('en-US') // "12:00:00 AM" d.toLocaleTimeString('zh-CN') // "上午12:00:00" get 类方法 1.Date对象提供了一系列get*方法,用来获取实例对象某个方面的值。 2.getTime():返回实例距离1970年1月1日00:00:00的毫秒数,等同于valueOf方法 3.getDate():返回实例对象对应每个月的几号(从1开始) 4.getDay():返回星期几,星期日为0,星期一为1,以此类推 5.getFullYear():返回四位的年份 6.getMonth():返回月份(0表示1月,11表示12月) 7.getHours():返回小时(0-23) 8.getMilliseconds():返回毫秒(0-999) 9.getMinutes():返回分钟(0-59) 10.getSeconds():返回秒(0-59) 11.getTimezoneOffset():返回当前时间与 UTC 的时区差异,以分钟表示,返回结果考虑到了夏令时因素 这些get*方法返回的都是整数,不同方法返回值的范围不一样。 分钟和秒:0 到 59 小时:0 到 23 星期:0(星期天)到 6(星期六) 日期:1 到 31 月份:0(一月)到 11(十二月) var d = new Date('January 6, 2019'); d.getDate() // 6 d.getMonth() // 0 d.getFullYear() // 2019 d.getTimezoneOffset() // -480 -480表示 UTC 比当前时间少480分钟,即当前时区比 UTC 早8个小时 上面get*方法返回的都是当前时区的时间,Date对象还提供了这些方法对应的 UTC 版本,用来返回 UTC 时间。 getUTCDate()/getUTCFullYear()/getUTCMonth()/getUTCDay()/getUTCHours()/getUTCMinutes()/getUTCSeconds()/getUTCMilliseconds() set 类方法 Date对象提供了一系列set*方法,用来设置实例对象的各个方面。 setDate(date):设置实例对象对应的每个月的几号(1-31),返回改变后毫秒时间戳 setFullYear(year [, month, date]):设置四位年份 setHours(hour [, min, sec, ms]):设置小时(0-23) setMilliseconds():设置毫秒(0-999) setMinutes(min [, sec, ms]):设置分钟(0-59) setMonth(month [, date]):设置月份(0-11) setSeconds(sec [, ms]):设置秒(0-59) setTime(milliseconds):设置毫秒时间戳 这些方法基本是跟get*方法一一对应的,但是没有setDay方法,因为星期几是计算出来的,而不是设置的。另外,需要注意的是,凡是涉及到设置月份,都是从0开始算的,即0是1月,11是12月 var d = new Date ('January 6, 2019'); d // Sun Jan 06 2019 00:00:00 GMT+0800 (中国标准时间) d.setDate(9) // 1546963200000 d // Wed Jan 09 2019 00:00:00 GMT+0800 (中国标准时间) set类方法和get类方法,可以结合使用,得到相对时间 var d = new Date(); d.setDate(d.getDate() + 1000); // 将日期向后推1000天 d.setHours(d.getHours() + 6); // 将时间设为6小时后 d.setFullYear(d.getFullYear() - 1); // 将年份设为去年 set*系列方法除了setTime(),都有对应的 UTC 版本,即设置 UTC 时区的时间 setUTCDate()/setUTCFullYear()/setUTCHours()/setUTCMilliseconds()/setUTCMinutes()/setUTCMonth()/setUTCSeconds() RegExp 对象 RegExp对象提供正则表示式的功能 概述 正则表达式(regular expression)是一种表达文本模式(即字符串结构)的方法,有点像字符串的模板,常常用来按照“给定模式”匹配文本。比如,正则表达式给出一个 Email 地址的模式,然后用它来确定一个字符串是否为 Email 地址。JavaScript 的正则表达式体系是参照 Perl 5 建立的。新建正则表达式有两种方法 1.使用字面量,以斜杠表示开始和结束 var regex = /xyz/; 2.使用RegExp构造函数 var regex = new RegExp('xyz'); 两种写法是等价的,都新建了一个内容为xyz的正则表达式对象。它们的主要区别是,第一种方法在引擎编译代码时就会新建正则表达式,第二种方法在运行时新建正则表达式,所以前者的效率较高。而且,前者比较便利和直观,所以实际应用中,基本上都采用字面量定义正则表达式。RegExp构造函数还可以接受第二个参数,表示修饰符 var regex = new RegExp('xyz', 'i'); // 等价于 var regex = /xyz/i; 实例属性 正则对象的实例属性分成两类: 1.修饰符相关,返回一个布尔值,表示对应的修饰符是否设置 (1).RegExp.prototype.ignoreCase:返回一个布尔值,表示是否设置了i修饰符 (2).RegExp.prototype.global:返回一个布尔值,表示是否设置了g修饰符 (3).RegExp.prototype.multiline:返回一个布尔值,表示是否设置了m修饰符 上面三个属性都是只读的 var r = /abc/igm; r.ignoreCase // true r.global // true r.multiline // true 2.与修饰符无关的属性 (1).RegExp.prototype.lastIndex:返回一个整数,表示下一次开始搜索的位置。该属性可读写,但是只在进行连续搜索时有意义 (2).RegExp.prototype.source:返回正则表达式的字符串形式(不包括反斜杠),该属性只读 var r = /abc/igm; r.lastIndex // 0 r.source // "abc" 实例方法 RegExp.prototype.test() 正则实例对象的test方法返回一个布尔值,表示当前模式是否能匹配参数字符串 /cat/.test('cats and dogs') // true 如果正则表达式带有g修饰符,则每一次test方法都从上一次结束的位置开始向后匹配 var r = /x/g; var s = '_x_x'; r.lastIndex // 0 r.test(s) // true r.lastIndex // 2 r.test(s) // true r.lastIndex // 4 r.test(s) // false 带有g修饰符时,可以通过正则对象的lastIndex属性指定开始搜索的位置 var r = /x/g; var s = '_x_x'; r.lastIndex = 4; r.test(s) // false r.lastIndex // 0 r.test(s) //true 上面代码指定从字符串的第五个位置开始搜索,这个位置为空,所以返回false。同时,lastIndex属性重置为0,所以第二次执行r.test(s)会返回true。 注意,带有g修饰符时,正则表达式内部会记住上一次的lastIndex属性,这时不应该更换所要匹配的字符串,否则会有一些难以察觉的错误 var r = /bb/g; r.test('bb') // true r.test('-bb-') // false lastIndex属性只对同一个正则表达式有效 var count = 0; while (/a/g.test('babaa')) count++; 上面代码会导致无限循环,因为while循环的每次匹配条件都是一个新的正则表达式,导致lastIndex属性总是等于0。如果正则模式是一个空字符串,则匹配所有字符串 new RegExp('').test('abc') // true RegExp.prototype.exec() 正则实例对象的exec方法,用来返回匹配结果。如果发现匹配,就返回一个数组,成员是匹配成功的子字符串,否则返回null var s = '_x_x'; var r1 = /x/; var r2 = /y/; r1.exec(s) // ["x"] r2.exec(s) // null 如果正则表示式包含圆括号(即含有“组匹配”),则返回的数组会包括多个成员。第一个成员是整个匹配成功的结果,后面的成员就是圆括号对应的匹配成功的组。也就是说,第二个成员对应第一个括号,第三个成员对应第二个括号,以此类推。整个数组的length属性等于组匹配的数量再加1 var s = '_x_x'; var r = /_(x)/; r.exec(s) // ["_x", "x"] exec方法的返回数组还包含以下两个属性: 1.input:整个原字符串 2.index:整个模式匹配成功的开始位置(从0开始计数) var r = /a(b+)a/; var arr = r.exec('_abbba_aba_'); arr // ["abbba", "bbb"] arr.index // 1 arr.input // "_abbba_aba_" 上面代码中的index属性等于1,是因为从原字符串的第二个位置开始匹配成功。如果正则表达式加上g修饰符,则可以使用多次exec方法,下一次搜索的位置从上一次匹配成功结束的位置开始 var reg = /a/g; var str = 'abc_abc_abc' var r1 = reg.exec(str); r1 // ["a"] r1.index // 0 reg.lastIndex // 1 var r2 = reg.exec(str); r2 // ["a"] r2.index // 4 reg.lastIndex // 5 var r3 = reg.exec(str); r3 // ["a"] r3.index // 8 reg.lastIndex // 9 var r4 = reg.exec(str); r4 // null reg.lastIndex // 0 利用g修饰符允许多次匹配的特点,可以用一个循环完成全部匹配 var reg = /a/g; var str = 'abc_abc_abc' while(true) { var match = reg.exec(str); if (!match) break; console.log('#' + match.index + ':' + match[0]); } // #0:a // #4:a // #8:a 正则实例对象的lastIndex属性不仅可读,还可写。设置了g修饰符的时候,只要手动设置了lastIndex的值,就会从指定位置开始匹配 字符串的实例方法 字符串的实例方法之中,有4种与正则表达式有关 1.String.prototype.match():返回一个数组,成员是所有匹配的子字符串。 2.String.prototype.search():按照给定的正则表达式进行搜索,返回一个整数,表示匹配开始的位置。 3.String.prototype.replace():按照给定的正则表达式进行替换,返回替换后的字符串。 4.String.prototype.split():按照给定规则进行字符串分割,返回一个数组,包含分割后的各个成员 String.prototype.match() 字符串实例对象的match方法对字符串进行正则匹配,返回匹配结果 var s = '_x_x'; var r1 = /x/; var r2 = /y/; s.match(r1) // ["x"] s.match(r2) // null 从上面代码可以看到,字符串的match方法与正则对象的exec方法非常类似:匹配成功返回一个数组,匹配失败返回null。如果正则表达式带有g修饰符,则该方法与正则对象的exec方法行为不同,会一次性返回所有匹配成功的结果 var s = 'abba'; var r = /a/g; s.match(r) // ["a", "a"] r.exec(s) // ["a"] 设置正则表达式的lastIndex属性,对match方法无效,匹配总是从字符串的第一个字符开始 String.prototype.search() 字符串对象的search方法,返回第一个满足条件的匹配结果在整个字符串中的位置。如果没有任何匹配,则返回-1 '_x_x'.search(/x/) // 1 String.prototype.replace() 字符串对象的replace方法可以替换匹配的值。它接受两个参数,第一个是正则表达式,表示搜索模式,第二个是替换的内容 str.replace(search, replacement) 正则表达式如果不加g修饰符,就替换第一个匹配成功的值,否则替换所有匹配成功的值 'aaa'.replace('a', 'b') // "baa" 'aaa'.replace(/a/, 'b') // "baa" 'aaa'.replace(/a/g, 'b') // "bbb" replace方法的一个应用,就是消除字符串首尾两端的空格 var str = ' #id div.class '; str.replace(/^\s+|\s+$/g, '') // "#id div.class" replace方法的第二个参数可以使用美元符号$,用来指代所替换的内容。 $&:匹配的子字符串 $`:匹配结果前面的文本 $':匹配结果后面的文本 $n:匹配成功的第n组内容,n是从1开始的自然数 $$:指代美元符号$ 'hello world'.replace(/(\w+)\s(\w+)/, '$2 $1') // "world hello" 'abc'.replace('b', '[$`-$&-$\']') // "a[a-b-c]c" 上面代码中,第一个例子是将匹配的组互换位置,第二个例子是改写匹配的值。replace方法的第二个参数还可以是一个函数,将每一个匹配内容替换为函数返回值 '3 and 5'.replace(/[0-9]+/g, function (match) { return 2 * match; }) // "6 and 10" var a = 'The quick brown fox jumped over the lazy dog.'; var pattern = /quick|brown|lazy/ig; a.replace(pattern, function replacer(match) { return match.toUpperCase(); }); // The QUICK BROWN fox jumped over the LAZY dog. 作为replace方法第二个参数的替换函数,可以接受多个参数。其中,第一个参数是捕捉到的内容,第二个参数是捕捉到的组匹配(有多少个组匹配,就有多少个对应的参数)。此外,最后还可以添加两个参数,倒数第二个参数是捕捉到的内容在整个字符串中的位置(比如从第五个位置开始),最后一个参数是原字符串。下面是一个网页模板替换的例子 var prices = { 'p1': '$1.99', 'p2': '$9.99', 'p3': '$5.00'}; var template = '<span id="p1"></span><span id="p2"></span><span id="p3"></span>'; template.replace( /(<span id=")(.*?)(">)(<\/span>)/g, function(match, $1, $2, $3, $4){ return $1 + $2 + $3 + prices[$2] + $4; } ); // "<span id="p1">$1.99</span><span id="p2">$9.99</span><span id="p3">$5.00</span>" 上面代码的捕捉模式中,有四个括号,所以会产生四个组匹配,在匹配函数中用$1到$4表示。匹配函数的作用是将价格插入模板中 String.prototype.split() 字符串对象的split方法按照正则规则分割字符串,返回一个由分割后的各个部分组成的数组 str.split(separator, [limit]) 该方法接受两个参数,第一个参数是正则表达式,表示分隔规则,第二个参数是返回数组的最大成员数 // 非正则分隔 'a, b,c, d'.split(',') // [ 'a', ' b', 'c', ' d' ] // 正则分隔,去除多余的空格 'a, b,c, d'.split(/, */) // [ 'a', 'b', 'c', 'd' ] // 指定返回数组的最大成员 'a, b,c, d'.split(/, */, 2) [ 'a', 'b' ] 上面代码使用正则表达式,去除了子字符串的逗号后面的空格 'aaa*a*'.split(/a*/) // [ '', '*', '*' ] 'aaa**a*'.split(/a*/) // ["", "*", "*", "*"] 上面代码的分割规则是0次或多次的a,由于正则默认是贪婪匹配,所以例一的第一个分隔符是aaa,第二个分割符是a,将字符串分成三个部分,包含开始处的空字符串。例二的第一个分隔符是aaa,第二个分隔符是0个a(即空字符),第三个分隔符是a,所以将字符串分成四个部分。如果正则表达式带有括号,则括号匹配的部分也会作为数组成员返回 'aaa*a*'.split(/(a*)/) // [ '', 'aaa', '*', 'a', '*' ] 匹配规则 正则表达式的规则很复杂 字面量字符和元字符 大部分字符在正则表达式中就是字面的含义,比如/a/匹配a,/b/匹配b。如果在正则表达式中,某个字符只表示它字面的含义(就像前面的a和b),那么它们就叫做“字面量字符”(literal characters) /dog/.test('old dog') // true 除了字面量字符以外,还有一部分字符有特殊含义,不代表字面的意思。它们叫做“元字符”(metacharacters),主要有以下几个 点字符(.) 点字符(.)匹配除回车(r)、换行(n) 、行分隔符(u2028)和段分隔符(u2029)以外的所有字符。注意,对于码点大于0xFFFF字符,点字符不能正确匹配,会认为这是两个字符 /c.t/ c.t匹配c和t之间包含任意一个字符的情况,只要这三个字符在同一行,比如cat、c2t、c-t等等,但是不匹配coot 位置字符 位置字符用来提示字符所处的位置,主要有两个字符: 1.^ 表示字符串的开始位置2.$ 表示字符串的结束位置 // test必须出现在开始位置 /^test/.test('test123') // true // test必须出现在结束位置 /test$/.test('new test') // true // 从开始位置到结束位置只有test /^test$/.test('test') // true /^test$/.test('test test') // false 选择符(|) 竖线符号(|)在正则表达式中表示“或关系”(OR),即cat|dog表示匹配cat或dog /11|22|33/.test('911') // true 选择符会包括它前后的多个字符,比如/ab|cd/指的是匹配ab或者cd,而不是指匹配b或者c。如果想修改这个行为,可以使用圆括号 /a( |\t)b/.test('a\tb') // true 上面代码指的是,a和b之间有一个空格或者一个制表符。其他的元字符还包括、*、+、?、()、[]、{}等 转义符 正则表达式中那些有特殊含义的元字符,如果要匹配它们本身,就需要在它们前面要加上反斜杠。比如要匹配+,就要写成+ /1+1/.test('1+1') // false /1\+1/.test('1+1') // true 正则表达式中,需要反斜杠转义的,一共有12个字符:^、.、[、$、(、)、|、*、+、?、{和。需要特别注意的是,如果使用RegExp方法生成正则对象,转义需要使用两个斜杠,因为字符串内部会先转义一次 (new RegExp('1\+1')).test('1+1') // false (new RegExp('1\\+1')).test('1+1') // true 上面代码中,RegExp作为构造函数,参数是一个字符串。但是,在字符串内部,反斜杠也是转义字符,所以它会先被反斜杠转义一次,然后再被正则表达式转义一次,因此需要两个反斜杠转义 特殊字符 正则表达式对一些不能打印的特殊字符,提供了表达方法 cX 表示Ctrl-[X],其中的X是A-Z之中任一个英文字母,用来匹配控制字符 [b] 匹配退格键(U+0008),不要与b混淆 n 匹配换行键 r 匹配回车键 t 匹配制表符 tab(U+0009) v 匹配垂直制表符(U+000B) f 匹配换页符(U+000C) 0 匹配null字符(U+0000) xhh 匹配一个以两位十六进制数(x00-xFF)表示的字符 uhhhh 匹配一个以四位十六进制数(u0000-uFFFF)表示的 Unicode 字符 字符类 字符类(class)表示有一系列字符可供选择,只要匹配其中一个就可以了。所有可供选择的字符都放在方括号内,比如[xyz] 表示x、y、z之中任选一个匹配 /[abc]/.test('hello world') // false /[abc]/.test('apple') // true 有两个字符在字符类中有特殊含义: 脱字符(^) 如果方括号内的第一个字符是[^],则表示除了字符类之中的字符,其他字符都可以匹配。比如,1表示除了x、y、z之外都可以匹配 /[^abc]/.test('hello world') // true /[^abc]/.test('bbc') // false 如果方括号内没有其他字符,即只有[^],就表示匹配一切字符,其中包括换行符。相比之下,点号作为元字符(.)是不包括换行符的 var s = 'Please yes\nmake my day!'; s.match(/yes.*day/) // null s.match(/yes[^]*day/) // [ 'yes\nmake my day'] 上面代码中,字符串s含有一个换行符,点号不包括换行符,所以第一个正则表达式匹配失败;第二个正则表达式[^]包含一切字符,所以匹配成功。注意,脱字符只有在字符类的第一个位置才有特殊含义,否则就是字面含义 连字符(-) 某些情况下,对于连续序列的字符,连字符(-)用来提供简写形式,表示字符的连续范围。比如,[abc]可以写成[a-c],[0123456789]可以写成[0-9],同理[A-Z]表示26个大写字母 /a-z/.test('b') // false /[a-z]/.test('b') // true 上面代码中,当连字号(dash)不出现在方括号之中,就不具备简写的作用,只代表字面的含义,所以不匹配字符b。只有当连字号用在方括号之中,才表示连续的字符序列。以下都是合法的字符类简写形式 [0-9.,] [0-9a-fA-F] [a-zA-Z0-9-] [1-31] 上面代码中最后一个字符类[1-31],不代表1到31,只代表1到3。连字符还可以用来指定 Unicode 字符的范围 var str = "\u0130\u0131\u0132"; /[\u0128-\uFFFF]/.test(str) // true 另外,不要过分使用连字符,设定一个很大的范围,否则很可能选中意料之外的字符。最典型的例子就是[A-z],表面上它是选中从大写的A到小写的z之间52个字母,但是由于在 ASCII 编码之中,大写字母与小写字母之间还有其他字符,结果就会出现意料之外的结果 /[A-z]/.test('\\') // true 预定义模式 预定义模式指的是某些常见模式的简写方式。 \d 匹配0-9之间的任一数字,相当于[0-9]。 \D 匹配所有0-9以外的字符,相当于[^0-9]。 \w 匹配任意的字母、数字和下划线,相当于[A-Za-z0-9_]。 \W 除所有字母、数字和下划线以外的字符,相当于[^A-Za-z0-9_]。 \s 匹配空格(包括换行符、制表符、空格符等),相等于[ \t\r\n\v\f]。 \S 匹配非空格的字符,相当于[^ \t\r\n\v\f]。 \b 匹配词的边界。 \B 匹配非词边界,即在词的内部 通常,正则表达式遇到换行符(n)就会停止匹配 var html = "<b>Hello</b>\n<i>world!</i>"; /.*/.exec(html)[0] // "<b>Hello</b>" /[\S\s]*/.exec(html)[0] // "<b>Hello</b>\n<i>world!</i>" 字符串html包含一个换行符,结果点字符(.)不匹配换行符,导致匹配结果可能不符合原意。这时使用s字符类,就能包括换行符 重复类 模式的精确匹配次数,使用大括号({})表示。{n}表示恰好重复n次,{n,}表示至少重复n次,{n,m}表示重复不少于n次,不多于m次 /lo{2}k/.test('look') // true /lo{2,5}k/.test('looook') // true 量词符 量词符用来设定某个模式出现的次数。 // ? 问号表示某个模式出现0次或1次,等同于{0, 1};t 出现0次或1次 /t?est/.test('test') // true /t?est/.test('est') // true //+ 加号表示某个模式出现1次或多次,等同于{1,}; t 出现1次或多次 /t+est/.test('ttest') // true /t+est/.test('est') // false //* 星号表示某个模式出现0次或多次,等同于{0,}; t 出现0次或多次 /t*est/.test('tttest') // true /t*est/.test('est') // true 贪婪模式 上面的三个量词符,默认情况下都是最大可能匹配,即匹配直到下一个字符不满足匹配规则为止。这被称为贪婪模式 var s = 'aaa'; s.match(/a+/) // ["aaa"] 上面代码中,模式是/a+/,表示匹配1个或多个a,那么到底会匹配几个a呢?因为默认是贪婪模式,会一直匹配到字符a不出现为止,所以匹配结果是3个a。如果想将贪婪模式改为非贪婪模式,可以在量词符后面加一个问号 var s = 'aaa'; s.match(/a+?/) // ["a"] 上面代码中,模式结尾添加了一个问号/a+?/,这时就改为非贪婪模式,一旦条件满足,就不再往下匹配。除了非贪婪模式的加号,还有非贪婪模式的星号(*)和非贪婪模式的问号(?) //+?:表示某个模式出现1次或多次,匹配时采用非贪婪模式;*?:表示某个模式出现0次或多次,匹配时采用非贪婪模式;??:表示某个模式出现0次或1次,匹配时采用非贪婪模式 'abb'.match(/ab*b/) // ["abb"] 'abb'.match(/ab*?b/) // ["ab"] 'abb'.match(/ab?b/) // ["abb"] 'abb'.match(/ab??b/) // ["ab"] 修饰符 修饰符(modifier)表示模式的附加规则,放在正则模式的最尾部。修饰符可以单个使用,也可以多个一起使用 g 修饰符 默认情况下,第一次匹配成功后,正则对象就停止向下匹配了。g修饰符表示全局匹配(global),加上它以后,正则对象将匹配全部符合条件的结果,主要用于搜索和替换 var regex = /b/; var str = 'abba'; regex.test(str); // true regex.test(str); // true regex.test(str); // true 上面代码中,正则模式不含g修饰符,每次都是从字符串头部开始匹配。所以连续做了三次匹配都返回true var regex = /b/g; var str = 'abba'; regex.test(str); // true regex.test(str); // true regex.test(str); // false 上面代码中,正则模式含有g修饰符,每次都是从上一次匹配成功处,开始向后匹配。因为字符串abba只有两个b,所以前两次匹配结果为true,第三次匹配结果为false i 修饰符 默认情况下,正则对象区分字母的大小写,加上i修饰符以后表示忽略大小写(ignoreCase) /abc/.test('ABC') // false /abc/i.test('ABC') // true m 修饰符 m修饰符表示多行模式(multiline),会修改^和$的行为。默认情况下(即不加m修饰符时),^和$匹配字符串的开始处和结尾处,加上m修饰符以后,^和$还会匹配行首和行尾,即^和$会识别换行符(n) /world$/.test('hello world\n') // false /world$/m.test('hello world\n') // true 上面的代码中,字符串结尾处有一个换行符。如果不加m修饰符,匹配不成功,因为字符串的结尾不是world;加上以后,$可以匹配行尾 /^b/m.test('a\nb') // true 上面代码要求匹配行首的b,如果不加m修饰符,就相当于b只能处在字符串的开始处。加上b修饰符以后,换行符n也会被认为是一行的开始 组匹配 概述 正则表达式的括号表示分组匹配,括号中的模式可以用来匹配分组的内容 /fred+/.test('fredd') // true /(fred)+/.test('fredfred') // true 上面代码中,第一个模式没有括号,结果+只表示重复字母d,第二个模式有括号,结果+就表示匹配fred这个词。 var m = 'abcabc'.match(/(.)b(.)/); m // ['abc', 'a', 'c'] 上面代码中,正则表达式/(.)b(.)/一共使用两个括号,第一个括号捕获a,第二个括号捕获c 注意,使用组匹配时,不宜同时使用g修饰符,否则match方法不会捕获分组的内容 var m = 'abcabc'.match(/(.)b(.)/g); m // ['abc', 'abc'] 上面代码使用带g修饰符的正则表达式,结果match方法只捕获了匹配整个表达式的部分。这时必须使用正则表达式的exec方法,配合循环,才能读到每一轮匹配的组捕获 var str = 'abcabc'; var reg = /(.)b(.)/g; while (true) { var result = reg.exec(str); if (!result) break; console.log(result); } // ["abc", "a", "c"] // ["abc", "a", "c"] 正则表达式内部,还可以用n引用括号匹配的内容,n是从1开始的自然数,表示对应顺序的括号 /(.)b(.)\1b\2/.test("abcabc") // true 上面的代码中,1表示第一个括号匹配的内容(即a),2表示第二个括号匹配的内容(即c)。括号还可以嵌套 /y((..)\2)\1/.test('yabababab') // true 上面代码中,1指向外层括号,2指向内层括号 var html = '<b class="hello">Hello</b><i>world</i>'; var tag = /<(\w+)([^>]*)>(.*?)<\/\1>/g; var match = tag.exec(html); match[1] // "b" match[2] // " class="hello"" match[3] // "Hello" match = tag.exec(html); match[1] // "i" match[2] // "" match[3] // "world" 非捕获组 (?:x)称为非捕获组(Non-capturing group),表示不返回该组匹配的内容,即匹配的结果中不计入这个括号。 非捕获组的作用请考虑这样一个场景,假定需要匹配foo或者foofoo,正则表达式就应该写成/(foo){1, 2}/,但是这样会占用一个组匹配。这时,就可以使用非捕获组,将正则表达式改为/(?:foo){1, 2}/,它的作用与前一个正则是一样的,但是不会单独输出括号内部的内容 var m = 'abc'.match(/(?:.)b(.)/); m // ["abc", "c"] 上面代码中的模式,一共使用了两个括号。其中第一个括号是非捕获组,所以最后返回的结果中没有第一个括号,只有第二个括号匹配的内容 // 正常匹配 var url = /(http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/; url.exec('http://google.com/'); // ["http://google.com/", "http", "google.com", "/"] // 非捕获组匹配 var url = /(?:http|ftp):\/\/([^/\r\n]+)(\/[^\r\n]*)?/; url.exec('http://google.com/'); // ["http://google.com/", "google.com", "/"] 上面的代码中,前一个正则表达式是正常匹配,第一个括号返回网络协议;后一个正则表达式是非捕获匹配,返回结果中不包括网络协议 先行断言 x(?=y)称为先行断言(Positive look-ahead),x只有在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟着百分号的数字,可以写成/d+(?=%)/。“先行断言”中,括号里的部分是不会返回的 var m = 'abc'.match(/b(?=c)/); m // ["b"] 先行否定断言 x(?!y)称为先行否定断言(Negative look-ahead),x只有不在y前面才匹配,y不会被计入返回结果。比如,要匹配后面跟的不是百分号的数字,就要写成/d+(?!%)/ /\d+(?!\.)/.exec('3.14') // ["14"] 上面代码中,正则表达式指定,只有不在小数点前面的数字才会被匹配,因此返回的结果就是14。“先行否定断言”中,括号里的部分是不会返回的 var m = 'abd'.match(/b(?!c)/); m // ['b'] JSON 对象 JSON 格式 JSON 格式(JavaScript Object Notation 的缩写)是一种用于数据交换的文本格式,2001年由 Douglas Crockford 提出,目的是取代繁琐笨重的 XML 格式。相比 XML 格式,JSON 格式有两个显著的优点:书写简单,一目了然;符合 JavaScript 原生语法,可以由解释引擎直接处理,不用另外添加解析代码。所以,JSON 迅速被接受,已经成为各大网站交换数据的标准格式,并被写入标准。每个 JSON 对象就是一个值,可能是一个数组或对象,也可能是一个原始类型的值。总之,只能是一个值,不能是两个或更多的值。JSON 对值的类型和格式有严格的规定 1.字符串必须使用双引号表示,不能使用单引号 2.数组或对象最后一个成员的后面,不能加逗号 3.对象的键名必须放在双引号里面 { name: "张三", 'age': 32 } 4.原始类型的值只有四种:字符串、数值(必须以十进制表示)、布尔值和null(不能使用NaN, Infinity, -Infinity和undefined) [32, 64, 128, 0xFFF] { "name": "张三", "age": undefined } 5.复合类型的值只能是数组或对象,不能是函数、正则表达式对象、日期对象 { "name": "张三", "birthday": new Date('Fri, 26 Aug 2011 07:13:10 GMT'), "getName": function () { return this.name; } } 注意,null、空数组和空对象都是合法的 JSON 值 JSON 对象 JSON对象是 JavaScript 的原生对象,用来处理 JSON 格式数据。它有两个静态方法:JSON.stringify()和JSON.parse() JSON.stringify() 基本用法 JSON.stringify方法用于将一个值转为 JSON 字符串。该字符串符合 JSON 格式,并且可以被JSON.parse方法还原 JSON.stringify('abc') // ""abc"" JSON.stringify([1, "false", false]) // '[1,"false",false]' JSON.stringify({ name: "张三" }) // '{"name":"张三"}' 注意,对于原始类型的字符串,转换结果会带双引号 JSON.stringify('foo') === "foo" // false JSON.stringify('foo') === "\"foo\"" // true 如果对象的属性是undefined、函数或 XML 对象,该属性会被JSON.stringify过滤 var obj = { a: undefined, b: function () {} }; JSON.stringify(obj) // "{}" 如果数组的成员是undefined、函数或 XML 对象,则这些值被转成null var arr = [undefined, function () {}]; JSON.stringify(arr) // "[null,null]" 正则对象会被转成空对象 JSON.stringify(/foo/) // "{}" JSON.stringify方法会忽略对象的不可遍历的属性 var obj = {}; Object.defineProperties(obj, { 'foo': { value: 1, enumerable: true }, 'bar': { value: 2, enumerable: false } }); JSON.stringify(obj); // "{"foo":1}" 上面代码中,bar是obj对象的不可遍历属性,JSON.stringify方法会忽略这个属性 第二个参数 JSON.stringify方法还可以接受一个数组,作为第二个参数,指定需要转成字符串的属性 var obj = { 'prop1': 'value1', 'prop2': 'value2', 'prop3': 'value3' }; var selectedProperties = ['prop1', 'prop2']; JSON.stringify(obj, selectedProperties) // "{"prop1":"value1","prop2":"value2"}" 上面代码中,JSON.stringify方法的第二个参数指定,只转prop1和prop2两个属性。这个类似白名单的数组,只对对象的属性有效,对数组无效 JSON.stringify(['a', 'b'], ['0']) // "["a","b"]" JSON.stringify({0: 'a', 1: 'b'}, ['0']) // "{"0":"a"}" 第二个参数还可以是一个函数,用来更改JSON.stringify的返回值 function f(key, value) { if (typeof value === "number") { value = 2 * value; } return value; } JSON.stringify({ a: 1, b: 2 }, f) // '{"a": 2,"b": 4}' 注意,这个处理函数是递归处理所有的键 var o = {a: {b: 1}}; function f(key, value) { console.log("["+ key +"]:" + value); return value; } JSON.stringify(o, f) // []:[object Object] // [a]:[object Object] // [b]:1 // '{"a":{"b":1}}' 上面代码中,对象o一共会被f函数处理三次,最后那行是JSON.stringify的输出。第一次键名为空,键值是整个对象o;第二次键名为a,键值是{b: 1};第三次键名为b,键值为1。递归处理中,每一次处理的对象,都是前一次返回的值 var o = {a: 1}; function f(key, value) { if (typeof value === 'object') { return {b: 2}; } return value * 2; } JSON.stringify(o, f) // "{"b": 4}" 如果处理函数返回undefined或没有返回值,则该属性会被忽略 function f(key, value) { if (typeof(value) === "string") { return undefined; } return value; } JSON.stringify({ a: "abc", b: 123 }, f) // '{"b": 123}' 第三个参数 JSON.stringify还可以接受第三个参数,用于增加返回的 JSON 字符串的可读性。如果是数字,表示每个属性前面添加的空格(最多不超过10个);如果是字符串(不超过10个字符),则该字符串会添加在每行前面 JSON.stringify({ p1: 1, p2: 2 }, null, 2); /* "{ "p1": 1, "p2": 2 }" */ JSON.stringify({ p1:1, p2:2 }, null, '|-'); /* "{ |-"p1": 1, |-"p2": 2 }" */ 参数对象的 toJSON 方法 如果参数对象有自定义的toJSON方法,那么JSON.stringify会使用这个方法的返回值作为参数,而忽略原对象的其他属性 一个普通的对象 var user = { firstName: '三', lastName: '张', get fullName(){ return this.lastName + this.firstName; } }; JSON.stringify(user) // "{"firstName":"三","lastName":"张","fullName":"张三"}" 为这个对象加上toJSON方法 var user = { firstName: '三', lastName: '张', get fullName(){ return this.lastName + this.firstName; }, toJSON: function () { return { name: this.lastName + this.firstName }; } }; JSON.stringify(user) // "{"name":"张三"}" 上面代码中,JSON.stringify发现参数对象有toJSON方法,就直接使用这个方法的返回值作为参数,而忽略原对象的其他参数。Date对象就有一个自己的toJSON方法 var date = new Date('2015-01-01'); date.toJSON() // "2015-01-01T00:00:00.000Z" JSON.stringify(date) // ""2015-01-01T00:00:00.000Z"" toJSON方法的一个应用是,将正则对象自动转为字符串。因为JSON.stringify默认不能转换正则对象,但是设置了toJSON方法以后,就可以转换正则对象了 var obj = { reg: /foo/ }; // 不设置 toJSON 方法时 JSON.stringify(obj) // "{"reg":{}}" // 设置 toJSON 方法时 RegExp.prototype.toJSON = RegExp.prototype.toString; JSON.stringify(/foo/) // ""/foo/"" 上面代码在正则对象的原型上面部署了toJSON方法,将其指向toString方法,因此遇到转换成JSON时,正则对象就先调用toJSON方法转为字符串,然后再被JSON.stingify方法处理 JSON.parse() JSON.parse方法用于将 JSON 字符串转换成对应的值 JSON.parse('true') // true JSON.parse('[1, 5, "false"]') // [1, 5, "false"] JSON.parse('null') // null var o = JSON.parse('{"name": "张三"}'); o.name // 张三 如果传入的字符串不是有效的 JSON 格式,JSON.parse方法将报错 JSON.parse("'String'") // SyntaxError: Unexpected token ILLEGAL 上面代码中,双引号字符串中是一个单引号字符串,因为单引号字符串不符合 JSON 格式,所以报错。为了处理解析错误,可以将JSON.parse方法放在try...catch代码块中 try { JSON.parse("'String'"); } catch(e) { console.log('parsing error'); } JSON.parse方法可以接受一个处理函数,作为第二个参数,用法与JSON.stringify方法类似 function f(key, value) { if (key === 'a') { return value + 10; } return value; } JSON.parse('{"a": 1, "b": 2}', f) // {a: 11, b: 2} 面向对象编程 实例对象与 new 命令 JavaScript 语言具有很强的面向对象编程能力,这里介绍 JavaScript 面向对象编程的基础知识 对象是什么 面向对象编程(Object Oriented Programming,缩写为 OOP)是目前主流的编程范式。它将真实世界各种复杂的关系,抽象为一个个对象,然后由对象之间的分工与合作,完成对真实世界的模拟。每一个对象都是功能中心,具有明确分工,可以完成接受信息、处理数据、发出信息等任务。对象可以复用,通过继承机制还可以定制。因此,面向对象编程具有灵活、代码可复用、高度模块化等特点,容易维护和开发,比起由一系列函数或指令组成的传统的过程式编程(procedural programming),更适合多人合作的大型软件项目。那么,“对象”(object)到底是什么?我们从两个层次来理解 对象是单个实物的抽象 一本书、一辆汽车、一个人都可以是对象,一个数据库、一张网页、一个与远程服务器的连接也可以是对象。当实物被抽象成对象,实物之间的关系就变成了对象之间的关系,从而就可以模拟现实情况,针对对象进行编程 对象是一个容器,封装了属性(property)和方法(method) 属性是对象的状态,方法是对象的行为(完成某种任务)。比如,我们可以把动物抽象为animal对象,使用“属性”记录具体是那一种动物,使用“方法”表示动物的某种行为(奔跑、捕猎、休息等等) 构造函数 面向对象编程的第一步,就是要生成对象。前面说过,对象是单个实物的抽象。通常需要一个模板,表示某一类实物的共同特征,然后对象根据这个模板生成。典型的面向对象编程语言(比如 C++ 和 Java),都有“类”(class)这个概念。所谓“类”就是对象的模板,对象就是“类”的实例。但是,JavaScript 语言的对象体系,不是基于“类”的,而是基于构造函数(constructor)和原型链(prototype)。JavaScript 语言使用构造函数(constructor)作为对象的模板。所谓”构造函数”,就是专门用来生成实例对象的函数。它就是对象的模板,描述实例对象的基本结构。一个构造函数,可以生成多个实例对象,这些实例对象都有相同的结构。构造函数就是一个普通的函数,但是有自己的特征和用法 var Vehicle = function () { this.price = 1000; }; 上面代码中,Vehicle就是构造函数。为了与普通函数区别,构造函数名字的第一个字母通常大写 构造函数的特点有两个。 1.函数体内部使用了this关键字,代表了所要生成的对象实例 2.生成对象的时候,必须使用new命令 new 命令 基本用法 new命令的作用,就是执行构造函数,返回一个实例对象 var Vehicle = function () { this.price = 1000; }; var v = new Vehicle(); v.price // 1000 上面代码通过new命令,让构造函数Vehicle生成一个实例对象,保存在变量v中。这个新生成的实例对象,从构造函数Vehicle得到了price属性。new命令执行时,构造函数内部的this,就代表了新生成的实例对象,this.price表示实例对象有一个price属性,值是1000。使用new命令时,根据需要构造函数也可以接受参数 var Vehicle = function (p) { this.price = p; }; var v = new Vehicle(500); new命令本身就可以执行构造函数,所以后面的构造函数可以带括号,也可以不带括号。下面两行代码是等价的,但是为了表示这里是函数调用,推荐使用括号 var v = new Vehicle(); // 推荐的写法 var v = new Vehicle; // 不推荐的写法 一个很自然的问题是,如果忘了使用new命令,直接调用构造函数会发生什么事?这种情况下,构造函数就变成了普通函数,并不会生成实例对象。而且由于后面会说到的原因,this这时代表全局对象,将造成一些意想不到的结果 var Vehicle = function (){ this.price = 1000; }; var v = Vehicle(); v // undefined price // 1000 上面代码中,调用Vehicle构造函数时,忘了加上new命令。结果,变量v变成了undefined,而price属性变成了全局变量。因此,应该非常小心,避免不使用new命令、直接调用构造函数。为了保证构造函数必须与new命令一起使用,一个解决办法是,构造函数内部使用严格模式,即第一行加上use strict。这样的话,一旦忘了使用new命令,直接调用构造函数就会报错 function Fubar(foo, bar){ 'use strict'; this._foo = foo; this._bar = bar; } Fubar() // TypeError: Cannot set property '_foo' of undefined 上面代码的Fubar为构造函数,use strict命令保证了该函数在严格模式下运行。由于严格模式中,函数内部的this不能指向全局对象,默认等于undefined,导致不加new调用会报错(JavaScript 不允许对undefined添加属性)。另一个解决办法,构造函数内部判断是否使用new命令,如果发现没有使用,则直接返回一个实例对象 function Fubar(foo, bar) { if (!(this instanceof Fubar)) { return new Fubar(foo, bar); } this._foo = foo; this._bar = bar; } Fubar(1, 2)._foo // 1 (new Fubar(1, 2))._foo // 1 上面代码中的构造函数,不管加不加new命令,都会得到同样的结果 new 命令的原理 使用new命令时,它后面的函数依次执行下面的步骤 1.创建一个空对象,作为将要返回的对象实例 2.将这个空对象的原型,指向构造函数的prototype属性 3.将这个空对象赋值给函数内部的this关键字 4.开始执行构造函数内部的代码 也就是说,构造函数内部,this指的是一个新生成的空对象,所有针对this的操作,都会发生在这个空对象上。构造函数之所以叫“构造函数”,就是说这个函数的目的,就是操作一个空对象(即this对象),将其“构造”为需要的样子。如果构造函数内部有return语句,而且return后面跟着一个对象,new命令会返回return语句指定的对象;否则,就会不管return语句,返回this对象 var Vehicle = function () { this.price = 1000; return 1000; }; (new Vehicle()) === 1000 // false 上面代码中,构造函数Vehicle的return语句返回一个数值。这时,new命令就会忽略这个return语句,返回“构造”后的this对象。但是,如果return语句返回的是一个跟this无关的新对象,new命令会返回这个新对象,而不是this对象。这一点需要特别引起注意 var Vehicle = function (){ this.price = 1000; return { price: 2000 }; }; (new Vehicle()).price // 2000 上面代码中,构造函数Vehicle的return语句,返回的是一个新对象。new命令会返回这个对象,而不是this对象。另一方面,如果对普通函数(内部没有this关键字的函数)使用new命令,则会返回一个空对象 function getMessage() { return 'this is a message'; } var msg = new getMessage(); msg // {} typeof msg // "object" 上面代码中,getMessage是一个普通函数,返回一个字符串。对它使用new命令,会得到一个空对象。这是因为new命令总是返回一个对象,要么是实例对象,要么是return语句指定的对象。本例中,return语句返回的是字符串,所以new命令就忽略了该语句。new命令简化的内部流程,可以用下面的代码表示 function _new(/* 构造函数 */ constructor, /* 构造函数参数 */ params) { var args = [].slice.call(arguments); // 将 arguments 对象转为数组 var constructor = args.shift(); // 取出构造函数 var context = Object.create(constructor.prototype); // 创建一个空对象,继承构造函数的 prototype 属性 var result = constructor.apply(context, args); // 执行构造函数 return (typeof result === 'object' && result != null) ? result : context; // 如果返回结果是对象,就直接返回,否则返回 context 对象 } var actor = _new(Person, '张三', 28); // 实例 new.target 函数内部可以使用new.target属性。如果当前函数是new命令调用,new.target指向当前函数,否则为undefined function f() { console.log(new.target === f); } f() // false new f() // true 使用这个属性,可以判断函数调用的时候,是否使用new命令 function f() { if (!new.target) { throw new Error('请使用 new 命令调用!'); } // ... } f() // Uncaught Error: 请使用 new 命令调用! Object.create() 创建实例对象 构造函数作为模板,可以生成实例对象。但是,有时拿不到构造函数,只能拿到一个现有的对象。我们希望以这个现有的对象作为模板,生成新的实例对象,这时就可以使用Object.create()方法 var person1 = { name: '张三', age: 38, greeting: function() { console.log('Hi! I\'m ' + this.name + '.'); } }; var person2 = Object.create(person1); person2.name // 张三 person2.greeting() // Hi! I'm 张三. 上面代码中,对象person1是person2的模板,后者继承了前者的属性和方法 this 关键字 涵义 this关键字是一个非常重要的语法点。毫不夸张地说,不理解它的含义大部分开发任务都无法完成。this可以用在构造函数之中,表示实例对象;除此之外,this还可以用在别的场合。但不管是什么场合,this都有一个共同点:它总是返回一个对象;简单说,this就是属性或方法“当前”所在的对象 var person = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; person.describe() // "姓名:张三" 上面代码中,this.name表示name属性所在的那个对象。由于this.name是在describe方法中调用,而describe方法所在的当前对象是person,因此this指向person,this.name就是person.name。由于对象的属性可以赋给另一个对象,所以属性所在的当前对象是可变的,即this的指向是可变的 var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var B = { name: '李四' }; B.describe = A.describe; B.describe() // "姓名:李四" 上面代码中,A.describe属性被赋给B,于是B.describe就表示describe方法所在的当前对象是B,所以this.name就指向B.name。稍稍重构这个例子,this的动态指向就能看得更清楚 function f() { return '姓名:'+ this.name; } var A = { name: '张三', describe: f }; var B = { name: '李四', describe: f }; A.describe() // "姓名:张三" B.describe() // "姓名:李四" 只要函数被赋给另一个变量,this的指向就会变 var A = { name: '张三', describe: function () { return '姓名:'+ this.name; } }; var name = '李四'; var f = A.describe; f() // "姓名:李四" 上面代码中,A.describe被赋值给变量f,内部的this就会指向f运行时所在的对象(本例是顶层对象)。再看一个网页编程的例子 <input type="text" name="age" size=3 onChange="validate(this, 18, 99);"> <script> function validate(obj, lowval, hival){ if ((obj.value < lowval) || (obj.value > hival)) console.log('Invalid Value!'); } </script> 上面代码是一个文本输入框,每当用户输入一个值,就会调用onChange回调函数,验证这个值是否在指定范围。浏览器会向回调函数传入当前对象,因此this就代表传入当前对象(即文本框),然后就可以从this.value上面读到用户的输入值 总结一下,JavaScript 语言之中,一切皆对象,运行环境也是对象,所以函数都是在某个对象之中运行,this就是函数运行时所在的对象(环境)。这本来并不会让用户糊涂,但是 JavaScript 支持运行环境动态切换,也就是说,this的指向是动态的,没有办法事先确定到底指向哪个对象,这才是最让初学者感到困惑的地方 实质 JavaScript 语言之所以有 this 的设计,跟内存里面的数据结构有关系 var obj = { foo: 5 }; 上面的代码将一个对象赋值给变量obj。JavaScript 引擎会先在内存里面,生成一个对象{ foo: 5 },然后把这个对象的内存地址赋值给变量obj。也就是说,变量obj是一个地址(reference)。后面如果要读取obj.foo,引擎先从obj拿到内存地址,然后再从该地址读出原始的对象,返回它的foo属性。原始的对象以字典结构保存,每一个属性名都对应一个属性描述对象。举例来说,上面例子的foo属性,实际上是以下面的形式保存的 { foo: { [[value]]: 5 [[writable]]: true [[enumerable]]: true [[configurable]]: true } } 注意,foo属性的值保存在属性描述对象的value属性里面。这样的结构是很清晰的,问题在于属性的值可能是一个函数;这时,引擎会将函数单独保存在内存中,然后再将函数的地址赋值给foo属性的value属性 var obj = { foo: function () {} }; 由于函数是一个单独的值,所以它可以在不同的环境(上下文)执行 var f = function () {}; var obj = { f: f }; f() // 单独执行 obj.f() // obj 环境执行 JavaScript 允许在函数体内部,引用当前环境的其他变量 var f = function () { console.log(x); }; 上面代码中,函数体里面使用了变量x,该变量由运行环境提供。现在问题就来了,由于函数可以在不同的运行环境执行,所以需要有一种机制,能够在函数体内部获得当前的运行环境(context)。所以,this就出现了,它的设计目的就是在函数体内部,指代函数当前的运行环境 var f = function () { console.log(this.x);} var x = 1; var obj = { f: f, x: 2}; // 单独执行 f() // 1 // obj 环境执行 obj.f() // 2 上面代码中,函数f在全局环境执行,this.x指向全局环境的x;在obj环境执行,this.x指向obj.x 使用场合 this主要有以下几个使用场合 全局环境 全局环境使用this,它指的就是顶层对象window this === window // true function f() { console.log(this === window); } f() // true 上面代码说明,不管是不是在函数内部,只要是在全局环境下运行,this就是指顶层对象window 构造函数 构造函数中的this,指的是实例对象 var Obj = function (p) { this.p = p; }; var o = new Obj('Hello World!'); o.p // "Hello World!" 上面代码定义了一个构造函数Obj。由于this指向实例对象,所以在构造函数内部定义this.p,就相当于定义实例对象有一个p属性 对象的方法 如果对象的方法里面包含this,this的指向就是方法运行时所在的对象。该方法赋值给另一个对象,就会改变this的指向。但是,这条规则很不容易把握。请看下面的代码 var obj ={ foo: function () { console.log(this); } }; obj.foo() // obj 上面代码中,obj.foo方法执行时,它内部的this指向obj。但是,下面这几种用法,都会改变this的指向 // 情况一 (obj.foo = obj.foo)() // window // 情况二 (false || obj.foo)() // window // 情况三 (1, obj.foo)() // window 上面代码中,obj.foo就是一个值。这个值真正调用的时候,运行环境已经不是obj了,而是全局环境,所以this不再指向obj。可以这样理解,JavaScript 引擎内部,obj和obj.foo储存在两个内存地址,称为地址一和地址二。obj.foo()这样调用时,是从地址一调用地址二,因此地址二的运行环境是地址一,this指向obj。但是,上面三种情况,都是直接取出地址二进行调用,这样的话,运行环境就是全局环境,因此this指向全局环境。上面三种情况等同于下面的代码 // 情况一 (obj.foo = function () { console.log(this);})() // 等同于 (function () { console.log(this);})() // 情况二 (false || function () { console.log(this);})() // 情况三 (1, function () { console.log(this);})() 如果this所在的方法不在对象的第一层,这时this只是指向当前一层的对象,而不会继承更上面的层 var a = { p: 'Hello', b: { m: function() { console.log(this.p); } } }; a.b.m() // undefined 上面代码中,a.b.m方法在a对象的第二层,该方法内部的this不是指向a,而是指向a.b,因为实际执行的是下面的代码 var b = { m: function() { console.log(this.p); } }; var a = { p: 'Hello', b: b }; (a.b).m() // 等同于 b.m() 如果要达到预期效果,只有写成下面这样 var a = { b: { m: function() { console.log(this.p); }, p: 'Hello' } }; 如果这时将嵌套对象内部的方法赋值给一个变量,this依然会指向全局对象 var a = { b: { m: function() { console.log(this.p); }, p: 'Hello' } }; var hello = a.b.m; hello() // undefined 上面代码中,m是多层对象内部的一个方法。为求简便,将其赋值给hello变量,结果调用时,this指向了顶层对象。为了避免这个问题,可以只将m所在的对象赋值给hello,这样调用时,this的指向就不会变 var hello = a.b; hello.m() // Hello xyz ↩

优秀的个人博客,低调大师

Scrapyd使用教程

先上github地址:Scrapyd Scrapyd是一个服务,用来运行scrapy爬虫的 它允许你部署你的scrapy项目以及通过HTTP JSON的方式控制你的爬虫 官方文档:http://scrapyd.readthedocs.org/ 安装 pip install scrapyd 安装完成后,在你当前的python环境根目录C:\Program Files\Python35\Scripts下,有一个scrapyd.exe, 打开命令行,输入scrapyd,如下图: image.png 这样scrapyd就运行起来了,访问127.0.0.1:6800即可看到可视化界面。 注:如果在命令行运行scrapyd报错如下图: image.png 先pip list 查看你的attrs这个包的版本 然后降到16.3 再试试,如果还不行,请评论区提问。 理解scrapyd scrapyd其实就是一个服务器端,真正在部署爬虫的时候,我们需要两个东西: scrapyd (安装在服务器端) scrapy-client (客户端) scrapy-client,它允许我们将本地的scrapy项目打包发送到scrapyd 这个服务端 安装 scrapy-client:pip install scrapy-client 部署scrapy项目 在scrapy项目目录下,有一个scrapy.cfg的配置文件: image.png # Automatically created by: scrapy startproject # # For more information about the [deploy] section see: # https://scrapyd.readthedocs.org/en/latest/deploy.html [settings] default = china.settings [deploy:demo] url = http://localhost:6800/ project = china 把原先注释掉的url那一行取消注释,这个就是我们要部署到目标服务器的地址, 然后,把[deploy]这里改为[deploy:demo],这里是命名为demo,命名可以任意怎么都可以,只要能标识出来项目就可以。 下边的project 就是我们的工程名,到此配置文件更改完成。 接着,执行scrapyd-deploy,这个命令在windows下是运行不了的,(在mac和linux下都是可以的)因为在我们安装的根目录C:\Program Files\Python35\Scripts中可以查看这个文件是没有后缀名的: image.png 解决方法:在同目录下,新建文件scrapyd-deploy.bat @echo off "C:\Program Files\Python35\python.exe" "C:\Program Files\Python35\Scripts\scrapyd-deploy" %1 %2 %3 %4 %5 %6 %7 %8 %9 上边代码是调用的我的环境中的python.exe路径,大家可以根据自己环境来改变路径做配置。 这样就可以执行scrapyd-deploy这个命令了。 然后,进入到我们爬虫的根目录,运行scrapyd-deploy: image.png 显示这个就证明我们成功执行了scrapyd-deploy,注意:一定要进入爬虫根目录,就是带有scrapy.cfg的那一层及目录。 接着: 运行:scrapyd-deploy demo -p china 因为上边我们已经配置过scrapy.cfg文件了,这里直接使用配置完的参数即可: image.png 这里显示我们部署成功,可以查看执行启动scrapyd服务端的当先目录下有两个文件夹: image.png 到这一步,只是把爬虫项目上传到服务端,并没有启动, 接下来看看如何启动: 先运行命令查看服务端状态: curl http://localhost:6800/daemonstatus.json image.png 返回的信息告诉我们:都为0 再执行启动命令: curl http://localhost:6800/schedule.json -d project=china -d spider=china 然后查看网页127.0.0.1:6800 image.png 再点jobs, image.png 这边就是我们爬虫运行的状态以及日志。 后续补充: 我们在生产环境中,一般scrapyd是部署在服务器,而我们一般会从本地直接发送到服务器端,这时需要调整vim /usr/lib/python3/site-packages/scrapyd/default_scrapyd.conf scrapyd的默认配置文件: 默认scrapyd启动bind绑定的ip地址是127.0.0.1端口是:6800, 将ip地址设置为0.0.0.0 打开配置文件不需要翻页就能够找到bind_address 还有更多更好用的命令,请查阅官方文档,这里只介绍基本用法。

资源下载

更多资源
Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册