众所周知,JavaScript是一种弱类型的语言。博主在学习JS前,只接触过C语言和C++,所以在一开始学习时,思维难免转换不过来。而当学习深入后,博主逐渐明白了其精妙所在,故在此做一个总结,如果同时可以对大家有一点学习上的帮助,就再好不过了。(欢迎在评论区讨论学习!)
在开始之前,我要先给大家灌输(?)一个概念:在JavaScript中,一切皆为对象!(Everything is an object) ,这一点学过Java的同学可能很好理解,如果你现在还不太明白,也没关系,跟着我往下看吧,你总会明白的!
全局与块
JS是一种弱类型的语言,它是可以在运行中动态生成类(不仅是对象!)的,也就是说,它不需要提前指定变量的类型,这与我们之前学习的强类型语言C++、JAVA等有很大不同。JS中可以用var或let来声明变量,它们有什么区别呢?
我们先来看一段代码:
var a = "abcdef";
var b = a;
b += "ghi";
console.log("abcdefghi".length);
console.log(a, b);
var c = {
x: 123,
y: "456"
};
var d = c;
d.y = "789";
console.log(c, d);
var e = [1, 2, 3, 4];
var f = e;
f.pop();
console.log(e, f);
已安装node.js的同学可以尝试在终端运行,如果没有安装,也没有关系,可以将代码包装入HTML里在网页中运行,看看结果。
跟你想的一样吗?我们一个个来说:
-
对于常量,这里的 var b =a 相当于var b = "abcdef" ,也就是说,a和b是两个对象,那么下面改变b的操作,对a是没有影响的。var a = "abcdef"; var b = a; b += "ghi"; console.log("abcdefghi".length); console.log(a, b);
-
var c = { x: 123, y: "456" }; var d = c; d.y = "789"; console.log(c, d);
这里我们先声明并初始化了一个对象,那么, var d = c 这段代码表示什么呢?是C++里的复制构造函数吗?不是的,在这里,是对d的一个赋值语句!简单来说,就是c和d其实都是一个引用,它们指向了同一个对象!那么自然,对d所指向的对象的改变,也就是对c所指向的对象的改变。
-
var e = [1, 2, 3, 4]; var f = e; f.pop(); console.log(e, f);
同样的,只不过这里定义的是一个数组而已。不过,数组也是个对象,毕竟在JS里,一切皆为对象嘛。
总结一下,在JS里,对象名(如d,c,e,f)是一个引用,它们之间的赋值,实现的是别名绑定,而不是备份。
好好观察一下我们上面定义的
var c = { x: 123, y: "456" };
它其实是一个键值对的格式,即JSON(JavaScript Object Notation)格式。
let则是ES6(ECMAScript 6.0,JavaScript 语言的下一代标准。不太了解的同学,这里推荐阮一峰老师的教程)中新增的命令,用来指示块级作用域中的变量(即用let命令声明的变量仅在其所在的代码块内有效)。
什么意思呢?再来看一段代码:
{
var a = 1;
let b = 2;
}
console.log(a, b);
猜猜会输出什么?
哈哈,高手可能不屑一笑:当然是报错啦!——
现在理解“用let命令声明的变量仅在其所在的代码块内有效”这句话了吗?没错,b 是let定义在上面的花括号里的(一个块作用域),出了这个作用域,还想调用console.log输出它,当然是 undefined 了!
常量
来看一段代码:
const a = {};
a.x = 123;
a.y = "456";
console.log(a);
a = {a:1,b:2}; // Assignment to constant variable.
这里我们要讲下,个人理解,在JS里,const a = 2 就类似于C++里的 int * const a 即a是指向变量的常量指针。(注意与const int *a,变量指针指向常量 的区别),即当你声明 a 是 "const" 的,又把它指向一个对象(一切皆对象,还记得吗?),那么它的指向是不能改变的!
闭包
什么是闭包?对这个概念模模糊糊的同学,可以先去看看这个博主的文章,个人感觉讲得还是比较有趣、易于理解的,他认为,有权访问另一个函数作用域内变量的函数都是闭包。
然后我们来看一段代码:
var a = [];
for (var i = 0; i < 10; ++i)
a[i] = function () { console.log(i); }
a[0]();
a[5]();
a[9]();
你觉得它会输出啥?0,5,9对不对!嗨呀,这就犯了跟博主初学时一样的错误啦~
那么它会输出什么呢?我们先看看结果再来吹(?):
当当!有没有幡然醒悟?没有?ok,那就往下看——
在这里,a其实是一个闭包类型(函数),而 i 是用 var 定义的全局变量! 所以在结束循环后,根据判定条件,i 的值应该是10。
那么当我们用a[0]()等来调用函数时,console.log(i) 输出的其实都是10。
其实关键就是,这里的 i 存储的是“引用”而不是值,所以其“值”只有在执行时才能确定,而 i 是全局变量,当执行时,它早已自增到10!
如果想得到你想要的结果,即a[0]()输出0,a[5]()输出5等,只需把 var i = 0 中的 var 改为 let 即可!
循环
比较简单,基本只有记忆的部分,直接看代码吧:
var a = [1, 2, 3, 4, 5];
//traditional
for (var i = 0; i < 5; ++i) console.log(a[i]);
//foreach: in -- i is index
for (var i in a) console.log(a[i]);
//foreach: of -- i is element
for (let i of a) console.log(i);
第一种是我们传统使用的遍历方法,不再赘述;
第二种,用的是 in ,所以 i 是数组的下标;
第三种,用的是 of,所以 i 就是数组里每个对象。
数组(解构)
首先,在JS里,定义数组有三种方式:
var a=[];
var b=[1,2,3];
var c=new Array();
然后我们来看一段代码吧:
let a = [1, 2, 3];
console.log(a);
let [x, y, z] = a;
console.log(x, y, z);
[x, z] = [z, x];
console.log(x, z);
- 这里, let[x,y,z] = a 其实是数组的解构。 因为它把数组里的三个值分别赋给x,y,z三个值。
- [x,z] = [z,x] 的作用其实是借助数组交换x和z的值。想想在C++里,我们要交换两个数的值,至少需要三行代码,如果涉及到函数传值,就更多了。而这里,我们只需要一行代码,就可以交换两个数的值。
字符串
常用函数
let s = "this is a sample";
console.log(s);
console.log(s.includes("sample"));
console.log(s.startsWith("this"));
console.log(s.endsWith("le"));
JS提供的这些函数对coder非常友好。(省多少事啊!)
Template String
(这里用了英文表示,因为我不知道该怎么翻译合适。。)
let v = 123;
let t = `value = ${v};`;
console.log(t);
console.log(t.repeat(3));
在想输出语句 + 变量值时,我们常常会采用这样的方式:let t = 'value = '+v+";" , 即字符串拼接,这样固然没有语法错误,但似乎过于麻烦。
而看看我们上面代码中采用的反引号的方式,并用 $ 引用变量的值,使得常量和变量可以在一个(反)引号里输出,简便多了。
其实,JS的这个语法借鉴了Linux操作系统的命令。有用过Linux操作系统的同学肯定不陌生批量操作文件的命令。比如,有一个文件在多个文件夹里都有,现在我发现这个文件写漏了一点,要在文件最后加一行 “hello”才正确,怎么办?难道要一个一个文件夹打开去修改吗?no,这也太不优雅了!其实,可以用类似 $ sed -i '$a\hello' `find-name...` 的命令(注意,前面是单引号,表示插入一行"hello",后面是反引号,表示找到所有名字为...的文件),这就实现了批量处理。
正则表达式
var REG = /(\d{4})-(\d{2})-(\d{2})/;
var s = "2017-11-28";
var res = REG.exec(s);
// 得到一个数组,0为原字符串,123分别为3个子模式
// 数组解构
var [oldstr, year, month, day] = res;
console.log(year, month, day);
正则表达式,多么伟大的发明!它描述了一种字符串匹配的模式,可以用来检查一个字符串是否含有某种子串、将匹配的子串做替换或者从某个字符串中取出符合某个条件的子串等等。在它未出现之前,字符串匹配对于coder来说是一件多么痛苦的事情!好在,它出现了,而我们只需学好它就行了——
由于对之前完全没接触过这个概念的同学来说,理解会有点困难(比如一开始的博主),所以我们一个字符一个字符分析讲解!
var REG = /(\d{4}) - (\d{2}) - (\d{2})/
- 两边的 / ,指定正则表达式的范围
- () 括号,包起分组
- \d ,表示转义(10进制)
- {4} ,表示4位数
- - ,表示分隔符
用正则表达式处理字符串后,得到一个数组,下标为0的值为原字符串,下标为1、2、3的值分别为3个子模式。
经过数组解构后,我们分别得到了三个值。
函数
缺省值
在JS中,函数可以被当作对象返回(与C++不同);而且,JS里也没有函数重载的概念,毕竟是弱类型嘛。
function show(x, y)
{
x = x || 0;
y = y || "default";
console.log(x, y);
}
show(1, "hello");
show(2);
show();
这里的 x= x || 0 和 y = y || "default" 表示,如果x有定义则x=x,否则为右边的值。
这里我们就顺带讲一下 null 和 undefined 吧。
个人理解,null 表示什么都没有(类似C++里的void),用数学的思想来说,就是没有一个集合;
而 undefined 表示有一个集合,只不过这个集合里没有东西!
所以,如果用 show(null) 调用函数,表示 x 的值为null,y 的值为undefined。
上面代码里的写法有人就不爽,比如我。为啥要用 x= x || 0 这种表意不清的写法呢?所以,ES6推出了缺省值的方法——
function show(x = 0, y = "default")
{
console.log(x, y);
}
show(1, "hello");
show(2);
show();
作用跟上面是一样的。
这里提一下,虽然现在几乎所有主流浏览器都支持了ES6的语法,但如果你想适配ES5去降级,可以用Babel。
不定参数
JS支持不定参数的用法。
function add(...numbers)
{
var sum = 0;
for (let e of numbers) sum += e;
return sum;
}
// 完成最后字符串的连接
console.log(add(1, 2, 3, 4,"abc"));
结果:
箭头函数
箭头函数有点类似C++里的lambda函数,其实就是函数简写。
var f = function (x)
{
return x * 2;
}
console.log(f(10));
var g = x => x * 2;
console.log(g(10));
// map:映射,数组[1,2,3]的每个值都要改变
console.log([1, 2, 3].map(x => x * x));
// ()扩起,返回一个对象。其函数体只有一条赋值语句{a: x, b: y}
var h = (x, y) => ({a: x, b: y});
console.log(h(5, 6));
尾调用
尾调用:调用函数f是函数g的最后一个操作(return)。
function f(x)
{
return x * 2;
}
// 尾调用
function g(x)
{
if (x < 0)
return f(-x);
return f(x);
}
console.log(g(-2));
console.log(g(2));
// 不是尾调用
function g1(x)
{
let y = f(x);
return y;
}
// 不是尾调用
function g2(x)
{
return f(x) + 1;
}
// 不是尾调用。没有return语句,而每个函数最后都会有一个默认的return语句(只是这里省略)
function g3(x)
{
f(x);
}
尾调用有很大的缺点。因为调用帧(堆栈)的,时间复杂度为O(n)。
尾调用是可以被优化的。如上面的g1,将中间结果参数化,只占用1帧,时间复杂度为O(1)。
这里用斐波拉切数列的例子讲解函数递归的优化:
// 函数递归
// 耗费资源 消耗堆栈
// 优化:中间结果参数化!(func6)
function fibo(n)
{
return n <= 2 ? 1 : fibo(n - 1) + fibo(n - 2);
}
// 获得命令行参数
var n = parseInt(process.argv.splice(2));
console.log(fibo(n));
可以将中间结果参数化:
// func6
// 递归参数化
function fibo(n, a, b)
{
return n <= 1 ? a : fibo(n - 1, b, a + b);
}
var n = parseInt(process.argv.splice(2));
console.log(fibo(n, 1, 1));
也可以进行柯里化:
//currying(柯里化) 参数只有一个
function fibo(n)
{
let _fibo = (n, a, b) => n <= 1 ? a : _fibo(n - 1, b, a + b);
return _fibo(n, 1, 1);
}
//default parameters(缺省参数,ES6实现)
function fibo2(n, a = 1, b = 1)
{
return n <= 1 ? a : fibo2(n - 1, b, a + b);
}
var n = parseInt(process.argv.splice(2));
console.log(fibo(n));
console.log(fibo2(n));