JavaScript , 简称JS, JS最初只是为了进行前端页面开发, 但随这后来JS越来越火之后, JS就被赋予了更多的功能, 可以用来开发桌面程序, 手机App, 服务器端的程序等…
JS是一种动态类型, 弱类型的脚本语言, 通过解释器运行, 主要在客户端和浏览器上运行, 比如Chrome里面专门有一个模块, 就是JS引擎, 就相当于JVM一样, 能够解释执行js代码, 后来这个部分的代码就被大佬们单独拎了出来, 封装成了独立的程序, 称为V8引擎, 这就使JS的适用范围更广泛了, 可以使服务器也能解析JS代码, 完成交互.
JS之父是 布兰登·艾奇(Brendan Eich), 如下图, 还是很有大佬气质的,
他在1995年, 用10天时间完成JS的设计, 最初在网景公司(Netscape), 命名为 LiveScript, 一般认为, 当时 Netscape之所以将LiveScript命名为JavaScript, 是因为Java是当时最流行的编程语言, 带有 “Java” 的名字有助于这门新生语言的传播, 但实际上Java和JavaScript之间的语法风格相差甚远, 基本是没有关系的.
JS主要在前端中可以完成用户在网页和Web服务器的交互, HTML描述了网页的结构(骨), CSS描述了网页的样式(皮), JavaScript则描述了网页的行为(魂).
再来简单了解一下JS的运行过程, JS编写的代码是保存在文件中的, 也就是存储在硬盘(外存上), 双击.html文件浏览器(应用程序)就会读取文件, 把文件内容加载到内存中(数据流向: 硬盘 => 内存), 浏览器会解析用户编写的代码, 把代码翻译成二进制的, 能让计算机识别的指令(解释器的工作), 得到的二进制指令会被 CPU 加载并执行(数据流向: 内存 => CPU).
浏览器分成渲染引擎 + JS 引擎, 渲染引擎的工作是, 解析 html + CSS, 俗称 “内核”; JS引擎也就是JS解释器, 典型的就是Chrome中内置的V8; JS引擎会逐行读取JS代码内容, 然后解析成二进制指令, 再执行.
在html中引入JS代码主要有行内式, 内嵌式, 和外部式三种, 行内式直接将JS代码嵌入到了html元素内部, 内嵌式将代码写入到了script标签中, 外部式将代码写到了单独的.js文件当中.
在js中有两种注释风格, 单行注释使用//来表示,/**/表示多行注释.
要注意的是外部式的script标签中间不能写代码, 必须空着(写了也不会执行).
在JS中可以使用alert()这个函数来弹出一个对话警示框, 我们搭配使用html中的button标签使用作为JS的第一个程序, 顺便演示一下上面的三种引入JS的方式.
首先采用行内式来写这个代码, 要注意JS中字符串常量可以使用单引号表示, 也可以使用双引号表示, 但更推荐的写法是在html中使用双引号, JS中使用单引号.
内嵌式写法:
外部式写法.
//hello.js alert('helloJS');
得到的效果和上面是一样的.
上面使用alert显示弹窗提示可以让用户看到程序的输出, 是一种很常见的交互方式, 但弹框操作不方便的地方在于, 有些对话框一弹出来用户就不能在操作页面的其他部分了, 必须把弹窗点掉才能继续其他操作.
在JS中可以使用console.log来进行调试输出, 在浏览器开发者工具中, 有一个控制台, 在这个控制台上就能看到console.log输出的内容, 如果出现了一些异常在控制台上也会有提示.
语法格式:
//第一种定义方式 var 变量名 = 值; //第二种定义方式 let 变量名 = 值;
与java/c/c++不同的是, 关键字var/let定义的变量可以接收多种类型的变量值, 可以是整数, 浮点数, 字符串, 数组等, 甚至还可以是函数变量(有点像C语言中的函数指针), JS代码中每个语句最后带有一个;结尾, 可以省略, 但是建议还是加上.
var a = 10; console.log(a); let b = 'hello'; console.log(b);
JS中定义变量不用声明类型并不意味着变量没有类型, 而是变量的类型是通过初始化操作的值来确定的, 比如值为10变量的类型就是number类型, 值为字符串常量变量的类型就是string类型.
虽然JS中var和let关键字都可以定义变量, 但更推荐的写法是无脑使用let, 这是因为var是老式的写法, 是有很多坑在里面的, 比如这样一个代码,
var num = 10; var num = 20; console.log(num)
使用var关键字定义两个相同的变量是可行的, 而在Java/c/c++中是定义两个相同的变量是会报错的, 而如果使用let关键字这样定义也是会报错的,
let num = 10; let num = 20; console.log(num)
let这种写法是新式写法, 语法上更接近Java/C++等主流语言, 更符合我们的直觉, 无脑使用即可, 具体细节不用区分.
JS是一种动态类型的语言, 一个变量在运行过程种类型可以发生改变, 就是动态类型(JS, Pychon, PHP, Lua…,); 反之, 类型不能发生改变就是静态类型了(c, c++, java, go…).
let a = 10; console.log(a+" type: "+typeof a); a = 'hello'; console.log(a+" type: "+typeof a);
使用typeof可以查看变量类型, 可以看到程序运行过程中a变量的类型是发生了改变了的.
动态类型相比于静态类型更加的灵活, 但静态类型的语言在编译检查方面可以做的更好, 写出来的代码可维护性就比较高; 而动态变量当下到底是什么类型, 里面是什么样的值这都是不确定的, 这就使其的类型检查不是很严格, 代码维护起来相较于静态类型就比较麻烦.
JS中的数字不区分整形和浮点型, 统一都是number类型, 与Java一样, 支持用二进制, 八进制, 十六进制的表示.
let a = 0o66; //八进制整数,以0o开头 let b = 0xa; //十六进制整数,以0x开头 let c = 0b10; //二进制整数,以0b开头
JS中的有关数字运算结果可能出现一些特殊的数字值, 有如下几种:
let max = Number.MAX_VALUE; // 得到 Infinity console.log(max * 2); // 得到 -Infinity console.log(-max * 2); // 得到 NaN console.log('hehe' - 10);
字符串字面值需要使用引号引起来, 单引号双引号均可, JS中的字符串和Java一样, 可以使用+运算符进行不同类型的字符串拼接, 可以使用变量名.lenght求得字符串的长度.
let s = "hello"; console.log(s.length); let b = 66; console.log(s+b);
这里的拼接本质是将数字隐式转成字符串, 再进行字符串拼接, 还有一些字符串的其他操作, 不过和Java也差不多, 了解为主, 不多介绍了.
JS中布尔类型和C语言类似, true和非0表示可以表示真, false和0可以表示假, JS的布尔类型支持与数字运算, 运算时, true表示1, false表示0, 本质上也是隐式类型转换, 这在Java中是不行的.
let a = true; let b = false; console.log(a + 10);//11 console.log(b + 10);//10 console.log(a + b);//1
如果一个变量没有被初始化过, 那这个变量就具有唯一的值undefined, 是undefined类型, 这个操作其实就是将其它语言中非法的行为合法化了, 我们知道在Java中未经初始化的变量直接访问是会报空指针异常的, 但在JS中不会报错, 它会给你返回一个undefined.
let a; console.log(a); console.log(a + "10"); console.log(a + 10);
null与undefined不同, 它表示变量初始化了, 但值为null, null和undefined都表示取值非法的情况, 但是侧重点不同, null表示当前的值为空(相当于有一个空的盒子), undefined表示当前的变量未定义(相当于连盒子都没有).
let b = null; console.log(b + 10); // 10 console.log(b + "10"); // null10
JS中的绝大部分运算符合其他语言的运算符是差不多的, 主要介绍和其他语言有差异的比较相等运算符, 其他的就不多说了.
==与!=: 表示比较两个变量的值是否相等.
===与!==: 表示比较变量的值与类型是否相等.
let a = 10; let b = "10"; console.log(a == b);//true console.log(a === b);//false
JS中a==b这个判断其实触发了隐式类型转换, 也就是说, JS中针对不相同的类型进行比较(==)运算, 会尝试尽可能的转成相同类型, 而使用===不会触发隐式类型转换了, 所以有了上面的结果.
像JS这种, 比较能支持隐式类型转换的语言, 称为 “弱类型” 语言; 反过来像Java这种, 不太支持隐式类型转换的语言就被称为 “强类型” 语言, 常见语言的强/弱, 动态/静态类型就如下图:
其中C++处在一个比较特殊的位置了, 这是因为C++即要保证和C语言兼容(C语言支持的隐式类型转换C++也得支持), 而同时C++又在努力的加强自身的类型系统, 设定一些新规则尽量规避旧的问题…
首先来看数组的创建:
let arr1 = new Array(); //很少这样写 let arr2 = []; let arr3 = [1,2,3,4,5,6,7,8,9,0]; let arr4 = [1, 'hello', this, [1,'hhh'], null, undefined];
JS中使用[]来表示数组, 同时JS中数组的元素类型不要求是统一的, 可以是任意的类型, 动态类型的语言基本上都如此.
接下来看数组的遍历, 可以直接使用console.log(数组名);输出整个数组, 如下:
console.log(arr1); console.log(arr2); console.log(arr3); console.log(arr4);
JS中的数组也是通过下标的方式来访问的, 所以还可以使用下面几种for循环来进行遍历.
let arr = [1,2,3,4,5,6,7,8,9,0]; //写法1 /*for (let i = 0; i < arr.length; i++) { console.log(arr[i]); }*/ //写法2 /*for (let i in arr) { // 此处的 i 是数组下标 console.log(arr[i]); }*/ //写法3 for (let elem of arr) { // 此处 elem 是数组元素 console.log(elem); }
在JS中数组的越界访问是合法的, 不会像Java一样报空指针异常, 得到的结果是undefined.
let arr = [1,2,3,4,5,6,7,8,9,0]; console.log(arr[100]); console.log(arr[-1]);
除了越界访问, 越界修改数组的值也是合法的, 此时数组的长度也会随之发生改变.
let arr = [1,2,3,4,5,6,7,8,9,0]; arr[100] = 66; console.log(arr);
观察结果可以发现, 当数组越界访问将下标为100的位置的值修改为66时, 数组的长度就变成了101, 那么中间未初始化的元素值就都为undefined了.
如果说初次接触看到上面的内容还比较合理, 可以接受, 那么下面的内容对于第一次接触JS的来说就可能觉的有些打破认知了.
JS在数组中可以将任意类型作为数组的下标向其中添加元素, 比如负数, 字符串等作为下标, 如下代码.
let arr = [1,2,3,4,5,6,7,8,9,0]; arr[-1] = -1; arr['hello'] = 'JS'; console.log(arr);
观察结果可以看到, 此时虽然将两个值成功添加到了数组中, 但数组的长度并没有发生改变, 实际上, JS中的数组不仅仅只是一个传统意义的数组(只能按下标来访问元素), 当使用负数, 字符串这些去访问数组时, 会生成一个键值对添加到数组中, 它更像是数组+Map的结合体, 这就得数组也能够按照Map键值对的方式来组织数据.
可以使用push方法给数组进行尾插式的添加元素.
let arr = [1,2,3,4,5]; arr.push(6); arr.push(7); arr.push(8); arr.push(9); arr.push(0); console.log(arr);
这里再介绍一个进行数组操作的万能方法splice, 它可以完成对数组的插入, 修改, 删除等操作.
array.splice(index, howmany, item1, ....., itemX);
它有三个部分的参数, inded是必须要有的, 表示在什么位置添加/删除元素, howmany是可选参数, 表示要删除的元素的个数, item1, …, itemX是可选的变长参数, 表示要添加到数组中的新元素.
这三个部分的参数,如果没有变长参数就表示这个函数起到的作用是删除元素; 如果变长参数和前面指定的区间个数一样就表示, 此时就表示这个函数起到的作用是修改/替换元素; 如果后面变长参数比前面指定的区间个数要大就表示这个函数起到的作用是新增/添加元素.
删除元素:
let arr = [1,2,3,4,5,6,7,8,9,0]; console.log(arr);//删除前 arr.splice(2, 3);//表示从下标2开始删除,删除3个元素 console.log(arr);
修改元素:
let arr = [1,2,3,4,5,6,7,8,9,0]; console.log(arr);//修改前 arr.splice(2, 3, 333, 444, 555);//表示从下标2修改,修改3个元素 console.log(arr);
添加元素:
let arr = [1,2,3,4,5,6,7,8,9,0]; console.log(arr);//插入前 arr.splice(2, 0, 666);//表示在2下标插入一个666 console.log(arr);
JS中需要使用function关键字来声明一个函数, 结构包括函数名, 形参列表(不必写参数类型), 函数体; 不必写返回值类型, 但是可以返回值的.
函数的定义, 函数声明, 函数调用的格式如下:
// 创建函数/函数声明/函数定义 function 函数名(形参列表) { 函数体 return 返回值; } // 函数调用 函数名(实参列表) // 不考虑返回值 返回值 = 函数名(实参列表) // 考虑返回值
函数的调用可以出现在函数声明之前, 也可以出现在函数声明之后.
hello(); function hello() { console.log("hello"); } hello();
在JS中调用函数传入的实参个数比形参多或者是少都是没关系的, 只不过如果是少的话没有收到传入参数值的变量默认值就为undefined了, 此时进行数值运算结果就为NaN了; 而如果是多的话形参中只能拿到形参个数的值, 后面的就拿不到不会参与运算了.
function add(x, y) { return x + y; } console.log(add(10)); console.log(add(10, 20)); console.log(add(10, 20, 30)); console.log(add(10, 20, 30, 40));
但上面的代码中传入的实参个数比形参少运算后得到的结果是NaN, 这个设定是JS一个比较坑的地方, 这个结果明显是不符合用户期望的, 但它也不报错…
其实还有一种写法可以避免上面的问题, 其实在每个函数里面都会自动定义一个arguments变量, 它是个数组, 其中包含了所有的实参, 可以从arguments变量中拿到元素判断其的值是不是undefined, 然后再进行运算.
function add(x, y) { let result = 0; for (let elem of arguments) { if (elem !== undefined) { result += elem; } } return result; } console.log(add(10)); console.log(add(10, 20)); console.log(add(10, 20, 30)); console.log(add(10, 20, 30, 40));
JS中函数和普通变量一样, 可以赋值给变量, 此时变量的类型就是function, 然后该变量可以调用该函数或者作为一个返回值, 就像C语言当中的函数指针一样, 这点在Java中无法做到, 因为在JS中函数拥有这种特性, 所以JS函数也被叫做 “一等公民”.
function print(s) { console.log(s); } let p = print; p("hello"); console.log(typeof p);
JS中还支持函数表达式的写法, 即定义一个匿名函数, 形如: function() { }, 然后将这个匿名函数赋值给一个变, 后面就可以通过这个变量来调用函数了.
将上面的例子改写成函数表达式如下:
let p = function (s) { console.log(s); } p("hello");
在JS中函数里面是可以继续声明函数函数的, 可以无限的进行嵌套, 这点在Java当中也是不行的.
还有就是在JS中变量的作用域也是有一些特殊的, 当代码中访问某个变量的时候, 会先在当前作用域去寻找, 如果当前的没有, 就继续往上层作用域一级一级的找, 直到找至全局作用域, 如果还是找不到, 变量的值就为undefined了, 这个语法设定和Java中的变量捕获是类似的.
let num = 1; function test1() { function test2() { let num = 3; console.log("test2: " + num); } test2(); console.log("test1: " + num); } test1(); console.log("global: " + num);
JS不是面向对象的语言但是存在对象的概念, JS中的对象设定和Java中的差异较大, JS中没有继承, 封装, 多态, 甚至没有类, JS中所有的对象的类型都是object, js的对象有属性也有方法, 不过JS中的方法本质上也是属性(一等公民), 下面介绍JS中对象的创建方式, JS里面的对象是通过键值对的方式来组织的.
第一种创建方式是可以直接使用{ }来创建对象, { }里面可以写对象的属性, 键值对之间使用,分割, 键和值之间使用:分割, 方法的值是一个匿名函数.
let 变量名 = { //属性 键:值, ..., //函数 键:function (){ //函数语句块 }, ..., //最后一个属性逗号可以省略 }
示例:
访问属性可以使用.或[ ], 使用[ ]访问属性, 此时属性需要加上引号, 调用方法要记的加上().
let a = {}; // 创建了一个空的对象 let student = { name: '喜羊羊', height: 175, weight: 170, sayHello: function() { console.log("hello"); } }; console.log(student.name); console.log(student['height']); console.log(student.weight); student.sayHello();
第二种方式是可以使用new Object先创建对象, 然后再添加属性, 上面使用{ }创建的对象也可以随时可以在声明的对象外新增属性.
let student = new Object(); // 和创建数组类似 student.name = "美羊羊"; student.height = 165; student['weight'] = 99; student.sayHello = function () { console.log("hello"); } console.log(student.name); console.log(student['height']); console.log(student.weight); student.sayHello();
第三种方式是使用构造函数来创建对象, 使用构造函数可以把相同的属性创建提取出来, 简化开发过程.
function 构造函数名(形参列表) { this.属性 = 值; this.函数 = function...; } //创建 let obj = new 构造方法名(实参);
示例:
function Cat(name, type, sound) { this.name = name; this.type = type; this.miao = function () { console.log(sound); // 别忘了作用域的链式访问规则 } } let mimi = new Cat('咪咪', '中华田园喵', '喵'); let xiaohei = new Cat('小黑', '波斯喵', '猫呜'); let ciqiu = new Cat('刺球', '金渐层', '咕噜噜'); console.log(mimi); mimi.miao();
在JS的ES6版本中, 引入了class关键字, JS中就可以定义 “类” 了, 然后可以通过类去创建实例对象, 这种规则就更接近Java了, 不过本篇博客以扫盲JS语法为目的, 就不在往后面介绍了.