2.11.3、函数调用
- 对于无参函数调用:
// 函数声明 var fun = function () { console.log("哈哈,我执行啦!"); } // 函数调用 fun();
对于有参函数调用:
// 函数声明 var sum = function (num1, num2) { var result = num1 + num2; console.log("num1 + num2 = " + result); } // 函数调用 sum(10, 20);
2.11.4、函数参数
JS中的所有的参数传递都是按值传递的,也就是说把函数外部的值赋值给函数内部的参数,就和把值从一个变量赋值给另一个变量是一样的,在调用函数时,可以在()中指定实参(实际参数),实参将会赋值给函数中对应的形参
调用函数时,解析器不会检查实参的类型,所以要注意,是否有可能会接收到非法的参数,如果有可能,则需要对参数进行类型的检查,函数的实参可以是任意的数据类型
调用函数时,解析器也不会检查实参的数量,多余实参不会被赋值,如果实参的数量少于形参的数量,则没有对应实参的形参将是undefined
2.11.5、函数返回值
可以使用 return 来设置函数的返回值,return后的值将会作为函数的执行结果返回,可以定义一个变量,来接收该结果。
注意:在函数中return后的语句都不会执行,如果return语句后不跟任何值就相当于返回一个undefined,如果函数中不写return,则也会返回undefined,return后可以跟任意类型的值
语法格式:return 值
案例演示:
function sum(num1, num2) { return num1 + num2; } var result = sum(10, 20); console.log(result);
2.11.6、嵌套函数
嵌套函数:在函数中声明的函数就是嵌套函数,嵌套函数只能在当前函数中可以访问,在当前函数外无法访问。
案例演示:
function fu() { function zi() { console.log("我是儿子") } zi(); } fu();
2.11.7、匿名函数
匿名函数:没有名字的函数就是匿名函数,它可以让一个变量来接收,也就是用 “函数表达式” 方式创建和接收。
案例演示:
var fun = function () { alert("我是一个匿名函数"); } fun();
2.11.8、立即执行函数
立即执行函数:函数定义完,立即被调用,这种函数叫做立即执行函数,立即执行函数往往只会执行一次。
案例演示:
(function () { alert("我是一个匿名函数"); })();
2.11.9、对象中的函数
对象的属性值可以是任何的数据类型,也可以是个函数。
如果一个函数作为一个对象的属性保存,那么我们称这个函数是这个对象的方法,调用这个函数就说调用对象的方法(method)。
注意:方法和函数只是名称上的区别,没有其它别的区别
案例演示:
var person = { name: "zhangsan", age: 18, sayHello: function () { console.log(name + " hello") } } person.sayHello();
2.11.10、this对象
解析器在调用函数每次都会向函数内部传递进一个隐含的参数,这个隐含的参数就是this,this指向的是一个对象,这个对象我们称为函数执行的上下文对象,根据函数的调用方式的不同,this会指向不同的对象
- 以函数的形式调用时,this永远都是window
- 以方法的形式调用时,this就是调用方法的那个对象
案例演示:
//创建一个全局变量name var name = "全局变量name"; //创建一个函数 function fun() { console.log(this.name); } //创建一个对象 var obj = { name: "孙悟空", sayName: fun }; //我们希望调用obj.sayName()时可以输出obj的名字而不是全局变量name的名字 obj.sayName();
2.12、对象进阶
2.12.1、用工厂方法创建对象
我们之前已经学习了如何创建一个对象,那我们要是想要创建多个对象又该怎么办?聪明的同学可能会说,直接在写几个对象不就好了吗?比如下边的代码:
var person1 = { name: "孙悟空", age: 18, sayName: function () { console.log(this.name); } }; var person2 = { name: "猪八戒", age: 19, sayName: function () { console.log(this.name); } }; var person3 = { name: "沙和尚", age: 20, sayName: function () { console.log(this.name); } }; console.log(person1); console.log(person2); console.log(person3);
的确,上述代码确实可以创建多个对象,但是,这样的解决方案真的好吗?对于少量对象可能使用,我们假设说,要用循环创建1000个对象,那你这种办法似乎就没用了,我们可以这么做,如下代码:
// 使用工厂模式创建对象 function createPerson() { // 创建新的对象 var obj = new Object(); // 设置对象属性 obj.name = "孙悟空"; obj.age = 18; // 设置对象方法 obj.sayName = function () { console.log(this.name); }; //返回新的对象 return obj; } var person1 = createPerson(); var person2 = createPerson(); var person3 = createPerson(); console.log(person1); console.log(person2); console.log(person3);
上述代码看起来更加简洁,但是你会发现每一个人都是孙悟空,我们要是想要给每一个人不同的属性值,请参考:
// 使用工厂模式创建对象 function createPerson(name, age) { // 创建新的对象 var obj = new Object(); // 设置对象属性 obj.name = name; obj.age = age; // 设置对象方法 obj.sayName = function () { console.log(this.name); }; //返回新的对象 return obj; } var person1 = createPerson("孙悟空", 18); var person2 = createPerson("猪八戒", 19); var person3 = createPerson("沙和尚", 20); console.log(person1); console.log(person2); console.log(person3);
现在再看上述代码,发现好像已经完美的解决了创建多个对象的难题,那我们是不是可以用循环批量创建1000个对象了呢?那我们就来试试:
// 使用工厂模式创建对象 function createPerson(name, age) { // 创建新的对象 var obj = new Object(); // 设置对象属性 obj.name = name; obj.age = age; // 设置对象方法 obj.sayName = function () { console.log(this.name); }; //返回新的对象 return obj; } for (var i = 1; i <= 1000; i++) { var person = createPerson("person" + i, 18); console.log(person); }
这样我们就实现了批量创建对象的功能,至于对象的名称和年龄,我们可以通过名称数组和年龄数组来获取,但这并不是我们本小节的重点,我们就忽略了。
2.12.2、用构造函数创建对象
在前一节中,我们学会了使用工厂模式创建对象,但是,你会发现我们所创建的对象类型都是Object,具体代码如下:
// 使用工厂模式创建对象 function createPerson(name, age) { // 创建新的对象 var obj = new Object(); // 设置对象属性 obj.name = name; obj.age = age; // 设置对象方法 obj.sayName = function () { console.log(this.name); }; //返回新的对象 return obj; } for (var i = 1; i <= 1000; i++) { var person = createPerson("person" + i, 18); console.log(typeof person); }
那这有问题吗?看起来有,看起来好像又没有,每创建一个都是对象,但是在实际生活中,人应该是一个确定的类别,属于人类,对象是一个笼统的称呼,万物皆对象,它并不能确切的指明当前对象是人类,那我们要是既想实现创建对象的功能,同时又能明确所创建出来的对象是人类,那么似乎问题就得到了解决,这就用到了构造函数,每一个构造函数你都可以理解为一个类别,用构造函数所创建的对象我们也成为类的实例,那我们来看看是如何做的:
// 使用构造函数来创建对象 function Person(name, age) { // 设置对象的属性 this.name = name; this.age = age; // 设置对象的方法 this.sayName = function () { console.log(this.name); }; } var person1 = new Person("孙悟空", 18); var person2 = new Person("猪八戒", 19); var person3 = new Person("沙和尚", 20); console.log(person1); console.log(person2); console.log(person3);
那这构造函数到底是什么呢?我来解释一下:
构造函数:构造函数就是一个普通的函数,创建方式和普通函数没有区别,不同的是构造函数习惯上首字母大写,构造函数和普通函数的还有一个区别就是调用方式的不同,普通函数是直接调用,而构造函数需要使用new关键字来调用。
那构造函数是怎么执行创建对象的过程呢?我再来解释一下:
调用构造函数,它会立刻创建一个新的对象
将新建的对象设置为函数中this,在构造函数中可以使用this来引用新建的对象
逐行执行函数中的代码
将新建的对象作为返回值返回
你会发现构造函数有点类似工厂方法,但是它创建对象和返回对象都给我们隐藏了,使用同一个构造函数创建的对象,我们称为一类对象,也将一个构造函数称为一个类。我们将通过一个构造函数创建的对象,称为是该类的实例。
现在,this又出现了一种新的情况,为了不让大家混淆,我再来梳理一下:
当以函数的形式调用时,this是window
当以方法的形式调用时,谁调用方法this就是谁
当以构造函数的形式调用时,this就是新创建的那个对象
我们可以使用 instanceof 运算符检查一个对象是否是一个类的实例,它返回true或false
语法格式:
对象 instanceof 构造函数
案例演示:
console.log(person1 instanceof Person);
2.12.3、原型
在前一节中,我们学习了使用构造函数的方式进行创建对象,但是,它还是存在一个问题,那就是,你会发现,每一个对象的属性不一样这是一定的,但是它的方法似乎好像是一样的,如果我创建1000个对象,那岂不是内存中就有1000个相同的方法,那要是有10000个,那对内存的浪费可不是一点半点的,我们有没有什么好的办法解决,没错,我们可以把函数抽取出来,作为全局函数,在构造函数中直接引用就可以了,上代码:
// 使用构造函数来创建对象 function Person(name, age) { // 设置对象的属性 this.name = name; this.age = age; // 设置对象的方法 this.sayName = sayName } // 抽取方法为全局函数 function sayName() { console.log(this.name); } var person1 = new Person("孙悟空", 18); var person2 = new Person("猪八戒", 19); var person3 = new Person("沙和尚", 20); person1.sayName(); person2.sayName(); person3.sayName();
但是,在全局作用域中定义函数却不是一个好的办法,为什么呢?因为,如果要是涉及到多人协作开发一个项目,别人也有可能叫sayName这个方法,这样在工程合并的时候就会导致一系列的问题,污染全局作用域,那该怎么办呢?有没有一种方法,我只在Person这个类的全局对象中添加一个函数,然后在类中引用?答案肯定是有的,这就需要原型对象了,我们先看看怎么做的,然后在详细讲解原型对象。
// 使用构造函数来创建对象 function Person(name, age) { // 设置对象的属性 this.name = name; this.age = age; } // 在Person类的原型对象中添加方法 Person.prototype.sayName = function() { console.log(this.name); }; var person1 = new Person("孙悟空", 18); var person2 = new Person("猪八戒", 19); var person3 = new Person("沙和尚", 20); person1.sayName(); person2.sayName(); person3.sayName();
那原型(prototype)到底是什么呢?
我们所创建的每一个函数,解析器都会向函数中添加一个属性prototype,这个属性对应着一个对象,这个对象就是我们所谓的原型对象,即显式原型,原型对象就相当于一个公共的区域,所有同一个类的实例都可以访问到这个原型对象,我们可以将对象中共有的内容,统一设置到原型对象中。
如果函数作为普通函数调用prototype没有任何作用,当函数以构造函数的形式调用时,它所创建的对象中都会有一个隐含的属性,指向该构造函数的原型对象,我们可以通过__proto__(隐式原型)来访问该属性。当我们访问对象的一个属性或方法时,它会先在对象自身中寻找,如果有则直接使用,如果没有则会去原型对象中寻找,如果找到则直接使用。
以后我们创建构造函数时,可以将这些对象共有的属性和方法,统一添加到构造函数的原型对象中,这样不用分别为每一个对象添加,也不会影响到全局作用域,就可以使每个对象都具有这些属性和方法了。
2.12.4、原型链
访问一个对象的属性时,先在自身属性中查找,找到返回, 如果没有,再沿着__proto__这条链向上查找,找到返回,如果最终没找到,返回undefined,这就是原型链,又称隐式原型链,它的作用就是查找对象的属性(方法)。
我们使用一张图来梳理一下上一节原型案例的代码:
注意:Object对象是所有对象的祖宗,Object的原型对象指向为null,也就是没有原型对象
2.12.5、toString方法
toString()函数用于将当前对象以字符串的形式返回。该方法属于Object对象,由于所有的对象都"继承"了Object的对象实例,因此几乎所有的实例对象都可以使用该方法,所有主流浏览器均支持该函数。
// 使用构造函数来创建对象 function Person(name, age) { // 设置对象的属性 this.name = name; this.age = age; } //创建对象的一个实例对象 var p = new Person("张三", 20); console.log(p.toString());
JavaScript的许多内置对象都重写了该函数,以实现更适合自身的功能需要。
注意:这里我们只是演示toString()方法,其它的一些没有讲到的知识后边会将,我们只看效果就可。
// 字符串 var str = "Hello"; console.log(str.toString()); // 数字 var num = 15.26540; console.log(num.toString()); // 布尔 var bool = true; console.log(bool.toString()); // Object var obj = {name: "张三", age: 18}; console.log(obj.toString()); // 数组 var array = ["CodePlayer", true, 12, -5]; console.log(array.toString()); // 日期 var date = new Date(2013, 7, 18, 23, 11, 59, 230); console.log(date.toString()); // 错误 var error = new Error("自定义错误信息"); console.log(error.toString()); // 函数 console.log(Function.toString());
2.12.6、hasOwnProperty方法
前边章节我们学过,如何遍历一个对象所有的属性和值,那我们要是判断当前对象是否包含指定的属性或方法可以使用 in 运算符来检查,如下代码演示:
// 创造一个构造函数 function MyClass() { } // 向MyClass的原型中添加一个name属性 MyClass.prototype.name = "我是原型中的名字"; // 创建一个MyClass的实例 var mc = new MyClass(); mc.age = 18; // 使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true console.log("age" in mc); console.log("name" in mc);
如果我只想要检查自身对象是否含有某个方法或属性,我们可以使用Object的hasOwnProperty()方法,它返回一个布尔值,判断对象是否包含特定的自身(非继承)属性。如下代码演示:
// 创造一个构造函数 function MyClass() { } // 向MyClass的原型中添加一个name属性 MyClass.prototype.name = "我是原型中的名字"; // 创建一个MyClass的实例 var mc = new MyClass(); mc.age = 18; // 使用in检查对象中是否含有某个属性时,如果对象中没有但是原型中有,也会返回true console.log("age" in mc); console.log("name" in mc); // 可以使用对象的hasOwnProperty()来检查对象自身中是否含有该属性,使用该方法只有当对象自身中含有属性时,才会返回true console.log(mc.hasOwnProperty("age")); console.log(mc.hasOwnProperty("name"));
有同学可能会有疑问,我的这个MyClass类对象中没有hasOwnProperty这个方法啊,它是哪来的?对了,就是原型中的,在执行方法的时候它会通过原型链进行查找,这个方法是Object的特有方法,如下代码演示:
// 创造一个构造函数 function MyClass() { } // 向MyClass的原型中添加一个name属性 MyClass.prototype.name = "我是原型中的名字"; // 创建一个MyClass的实例 var mc = new MyClass(); mc.age = 18; // 检查当前对象 console.log(mc.hasOwnProperty("hasOwnProperty")); // 检查当前对象的原型对象 console.log(mc.__proto__.hasOwnProperty("hasOwnProperty")); // 检查当前对象的原型对象的原型对象 console.log(mc.__proto__.__proto__.hasOwnProperty("hasOwnProperty"));
2.12.7、对象继承
前边我们一直在说继承,那什么是继承?它有什么作用?如何实现继承?将会是本章节探讨的问题。
面向对象的语言有一个标志,那就是它们都有类的概念,而通过类可以创建任意多个具有相同属性和方法的对象。但是在JavaScript中没有类的概念,前边我们说所的类只是我们自己这么叫,大家要清楚。因此它的对象也与基于类的对象有所不同。实际上,JavaScript语言是通过一种叫做原型(prototype)的方式来实现面向对象编程的。
那实现继承有一个最大的好处就是子对象可以使用父对象的属性和方法,从而简化了一些代码。
JavaScript有六种非常经典的对象继承方式,但是我们只学习前三种:
原型链继承
借用构造函数继承
组合继承(重要)
原型式继承
寄生式继承
寄生组合式继承
2.12.7.1、原型链继承
核心思想: 子类型的原型为父类型的一个实例对象
基本做法:
定义父类型构造函数
给父类型的原型添加方法
定义子类型的构造函数
创建父类型的对象赋值给子类型的原型
将子类型原型的构造属性设置为子类型
给子类型原型添加方法
创建子类型的对象: 可以调用父类型的方法
案例演示:
// 定义父类型构造函数 function SuperType(name) { this.name = name; this.showSupperName = function () { console.log(this.name); }; } // 定义子类型的构造函数 function SubType(name, age) { // 在子类型中调用call方法继承自SuperType SuperType.call(this, name); this.age = age; } // 给子类型的原型添加方法 SubType.prototype.showSubName = function () { console.log(this.name); }; // 创建子类型的对象然后调用 var subType = new SubType("孙悟空", 20); subType.showSupperName(); subType.showSubName(); console.log(subType.name); console.log(subType.age);
缺点描述:
只能继承父类的实例属性和方法,不能继承原型属性和方法
无法实现构造函数的复用,每个子类都有父类实例函数的副本,影响性能,代码会臃肿
2.12.7.3、组合继承
核心思想: 原型链+借用构造函数的组合继承
基本做法:
利用原型链实现对父类型对象的方法继承
利用super()借用父类型构建函数初始化相同属性
案例演示:
function Person(name, age) { this.name = name; this.age = age; } Person.prototype.setName = function (name) { this.name = name; }; function Student(name, age, price) { Person.call(this, name, age); // 为了得到父类型的实例属性和方法 this.price = price; // 添加子类型私有的属性 } Student.prototype = new Person(); // 为了得到父类型的原型属性和方法 Student.prototype.constructor = Student; // 修正constructor属性指向 Student.prototype.setPrice = function (price) { // 添加子类型私有的方法 this.price = price; }; var s = new Student("孙悟空", 24, 15000); console.log(s.name, s.age, s.price); s.setName("猪八戒"); s.setPrice(16000); console.log(s.name, s.age, s.price);
缺点描述:
父类中的实例属性和方法既存在于子类的实例中,又存在于子类的原型中,不过仅是内存占用,因此,在使用子类创建实例对象时,其原型中会存在两份相同的属性和方法 。
注意:这个方法是JavaScript中最常用的继承模式。
2.12.8、垃圾回收
垃圾回收(GC):就像人生活的时间长了会产生垃圾一样,程序运行过程中也会产生垃圾,这些垃圾积攒过多以后,会导致程序运行的速度过慢,所以我们需要一个垃圾回收的机制,来处理程序运行过程中产生垃圾。
当一个对象没有任何的变量或属性对它进行引用,此时我们将永远无法操作该对象,此时这种对象就是一个垃圾,这种对象过多会占用大量的内存空间,导致程序运行变慢,所以这种垃圾必须进行清理。
在JS中拥有自动的垃圾回收机制,会自动将这些垃圾对象从内存中销毁,我们不需要也不能进行垃圾回收的操作,我们需要做的只是要将不再使用的对象设置null即可。
案例演示:
// 使用构造函数来创建对象 function Person(name, age) { // 设置对象的属性 this.name = name; this.age = age; } var person1 = new Person("孙悟空", 18); var person2 = new Person("猪八戒", 19); var person3 = new Person("沙和尚", 20); person1 = null; person2 = null; person3 = null;
2.13、作用域
作用域指一个变量的作用的范围,在JS中一共有两种作用域:
- 全局作用域
- 函数作用域
2.13.1、声明提前
变量的声明提前:使用var关键字声明的变量,会在所有的代码执行之前被声明(但是不会赋值),但是如果声明变量时不使用var关键字,则变量不会被声明提前
函数的声明提前:使用函数声明形式创建的函数 function 函数名(){} ,它会在所有的代码执行之前就被创建,所以我们可以在函数声明前来调用函数。使用函数表达式创建的函数,不会被声明提前,所以不能在声明前调用
2.13.2、作用域
2.13.2.1、全局作用域
直接编写在script标签中的JavaScript代码,都在全局作用域
全局作用域在页面打开时创建,在页面关闭时销毁
在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建,我们可以直接使用
在全局作用域中:
创建的变量都会作为window对象的属性保存
创建的函数都会作为window对象的方法保存
全局作用域中的变量都是全局变量,在页面的任意的部分都可以访问的到
2.13.2.2、函数作用域
调用函数时创建函数作用域,函数执行完毕以后,函数作用域销毁
每调用一次函数就会创建一个新的函数作用域,它们之间是互相独立的
在函数作用域中可以访问到全局作用域的变量,在全局作用域中无法访问到函数作用域的变量
在函数中要访问全局变量可以使用window对象
作用域链:当在函数作用域操作一个变量时,它会先在自身作用域中寻找,如果有就直接使用,如果没有则向上一级作用域中寻找,直到找到全局作用域,如果全局作用域中依然没有找到,则会报错ReferenceError
2.13.3、作用域链
多个上下级关系的作用域形成的链,它的方向是从下向上的(从内到外),查找变量时就是沿着作用域链来查找的。
查找一个变量的查找规则:
在当前作用域下的执行上下文中查找对应的属性,如果有直接返回,否则进入2
在上一级作用域的执行上下文中查找对应的属性,如果有直接返回,否则进入3
再次执行2的相同操作,直到全局作用域,如果还找不到就抛出找不到的ReferenceError异常
第三章 JavaScript常用对象
3.1、数组对象
3.1.1、概述
数组也是对象的一种,数组是一种用于表达有顺序关系的值的集合的语言结构,也就是同类数据元素的有序集合。
数组的存储性能比普通对象要好,在开发中我们经常使用数组来存储一些数据。但是在JavaScript中是支持数组可以是不同的元素,这跟JavaScript的弱类型有关,此处不用纠结,我们大多数时候都是相同类型元素的集合。数组内的各个值被称作元素,每一个元素都可以通过索引(下标)来快速读取,索引是从零开始的整数。
使用typeof检查一个数组对象时,会返回object。
3.1.2、创建数组
3.1.2.1、使用对象创建
同类型有序数组创建:
var arr = new Array(); arr[0] = 1; arr[1] = 2; arr[2] = 3; arr[3] = 4; arr[4] = 5; arr[5] = 6; arr[6] = 7; arr[7] = 8; arr[8] = 9;
不同类型有序数组创建:
var arr = new Array(); arr[0] = 1; arr[1] = "2"; arr[2] = 3; arr[3] = "4"; arr[4] = 5; arr[5] = "6"; arr[6] = 7; arr[7] = "8"; arr[8] = 9;
3.1.2.2、使用字面量创建
- 同类型有序数组创建:
var arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
不同类型有序数组创建:
var arr = [1, "2", 3, "4", 5, "6", 7, "8", 9];
3.1.3、遍历数组
for (var i = 0; i < arr.length; i++) { console.log(arr[i]); }
3.1.4、数组属性
constructor属性演示:返回创建数组对象的原型函数
var arr = [1,2,3,4]; console.log(arr.constructor);
length属性演示:设置或返回数组元素的个数
var arr = [1,2,3,4]; console.log(arr.length);
3.1.5、数组方法
push()方法演示:该方法可以向数组的末尾添加一个或多个元素,并返回数组的新的长度
var arr = ["孙悟空", "猪八戒", "沙和尚"]; var result = arr.push("唐僧", "蜘蛛精", "白骨精", "玉兔精"); console.log(arr); console.log(result);
pop()方法演示:该方法可以删除数组的最后一个元素,并将被删除的元素作为返回值返回
var arr = ["孙悟空", "猪八戒", "沙和尚"]; var result = arr.pop(); console.log(arr); console.log(result);
unshift()方法演示:该方法向数组开头添加一个或多个元素,并返回新的数组长度
var arr = ["孙悟空", "猪八戒", "沙和尚"]; var result = arr.unshift("牛魔王", "二郎神"); console.log(arr); console.log(result);
shift()方法演示:该方法可以删除数组的第一个元素,并将被删除的元素作为返回值返回
var arr = ["孙悟空", "猪八戒", "沙和尚"]; var result = arr.shift(); console.log(arr); console.log(result);