JavaScript学习笔记

190 阅读17分钟

1.基本语法

1.数据类型

  • 数值(number)、字符串(string)、布尔值(boolean)
  • undefined:表示“未定义”或不存在,即由于目前没有定义,所以此处暂时没有任何值。
  • null:表示空值,即此处的值为空。
  • 对象(object):各种值组成的集合。

对象则称为合成类型(complex type)的值,因为一个对象往往是多个原始类型的值的合成,可以看作是一个存放各种值的容器。

对象可以分为三个子类型:

  • 狭义的对象(object)
  • 数组(array)
  • 函数(function)

确定一个值到底是什么类型有三种方法:

  • typeof运算符
  • instanceof运算符
  • Object.prototype.toString方法

typeof
1.typeof可以用来检查一个没有声明的变量,而不报错(返回undefined)。
2.如果是对象的话,typeof返回object.

image.png

typeof null 返回object

3.函数返回function
image.png

null 和 undefined

语法效果几乎没区别,在if语句中,它们都会被自动转为false,相等运算符(==)甚至直接报告两者相等。
null是一个表示“空”的对象,转为数值时为0
undefined是一个表示"此处无定义"的原始值,转为数值时为NaN

返回undefined的典型情况:

image.png

true false

JavaScript 预期某个位置应该是布尔值,会将该位置上现有的值自动转为布尔值。转换规则是除了下面六个值被转为false,其他值都视为true

  • undefined
  • null
  • false
  • 0
  • NaN
  • ""''(空字符串)

空数组([])和空对象({})对应的布尔值,都是true

整数和浮点数

所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,11.0是相同的,是同一个数。

1 === 1.0 // true

当有些运算必须是整数时,64位浮点数会转化为32位整数,然后再进行运算。

数值精度
精度最多只能到53个二进制位,这意味着,绝对值小于2的53次方的整数,即-2^53到2^53,都可以精确表示。 所以简单的法则就是,JavaScript 对15位的十进制数都可以精确处理。

64位浮点数的指数部分的值最大为2047,分出一半表示负数,则 JavaScript 能够表示的数值范围为2^1024到2^-1023(开区间),超出这个范围的数无法表示。

JavaScript对正向溢出的数会返回Infinity,“负向溢出”会直接返回0.

JavaScript 提供Number对象的MAX_VALUEMIN_VALUE属性,返回可以表示的具体的最大值和最小值。

Number.MAX_VALUE // 1.7976931348623157e+308
Number.MIN_VALUE // 5e-324

以下两种情况,JavaScript 会自动将数值转为科学计数法表示,其他情况都采用字面形式直接表示。 (1)小数点前的数字多于21位。
(2)小数点后的零多于5个。

Infinity
Infinity表示“无穷”,用来表示两种场景。一种是一个正的数值太大,或一个负的数值太小,无法表示;另一种是非0数值除以0,得到Infinity

数据转换:

parseInt()
parseInt方法用于将字符串转为整数。

  • 如果字符串头部有空格,空格会被自动去除。
  • 如果parseInt的参数不是字符串,则会先转为字符串再转换。
  • 如果第一个字符不能转化为数字,就返回NaN
  • 如果字符串以0x0X开头,parseInt会将其按照十六进制数解析。如果字符串以0开头,将其按照10进制解析。
parseInt('0x10') // 16

parseInt('011') // 11

  • 字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分
parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15

所以,parseInt的返回值只有两种可能,要么是一个十进制整数,要么是NaN

parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。

parseInt('1000', 2) // 8
parseInt('1000', 6) // 216
parseInt('1000', 8) // 512

parseFloat()
parseFloat方法用于将一个字符串转为浮点数。

  • 如果字符串符合科学计数法,则会进行相应的转换。
parseFloat('314e-2') // 3.14
parseFloat('0.0314E+2') // 3.14
  • 如果参数不是字符串,则会先转为字符串再转换。
  • 如果字符串的第一个字符不能转化为浮点数,则返回NaN

isNaN()
isNaN方法可以用来判断一个值是否为NaN
isNaN只对数值有效,如果传入其他值,会被先转成数值。也就是说,isNaNtrue的值,有可能不是NaN,而是一个字符串。

isNaN('Hello') // true
// 相当于
isNaN(Number('Hello')) // true

出于同样的原因,对于对象和数组,isNaN也返回true

isFinite()
isFinite方法返回一个布尔值,表示某个值是否为正常的数值。
除了Infinity、-Infinity、NaN和undefined这几个值会返回falseisFinite对于其他的数值都会返回true

Base64 转码

JavaScript 原生提供两个 Base64 相关的方法。

  • btoa():任意值转为 Base64 编码
  • atob():Base64 编码转为原来的值
var string = 'Hello World!';
btoa(string) // "SGVsbG8gV29ybGQh"
atob('SGVsbG8gV29ybGQh') // "Hello World!"

强制转换

强制转换主要指使用Number()String()Boolean()三个函数,手动将各种类型的值,分别转换成数字、字符串或者布尔值。

  • Number函数将字符串转为数值,要比parseInt函数严格很多。基本上,只要有一个字符无法转成数值,整个字符串就会被转为NaN
  • String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式。
String({a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"
  • Boolean()除了以下五个值的转换结果为false,其他的值全部为true undefined null 0(包含-0+0NaN ''(空字符串)

自动转换

第一种情况,不同类型的数据互相运算。
第二种情况,对非布尔值类型的数据求布尔值
第三种情况,对非数值类型的值使用一元运算符(即+-)。

对象

对象就是一组“键值对”(key-value)的集合,是一种无序的复合数据集合。

var obj = {
  foo: 'Hello',
  bar: 'World'
};

对象赋值: 点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。

var obj = {};

obj.foo = 'Hello';
obj['bar'] = 'World';

属性的查看: 查看一个对象本身的所有属性,可以使用Object.keys方法。

Object.keys(obj);

属性的删除: delete命令用于删除对象的属性,删除成功后返回true

delete obj.p // true

属性是否存在:in 运算符,但是它不能识别哪些属性是对象自身的,哪些属性是继承的。可以使用对象的hasOwnProperty方法判断一下,是否为对象自身的属性。

var obj = { p: 1 };
'p' in obj // true
'toString' in obj // true

属性的遍历:for...in 循环

函数闭包

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,通过闭包getAgesetAge,变成了返回对象p1的私有变量。

二进制运算符

  • 二进制或运算符(or):符号为|,表示若两个二进制位都为0,则结果为0,否则为1
  • 二进制与运算符(and):符号为&,表示若两个二进制位都为1,则结果为1,否则为0。
  • 二进制否运算符(not):符号为~,表示对一个二进制位取反。
  • 异或运算符(xor):符号为^,表示若两个二进制位不相同,则结果为1,否则为0。
  • 左移运算符(left shift):符号为<<
  • 右移运算符(right shift):符号为>>
  • 头部补零的右移运算符(zero filled right shift):符号为>>>

2.错误处理机制

finally 代码块

try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句。
catch代码块结束执行之前,会先执行finally代码块。

3. console 对象与控制台

console.log(),console.info(),console.debug()

console.infoconsole.log方法的别名,用法完全一样。只不过console.info方法会在输出信息的前面,加上一个蓝色图标。

console.debug方法与console.log方法类似,会在控制台输出调试信息。但是,默认情况下,console.debug输出的信息不会显示,只有在打开显示级别在verbose的情况下,才会显示。

console.warn(),console.error()

warn方法输出信息时,在最前面加一个黄色三角,表示警告;error方法输出信息时,在最前面加一个红色的叉,表示出错。

console.table()

对于某些复合类型的数据,console.table方法可以将其转为表格显示。

console.count()

方法用于计数,输出它被调用了多少次。

console.dir(),console.dirxml()

dir方法用来对一个对象进行检查(inspect),并以易于阅读和打印的格式显示 dirxml方法主要用于以目录树的形式,显示 DOM 节点。

console.assert()

console.assert方法主要用于程序运行过程中,进行条件判断,如果不满足条件,就显示一个错误,但不会中断程序执行。这样就相当于提示用户,内部状态不正确。

console.time(),console.timeEnd()

这两个方法用于计时,可以算出一个操作所花费的准确时间。time方法表示计时开始,timeEnd方法表示计时结束。它们的参数是计时器的名称。调用timeEnd方法之后,控制台会显示“计时器名称: 所耗费的时间”。

console.group(),console.groupEnd(),console.groupCollapsed()

console.groupconsole.groupEnd这两个方法用于将显示的信息分组。它只在输出大量信息时有用,分在一组的信息,可以用鼠标折叠/展开。

image.png

console.groupCollapsed方法与console.group方法很类似,唯一的区别是该组的内容,在第一次显示时是收起的(collapsed),而不是展开的。

image.png

console.trace(),console.clear()

console.trace方法显示当前执行的代码在堆栈中的调用路径。 console.clear方法用于清除当前控制台的所有输出,将光标回置到第一行。

异步操作

单线程模型

单线程模型指的是,JavaScript 只在一个线程上运行。也就是说,JavaScript 同时只能执行一个任务,其他任务都必须在后面排队等待。

同步任务和异步任务

同步任务是那些没有被引擎挂起、在主线程上排队执行的任务。只有前一个任务执行完毕,才能执行后一个任务。

异步任务是那些被引擎放在一边,不进入主线程、而进入任务队列的任务。

只有引擎认为某个异步任务可以执行了(比如 Ajax 操作从服务器得到了结果),该任务(采用回调函数的形式)才会进入主线程执行。排在异步任务后面的代码,不用等待异步任务结束会马上运行,也就是说,异步任务不具有“堵塞”效应。

任务队列和事件循环

JavaScript 运行时,除了一个正在运行的主线程,引擎还提供一个任务队列(task queue),里面是各种需要当前程序处理的异步任务。

首先,主线程会去执行所有的同步任务。等到同步任务全部执行完,就会去看任务队列里面的异步任务。如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就变成同步任务了。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。

异步任务的写法通常是回调函数。一旦异步任务重新进入主线程,就会执行对应的回调函数。如果一个异步任务没有回调函数,就不会进入任务队列,也就是说,不会重新进入主线程,因为没有用回调函数指定下一步的操作。

JavaScript 引擎怎么知道异步任务有没有结果,能不能进入主线程呢?答案就是引擎在不停地检查,一遍又一遍,只要同步任务执行完了,引擎就会去检查那些挂起来的异步任务,是不是可以进入主线程了。这种循环检查的机制,就叫做事件循环(Event Loop)。维基百科的定义是:“事件循环是一个程序结构,用于等待和发送消息和事件

异步操作的模式

1.回调函数

function f1(callback) {
  // ...
  callback();
}

function f2() {
  // ...
}

f1(f2);

2.事件监听

还是以f1f2为例。首先,为f1绑定一个事件(这里采用的 jQuery 的写法)。

f1.on('done', f2);

然后,对f1进行改写

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

上面代码中,f1.trigger('done')表示,执行完成后,立即触发done事件,从而开始执行f2

发布/订阅

事件完全可以理解成“信号”,如果存在一个“信号中心”,某个任务执行完成,就向信号中心“发布”(publish)一个信号,其他任务可以向信号中心“订阅”(subscribe)这个信号,从而知道什么时候自己可以开始执行。这就叫做”发布/订阅模式”(publish-subscribe pattern),又称“观察者模式”(observer pattern)。 首先,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);
}

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函数。

定时器

JavaScript 提供定时执行代码的功能,叫做定时器(timer),主要由setTimeout()setInterval()这两个函数来完成。他们向任务队列添加定时任务

setTimeout()

setTimeout函数用来指定某个函数或某段代码,在多少毫秒之后执行。它返回一个整数,表示定时器的编号,以后可以用来取消这个定时器。

console.log(1);
setTimeout('console.log(2)',1000);
console.log(3);

上面代码会先输出1和3,然后等待1000毫秒再输出2。注意,console.log(2)必须以字符串的形式,作为setTimeout的参数。

如果推迟执行的是函数,就直接将函数名,作为setTimeout的参数

function f() {
  console.log(2);
}

setTimeout(f, 1000);

setInterval()

setInterval函数的用法与setTimeout完全一致,区别仅仅在于setInterval指定某个任务每隔一段时间就执行一次,也就是无限次的定时执行。

var i = 1
var timer = setInterval(function() {
  console.log(2);
}, 1000)

setInterval指定的是“开始执行”之间的间隔,并不考虑每次任务执行本身所消耗的时间。因此实际上,两次执行之间的间隔会小于指定的时间。比如,setInterval指定每 100ms 执行一次,每次执行需要 5ms,那么第一次执行结束后95毫秒,第二次执行就会开始。如果某次执行耗时特别长,比如需要105毫秒,那么它结束后,下一次执行就会立即开始。

运行机制
setTimeoutsetInterval的运行机制,是将指定的代码移出本轮事件循环,等到下一轮事件循环,再检查是否到了指定时间。如果到了,就执行对应的代码;如果不到,就继续等待。

这意味着,setTimeoutsetInterval指定的回调函数,必须等到本轮事件循环的所有同步任务都执行完,才会开始执行。由于前面的任务到底需要多少时间执行完,是不确定的,所以没有办法保证,setTimeoutsetInterval指定的任务,一定会按照预定时间执行。

clearTimeout(),clearInterval()

setTimeoutsetInterval函数,都返回一个整数值,表示计数器编号。将该整数传入clearTimeoutclearInterval函数,就可以取消对应的定时器。

promise对象的状态

Promise 对象通过自身的状态,来控制异步操作。Promise 实例具有三种状态。

  • 异步操作未完成(pending)
  • 异步操作成功(fulfilled)
  • 异步操作失败(rejected)

上面三种状态里面,fulfilledrejected合在一起称为resolved(已定型)

因此,Promise 的最终结果只有两种。

  • 异步操作成功,Promise 实例传回一个值(value),状态变为fulfilled
  • 异步操作失败,Promise 实例抛出一个错误(error),状态变为rejected

2.知识点:

(1)一个;算一个结束语句,多个语句可以写在一行内。

var a = 1 + 3 ; var b = 'abc';

(2)JavaScript 是一种动态类型语言,也就是说,变量的类型没有限制,变量可以随时更改类型。 (3)中文是合法的标识符
(4)<!---->也被视为合法的单行注释。

x = 1; <!-- x = 2;
--> x = 3;

上面代码中,只有x = 1会执行
(5)赋值表达式(=)、严格相等运算符(===)和相等运算符(==),严格运算表达式要值和类型都相同
(6)JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)
(7)长字符串必须分成多行,可以在每一行的尾部使用反斜杠。

3.变量提升

JavaScript 引擎的工作方式是,先解析代码,获取所有被声明的变量,然后再一行一行地运行。这造成的结果,就是所有的变量的声明语句,都会被提升到代码的头部,这就叫做变量提升(hoisting)。

console.log(a);
var a = 1;

最后的结果是显示undefined,表示变量a已声明,但还未赋值。