学习目标:
- 每天记录一章笔记
学习内容:
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 语句