语法细节——字符串和列表
语法细节——字符串和列表
基于众所周知的原因,我们在这个课上不会(也不应该)再讲太多基础语法,一般我们只会介绍JS跟C语言有所不同的部分。
在开始之前我要再次强调:不实际动手就没有任何意义,请切实动起来。
基础语法
运算符
JS运算符基本与C语言相同,但有三点需要注意:
运算符 | 作用 | 注意 |
---|---|---|
/ | 除法 | 不是整数除法,3/2=1.5 |
** | 幂运算 | n次方计算,ES2016后支持 |
=== | 全等 | 值和类型都相等 |
第一是除法和C不同,JavaScript的除法不是整数除法。实际上JavaScript并不区分整数和浮点数,内部计算一律都是浮点数。 因此你不能预期得到3/2=1这样结果。
但有些时候你确实需要取整,JavaScript有一些方法来做到:
>>parseInt(7/2)
3
>>Math.ceil(7/2)
4
>>Math.round(7/2)
4
>>Math.floor(7/2)
3
函数 | 作用 | 注意 |
---|---|---|
parseInt | 转换为整数 | 不仅对数字有效,对字符串也有效,甚至16进制数 |
Math.ceil | 向上取整 | 结果一定比参数大 |
Math.floor | 向下取整 | 结果一定比参数小 |
Math.round | 四舍五入 |
你知道吗?
你觉得0.1+0.2
等于多少?在终端里面试试。
看下面的计算,自己想想结果是多少,试一试,你想的对不对?
parseInt("11")
parseInt("0x20")
parseInt(0x20)
Math.ceil(-1.5)
Math.floor(-1.5)
Math.round(-1.5)
此外,如果你需要保留n位小数,你可以使用toFixed
这个函数。注意这个函数返回的是string,也就是字符串。
最后的运算符===
是一个其它语言基本都没有的运算符——某种程度上说明JS是一种多么独特的语言。
要理解为什么需要找个运算符,可以从下面找个算式开始:
11 == "11"
试试,他会输出true
,也就是说JS认为11
和"11"
是相等的。这种奇怪的现象是因为JS真的非常努力地让每个算式都能得出有意义的值(此处应有表情包)。
因此他经常会偷偷进行隐式转换。
为了避免这种情况,你可以要求以更严格的标准来判断是否相等——不仅值要相等,类型也要相等,所以就有了===
这个全等判断(以及!==
,应该很好理解)。
11 === "11"
这个式子就返回false
了。
基本数据类型
JavaScript常用的数据类型包括:数字、字符串、布尔值等。
数字
和C不一样,JavaScript的数字只有一种,其内部大体上都是用64位双精度数字来处理的,但依然可以处理整数,是比较智能的数字。
字符串
多数解释型语言下,字符串都是基本类型,是可以直接处理的:
>>let s = "Hello"
>>type(s)
"string"
>>s+=" World!"
>>console.log(s)
Hello World!
>>console.log("hello"+" world!")
hello world!
在JS里,字符串可以相加,+=,记住,不要在C语言里这么做,你会死得很惨。
布尔
对于真/假的值,JS有专门的类型来处理(C语言用的是int
来处理)。这就是布尔类型。 布尔类型只能取true
和false
两种值。可以用逻辑运算符来运算。
语句
和许多类C语言一样,JavaScript每条语句的后面用一个分号表示语句结束。前面你们已经看到了。 但跟他们不一样的是,JavaScript真的是一种非常随性的语言,你可以不写分号,照样OK
let a=1
let b=2
console.log(a+b)
但我强烈要求你不要这么做,因为很多时候,你是不知道JavaScript如何理解的:
let a
a
=
3
console.log(a)
JavaScript会正确将其解析成:
let a;
a=3;
console.log(a);
return
true;
则会被解析成:
return;
true;
这可能就不是你想要的结果 还有:
x
++
y
会被理解成:
x;
++y;
很可能跟你想的不一样。 总之,不要调戏JavaScript解释器。
进阶语法
函数
我们之前已经看过函数了,现在我们可以更进一步了解一下:
function add(x,y) {
return x+y;
}
alert(add(1,2))
alert(add(4,8))
打开页面,你会看到执行的结果。
这里,我们通过function
关键字定义一个函数add,这个函数有两个参数,x和y,返回x和y的和。 JS的函数会稍微有些特别,事实上JS里的所有东西都可以赋值。因此你可以这样写:
let add = function(x,y){
return x+y;
}
这个段代码可以这么理解,定义一个函数,这个函数没有名字,因此是匿名函数,这个匿名函数有两个参数,返回两个参数的和。同时将这个函数返回给变量add。 既然函数可以被赋值给变量,那么他当然是可以被传递给其他变量的:
let add = function(x,y){
return x+y;
}
another_add = add;
alert(another_add("Hello"," World!"))
这里你还会发现,x和y,并没有必须是某种类型,只要可以相加,代码就可以正确执行。这和C语言的函数很不一样,需要注意。
这就是弱类型语言与强类型语言的一大区别——弱类型语言的类型都是到了跑到了,才能推导出来。
列表
将一系列变量放在一起,用[]包裹起来,就构成了列表。你可以把列表看成JS的数组,和数组一样列表可以用下标访问:
>>a=[1,2,3,"hello","world"]
>>a[0]
1
>>a[1]
2
>>a[2]
3
>>a[3]
"hello"
>>a[4]
"world"
注意程序员数数都是从0开始的,一定要记得这点。 列表可以通过push在后方插入新元素。
>>a=[]
>>a.push(1)
>>a.push(2)
>>a.push(3)
>>a
Array(3) [ 1, 2, 3 ]
当然,下标可以是变量:
>>a=[1,2,3,"hello","world"]
>>for( i=0;i<a.length;++i)console.log(a[i]);
1
2
3
hello
world
列表天然有一个length属性,可以获得其尺寸,我们通过for循环遍历列表,并且输出他的值。就可以打印出所有列表的元素。
对象
你可以把对象看作JS的结构体,只不过定义的方式略有不同:
let a = {
name : "tom",
password : "123",
}
console.log(a.name); // tom
console.log(a.password); // 123
这个定义方式与C语言结构体是很类似的,不同的在于你并不是定义一个类型,而是定义一个变量。所以我们直接使用a.name
来得到变量a
的name
成员变量。
注意对象的定义方式,以{
和}
来包裹定义,这与C语言(以及大多数脚本语言)是一样的。
有时候我们会把冒号:
前面的这部分也就是name
,称为key或者键值。把后面的部分,也就是"tom"
,称为value或者值。
key必须是字符串,而后面的value则可以是任意类型。
key怎么是字符串?
你可能看不出他时字符串,这是因为上面的写法可以视为以下写法的简写法:
let a = {
"name" : "tom",
"password" : "123",
}
这下看出是字符串了吧。
之前的写法是一种简写的方式,即如果前面的key符合JS变量的定义规则(规则是什么样的?)的话,可以写引号来定义和使用。
所以上面的两种写法是等价的。
引用时,也有另一种引用方法:
// 以下两种写法等价
console.log(a.name);
console.log(a["name"]);
当然你定义key的时候可以不遵循JS变量的定义规则,但此时第一种写法就不可用了。
let a = {
"321":123, // 合法
}
console.log(a["321"]); // 只能用这种方式引用。
当然,用变量来索引也是可以的:
>>a={name:"tom",password:"123"}
>>b="name"
>>a[b]
"tom"
对象和列表对JS来说非常重要。首先他们是JS这样的语言区分于C语言最大的标志——灵活方便且十分强大的数据类型。
其次他们也是组织JSON数据的主要组成部分。JSON数据就是文本化的对象或列表,或者他们的嵌套。
一些其他琐碎的细节
typeof和instanceof运算符
用来获得变量的类型。 两者都可以作为函数或者运算符使用。即:
typeof(1) //可以,当作函数用
typeof 1 //也可以,当作运算符使用
[1,2,3] instanceof(Array) //函数
[1,2,3] instanceof Array //运算符
注意,instanceof不能用于值类型,用于值类型总是返回false。
注意下两者返回的类型,typeof返回一个字符串,说明是什么类型:
typeof(typof(1))
你会得到string instanceof你必须显式告诉他计算“某某变量是不是什么类型” 因此instanceof必须填入类型名称,也就是Array,Object和Function。
undefine和null类型
变量申明未赋值时,其值是undefined
:尚未定义。 变量不存在,或变量指向空间不存在:null
,空对象。 如果你拿不准用哪个,用null
。 一般undefined是系统告诉我们不存在。 试试:
null==undefined
特殊的number类型
无穷大Infinity
注意正无穷和负无穷都有,两者相等 应尽量用isFinite函数来判断是否为无穷
NaN
不是数字,当结果无法表示为数字时,返回NaN,比如parseInt("hello") 注意NaN和自己不相等。
NaN == NaN
你会得到false, 和Infinity一样,最好用isNaN来做判断。
字符串
字符串是很独特的,他可变大小,但又不是引用类型。你可以认为字符串对象的内容不能修改。 字符串既可以用双引号申明,也可以用单引号申明,这会非常有用:
a = "Hello World!"; //可以,双引号。
a = 'Hello World!'; //也可以,单引号
a = 'He say:"Hello World!"'; //可以理解,字符串中要出现双引号,用单引号来申明
a = "He say:'Hello World!'"; //也可以理解,字符串中要出现单引号,用双引号来申明
注意后面两种写法,因为字符串中要出现单/双引号,如果是C语言,必须转义。但js解释器可以正确识别你的意图,省去你转义的麻烦。这在脚本语言里很常见。 字符串经常要跟其他类型互转,比较典型的是和数字互转。 数字转字符串 一般类型都有toString函数来转换成对应的字符串
a = 123;
b = 456;
console.log(a+b);
console.log(a.toString()+b.toString());
你会得到579和"123456"。 字符串转数字 一般可以通过parseInt或者parseFloat来将字符串转成数字
a = "123";
b = "456";
console.log(a+b);
console.log(parseInt(a)+parseInt(b)));
你会得到123456和579。
再看列表
这一节我们详细地讨论下列表和他对应的操作,注意我们的讲义不会罗列所有的操作函数然后一一说明。 你应该学会查手册。
另外还需要一点点小信心——你想要的功能,基本都有人实现过了。
创建和申明
a = new Array();
a = new Array(3); //有三个元素,都为undefined
a = new Array(3,4); //有两个元素,一个3,一个4
a = new Array("123");
a = [1,2,3,4]; //最常用的方式
注意第二种和第三种的区别,当Array函数里填入一个数字时,系统会认为这个参数是表明你要创建多大的数组,多个数字时,会认为你要用这些数字初始化数组。而填入其他类型,都会认为时初始化元素,不会有歧义。
试试
你认为填入一个浮点数会怎样?比如a = new Array(1.2)?
验证语法的最好方法是试一下。
添加元素
数组提供了丰富的添加元素的方法,其中最简单的是直接用下标赋值:
a=[];//空数组
a[0] = 1;
a[1] = 2;
console.log(a);
你会得到一个两个元素的数组:[1,2],当你写的下标在数组中并不存在时,数组会给你创建一个。
你甚至可以跳过一些元素直接为后面的元素赋值:
a=[];
a[9] = 10;
console.log(a);
你会得到一个10个元素的数组:
[ undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, 10 ]
除了下标9的元素之外其他都是undefined
。
有时候我们把这种数组被称为稀疏数组,这个名字非常直观。
用下标赋值的方式不是很直观,有可能出问题。数组提供了push,向数组的最后添加新元素:
a = []
a.push(1);
a.push(2);
a.push(3);
a.push(4);
console.log(a);
你得到一个[1,2,3,4]的数组。
数组元素个数
也就是长度,用length属性来获得:
console.log([1,2,3,4].length);
你得到4。
注意稀疏数组的尺寸是最长尺寸,而不是元素的数目:
a = []
a[9] = 10;
console.log(a.length);
你会得到10(最大下标+1)而不是1(有效元素的数目)。
显然遍历数组时,length会很有用:
a = [1,2,3,4];
for(i=0;i<a.length;++i){
console.log(a[i]);
}
这也是最常见的遍历方式,虽然有时候你会遇到for( let i in a )
这样的用法,不过我建议初学者暂时不要这么用。
删除元素
有三个函数可以执行删除,分别从尾部、头部和任意位置删除元素。
a = [1,2,3,4,5,6,7]
t = a.pop(); //删除尾部元素,注意函数有返回值,就是被删除的元素。
console.log(t); //7
console.log(a); //此时a=[1,2,3,4,5,6]
h = a.shift(); //删除头部元素,返回值为被删除元素。
console.log(h); //1
console.log(a); //此时a=[2,3,4,5,6]
m = a.splice(1,3); //从下标1开始删除3个元素,返回值是删除元素组成的数组。
console.log(m); // m为删除的元素组成的数组,即[3,4,5]
console.log(a); //此时a=[2,6]
理解下这三个函数。他们都会将被删除的元素返回。
注意splice
函数,即使你只删除一个元素,返回也是一个数组,只不过该数组只有一个元素。
插入元素
从尾部加入元素即添加元素,用push即可。如果需要在头部插入元素,你可以用unshift函数:
a=[5];
a.unshift(4);
a.unshift(3);
a.unshift(2);
a.unshift(1);
console.log(a);
你会得到5个元素的数组:[1,2,3,4,5]。
当你需要在任意位置插入数据时,需要一些技巧。 splice这个函数,不仅可以删除元素,也可以用来添加元素:
a=[1,2,3,4,5];
a.splice(1,2,8,9);//从下标1开始删除2个元素,同时插入8、9两个元素。
console.log(a);
你会得到[1,8,9,4,5]
这就是为什么这个函数叫splice(电影剪辑)的原因。 因此当你需要在某个位置插入元素时,你可以删除0个元素并插入你想要的元素:
a=[1,5];
a.splice(1,0,2,3,4);//从第1号下标开始删除0个元素,并插入2、3、4三个元素
console.log(a);
你会得到[1,2,3,4,5]
数组的排序
用sort函数可以给数组排序,默认情况下是升序排列(小的在前面)。
a=[1,4,2,5,3];
a.sort();
你会得到[1,2,3,4,5]
注意一下,排序默认以字符串形式进行排序。也就是说对比两个元素大小时,会对比两者的字符串形式。注意这里的描述:对比两者的字符串形式。
这意味着并不是说排序完元素就编程字符串了,只不过是比较的时候,会转为一个临时的字符串,再对比,并不会影响原来的元素。
看例子
a=[100,23,5];
a.sort();
console.log(a);
你可能会期望得到[5,23,100],事实确是[100,23,5],所以这里比较的实际上并不是5,23,100
而是"5","23","100"
。
因为"100"<"23"<"5",因此100会放在最前面。
如果你想要根据你的要求来排序,你需要为sort添加一个参数,这个参数是个回调函数:
function cmp(a,b){
return a-b;
}
a=[100,23,5];
a.sort(cmp);
console.log(a);
cmp函数必须这样定义:入参为两个值,本例定义成a和b。当a小于b时,返回负数。当a大于b时,返回正数。两者相等时返回0。 sort函数执行时,会调用传入的这个函数,根据传入函数的值来判断两个元素的关系。显然这是一个回调函数——我们自己不调用,而是定义出来交给sort函数调用的。 针对这种情况,js有一种非常魔性的写法,这种写法构成了大多数javascript代码:
a=[100,23,5];
a.sort(function(a,b){
return a-b;
});
console.log(a);
注意sort函数的那行,既然sort需要一个函数,我们就现场写一个函数给他,这样的函数称为“匿名函数”,这个函数只在这里定义并使用。因此我们直接定义出来就好了,不必额外去专门定义一个函数。这样可以减少全局范围内的函数定义,避免污染命名空间。 你会看到,这种写法广泛应用于各种js代码,是js代码的标志。其他基本没有什么语言用这种写法(可能lisp是个例外)。
在ES2015之后,可以用箭头函数,提供一种比function
更简洁的写法:
a=[100,23,5];
a.sort((a,b) => a-b );
console.log(a);
箭头函数请自行查看手册。
思考一下
假如有一个对象数组:
let students = [
{ no:"2300000001",name:"张三", score:100 },
{ no:"2300000002",name:"李四", score:80 },
{ no:"2300000003",name:"王五", score:91 },
{ no:"2300000004",name:"赵六", score:82 },
]
要如何实现分别以学号、姓名或者得分进行排序?
其他操作
合并
concat函数提供数组的合并,会依次合并:
a = [1,2];
ret = a.concat([3,4],[5,6]); //先合并[3,4],再合并[5,6]
console.log(ret);
你会得到[1,2,3,4,5,6]
反转
reverse函数会翻转整个数组:
a=[1,2,3,4,5];
a.reverse();
console.log(a);
你会得到[5,4,3,2,1]
二维和多维数组
数组的元素也可以是数组:
a=[[1,2],[3,4]];
console.log(a[0][0]);
console.log(a[0][1]);
console.log(a[1][0]);
console.log(a[1][1]);
a是一个2个元素的数组,每个元素又是一个2个元素的数组。 先别在浏览器上测试,你能给出上面四个语句的输出吗?
数组的截取
slice函数可以截取数组的一段作为新数组:
a=[1,2,3,4,5];
b = a.slice(1,3); //从下标1开始,截取到下标3,也就是2个元素。
console.log(a); // a不变
console.log(b);
b = a.slice(1); //从下标1开始,截取到最后
console.log(b);
b = a.slice(); //截取整个a的元素,相当于复制。
console.log(b);
注意一下slice和splice的区别,第一个参数都是开始的下标,但对应splice,第二个参数是截取几个元素,而slice是结束的下标。 因此上面b=[2,3] 从输出你可以看出a的值并不会改变,这和splice是不同的。 如果你不写第二个参数,那么会截取到结束。 如果你什么参数都不写,就截取整个数组,也就是复制一份。 注意这个功能很重要,因为简单的赋值并不能复制数组:
a=[1,2,3,4,5];
b=a; //并不是复制,而是将b指向a指向的对象。两者是同一个对象。
b[0] = 100;
console.log(a); // a的值会变。
a=[1,2,3,4,5];
b=a.slice(); //复制一份。a和b指向不同的对象。
b[0] = 100;
console.log(a); // a的值会变。
体会下这种区别。
数组和字符串
数组和字符串可以通过以下的两个函数来进行转换
拼接数组 join
join函数可以用指定字符串将数组拼接起来:
a = [1,2,3,4,5];
s = a.join("+"); //用+将数组元素拼接起来;
console.log(s);
你会得到"1+2+3+4+5",注意,join函数会先将元素转成字符串,再用指定字符串将他们拼接起来。
分离字符串 split
通过字符串的split函数,可以按指定字符分隔字符串,形成数组:
s = "1+2+3+4+5";
a = s.split("+");
console.log(a);
你会得到["1","2","3","4","5"] 注意如果分隔符在边缘,那最后会多一个空字符串:
s = "1+2+3+"; //最后有一个分隔符
a = s.split("+");
console.log(a);
你会得到["1","2","3",""] 如果不给split传入参数,那么只会产生一个元素的数组,如果传入的参数是"",也就是空字符串,那么他会在每个字符处都进行分隔:
s = "abc";
a = s.split(); //不传入参数
console.log(a); //你得到["abc"]
a = s.split(""); //传入空串
console.log(a); //你得到["a","b","c]
为什么需要这些操作?
字符串和数组在一定程度上有很大的相似程度。对于C/C++,他们其实是一样的。不过对于JS来说,两者有本质的不同:
- 字符串是基础类型,数组不是。
- 有一些操作数组有,而字符串没有,例如排序,删除某个值,等等。
这点与一些语言比如Python,是不太一样的,对于Python来说你基本可以把字符串当作数组一样操作。
那么如果有一些复杂的字符串操作,例如排序之类的,要怎么办呢?有了以上的操作,就可以:转为数组->排序->拼接成字符串。