一、函数
1.1、函数定义和调用
- 定义函数
(1)定义函数方式一
一旦执行到// 绝对值函数 function abs(x) { if (x >= 0) { return x; }else{ return -x; } }
return
时,函数就执行完毕,并将结果返回。
如果没有return
语句,函数执行完毕后也会返回结果,只是结果为NaN
。
(2)定义函数方式二
在这种方式下,var abs = function(x) { if (x >= 0) { return x; }else{ return -x; } }
function(x) {...}
是一个匿名函数,它没有函数名。
但是这个匿名函数赋值给了变量abs,所以通过abs就可以调用该函数。
以上两种方式完全等价。第二种方式按照完整语法需要在函数体末尾加上一个;
,表示赋值语句结束。
-
调用函数
abs(10) // 10 abs(-10) // 10
由于JavaScript允许传入任意个参数而不影响调用,因此传入的参数比定义的多也没有问题,虽然函数内部不需要这些参数。
abs(10, 'asdasd') // 10 abs(-10, 'sadasda', null) // 10
传入的参数比定义的少也没有问题。
abs() // NaN
不传参数的话
abs(x)
函数的参数x
收到的参数为undefined
,计算结果为NaN
。
如果要避免收到undefined
这一情况,可以对参数进行检查。function abs(x) { if (typeof x !== 'number') { throw 'Not a Number'; } if (x >= 0) { return x; }else{ return -x; } }
-
arguments
arguments
是一个JS免费赠送的关键词,它只在函数内部起作用。
代表传递进来的所有参数是一个数组。
也就是说,即使函数不定义任何参数,还是可以拿到参数的值。var abs = function(x) { for (var i = 0; arguments.length; i++) { console.log(arguments[i]); // 10, 20, 30 } if (x >= 0) { return x; }else{ return -x; } }; abs(10, 20, 30)
-
rest
问题:arguments
会包含所有的参数。如果需要使用多余的参数来进行附加的操作,需要排除已有的参数
以前:function aaa(a, b) { console.log("a=>" + a); console.log("b=>" + b); if (arguments.length > 2) { for (var i = 2; i < arguments.length; i++) { 。。。。 } } }
而在ES6标准中,新引入了
rest
参数
rest
参数可以获取除了已经定义的参数之外的所有参数
现在:function aaa(a, b,...rest) { console.log("a=>" + a); console.log("b=>" + b); console.log(rest); if (arguments.length > 2) { for (var i = 2; i < arguments.length; i++) { } } }
rest
参数只能写在最后,前面用...
标识。
传入的参数先绑定a
,b
,多余的参数以数组形式交给变量rest
。
1.2、变量的作用域
-
变量的作用域
在JavaScript中,var定义的变量实际是有作用域的。
(1)假设在函数体中声明,则在函数体外不可用(但闭包可以实现)function test01() { var x = 1; x = x + 1; } x = x + 2; // Uncaught ReferenceError: x is not defined
(2)如果两个函数使用了相同的变量名,只要在函数内部,就不冲突
该变量只在各自的函数体内部起作用,即不同函数内部的同名变量相互独立。
function test01() { var x = 1; x = x + 1; } function test02() { var x = 'A'; x = x + 1; }
(3)内部函数可以访问外部函数的成员,反之则不行
function test01() { var x = 1; // 内部函数可以访问外部函数的成员,反之则不行 function test02() { var y = x + 1; } var z = y + 1; // ReferenceError test01 不可以访问 test02 的变量 y }
(4)假设内部函数变量和外部函数的变量重名
function test01() { var x = 1; // 内部函数可以访问外部函数的成员,反之则不行 function test02() { var x = 'A'; console.log('inner' + x); // 'A' } test02(); console.log('outer' + x); // 1 } test01();
这说明JavaScript函数在查找变量时是从自身函数定义开始,从内向外查找。
如果函数内部定义了与外部函数重名的变量,则内部函数的变量将屏蔽外部函数的变量。 -
提升变量的作用域
function test01() { var x = "x" + y; console.log(x); // 结果为 xundefined,说明y的值为undefined var y = 'y'; }
y
为undefined
的原因是JavaScript引擎自动提升了变量y
的声明,但不会提升变量y
的赋值
对于这个函数,JavaScript引擎看到的代码相当于function test01() { var y; // 提升变量 y 的声明,此时 y 为undefined var x = "x" + y; console.log(x); y = 'y'; }
这个是在JavaScript建立之初就存在的特性。
养成规范:所有变量的定义都放在函数的头部,不要乱放,便于代码维护function test01() { var x = 1, // x初始化为1 y = x + 1, // y 初始化为2 z, i; // z, i为undefined // 其他语句.... }
-
全局作用域
不在任何函数内定义的变量就具有全局作用域。
JavaScript默认有一个全局对象window
,全局作用域的变量实际上被绑定到window
的一个属性var course = 'xxx'; alert(course); alert(window.course); // 默认所有的全局变量都会自动绑定在window对象下
因此,直接访问全局变量
course
和访问window.course
是完全一样的。
同理,顶层函数的定义也被视为一个全局变量,并绑定到window
对象function test01() { alert('test'); } test01(); // 直接调用test01() window.test01(); // 通过window.test01()调用
其实
alert()
函数也是window
的一个变量window.alert('调用window.alert()'); // 把alert保存到另一个变量 var old_alert = window.alert; // 给alert赋一个新函数 window.alert = function () {} alert('无法用alert()显示了'); // 恢复alert window.alert = old_alert; alert('又可以用alert显示了')
JavaScript实际上只有一个全局作用域
任何变量(函数也视为变量),假设没有在函数作用域内找到,就会向外查找
如果在全局作用域都没有找到,就会报引用异常RefrenceError由于所有的全局变量都会绑定到
window
上
如果不同的js文件使用了相同的全局变量,或者定义了相同名字的顶层函数,都会造成命名冲突。
减少冲突的一个办法就是把自己的所有变量和函数全部绑定到一个全局变量中:// 唯一全局变量,把自己的代码全部放入自己定义的唯一空间名字中 var KuangApp = {}; // 定义全局变量 KuangApp.name = 'kuang'; KuangApp.add = function (a,b) { return a + b; }
-
局部作用域
由于JavaScript的变量作用域实际上是函数的内部,所以在for
循环等语句块中是无法定义具有局部作用域的变量的function aaa() { for (var i = 0; i<100; i++) { console.log(i); } console.log(i+1); // i出了作用域还可以使用 }
所以为了解决块级作用域,ES6引入了新的关键字
let
,用let
替代var
可以声明一个块级作用域的变量function aaa() { for (let i = 0; i<100; i++) { console.log(i); } console.log(i+1); // SyntaxError }
-
常量
由于let
和var
声明的都是变量,如果要声明一个常量,在ES6之前,只能用大写的变量来表示这是一个常量。var PI = 3.14;
在ES6 引入了新的关键字来定义常量:
const
。
const
和let
都具有块级作用域'use strict' const PI = 3.14; PI = 3; // 某些浏览器可能不报错,但是无效果 PI; // 3.14
1.3、方法
方法就是把函数放在对象里面,称为这个对象的方法。
对象只有两个东西:属性和方法。
在JavaScript中,对象是这样定义的:
var xiaoming= {
name: '小明',
birth: 1990,
}
但是我们可以给小明绑定一个函数,比如写一个age()方法,返回小明的年龄
var xiaoming= {
name: '小明',
birth: 1990,
// 方法
age: function () {
// 今年 减去 出生的年
var now = new Date().getFullYear();
return now - this.birth;
}
}
xiaoming.age; // funcation xiaoming.age()
xiaoming.age();// 调用方法,一定要带括号
绑定到对象上的函数叫方法,和普通函数没有什么区别,但是它在内部使用了一个this
关键字。
在一个方法内部,this
是一个特殊变量,它始终指向当前对象,也就是xiaoming
这个变量。
所以this.birth
可以拿到xiaoming
的birth
属性。
将上面的对象拆开来写:
function getAge() {
// 今年 减去 出生的年
var now = new Date().getFullYear();
return now - this.birth;
}
var xiaoming= {
name: '小明',
birth: 1990,
// 方法
age: getAge
}
xiaoming.age(); // 31,正常结果
getAge(); // NaN
单独调用getAge()
时返回了一个NaN
,这里是JavaScript的一个大坑。
JavaScript的函数内部如果调用了this
,这个this
指向谁要视情况而定。
如果以对象的方法形式调用,比如xiaoming.age()
,该函数的this
指向被调用的对象,也就是xiaoming
。
如果单独调用getAge()
,此时该函数的this
指向全局对象,也就是window
。
apply
我们可以通过apply
来控制this
的指向。
要指定函数的this
指向哪个对象,可以用函数本身的apply
方法,它接收两个参数,第一个参数就是需要绑定的this
变量,第二个参数是Array
,表示函数本身的参数。
function getAge() {
// 今年 减去 出生的年
var now = new Date().getFullYear();
return now - this.birth;
}
var xiaoming= {
name: '小明',
birth: 2020,
// 方法
age: getAge
};
getAge.apply(xiaoming,[]); // this指向了kaung这个对象,参数为空
二、标准对象
2.1、标准对象
在JavaScript中,某些对象和其他对象不太一样。为了区分对象的类型,我们用typeof
操作符获取对象的类型,它会返回一个字符串。
typeof 123; // 'number'
typeof NaN; // 'number'
typeof 'str'; // 'string'
typeof true; // 'boolean'
typeof undefined; // 'undefined'
typeof Math.abs; // 'function'
typeof null; // 'object'
typeof []; // 'object'
typeof {}; // 'object'
2.2、Date
在JavaScript中,Date
对象用来表示日期和时间。
要获取系统当前时间,用:
var now = new Date();
now.getFullYear(); // 年
now.getMonth(); // 月 0-11个月
now.getDate(); // 日
now.getDay(); // 星期
now.getHours(); // 时
now.getMinutes(); // 分
now.getSeconds(); // 秒
now.getTime(); // 时间戳
new Date(1578105175994); // 时间戳转换为时间
注意:当前时间是浏览器从本机操作系统获取的时间,所以不一定准确,因为用户可以把当前时间设为任何值。
Date
对象表示的时间是按浏览器所在时区显示的,不过我们既可以显示本地时间,也可以显示调整后的UTC时间:
now = new Date(1578105175994);
// 注意:调用是一个方法,不是一个属性
now.toLocaleString; // 本地时间(北京时区)
now.toUTCString; // UTC时间
要获取当前时间戳,可以用:
if (Date.now()) {
console.log(Date.now()); // 老版本IE没有now()方法
}else {
console.log(new Date().getTime());
}
2.3、JSON
- JSON是一种轻量级的数据交换格式
- 简介和清晰的层次结构使JSON成为理想的数据交换语言
- 提升网络传输效率
在JavaScript中,一切皆为对象,任何JS支持的类型都可以用JSON来表示
JSON的格式要求:
- 对象都用
{}
- 数组都用
[]
- 所有的键值对都使用
key:value
,多个键值对之间用,
分隔。
JSON是用来保存JavaScript对象的一种方式,和JavaScript对象的写法也类似:
键值对的键名写在前面用" "
包裹,是用:
分隔,然后写值,如:
'{"name": "zhang", "age": "3",sex: "男"}'
其实JSON可以理解为JavaScript对象的字符串表示,它使用文本表示一个JS对象的信息,本质是一个字符串。
var obj= {a: 'hello', b: 'hellob'}; // 这是一个JavaScript对象
var json= '{"a": "hello", "b": "hellob"}'; // 这是一个JSON字符串
JSON字符串和JS对象的转化:
var user = {
name: "zhang",
age: 3,
sex: "男"
};
// 对象转化为JSON字符串
var jsonUser = JSON.stringify(user);
// JSON字符串转化为对象,参数为JSON字符串
var obj = JSON.parse(jsonUser);
三、面向对象编程
3.1、面向对象
JavaScript的面向对象编程和大多数其他语言如Java、C#都不太一样。
面向对象的两个基本概念:
- 类:类是对象的类型模板。例如,定义一个
Student
类表示学生,类本身是一种类型,Student
表示学生类型,但不表示任何具体的某个学生。 - 实例:实例是根据类创建的对象。例如,根据
Student
类可以创建出xiaoming
、xiaohong
等多个实例,每个实例表示一个具体的学生,他们全都属于Student
类型。
但是在JavaScript中,这个概念需要改一改。JavaScript不区分类和实例的概念,而是通过原型(prototype)来实现面向对象编程。
原型是指但我们想要创建xiaoming
这个具体的学生时,我们并没有一个Student
类型可用。但是可以用原型的方法来实现:
var Student= {
name: "Robot",
age: 3,
run: function () {
console.log(this.name + "run...");
}
};
var xiaoming = {
name: "xiaoming"
};
// xiaomign的原型是Student
xiaoming.__proto__ = Student;
最后一行代码把xiaoming
的原型指向了对象Student
,看上去xiaoming
仿佛像是从Student
上继承下来的:
xiaoming.name; // 'xiaoming'
xiaoming.run; // 'xiaoming run...'
xiaoming
有自己的name
属性,但并没有定义run()
方法。但是由于xiaoming
是从Student
继承而来,只要Student
有run()
方法,xiaoming
也可以调用。
JavaScript的原型链和Java的Class区别就在,它没有“Class”的概念,所有的对象都是实例,所谓的继承关系不过是把一个对象的原型指向另一个而已。
如果再把xiaoming
的原型指向其他对象:
var bird = {
fly: function () {
console.log(this.name + "fly...");
}
};
// xiaomign的原型是bird
xiaoming.__proto__ = bird;
现在的xiaoming
已经无法run()
了,因为他已经变成了一只鸟:
xiaoming.fly(); // xiaoming fly...
在JavaScript代码运行时期,可以把xiaoming
从Student
变成bird
,或者变成任何对象。
3.2、class继承
从面向对象那可以看出,JavaScript的对象模型是基于原型实现的,特点是简单,缺点是理解起来比传统的类-实例模型要困难,最大的缺点是继承的实现需要编写大量代码,并且需要正确实现原型链。
所以,在ES6新引入一个关键字class
。
用函数实现Student
function Student(name) {
this.name = name;
}
// 现在要给这个Student新增一个方法
Student.prototype.hello = function () {
alert('Hello, ' + this.name + '!' );
}
如果用新的class
关键字来编写Student
:
class Student{
constructor(name) {
this.name = name;
}
hello() {
alert('hello, ' + this.name + '!');
}
}
可以发现,class
的定义包含了构造函数constructor
和定义在原型对象上的函数hello()
(注意没有function
关键字),这样就避免了Student.prototype.hello = function () {...}
这样分散的代码
最后,创建一个Student
对象代码和前面章节完全一样:
var xiaoming = new Student('小明');
xiaoming.hello();
用class
定义对象的另一个巨大好处是继承更方便了,不需要考虑原型继承的中间对象,原型对象的构造函数等,直接通过extends
来实现:
class xiaoStudent extends Student{
constructor(name, grade) {
super(name);
this.grade = grade;
}
myGrade() {
alert('小学生');
}
}