本文是我学习javascript的笔记,可以给使用c++的人学习js作一个参考。
鉴于工作需要和兼容性,所学习的javascript内容大部分来自于ES5版本。
javascript是什么
js(javascript)有和java相似的名字,常常让人误解二者的关系,其实它们根本没有什么关系,只是名字相似而已。
js是一门动态类型、基于原型的解释型语言(也可以即时编译),支持面向对象编程、命令式编程、函数式编程。
c++是一门静态类型检查的编译型语言。
js的语法和c语言比较相似,但又有许多根本性的不同(变量作用域、变量声明等);在ES6版本以后,js还借鉴了很多python、ruby的语法。
js主要用于构建网站等,而c++则是一门通用的程序设计语言,很多人拿它来做不同的事。
如何开发
运行环境
c++需要编译,Hello World需要编译,任何一个改动也需要重新编译;代码和可执行文件是分开的。
js因为是解释型语言,解释器是随手测试代码的利器,而且刚好浏览器又有js的解释器;于是你可以一边看网页,一边想写些东西验证一下时,按F12切换到控制台就可以写js了。
调试
调试程序最常用的2种方式是打印信息和断点调试。
c++中打印信息有多种方法,最基本的是标准流输出std::cout
js中有console.log()
可以在控制台输出信息,不过console的作用很丰富,输出类别、分类、计时、计数、调用堆栈等功能都有。
而调试程序的话,c++需要挂载调试器,一般会用IDE或者GDB终端调试。
js的话,一种是现代浏览器自带的调试器,或者是编辑器安装插件。
程序入口
c++兼容c语言,可执行的代码只能出现在函数内,而最开始的函数是main函数,全局区域是不能执行代码的。
js作用解释语言,是从头到尾连续执行的,没有这种限制。
不过可以将声明与执行的代码人为分开,提升程序可读性。
代码组织
c++因为编译顺序,声明尽量写在h文件中,定义写在cpp文件中;如果调用外部库,还有静态链接和动态链接的不同。
js中代码即程序,只要记住程序是从上往下解释的,在你的程序运行前,html中已经引入你需要的函数等就行了。
变量
数据类型
c++中有基本数据类型和用户自定义数据类型。
常用的基本数据类型有:bool, char, int, float, double, void, unsined int等等。
还允许用户利用基本数据类型来自定义数据类型,如struct X{…}等。
js有7种数据类型,分为原始类型和非原始类型。
原始类型:
- Boolean
- Null
- Undefined
- Number
- String
- Symbol(ES6)
非原始类型:
- Object
原始类型的值都是不可更改的,任何看似像更改的操作都会新创建一个新的值覆盖。
Number
js中只有一种数字类型Number,是双精度64位的浮点类型,并没有单独表示整数的整型。
可表示的错误取值有-Infinity
,+Infinity
,NaN
。
Number只有一个整数:0,可表示为+0和-0。
c++的浮点数错误值表示有NaN
和Inf
,c++中有多个函数可以判断此类情况,c++11中为isnan(Type x)
和isinf(Type x)
。
题外话:除零不是c++标准异常
String
字符串是js的原始类型之一,没有c++中char这种表示单字符的类型。
c++中字符串不是基本数据类型,是标准库中提供的。
js中字符串可以用双引号和单引号来表示。
c++中只双引号表字符串,单引号表字符char。
Undefined
js中变量没有初始化的话,默认值就是undefined
。
c++中未初始化的变量,其值是内存上的一个随机值(“赋值”在c++中和“初始化”是不同的操作,可见后文)。
Null 与 void
js中Null只有一个值null
,表空值。
c++中没有表示“空”的值,NULL
和nullptr
表空指针。
js中void是一个运算符,可以把表达式的返回值变为undifined
(这是js函数的默认返回值)。
c++中void函数没有返回值,而void*
可以去除类型。
枚举?
c++中枚举类型的主要作用是创建名称和值的关联,提升程序的可读性。
js中没有枚举类型。
常量
c++中可以用const
修饰符来限制类型,初始化后不可改变值。
js中原始类型是不能定义常量的,对象可以定义为常量。
ES6中有
const
关键字,可以定义常量。
变量声明
c++是静态类型检查的,类型名在前,变量名在后声明如:int i;
c++11虽然可以用auto
自动推导,但这只是可以在明确类型的情况下使用的,并不能auto i;
这种写法不能明确类型
静态类型的变量,指定类型后不可更改。
js是动态类型,即可以随时更改变量类型,所以指定变量类型也没什么意义,可以都用var
来声明变量:
var i;
i=1;
i='string';
i=true;
js中没有var声明的变量会是全局变量,即使在一个函数体内:
function f(){
a=1;
}
f();
console.log(a); // =>1
不要以上述方式定义全局变量,不便理解,且在ES5的严格模式下会有限制。
声明提升(Hoisting)
js会把所有变量的声明移动到作用域的开头位置。
js的变量可以在声明前使用。
因为js解释器的变量提升行为:
a=1;
var a=3;
等同于:
var a; // 作用域顶部
...
a=1;
a=3;
也因此重复声明是没有关系的,只是不建议为么做。
建议把所有变量的声明放在作用域顶部,这样更符合js的执行过程。
作用域
c++支持块级作用域,if, while, 函数,类等等语句都会产生作用域。
js的var创建的变量没有块级作用域,只有函数作用域。
在ES6中,可以用let创建变量,有块级作用域
js里语句块是不会创建作用域的,只有函数才创建作用域。
var a=0;
function f(){
var a=1;
if(true){
var a=2;
}
console.log(a);
}
f(); // =>打印2
console.log(a); // =>打印0
首先,全局作用域内有一个a为0
然后在调用f函数时,f函数创建一新的作用域,里面的a是局部变量,是属于f函数的
这是2个不同的a,所以最后一行打印的是全局变量a为0
f函数内,if并不会创建作用域,只是重复声明a而已,都是调用的同一个局部变量a;所以这里打印出的a是2
函数
c++与js的函数目的都是相同的,即用一组语句完成某种操作,可以有输入和输出;但从设计到语法上区别都比较明显,js是的函数表达能力比c++强很多。
- c++的函数是一个代码块
js的每个函数都是一个Function
对象 - c++98只能在全局和类中定义函数,在c++11才添加了lambda表达式可以在函数内定义函数
js的函数本来就是可以在函数内部声明的 - c++的函数必须指明返回值,没有返回值要写void
js的函数都有返回值,默认是undefined;如果函数体内有return,返回值会被改变 - c++的函数区分声明和定义,可以放在不同文件中;调用前必须声明
js的函数只有定义一种说法;调用前可以不定义 - c++的函数这种声明和定义分离的语法,导致重复定义会出错
js中函数本身就是对象,重复定义的话,内容会被改变
函数定义
js有3种定义函数的方式:函数声明,函数表达式与Function构造函数
- 函数声明
用function
关键字来定义函数,称为函数声明:
function 函数名(参数){
函数体
}
- 函数表达式
函数可以由函数表达式创建,函数名可以不写:
var f = function 函数名(参数){
函数体
}
- Function构造函数,不推荐使用这种方法
既然js中函数是对象,自然可以用构造对象的方式来构造函数。
var f = new Function(参数){
...
}
函数作参
函数作参可以大大减化代码,精炼逻辑
最常见的是用在对容器的操作,遍历、过滤、求和,都是传递一个函数作为参数的
上面js中函数表达式的语法,把函数赋给一个变量,是不是让你想起了c++里的函数指针?
c++中可以用函数指针来将函数作参,以传递函数。
由于静态类型检查,函数的指针类型必须明确
c++98中函数指针类型的语法很复杂:
返回值类型(*)([参数1类型][,参数2类型][...])
,
如函数int sum(int a,int b)
的函数指针类型是int(*)(int,int)
定义一个sum的函数指针a会是:
int (*a)(int,int) = sum;
如果要定义一个成员函数的指针,那就更麻烦了,编译器会为成员函数添加this指针,这是对程序员透明的,你定义指针类型的可没有this指针
一篇成员函数指针的博客
c++11中增加了std::function
函数封装器,std::bind
转发调用包装器
std::function封装了函数指针的声明定义
std::bind则可以控制参数绑定
二者一起使用,一个定义一个绑定,让函数指针作参数容易不少
而在js中,函数本身就是对象,对象可以赋值给变量,函数作参数是很自然的事情
function f1(fun){
fun();
}
function f2(){
console.log('f2()');
}
f1(f2); // =>打印f2()
函数提升
- 函数声明提升
js中函数和变量一样也会有声明提升,不过js会将整个函数定义都提升到作用域顶部
function f(){console.log('a');}
f(); // =>输出b
function f(){console.log('b');}
提升后:
function f(){console.log('a');}
function f(){console.log('b');}
f(); // =>输出b
最后定义的会覆盖前面定义的。
- 函数表达式声明提升
js中函数表达式的声明提升与变量提升相同,赋值(即函数体)不会提升,只提升变量声明
f = function(){console.log('a');}
f(); // =>输出a
var f = function(){console.log('b');}
f(); // =>输出b
提升后:
var f;
f = function(){console.log('a');}
f(); // =>输出a
f = function(){console.log('b');}
f(); // =>输出b
函数参数
c++中函数对参数的要求比js更严格。
参数数量
c++中参数数量必须严格匹配,不匹配是编译不过的。
js中参数数量不匹配是没关系的,没有填充的参数会是undefined
,多余的参数会被截断
js有一个arguments对象,这个对象保存了真正调用函数时的参数。
利用argumensts对象,可以让函数处理不定参数。
默认值
c++可以从后往前为参数填写默认值
js中所有参数都有默认值undefined
ES6支持自定义默认值
ES6之前想要达到默认值的效果,可以在函数体里作判断,这种写法没有直接定义默认值的可读性强:
function f(a, b){
a = a || 100;
b = b || "name";
...
}
传参方式
c++中你可以手动控制传参的方式,值、引用或指针。
js中原始类型的参数是按值传递的,非原始类型的参数则是引用传递;也就是说只有对象是以引用传递的。
注意,js中的“引用”是和c++中的“指针”对应的,你不能在函数中改变引用链:
function f(obj){
obj={};
}
var obj = {a:1};
f(obj);
console.log(obj); // =>输出{a:1}
这和c++中你不能给指针参数new个新对象是一样的。
void f(char* p)
{
p = new char[100];
}
int main()
{
char* chs = nullptr;
f(chs);
assert(chs); // 中断,chs依然是nullptr
return 0;
}
所以c++中的“引用”在js中没有对应。
函数重载?
c++中函数的参数类型或参数个数不同,可以实现同名函数共存的情况。
js的函数参数无类型,且对参数个数都没有严格要求,自然不需要函数重载。
立即调用的函数
c++在全局区域里不能执行任何代码,包括调用函数
而js没有这个限制,你可以写一个函数,并立即执行它,这个函数可以是匿名的(有点像c++中立即定义匿名struct对象的语法):
(function (){
console.log('function call');
})();
这么写有什么用呢?
前文说了var
声明的变量没有块作用域,只有函数作用域,所以这个匿名函数里任何声明的变量都不会成为全局变量。
这样写可以防止重名覆盖。
运算符
逻辑运算符
c++和js都有3种逻辑运算符:&&, || 和 !
c++中这3种逻辑运算结果都是bool
js中只有!
运算结果是Boolean
,&&
和||
结果都是2侧的表达式之一
a && b
,如果表达式a能被转换为false
,则返回表达式a;否则返回b
var v = a && b;
相当于
var v = a;
if(v){
v = b;
}
a || b
,如果表达式a能被转换为true
,则返回表达式a;否则返回b
var v = a || b;
相当于
var v = a;
if(!v){
v = b;
}
所以你经常会看到保护函数参数的惯用法:
function my_log(str){
str = str || '';
console.log(str);
}
!!
也是一个惯用法,连续2次取非操作,可以把表达式转换成Boolean
。
位运算符
位运算只对整数有意义。
c++中只有整型才能进行位运算。
js中只有Number
表示浮点数,却也可以进行位运算。
因为js在进行位运算前会将待操作数转换成32位有符号整型。
这会导致精度丢失和溢出的问题,所以不要使用位运算符。
你可能看到别人写:
var a = ...;
a = a | 0;
a = a << 0;
a = ~~a;
这些写法都是为了小数化整,不建议这么用。
小数化整可以用Math.round()
等。
取余
%
是取余操作符。
c++中只有对整型可以取余,对小数取余要用数学库里的fmod
或modf
js里只有浮点数,自然取余对整数小数都适用。
关系运算符
相等性判断
c++中相等性判断有==和!=,基本类型是支持相等性判断的
而对象需要手动重载==和!=运算符才能判断相等性
js中==
和!=
是宽松比较运算符,在比较时会执行类型转换
===
和!==
是严格比较运算符,并不会执行类型转换
js比较表
只使用严格比较运算符。
大于 小于
js中< <= > >= 都会进行类型转换,没有严格比较运算;会出现一些比较奇怪的情况:
0 < null; // false
0 <= null; // true
0 == null; // false
0 === null; // false
所以进行大于小于判断前,最好进行类型检查,看看是不是有意义的比较。
运算符重载?
c++可以重载对象的一些运算符,提升程序的可读性,还减少括号函数的调用。
当然如何正确重载运算符有点难。
js目前还不支持运算符重载。
流程控制
流程控制可以改变程序运行的顺序。
c++和js的流程控制语法几乎是相同的。
真值 假值
分清哪些值为真,哪些值为假才能准确控制流程。
c++中,流程控制语句需要的是bool类型的值,如果表达式结果不是bool类型,会隐式转换成bool
如果类型不能转换成bool,会编译失败
其中零值、空指针会转换为false,其余为true。
js中,false
undefined
null
0
-0
NaN
''
为假,其余为真
注意空数组[]
和空对象{}
空函数(){}
都是真,因为空对象是真,数组和函数都是对象,自然也是真。
而空字符串""
是假,这是原始变量。
switch
c++中switch语句中的表达式必须是一个整型或枚举类型,或者是一个class类型,其中class有一个单一的转换函数将其转换为整型或枚举类型。
这是我从菜鸟网摘的一句话。
什么叫class的单一转换函数?
后面我发现真有人这么做了,重载了类型转换运算符,代码如下:
class Test
{
public:
operator int() { return 1; }
};
int main()
{
Test obj;
switch(obj)
{
case 1: cout<<"Test class object";
break;
}
}
呃,这种写法太可怕了
c++里到处都是隐式转换,重载类型转换运算符这得引起多大的误会……
js里也有switch语句,语法和c++是完全一样的,其中表达式结果和分支条件会使用===
进行严格比较。
不过没有表达式类型的限制,能够比较字符串。
关于声明提升
js中声明提升如此怪异,为什么要这么设计呢?
函数声明提升
对于以下代码:
f();
function f(){ console.log('f()'); }
你将这段程序写到文件中再执行,是可以运行的。
而如果你在控制台中一句一句敲入上面的代码,就在你敲完第一行后,就会报引用错误说f没有定义
因为js在执行代码段前才有函数声明提升的过程。
调用的时候函数f已经被声明过了。
所以并不能说函数可以先调用再定义,而应该说函数声明可以写在调用后。
这是一个明显的优点,让你可以把函数的声明放在调用后面的地方。
变量声明提升
js中变量也有声明提升,但相比函数声明提升,带来结果不同。
变量声明提升可以让js中的变量:先使用再声明
但是呃,如果忘了声明呢?
function f(){
a=4;
//var a; 你忘了写!
}
f();
console.log(a); // =>4
很不幸,a成为了全局变量。
更不幸的是,程序运行没有任何错误。
所以,不要把变量的声明写在使用后。
ES6中let声明的变量不受变量声明提升的约束。
代码“风格”
js里代码“风格”居然会影响代码行为……
js里有行尾添加分号和不加分号2种风格,你可能认为分号是可有可无的。
其实js解释器是需要分号的,如果你少写了分号,js解释时会自动帮你加上,但是有时候你不想添加分号的地方也会被加上分号。
return
如以下代码:
function f(){
return
1
}
被解释后等同于:
function f(){
return;
1;
}
返回值被改变了。
再如:
var a = 1
(function (){
console.log('fun call')
})()
会被解释成:
var a = 1(function (){ // 语法错误
console.log('fun call');
})();
最好把return写到一行。
行末分号
如果下一行的第一个字元是
(
[
/
+
-
之一,那么它极有可能和前一条语句合并在一起解释。
如果你在每行末尾添加分号,要注意函数表达式后面要也要加分号:
var a = function(){
...
};
如果你不在每行末尾添加分号,
要注意(
和 [
开头的行,要在行首添加;
:
;(function (){
...
}
如果代码里有无分号混用,要做到上述的行首添加分号。