JavaScript 教程---互联网文档计划

学习目标:

  • 每天记录一章笔记

学习内容:

JavaScript 教程---互联网文档计划


笔记时间:

2023-6-5 --- 2023-6-11


学习产出:

1.入门篇

1、JavaScript 的核心语法包含部分

基本语法+标准库+宿主API
基本语法:比如操作符、控制结构、语句
标准库:一系列具有各种功能的对象 如Array Date Math
宿主API:只能在该环境使用的接口

浏览器控制类:操作浏览器
DOM 类:操作网页的各种元素
Web 类:实现互联网的各种功能

2、ECMAScript 和 JavaScript 的关系是,前者是后者的规格,后者是前者的一种实现。在日常场合,这两个词是可以互换的。

3、JavaScript 的基本语法

《1》语句

JavaScript 程序的执行单位为行(line),也就是一行一行地执行。一般情况下,每一行就是一个语句。

var a = 1 + 3;

语句以分号结尾,一个分号就表示一个语句结束。多个语句可以写在一行内。

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

分号前面可以没有任何内容,JavaScript 引擎将其视为空语句。

;;;

《2》变量

变量是对“值”的具名引用

var a = 1;

注意,JavaScript 的变量名区分大小写,A和a是两个不同的变量。
只是声明变量而没有赋值,则该变量的值是undefined。
undefined是一个特殊的值,表示“无定义”。

var a;
a // undefined

JavaScript 是一种动态类型语言,变量的类型没有限制,变量可以随时更改类型。

var a = 1;
a = 'hello';

变量提升(hoisting)
所有的变量的声明语句,都会被提升到代码的头部

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

存在变量提升,真正运行的是下面的代码

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

《3》标识符

第一个字符,可以是任意 Unicode 字母(包括英文字母和其他语言的字母),以及美元符号($)和下划线(_)。
第二个字符及后面的字符,除了 Unicode 字母、美元符号和下划线,还可以用数字0-9。

《4》注释

源码中被 JavaScript 引擎忽略的部分就叫做注释

// 这是单行注释

/*
 这是
 多行
 注释
*/

由于历史上 JavaScript 可以兼容 HTML 代码的注释,所以<!--和-->也被视为合法的单行注释。

《5》区块

JavaScript 使用大括号,将多个相关的语句组合在一起,称为“区块”(block)

{
    
    
  var a = 1;
}

a // 1

区块对于var命令不构成单独的作用域,与不使用区块的情况没有任何区别

《6》条件语句(if switch)

if结构

if (布尔值)
  语句;

// 或者
if (布尔值) 语句;

if…else 结构

if (m === 3) {
    
    
  // 满足条件时,执行的语句
} else {
    
    
  // 不满足条件时,执行的语句
}

else代码块总是与离自己最近的那个if语句配对。
switch 结构

switch (fruit) {
    
    
  case "banana":
    // ...
    break;
  case "apple":
    // ...
    break;
  default:
    // ...
}

三元运算符 ?:

(条件) ? 表达式1 : 表达式2

《7》循环语句

循环语句用于重复执行某个操作
while 循环

while (条件)
  语句;

// 或者
while (条件) 语句;

for 循环

for (初始化表达式; 条件; 递增表达式)
  语句

// 或者

for (初始化表达式; 条件; 递增表达式) {
    
    
  语句
}

do…while 循环

do
  语句
while (条件);

// 或者
do {
    
    
  语句
} while (条件);

break 语句和 continue 语句

break语句用于跳出代码块或循环
continue语句用于立即终止本轮循环,返回循环结构的头部,开始下一轮循环

标签(label)

label:
  语句
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语句后面不使用标签,则只能跳出内层循环,进入下一次的外层循环。

2.数据类型

《1》包含的所有数据类型

原始类型:number、string、boolean
合成类型:对象
两个特殊值:undefined、null
对象三个子类型

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

《2》确定一个值的数据类型

typeof运算符可以返回一个值的数据类型

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

在这里插入图片描述
instanceof运算符可以区分数组和对象
在这里插入图片描述

利用这一点,typeof可以用来检查一个没有声明的变量,而不报错

v
// ReferenceError: v is not defined

typeof v
// "undefined"

//实际编程中,这个特点通常用在判断语句
// 错误的写法
if (v) {
    
    
  // ...
}
// ReferenceError: v is not defined

// 正确的写法
if (typeof v === "undefined") {
    
    
  // ...
}

在这里插入图片描述
null的类型是object,这是由于历史原因造成的。1995年的 JavaScript 语言第一版,只设计了五种数据类型(对象、整数、浮点数、字符串和布尔值),没考虑null,只把它当作object的一种特殊值。后来null独立出来,作为一种单独的数据类型,为了兼容以前的代码,typeof null返回object就没法改变了。

//详细说明每一种数据类型

《3》null 和 undefined

两者的历史原因:
1995年 JavaScript 诞生时,最初像 Java 一样,只设置了null表示"无"。根据 C 语言的传统,null可以自动转为0。

Number(null) // 0
5 + null // 5

但是,JavaScript 的设计者 Brendan Eich,觉得这样做还不够。首先,第一版的 JavaScript 里面,null就像在 Java 里一样,被当成一个对象,Brendan Eich 觉得表示“无”的值最好不是对象。其次,那时的 JavaScript 不包括错误处理机制,Brendan Eich 觉得,如果null自动转为0,很不容易发现错误。

因此,他又设计了一个undefined。区别是这样的:null是一个表示“空”的对象,转为数值时为0;undefined是一个表示"此处无定义"的原始值,转为数值时为NaN。

用法和含义
null表示空值,即该处的值现在为空
undefined表示“未定义”

// 变量声明了,但没有赋值
var i;
i // undefined

// 调用函数时,应该提供的参数没有提供,该参数等于 undefined
function f(x) {
    
    
  return x;
}
f() // undefined

// 对象没有赋值的属性
var  o = new Object();
o.p // undefined

// 函数没有返回值时,默认返回 undefined
function f() {
    
    }
f() // undefined

《4》布尔值

下列运算符会返回布尔值:

前置逻辑运算符: ! (Not)
相等运算符:===!====!=
比较运算符:>>=<<=

转换规则是除了下面六个值被转为false,其他值都视为true

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

《5》数值

JavaScript 内部,所有数字都是以64位浮点数形式储存,即使整数也是如此。所以,1与1.0是相同的,是同一个数。
JavaScript 语言的底层根本没有整数,所有数字都是小数(64位浮点数)
在这里插入图片描述
浮点数不是精确的值

0.1 + 0.2 === 0.3
// false

0.3 / 0.1
// 2.9999999999999996

(0.3 - 0.2) === (0.2 - 0.1)
// false

数值精度

1位:符号位,0表示正数,1表示负数
第2位到第12位(共11位):指数部分
第13位到第64位(共52位):小数部分(即有效数字)

数值范围

Math.pow(2, 1024) // Infinity
Math.pow(2, -1075) // 0

特殊数值
正零和负零
几乎所有场合,正零和负零都会被当作正常的0。
唯一区别是:+0或-0当作分母,返回的值是不相等的

-0 === +0 // true
0 === -0 // true
0 === +0 // true

NaN
主要出现在将字符串解析成数字出错的场合

5 - 'x' // NaN

NaN不是独立的数据类型,而是一个特殊数值,它的数据类型依然属于Number

typeof NaN // 'number'

NaN不等于任何值,包括它本身

NaN === NaN // false
数组的indexOf方法内部使用的是严格相等运算符,所以该方法对NaN不成立。
NaN在布尔运算时被当作false。
NaN与任何数(包括它自己)的运算,得到的都是NaN。

Infinity
Infinity与NaN比较,总是返回false
Infinity的四则运算,符合无穷的数学计算规则

5 * Infinity // Infinity
5 - Infinity // -Infinity
Infinity / 5 // Infinity
5 / Infinity // 0

与数值相关的全局方法

1.parseInt方法用于将字符串转为整数

parseInt('123') // 123

自动去除空格

parseInt('   81') // 81

如果parseInt的参数不是字符串,则会先转为字符串再转换

parseInt(1.23) // 1
// 等同于
parseInt('1.23') // 1

字符串转为整数的时候,是一个个字符依次转换,如果遇到不能转为数字的字符,就不再进行下去,返回已经转好的部分

parseInt('8a') // 8
parseInt('12**') // 12
parseInt('12.34') // 12
parseInt('15e2') // 15
parseInt('15px') // 15

如果字符串的第一个字符不能转化为数字(后面跟着数字的正负号除外),返回NaN。

parseInt('abc') // NaN
parseInt('.3') // NaN
parseInt('') // NaN
parseInt('+') // NaN
parseInt('+1') // 1

进制转换
parseInt方法还可以接受第二个参数(2到36之间),表示被解析的值的进制,返回该值对应的十进制数。默认情况下,parseInt的第二个参数为10,即默认是十进制转十进制。
如果第二个参数不是数值,会被自动转为一个整数。这个整数只有在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

2.parseFloat 将一个字符串转为浮点数

parseFloat('3.14') // 3.14
parseFloat('') // NaN

3.isNaN() 判断一个值是否为NaN

isNaN(NaN) // true
isNaN(123) // false

isNaN为true的值,有可能不是NaN,而是一个字符串

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

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

isNaN({
    
    }) // true
// 等同于
isNaN(Number({
    
    })) // true

isNaN(['xzy']) // true
// 等同于
isNaN(Number(['xzy'])) // true

但是,对于空数组和只有一个数值成员的数组,isNaN返回false

isNaN([]) // false
isNaN([123]) // false
isNaN(['123']) // false

使用isNaN之前,最好判断一下数据类型

function myIsNaN(value) {
    
    
  return typeof value === 'number' && isNaN(value);
}

isNaN 函数用于检测一个值是否为非数字值(NaN)。当传递给它的参数不是数字时,该函数会将其转换为数字。如果转换后得到的结果是非数字值,则返回 true。
然而,当传递给 isNaN 函数的参数为非字符串类型时,会发生意外的情况。例如,如果传递给 isNaN 函数的参数为对象、数组或布尔值等,那么它们会被转换为数字并返回 false。这种情况下,即使参数本身不是数字类型,isNaN 函数也会返回 false。

4.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。

《4》字符串
字符串就是零个或多个排在一起的字符,放在单引号或双引号之中
单引号字符串的内部,可以使用双引号。双引号字符串的内部,可以使用单引号

'key = "value"'
"It's a long journey"

如果要在单引号字符串的内部,使用单引号,就必须在内部的单引号前面加上反斜杠,用来转义。双引号字符串内部使用双引号,也是如此。

'Did she say \'Hello\'?'
// "Did she say 'Hello'?"

"Did she say \"Hello\"?"
// "Did she say "Hello"?"

转义
需要用反斜杠转义的特殊字符

\0 :null(\u0000)
\b :后退键(\u0008)
\f :换页符(\u000C)
\n :换行符(\u000A)
\r :回车键(\u000D)
\t :制表符(\u0009)
\v :垂直制表符(\u000B)
\' :单引号(\u0027)
\" :双引号(\u0022)
\\ :反斜杠(\u005C)

《6》字符串

字符串可以被视为字符数组,因此可以使用数组的方括号运算符,用来返回某个位置的字符(位置编号从0开始)。

var s = 'hello';
s[0] // "h"
s[1] // "e"
s[4] // "o"

// 直接对字符串使用方括号运算符
'hello'[1] // "e"

如果方括号中的数字超过字符串的长度,或者方括号中根本不是数字,则返回undefined。

'abc'[3] // undefined
'abc'[-1] // undefined
'abc'['x'] // undefined

字符串与数组的相似性仅此而已。实际上,无法改变字符串之中的单个字符。

var s = 'hello';

delete s[0];
s // "hello"

s[1] = 'a';
s // "hello"

s[5] = '!';
s // "hello"

length 属性
返回字符串的长度,属性无法改变

var s = 'hello';
s.length // 5

s.length = 3;
s.length // 5

s.length = 7;
s.length // 5

《7》对象

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

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

对象的所有键名都是字符串(ES6 又引入了 Symbol 值也可以作为键名),所以加不加引号都可以。

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"

上面代码中,对象o1的属性foo指向对象o2,就可以链式引用o2的属性。
对象的属性之间用逗号分隔,最后一个属性后面可以加逗号(trailing comma),也可以不加。

var obj = {
    
    
  p: 123,
  m: function () {
    
     ... },
}

属性可以动态创建,不必在对象声明时就指定

var obj = {
    
    };
obj.foo = 123;
obj.foo // 123

对象的引用
如果不同的变量名指向同一个对象,那么它们都是这个对象的引用,也就是说指向同一个内存地址。修改其中一个变量,会影响到其他所有变量

var o1 = {
    
    };
var o2 = o1;

o1.a = 1;
o2.a // 1

o2.b = 2;
o1.b // 2

如果取消某一个变量对于原对象的引用,不会影响到另一个变量

var o1 = {
    
    };
var o2 = o1;

o1 = 1;
o2 // {}

这种引用只局限于对象,如果两个变量指向同一个原始类型的值。那么,变量这时都是值的拷贝。

var x = 1;
var y = x;

x = 2;
y // 1

如果行首是一个大括号,它到底是表达式还是语句(代码区块)?

{
    
     foo: 123 }

为了避免这种歧义,JavaScript 引擎的做法是,如果遇到这种情况,无法确定是对象还是代码块,一律解释为代码块。

{
    
     console.log(123) } // 123

上面的语句是一个代码块,而且只有解释为代码块,才能执行。

如果要解释为对象,最好在大括号前加上圆括号。因为圆括号的里面,只能是表达式,所以确保大括号只能解释为对象。

({
    
     foo: 123 }) // 正确
({
    
     console.log(123) }) // 报错

这种差异在eval语句(作用是对字符串求值)中反映得最明显。

eval('{foo: 123}') // 123
eval('({foo: 123})') // {foo: 123}

属性的操作
使用点运算符
使用方括号运算符

var obj = {
    
    
  p: 'Hello World'
};

obj.p // "Hello World"
obj['p'] // "Hello World"

方括号运算符内部还可以使用表达式

obj['hello' + ' world']
obj[3 + 3]

注意,数值键名不能使用点运算符(因为会被当成小数点),只能使用方括号运算符。

var obj = {
    
    
  123: 'hello world'
};

obj.123 // 报错
obj[123] // "hello world"

属性的赋值
点运算符和方括号运算符,不仅可以用来读取值,还可以用来赋值。

var obj = {
    
    };

obj.foo = 'Hello';
obj['bar'] = '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 命令 删除成功后返回true

var obj = {
    
     p: 1 };
Object.keys(obj) // ["p"]

delete obj.p // true
obj.p // undefined
Object.keys(obj) // []

注意,删除一个不存在的属性,delete不报错,而且返回true。

var obj = {
    
    };
delete obj.p // true

只有一种情况,delete命令会返回false,那就是该属性存在,且不得删除

var obj = Object.defineProperty({
    
    }, 'p', {
    
    
  value: 123,
  configurable: false
});

obj.p // 123
delete obj.p // false

delete命令只能删除对象本身的属性,无法删除继承的属性

var obj = {
    
    };
delete obj.toString // true
obj.toString // function toString() { [native code] }

属性是否存在:in 运算符
in运算符用于检查对象是否包含某个属性(注意,检查的是键名,不是键值),如果包含就返回true,否则返回false。

var obj = {
    
     p: 1 };
'p' in obj // true
'toString' in obj // 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

它遍历的是对象所有可遍历(enumerable)的属性,会跳过不可遍历的属性。 它不仅遍历对象自身的属性,还遍历继承的属性。

举例来说,对象都继承了toString属性,但是for…in循环不会遍历到这个属性 它默认是“不可遍历”的

var obj = {
    
    };

// toString 属性是存在的
obj.toString // toString() { [native code] }

for (var p in obj) {
    
    
  console.log(p);
} // 没有任何输出

更好的做法:

var person = {
    
     name: '老张' };

for (var key in person) {
    
    
  if (person.hasOwnProperty(key)) {
    
    
    console.log(key);
  }
}
// name

《8》函数

函数的声明
(1)function 命令

function print(s) {
    
    
  console.log(s);
}

(2)函数表达式

var print = function(s) {
    
    
  console.log(s);
};

采用函数表达式声明函数时,function命令后面不带有函数名。如果加上函数名,该函数名只在函数体内部有效,在函数体外部无效。

var print = function x(){
    
    
  console.log(typeof x);
};

x
// ReferenceError: x is not defined

print()
// function

(3)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

上面代码中,后一次的函数声明覆盖了前面一次。而且,由于函数名的提升(参见下文),前一次声明在任何时候都是无效的

第一等公民

函数与其他数据类型地位平等(比如string boolean number)

function add(x, y) {
    
    
  return x + y;
}

// 将函数赋值给一个变量
var operator = add;

// 将函数作为参数和返回值
function a(op){
    
    
  return op;
}
a(add)(1, 1)
// 2

函数名的提升
JavaScript 引擎将函数名视同变量名

//1.不会报错
f();

function f() {
    
    }

//2.会报错
f();
var f = function (){
    
    };
// TypeError: undefined is not a function

var f;
f();
f = function () {
    
    };

//3 f() // 1
var f = function () {
    
    
  console.log('1');
}

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

f() // 1

函数的属性和方法

name 属性(返回函数的名字)

function f1() {
    
    }
f1.name // "f1"

通过变量赋值定义的函数,那么name属性返回变量名(只有在变量的值是一个匿名函数时才是如此)

var f2 = function () {
    
    };
f2.name // "f2"

如果变量的值是一个具名函数,那么name属性返回function关键字之后的那个函数名

var f3 = function myName() {
    
    };
f3.name // 'myName'

name属性的一个用处,就是获取参数函数的名字

var myFunc = function () {
    
    };

function test(f) {
    
    
  console.log(f.name);
}

test(myFunc) // myFunc

length 属性

function f(a, b) {
    
    }
f.length // 2

length属性提供了一种机制,判断定义时和调用时参数的差异,以便实现面向对象编程的“方法重载”(overload)

toString() (返回一个字符串,内容是函数的源码)

function f() {
    
    
  a();
  b();
  c();
}

f.toString()
// function f() {
    
    
//  a();
//  b();
//  c();
// }

对于那些原生的函数,toString()方法返回function (){[native code]}
利用这一点,可以变相实现多行字符串

var multiline = function (fn) {
    
    
  var arr = fn.toString().split('\n');
  return arr.slice(1, arr.length - 1).join('\n');
};

function f() {
    
    /*
  这是一个
  多行注释
*/}

multiline(f);
// " 这是一个
//   多行注释"

函数作用域

指的是变量存在的范围:
《1》全局作用域
《2》函数作用域
《3》块级作用域
对于顶层函数来说,函数外部声明的变量就是全局变量(global variable),它可以在函数内部读取。

var v = 1;

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

f()
// 1

在函数内部定义的变量,外部无法读取,称为“局部变量”

function f(){
    
    
  var v = 1;
}

v // ReferenceError: v is not defined

函数内部定义的变量,会在该作用域内覆盖同名全局变量

var v = 1;

function f(){
    
    
  var v = 2;
  console.log(v);
}

f() // 2
v // 1

注意,对于var命令来说,局部变量只能在函数内部声明,在其他区块中声明,一律都是全局变量

if (true) {
    
    
  var x = 5;
}
console.log(x);  // 5

函数内部的变量提升
与全局作用域一样,函数作用域内部也会产生“变量提升”现象。var命令声明的变量,不管在什么位置,变量声明都会被提升到函数体的头部

function foo(x) {
    
    
  if (x > 100) {
    
    
    var tmp = x - 100;
  }
}

// 等同于
function foo(x) {
    
    
  var tmp;
  if (x > 100) {
    
    
    tmp = x - 100;
  };
}

函数本身的作用域

函数执行时所在的作用域,是定义时的作用域,而不是调用时所在的作用域

var a = 1;
var x = function () {
    
    
  console.log(a);
};

function f() {
    
    
  var a = 2;
  x();
}

f() // 1

如果函数A调用函数B,却没考虑到函数B不会引用函数A的内部变量

var x = function () {
    
    
  console.log(a);
};

function y(f) {
    
    
  var a = 2;
  f();
}

y(x)
// ReferenceError: a is not defined

函数体内部声明的函数,作用域绑定函数体内部

function foo() {
    
    
  var x = 1;
  function bar() {
    
    
    console.log(x);
  }
  return bar;
}

var x = 2;
var f = foo();
f() // 1

参数
函数运行的时候,有时需要提供外部数据,不同的外部数据会得到不同的结果,这种外部数据就叫参数

function square(x) {
    
    
  return x * x;
}

square(2) // 4
square(3) // 9

参数的省略

function f(a, b) {
    
    
  return a;
}

f(1, 2, 3) // 1
f(1) // 1
f() // undefined

//函数的length属性与实际传入的参数个数无关,只反映函数预期传入的参数个数。
f.length // 2

没有办法只省略靠前的参数,而保留靠后的参数。如果一定要省略靠前的参数,只有显式传入undefined

function f(a, b) {
    
    
  return a;
}

f( , 1) // SyntaxError: Unexpected token ,(…)
f(undefined, 1) // undefined

传递方式
值传递:原始类型 在函数体内修改参数值,不会影响到函数外部
在这里插入图片描述
传址传递:函数参数是复合类型的值(数组、对象、其他函数)

var obj = {
    
     p: 1 };

function f(o) {
    
    
  o.p = 2;
}
f(obj);

obj.p // 2

,如果函数内部修改的,不是参数对象的某个属性,而是替换掉整个参数,这时不会影响到原始值

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

如果要获得第一个a的值,可以使用arguments对象。

function f(a, a) {
    
    
  console.log(arguments[0]);
}

f(1) // 1

arguments 对象
由于 JavaScript 允许函数有不定数目的参数,所以需要一种机制,可以在函数体内部读取所有参数。这就是arguments对象的由来--只有在函数体内部,才可以使用

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

严格模式下,arguments对象与函数参数不具有联动关系。

var f = function(a, b) {
    
    
  'use strict'; // 开启严格模式
  arguments[0] = 3;
  arguments[1] = 2;
  return a + b;
}

f(1, 1) // 2

通过arguments对象的length属性,可以判断函数调用时到底带几个参数

function f() {
    
    
  return arguments.length;
}

f(1, 2, 3) // 3
f(1) // 1
f() // 0

与数组的关系
虽然arguments很像数组,但它是一个对象。数组专有的方法(比如slice和forEach),不能在arguments对象上直接使用
下面是两种常用的转换方法:slice方法和逐一填入新数组。

var args = Array.prototype.slice.call(arguments);

// 或者
var args = [];
for (var i = 0; i < arguments.length; i++) {
    
    
  args.push(arguments[i]);
}

arguments对象带有一个callee属性,返回它所对应的原函数

var f = function () {
    
    
  console.log(arguments.callee === f);
}

f() // true

函数的其他知识点
闭包
闭包是指函数和其相关的引用环境关联在一起的组合。也就是说,当一个函数访问了另一个函数内部的变量时,就会形成一个闭包。闭包可以让函数访问外部作用域中定义的变量、参数和函数,并保留它们的值,即使这些变量和函数已经超出了原本的作用域范围依然有效。

比如以下代码,内部函数innerFunction就是一个闭包,它可以访问到外部函数outerFunction里面的变量a:

function outerFunction() {
    
    
  var a = 1;
  function innerFunction() {
    
    
    console.log(a);
  }
  
  return innerFunction;
}

var myInnerFunction = outerFunction();
myInnerFunction(); // 输出1

这段代码中,调用outerFunction函数返回了innerFunction函数的引用,并将其赋值给了myInnerFunction。之后,每次调用myInnerFunction都会输出内部函数引用的外部变量a的值(1)。这就是典型的闭包模式。
变量作用域

var n = 999;

function f1() {
    
    
  console.log(n);
}
f1() // 999

正常情况下,函数外部无法读取函数内部声明的变量

function f1() {
    
    
  var n = 999;
}

console.log(n)
// Uncaught ReferenceError: n is not defined(

解决办法:
在函数的内部,再定义一个函数

function f1() {
    
    
  var n = 999;
  function f2() {
    
    
  console.log(n); // 999
  }
}

上面代码中,函数f2就在函数f1内部,这时f1内部的所有局部变量,对f2都是可见的。但是反过来就不行,f2内部的局部变量,对f1就是不可见的。这就是 JavaScript 语言特有的"链式作用域"结构(chain scope),子对象会一级一级地向上寻找所有父对象的变量。所以,父对象的所有变量,对子对象都是可见的,反之则不成立。

function f1() {
    
    
  var n = 999;
  function f2() {
    
    
    console.log(n);
  }
  return f2;
}

var result = f1();
result(); // 999

闭包就是函数f2,即能够读取其他函数内部变量的函数。
可以把闭包简单理解成“定义在一个函数内部的函数”。
闭包使得内部变量记住上一次调用时的运算结果

function createIncrementor(start) {
    
    
  return function () {
    
    
    return start++;
  };
}

var inc = createIncrementor(5);

inc() // 5
inc() // 6
inc() // 7

为什么闭包能够返回外层函数的内部变量?原因是闭包(上例的inc)用到了外层变量(start),导致外层函数(createIncrementor)不能从内存释放。只要闭包没有被垃圾回收机制清除,外层函数提供的运行环境也不会被清除,它的内部变量就始终保存着当前值,供闭包读取。

立即调用的函数表达式(IIFE)
JavaScript 规定,如果function关键字出现在行首,一律解释成语句。
函数定义后立即调用的解决方法,就是不要让function出现在行首,让引擎将其理解成一个表达式。最简单的处理,就是将其放在一个圆括号里面

(function(){
    
     /* code */ }());
// 或者
(function(){
    
     /* code */ })();

这样便是:立即调用的函数表达式

eval 命令
eval命令接受一个字符串作为参数,并将这个字符串当作语句执行

eval('var a = 1;');
a // 1

如果参数字符串无法当作语句运行,那么就会报错。

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

为了防止这种风险,JavaScript 规定,如果使用严格模式,eval内部声明的变量,不会影响到外部作用域。

(function f() {
    
    
  'use strict';
  eval('var foo = 123');
  console.log(foo);  // ReferenceError: foo is not defined
})()

即使在严格模式下,eval依然可以读写当前作用域的变量

(function f() {
    
    
  'use strict';
  var foo = 1;
  eval('foo = 2');
  console.log(foo);  // 2
})()

eval 的别名调用

var m = eval;
m('var x = 1');
x // 1

为了保证eval的别名不影响代码优化,JavaScript 的标准规定,凡是使用别名执行eval,eval内部一律是全局作用域

var a = 1;

function f() {
    
    
  var a = 2;
  var e = eval;
  e('console.log(a)');
}

f() // 1

《9》数组

数组(array)是按次序排列的一组值

var arr = ['a', 'b', 'c'];
var arr = [];

arr[0] = 'a';
arr[1] = 'b';
arr[2] = 'c';

任何类型的数据,都可以放入数组

var arr = [
  {
    
    a: 1},
  [1, 2, 3],
  function() {
    
    return true;}
];

arr[0] // Object {a: 1}
arr[1] // [1, 2, 3]
arr[2] // function (){return true;}

在这里插入图片描述
如果数组的元素还是数组,就形成了多维数组

var a = [[1, 2], [3, 4]];
a[0][1] // 2
a[1][1] // 4

数组的本质 数组属于一种特殊的对象。typeof运算符会返回数组的类型是object

typeof [1, 2, 3] // "object"
var arr = ['a', 'b', 'c'];
Object.keys(arr)
// ["0", "1", "2"]

数组的键名其实也是字符串。之所以可以用数值读取,是因为非字符串的键名会被转为字符串

var arr = ['a', 'b', 'c'];

arr['0'] // 'a'
arr[0] // 'a'

一个值总是先转成字符串,再作为键名进行赋值

var a = [];

a[1.00] = 6;
a[1] // 6

,对象有两种读取成员的方法:点结构(object.key)和方括号结构(object[key])。但是,对于数值的键名,不能使用点结构

var arr = [1, 2, 3];
arr.0 // SyntaxError

length 属性
数组的length属性,返回数组的成员数量

['a', 'b', 'c'].length // 3

数组的数字键不需要连续,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属性是可写的。如果人为设置一个小于当前成员个数的值,该数组的成员数量会自动减少到length设置的值

var arr = [ 'a', 'b', 'c' ];
arr.length // 3

arr.length = 2;
arr // ["a", "b"]

``清空数组的一个有效方法,就是将length属性设为0

var arr = [ 'a', 'b', 'c' ];

arr.length = 0;
arr // []

如果人为设置length大于当前元素个数,则数组的成员数量会增加到这个值,新增的位置都是空位

var a = ['a'];

a.length = 3;
a[1] // 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属性的值就是等于最大的数字键加1,而这个数组没有整数键,所以length属性保持为0。

如果数组的键名是添加超出范围的数值,该键名会自动转为字符串

var arr = [];
arr[-1] = 'a';
arr[Math.pow(2, 32)] = 'b';

arr.length // 0
arr[-1] // "a"
arr[4294967296] // "b"

in 运算符
检查某个键名是否存在的运算符in,适用于对象,也适用于数组

var arr = [ 'a', 'b', 'c' ];
2 in arr  // true
'2' in arr // true
4 in arr // false

注意,如果数组的某个位置是空位,in运算符返回false。

var arr = [];
arr[100] = 'a';

100 in arr // true
1 in arr // false

for…in 循环和数组的遍历

var a = [1, 2, 3];

for (var i in a) {
    
    
  console.log(a[i]);
}
// 1
// 2
// 3

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)

var a = [1, , 1];
a.length // 3

上面代码表明,数组的空位不影响length属性。虽然这个位置没有值,引擎依然认为这个位置是有效的。
如果最后一个元素后面有逗号,并不会产生空位。

var a = [1, 2, 3,];

a.length // 3
a // [1, 2, 3]

数组的空位是可以读取的,返回undefined

var a = [, , ,];
a[1] // undefined

使用delete命令删除一个数组成员,会形成空位,并且不会影响length属性

var a = [1, 2, 3];
delete a[1];

a[1] // undefined
a.length // 3

上面代码用delete命令删除了数组的第二个元素,这个位置就形成了空位,但是对length属性没有影响。也就是说,length属性不过滤空位。
数组的某个位置是空位,与某个位置是undefined,是不一样的。
如果是空位,使用数组的forEach方法、for…in结构、以及Object.keys方法进行遍历,空位都会被跳过。

var a = [, , ,];

a.forEach(function (x, i) {
    
    
  console.log(i + '. ' + x);
})
// 不产生任何输出

for (var i in a) {
    
    
  console.log(i);
}
// 不产生任何输出

Object.keys(a)
// []

如果某个位置是undefined,遍历的时候就不会被跳过。

var a = [undefined, undefined, undefined];

a.forEach(function (x, i) {
    
    
  console.log(i + '. ' + x);
});
// 0. undefined
// 1. undefined
// 2. undefined

for (var i in a) {
    
    
  console.log(i);
}
// 0
// 1
// 2

Object.keys(a)
// ['0', '1', '2']

类似数组的对象
如果一个对象的所有键名都是正整数或零,并且有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

这种length属性不是动态值,不会随着成员的变化而变化

var obj = {
    
    
  length: 0
};
obj[3] = 'd';
obj.length // 0

典型的“类似数组的对象”是函数的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遍历。

Array.prototype.forEach.call('abc', function (chr) {
    
    
  console.log(chr);
});
// a
// b
// c

3.运算符

《1》算术运算符

概述

加法运算符: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
//字符串+字符串=连接字符串
'a' + 'bc' // "abc"
//字符串+非字符串=连接字符串
1 + 'a' // "1a"
false + 'a' // "falsea"

加法运算符是在运行时决定,到底是执行相加,还是执行连接 ---‘重载’

'3' + 4 + 5 // "345"
3 + 4 + '5' // "75"

除了加法运算符,其他算术运算符(比如减法、除法和乘法)都不会发生重载。它们的规则是:所有运算子一律转为数值,再进行相应的数学运算。

1 - '2' // -1
1 * '2' // 2
1 / '2' // 0.5

对象的相加
如果运算子是对象,必须先转成原始类型的值,然后再相加

var obj = {
    
     p: 1 };
obj + 2 // "[object Object]2"

一般来说,对象的valueOf方法总是返回对象自身,这时再自动调用对象的toString方法,将其转为字符串。对象的toString方法默认返回[object Object],所以就得到了最前面那个例子的结果。

var obj = {
    
    
  valueOf: function () {
    
    
    return 1;
  }
};

obj + 2 // 3
var obj = {
    
    
  toString: function () {
    
    
    return 'hello';
  }
};

obj + 2 // "hello2"
var obj = new Date();
obj.valueOf = function () {
    
     return 1 };
obj.toString = function () {
    
     return 'hello' };

obj + 2 // "hello2"

余数运算符
余数运算符(%)返回前一个运算子被后一个运算子除,所得的余数。

12 % 5 // 2

运算结果的正负号由第一个运算子的正负号决定

-1 % 2 // -1
1 % -2 // 1

为了得到负数的正确余数值,可以先使用绝对值函数

// 错误的写法
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

余数运算符还可以用于浮点数的运算。但是,由于浮点数不是精确的值,无法得到完全准确的结果。

6.5 % 2.1
// 0.19999999999999973

自增和自减运算符

var x = 1;
++x // 2
x // 2

--x // 1
x // 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 ** 4 // 16
// 相当于 2 ** (3 ** 2)
2 ** 3 ** 2
// 512

赋值运算符

// 将 1 赋值给变量 x
var x = 1;

// 将变量 y 的值赋值给变量 x
var 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
x ^= y

《2》比较运算符

概述
比较运算符用于比较两个值的大小,然后返回一个布尔值
注意,比较运算符可以比较各种类型的值,不仅仅是数值。

2 > 1 // true

JavaScript 一共提供了8个比较运算符

> 大于运算符
< 小于运算符
<= 小于或等于运算符
>= 大于或等于运算符
== 相等运算符
=== 严格相等运算符
!= 不相等运算符
!== 严格不相等运算符

八个比较运算符分成两类:相等比较和非相等比较
非相等的比较,算法是先看两个运算子是否都是字符串,如果是的,就按照字典顺序比较(实际上是比较 Unicode 码点);否则,将两个运算子都转成数值,再比较数值的大小

非相等运算符:字符串的比较

'cat' > 'dog' // false
'cat' > 'catalog' // false
'cat' > 'Cat' // true'
'大' > '小' // false

非相等运算符:非字符串的比较
如果两个运算子之中,至少有一个不是字符串,需要分成以下两种情况

(1)原始类型值
如果两个运算子都是原始类型的值,则是先转成数值再比较

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

1 > NaN // false
1 <= NaN // false
'1' > NaN // false
'1' <= NaN // false
NaN > NaN // false
NaN <= NaN // false

(2)对象
如果运算子是对象,会转为原始类型的值,再进行比较。

var x = [2];
x > '11' // true
// 等同于 [2].valueOf().toString() > '11'
// 即 '2' > '11'

x.valueOf = function () {
    
     return '1' };
x > '11' // false
// 等同于 (function () { return '1' })() > '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]'

严格相等运算符

相等运算符(==)比较两个值是否相等,严格相等运算符(===)比较它们是否为“同一个值”。
相等运算符(==)会将它们转换成同一个类型,再用严格相等运算符进行比较。

(1)不同类型的值
如果两个值的类型不同,直接返回false

1 === "1" // false
true === "true" // false

(2)同一类的原始类型值
同一类型的原始类型的值(数值、字符串、布尔值)比较时,值相同就返回true,值不同就返回false。

1 === 0x1 // true

(3)复合类型值
比较它们是否指向同一个地址

{
    
    } === {
    
    } // false
[] === [] // false
(function () {
    
    } === function () {
    
    }) // false

如果两个变量引用同一个对象,则它们相等

var v1 = {
    
    };
var v2 = v1;
v1 === v2 // true

注意,对于两个对象的比较,严格相等运算符比较的是地址,而大于或小于运算符比较的是值

var obj1 = {
    
    };
var obj2 = {
    
    };

obj1 > obj2 // false
obj1 < obj2 // false
obj1 === obj2 // false

(4)undefined 和 null
undefined和null与自身严格相等

undefined === undefined // true
null === null // true

严格不相等运算符
先求严格相等运算符的结果,然后返回相反值

1 !== '1' // true
// 等同于
!(1 === '1')

相等运算符
比较不同类型的数据时,相等运算符会先将数据进行类型转换,然后再用严格相等运算符比较。
(1)原始类型值
原始类型的值会转换成数值再进行比较

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
// 因为字符串转为数字时,省略前置和后置的空格

(2)对象与原始类型值比较

// 数组与数值的比较
[1] == 1 // true

// 数组与字符串的比较
[1] == '1' // true
[1, 2] == '1,2' // true

// 对象与布尔值的比较
[1] == true // true
[2] == true // false

(3)undefined 和 null
undefined和null只有与自身比较,或者互相比较时,才会返回true;与其他类型的值比较时,结果都为false

undefined == undefined // true
null == null // true
undefined == null // true

false == null // false
false == undefined // false

0 == null // false
0 == undefined // false

(4)相等运算符的缺点
相等运算符隐藏的类型转换,会带来一些违反直觉的结果

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

建议不要使用相等运算符(==),最好只使用严格相等运算符(===)
不相等运算符
先求相等运算符的结果,然后返回相反值

1 != '1' // false

// 等同于
!(1 == '1')

《3》布尔运算符

取反运算符(!)
对于非布尔值,取反运算符会将其转为布尔值
以下六个值取反后为true,其他值都为false
undefined
null
false
0
NaN
空字符串(‘’)

!undefined // true
!null // true
!0 // true
!NaN // true
!"" // true

!54 // false
!'hello' // false
![] // false
!{
    
    } // false

如果对一个值连续做两次取反运算,等于将其转为对应的布尔值,与Boolean函数的作用相同。

!!x
// 等同于
Boolean(x)

且运算符(&&)
如果第一个运算子的布尔值为true,则返回第二个运算子的值(注意是值,不是布尔值);如果第一个运算子的布尔值为false,则直接返回第一个运算子的值,且不再对第二个运算子求值

't' && '' // ""
't' && 'f' // "f"
't' && (1 + 2) // 3
'' && 'f' // ""
'' && '' // ""

var x = 1;
(1 - 1) && ( x += 1) // 0
x // 1
if (i) {
    
    
  doSomething();
}

// 等价于

i && doSomething();

或运算符(||)
如果第一个运算子的布尔值为true,则返回第一个运算子的值,且不再对第二个运算子求值;如果第一个运算子的布尔值为false,则返回第二个运算子的值

't' || '' // "t"
't' || 'f' // "t"
'' || 'f' // "f"
'' || '' // ""
var x = 1;
true || (x = 2) // true
x // 1

三元条件运算符(?:)

't' ? 'hello' : 'world' // "hello"
0 ? 'hello' : 'world' // "world"

if...else是语句,没有返回值;三元条件表达式是表达式,具有返回值。所以,在需要返回值的场合,只能使用三元条件表达式,而不能使用if..else。

《4》二进制位运算符

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

《5》其他运算符,运算顺序

void 运算符
void运算符的作用是执行一个表达式,然后不返回任何值,或者说返回undefined

void 0 // undefined
void(0) // undefined

这个运算符的主要用途是浏览器的书签工具(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

在这里插入图片描述
上面代码中,先执行逗号之前的操作,然后返回逗号后面的值。

优先级
小于等于(<=)、严格相等(===)、或(||)、三元(?:)、等号(=)

圆括号的作用
一种是把表达式放在圆括号之中,提升运算的优先级;
另一种是跟在函数的后面,作用是调用函数

function f() {
    
    
  return 1;
}

(f) // function f(){return 1;}
f() // 1

上面代码中,函数放在圆括号之中会返回函数本身,圆括号跟在函数后面则是调用函数。

左结合与右结合

a OP b OP c
// 方式一 左结合
(a OP b) OP c

// 方式二 右结合
a OP (b OP c)
x + y + z

// 引擎解释如下
(x + y) + z

4.语法专题

《1》数据类型的转换

var x = y ? 1 : 'a';

(1)强制转换
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('42 cats') // 42
Number('42 cats') // NaN
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
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。

如果toString方法返回的不是原始类型的值,结果就会报错。

var obj = {
    
    
  valueOf: function () {
    
    
    return {
    
    };
  },
  toString: function () {
    
    
    return {
    
    };
  }
};

Number(obj)
// TypeError: Cannot convert object to primitive value

String()
String函数可以将任意类型的值转化成字符串
(1)原始类型值

数值:转为相应的字符串。
字符串:转换后还是原来的值。
布尔值:true转为字符串"true",false转为字符串"false"。
undefined:转为字符串"undefined"。
null:转为字符串"null"。

String(123) // "123"
String('abc') // "abc"
String(true) // "true"
String(undefined) // "undefined"
String(null) // "null"

(2)对象
String方法的参数如果是对象,返回一个类型字符串;如果是数组,返回该数组的字符串形式

String({
    
    a: 1}) // "[object Object]"
String([1, 2, 3]) // "1,2,3"
String({
    
    a: 1})
// "[object Object]"

// 等同于
String({
    
    a: 1}.toString())
// "[object Object]"

Boolean()
Boolean()函数可以将任意类型的值转为布尔值
除了以下五个值的转换结果为false,其他的值全部为true

undefined
null
0(包含-0+0)
NaN
''(空字符串)
Boolean(undefined) // false
Boolean(null) // false
Boolean(0) // false
Boolean(NaN) // false
Boolean('') // false

当然,true和false这两个布尔值不会发生变化

Boolean(true) // true
Boolean(false) // false

注意,所有对象(包括空对象)的转换结果都是true,甚至连false对应的布尔对象new Boolean(false)也是true

Boolean({
    
    }) // true
Boolean([]) // true
Boolean(new Boolean(false)) // true

(2)自动转换
遇到以下三种情况时,JavaScript 会自动转换数据类型,即转换是自动完成的,用户不可见
第一种情况,不同类型的数据互相运算。

123 + 'abc' // "123abc"

第二种情况,对非布尔值类型的数据求布尔值。

if ('abc') {
    
    
  console.log('hello')
}  // "hello"

第三种情况,对非数值类型的值使用一元运算符(即+和-)。

+ {
    
    foo: 'bar'} // NaN
- [1, 2, 3] // NaN

自动转换为布尔值
除了以下五个值,其他都是自动转为true

undefined
null
+0-0
NaN
''(空字符串)

自动转换为字符串
字符串的自动转换,主要发生在字符串的加法运算时。当一个值为字符串,另一个值为非字符串,则后者转为字符串

'5' + 1 // '51'
'5' + true // "5true"
'5' + false // "5false"
'5' + {
    
    } // "5[object Object]"
'5' + [] // "5"
'5' + function (){
    
    } // "5function (){}"
'5' + undefined // "5undefined"
'5' + null // "5null"

自动转换为数值

'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

《2》错误处理机制

(1)Error 实例对象
JavaScript 原生提供Error构造函数,所有抛出的错误都是这个构造函数的实例

var err = new Error('出错了');
err.message // "出错了"

大多数 JavaScript 引擎,对Error实例还提供name和stack属性,分别表示错误的名称和错误的堆栈,但它们是非标准的,不是每种实现都有
message:错误提示信息
name:错误名称(非标准属性)
stack:错误的堆栈(非标准属性)

if (error.name) {
    
    
  console.log(error.name + ': ' + error.message);
}

(2)原生错误类型
SyntaxError 对象
SyntaxError对象是解析代码时发生的语法错误。

// 变量名错误
var 1a;
// Uncaught SyntaxError: Invalid or unexpected token

// 缺少括号
console.log 'hello');
// Uncaught SyntaxError: Unexpected string

ReferenceError 对象
ReferenceError对象是引用一个不存在的变量时发生的错误。

// 使用一个不存在的变量
unknownVariable
// Uncaught ReferenceError: unknownVariable is not defined

另一种触发场景是,将一个值分配给无法分配的对象,比如对函数的运行结果赋值。

// 等号左侧不是变量
console.log() = 1
// Uncaught ReferenceError: Invalid left-hand side in assignment

RangeError 对象
RangeError对象是一个值超出有效范围时发生的错误。主要有几种情况,一是数组长度为负数,二是Number对象的方法参数超出范围,以及函数堆栈超过最大值。

// 数组长度不得为负数
new Array(-1)
// Uncaught RangeError: Invalid array length

TypeError 对象
TypeError对象是变量或参数不是预期类型时发生的错误。

new 123
// Uncaught TypeError: 123 is not a constructor

var obj = {
    
    };
obj.unknownMethod()
// Uncaught TypeError: obj.unknownMethod is not a function

URIError 对象
URIError对象是 URI 相关函数的参数不正确时抛出的错误,主要涉及encodeURI()、decodeURI()、encodeURIComponent()、decodeURIComponent()、escape()和unescape()这六个函数。

decodeURI('%2')
// URIError: URI malformed

EvalError 对象
eval函数没有被正确执行时,会抛出EvalError错误。该错误类型已经不再使用了,只是为了保证与以前代码兼容,才继续保留。

总结
以上这6种派生错误,连同原始的Error对象,都是构造函数

var err1 = new Error('出错了!');
var err2 = new RangeError('出错了,变量超出有效范围!');
var err3 = new TypeError('出错了,变量类型无效!');

err1.message // "出错了!"
err2.message // "出错了,变量超出有效范围!"
err3.message // "出错了,变量类型无效!"

(3)自定义错误

function UserError(message) {
    
    
  this.message = message || '默认信息';
  this.name = 'UserError';
}

UserError.prototype = new Error();
UserError.prototype.constructor = UserError;

new UserError('这是自定义的错误!');

(4)throw 语句
throw语句的作用是手动中断程序执行,抛出一个错误。

var x = -1;

if (x <= 0) {
    
    
  throw new Error('x 必须为正数');
}
// Uncaught Error: x 必须为正数

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: ƒ}

5)try…catch 结构

try {
    
    
  throw new Error('出错了!');
} catch (e) {
    
    
  console.log(e.name + ": " + e.message);
  console.log(e.stack);
}
// Error: 出错了!
//   at <anonymous>:3:9
//   ...

为了捕捉不同类型的错误,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);
  }
  // ...
}

(6)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
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

《3》编程风格

建议总是使用大括号表示区块

block {
    
    
  // ...
}

因为 JavaScript 会自动添加句末的分号,导致一些难以察觉的错误

圆括号

表示函数调用时,函数名与左括号之间没有空格。

表示函数定义时,函数名与左括号之间没有空格。

其他情况时,前面位置的语法元素与左括号之间,都有一个空格。

行尾的分号,不要省略

不使用分号的情况
(1)for 和 while 循环

for ( ; ; ) {
    
    
} // 没有分号

while (true) {
    
    
} // 没有分号

(2)分支语句:if,switch,try

if (true) {
    
    
} // 没有分号

switch () {
    
    
} // 没有分号

try {
    
    
} catch {
    
    
} // 没有分号

(3)函数的声明语句

function f() {
    
    
} // 没有分号

JavaScript 最大的语法缺点,可能就是全局变量对于任何一个代码块,都是可读可写。这对代码的模块化和重复使用,非常不利
考虑用大写字母表示变量名,这样更容易看出这是全局变量,比如UPPER_CASE
变量声明
JavaScript 会自动将变量声明“提升”(hoist)到代码块(block)的头部。

《4》console 对象与控制台

(1)console 对象
调试程序,显示网页代码运行时的错误信息
提供了一个命令行接口,用来与网页代码互动

(2)console 对象的静态方法
console对象提供的各种静态方法,用来与控制台窗口互动。

console.log(),console.info(),console.debug()
console.warn(),console.error()
console.table()
console.count()
console.dir(),console.dirxml()
console.assert()
console.time(),console.timeEnd()
console.group(),console.groupEnd(),console.groupCollapsed()
console.trace(),console.clear()
(3)控制台命令行 API
debugger 语句

5.标准库

6.面向对象编程

7.异步操作

8.DOM

9.事件

10.浏览器模型

11.附录:网页元素接口

猜你喜欢

转载自blog.csdn.net/Lixu_No_1/article/details/131143065